diff --git a/.size-limit.js b/.size-limit.js
index 3819ee2e91f6..36bf0607e840 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -233,7 +233,7 @@ module.exports = [
import: createImport('init'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
- limit: '152 KB',
+ limit: '154 KB',
},
{
name: '@sentry/node - without tracing',
diff --git a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts
index 07e0467382da..4d3d93fdf81e 100644
--- a/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts
+++ b/dev-packages/e2e-tests/test-applications/astro-4/tests/tracing.dynamic.test.ts
@@ -78,6 +78,11 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => {
'sentry.sample_rate': 1,
'sentry.source': 'route',
url: expect.stringContaining('/test-ssr'),
+ 'http.request.header.accept': expect.any(String),
+ 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd',
+ 'http.request.header.accept_language': 'en-US',
+ 'http.request.header.sec_fetch_mode': 'navigate',
+ 'http.request.header.user_agent': expect.any(String),
},
op: 'http.server',
origin: 'auto.http.astro',
@@ -223,6 +228,11 @@ test.describe('nested SSR routes (client, server, server request)', () => {
'sentry.origin': 'auto.http.astro',
'sentry.source': 'route',
url: expect.stringContaining('/user-page/myUsername123'),
+ 'http.request.header.accept': expect.any(String),
+ 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd',
+ 'http.request.header.accept_language': 'en-US',
+ 'http.request.header.sec_fetch_mode': 'navigate',
+ 'http.request.header.user_agent': expect.any(String),
},
},
},
@@ -256,6 +266,11 @@ test.describe('nested SSR routes (client, server, server request)', () => {
'sentry.origin': 'auto.http.astro',
'sentry.source': 'route',
url: expect.stringContaining('/api/user/myUsername123.json'),
+ 'http.request.header.accept': expect.any(String),
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': expect.any(String),
},
},
},
@@ -308,6 +323,11 @@ test.describe('nested SSR routes (client, server, server request)', () => {
'sentry.origin': 'auto.http.astro',
'sentry.source': 'route',
url: expect.stringContaining('/catchAll/hell0/whatever-do'),
+ 'http.request.header.accept': expect.any(String),
+ 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd',
+ 'http.request.header.accept_language': 'en-US',
+ 'http.request.header.sec_fetch_mode': 'navigate',
+ 'http.request.header.user_agent': expect.any(String),
},
},
},
@@ -360,6 +380,11 @@ test.describe('parametrized vs static paths', () => {
'sentry.origin': 'auto.http.astro',
'sentry.source': 'route',
url: expect.stringContaining('/user-page/settings'),
+ 'http.request.header.accept': expect.any(String),
+ 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd',
+ 'http.request.header.accept_language': 'en-US',
+ 'http.request.header.sec_fetch_mode': 'navigate',
+ 'http.request.header.user_agent': expect.any(String),
},
},
},
diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts
index 9151c13907af..0c69d1f59698 100644
--- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts
+++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.dynamic.test.ts
@@ -79,6 +79,11 @@ test.describe('tracing in dynamically rendered (ssr) routes', () => {
'sentry.sample_rate': 1,
'sentry.source': 'route',
url: expect.stringContaining('/test-ssr'),
+ 'http.request.header.accept': expect.any(String),
+ 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd',
+ 'http.request.header.accept_language': 'en-US',
+ 'http.request.header.sec_fetch_mode': 'navigate',
+ 'http.request.header.user_agent': expect.any(String),
},
op: 'http.server',
origin: 'auto.http.astro',
@@ -226,6 +231,11 @@ test.describe('nested SSR routes (client, server, server request)', () => {
'sentry.origin': 'auto.http.astro',
'sentry.source': 'route',
url: expect.stringContaining('/user-page/myUsername123'),
+ 'http.request.header.accept': expect.any(String),
+ 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd',
+ 'http.request.header.accept_language': 'en-US',
+ 'http.request.header.sec_fetch_mode': 'navigate',
+ 'http.request.header.user_agent': expect.any(String),
},
},
},
@@ -259,6 +269,11 @@ test.describe('nested SSR routes (client, server, server request)', () => {
'sentry.origin': 'auto.http.astro',
'sentry.source': 'route',
url: expect.stringContaining('/api/user/myUsername123.json'),
+ 'http.request.header.accept': expect.any(String),
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': expect.any(String),
},
},
},
@@ -311,6 +326,11 @@ test.describe('nested SSR routes (client, server, server request)', () => {
'sentry.origin': 'auto.http.astro',
'sentry.source': 'route',
url: expect.stringContaining('/catchAll/hell0/whatever-do'),
+ 'http.request.header.accept': expect.any(String),
+ 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd',
+ 'http.request.header.accept_language': 'en-US',
+ 'http.request.header.sec_fetch_mode': 'navigate',
+ 'http.request.header.user_agent': expect.any(String),
},
},
},
@@ -363,6 +383,11 @@ test.describe('parametrized vs static paths', () => {
'sentry.origin': 'auto.http.astro',
'sentry.source': 'route',
url: expect.stringContaining('/user-page/settings'),
+ 'http.request.header.accept': expect.any(String),
+ 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd',
+ 'http.request.header.accept_language': 'en-US',
+ 'http.request.header.sec_fetch_mode': 'navigate',
+ 'http.request.header.user_agent': expect.any(String),
},
},
},
diff --git a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts
index c7496d4e6247..354655a4ac7e 100644
--- a/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts
+++ b/dev-packages/e2e-tests/test-applications/astro-5/tests/tracing.serverIslands.test.ts
@@ -70,6 +70,11 @@ test.describe('tracing in static routes with server islands', () => {
'sentry.op': 'http.server',
'sentry.origin': 'auto.http.astro',
'sentry.source': 'route',
+ 'http.request.header.accept': expect.any(String),
+ 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd',
+ 'http.request.header.accept_language': 'en-US',
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': expect.any(String),
}),
op: 'http.server',
origin: 'auto.http.astro',
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts
index 1209eae1ada9..2f314a10817d 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts
@@ -38,6 +38,13 @@ test('Sends an API route transaction', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-transaction',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-8/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-8/tests/transactions.test.ts
index be0e03cdbc97..a7d5ed887049 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-8/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-8/tests/transactions.test.ts
@@ -38,6 +38,13 @@ test('Sends an API route transaction', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-transaction',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts
index c37eb8da7cc1..143ebcd9a9f0 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts
@@ -38,6 +38,13 @@ test('Sends an API route transaction', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-transaction',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts
index 78dfe680a453..0a23c1766b38 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-distributed-tracing/tests/propagation.test.ts
@@ -32,6 +32,10 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
const outgoingHttpSpanId = outgoingHttpSpan?.span_id;
+ const outgoingHttpSpanData = outgoingHttpSpan?.data || {};
+ // Outgoing span (`http.client`) does not include headers as attributes
+ expect(Object.keys(outgoingHttpSpanData).some(key => key.startsWith('http.request.header.'))).toBe(false);
+
expect(traceId).toEqual(expect.any(String));
// data is passed through from the inbound request, to verify we have the correct headers set
@@ -75,6 +79,13 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-outgoing-http/:id',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
@@ -106,6 +117,10 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-inbound-headers/:id',
+ 'http.request.header.baggage': expect.any(String),
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sentry_trace': expect.stringMatching(/[a-f0-9]{32}-[a-f0-9]{16}-1/),
},
op: 'http.server',
parent_span_id: outgoingHttpSpanId,
@@ -146,6 +161,10 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
const outgoingHttpSpanId = outgoingHttpSpan?.span_id;
+ const outgoingHttpSpanData = outgoingHttpSpan?.data || {};
+ // Outgoing span (`http.client`) does not include headers as attributes
+ expect(Object.keys(outgoingHttpSpanData).some(key => key.startsWith('http.request.header.'))).toBe(false);
+
expect(traceId).toEqual(expect.any(String));
// data is passed through from the inbound request, to verify we have the correct headers set
@@ -189,6 +208,13 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-outgoing-fetch/:id',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts
index ac4c8bdea83e..9730bfd6fa68 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts
@@ -38,6 +38,13 @@ test('Sends an API route transaction', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-transaction',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/tests/transactions.test.ts
index 5fb9c98ffc66..77cb616450f9 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules-decorator/tests/transactions.test.ts
@@ -38,6 +38,13 @@ test('Sends an API route transaction from module', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/example-module/transaction',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/transactions.test.ts
index b95aa1fe4406..63976a559898 100644
--- a/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nestjs-with-submodules/tests/transactions.test.ts
@@ -38,6 +38,13 @@ test('Sends an API route transaction from module', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/example-module/transaction',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/next-env.d.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/next-env.d.ts
index 1b3be0840f3f..4f11a03dc6cc 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15/next-env.d.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/next-env.d.ts
@@ -2,4 +2,4 @@
///
// NOTE: This file should not be edited
-// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
index 052dd62697a1..7f9b3e822628 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/package.json
@@ -6,6 +6,7 @@
"build": "next build > .tmp_build_stdout 2> .tmp_build_stderr || (cat .tmp_build_stdout && cat .tmp_build_stderr && exit 1)",
"clean": "npx rimraf node_modules pnpm-lock.yaml .tmp_dev_server_logs",
"test:prod": "TEST_ENV=production playwright test",
+ "test:prod-turbo": "TEST_ENV=prod-turbopack playwright test",
"test:dev": "TEST_ENV=development playwright test",
"test:dev-turbo": "TEST_ENV=dev-turbopack playwright test",
"test:build": "pnpm install && pnpm build",
@@ -46,7 +47,7 @@
},
{
"build-command": "pnpm test:build-turbo",
- "assert-command": "pnpm test:prod && pnpm test:dev-turbo",
+ "assert-command": "pnpm test:prod-turbo && pnpm test:dev-turbo",
"label": "nextjs-15 (turbo)"
}
]
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nextjs-15/playwright.config.mjs
index e1be6810f4dc..2eaa19f24532 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15/playwright.config.mjs
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/playwright.config.mjs
@@ -14,7 +14,7 @@ const getStartCommand = () => {
return 'pnpm next dev -p 3030 2>&1 | tee .tmp_dev_server_logs';
}
- if (testEnv === 'production') {
+ if (testEnv === 'production' || testEnv === 'prod-turbopack') {
return 'pnpm next start -p 3030';
}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/pageload-tracing.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/pageload-tracing.test.ts
index 3e41c04e2644..3cad4a546508 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-15/tests/pageload-tracing.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-15/tests/pageload-tracing.test.ts
@@ -22,3 +22,38 @@ test('App router transactions should be attached to the pageload request span',
expect(pageloadTraceId).toBeTruthy();
expect(serverTransaction.contexts?.trace?.trace_id).toBe(pageloadTraceId);
});
+
+test('extracts HTTP request headers as span attributes', async ({ baseURL }) => {
+ test.skip(
+ process.env.TEST_ENV === 'prod-turbopack' || process.env.TEST_ENV === 'dev-turbopack',
+ 'Incoming fetch request headers are not added as span attributes when Turbopack is enabled (addHeadersAsAttributes)',
+ );
+
+ const serverTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
+ return transactionEvent?.transaction === 'GET /pageload-tracing';
+ });
+
+ await fetch(`${baseURL}/pageload-tracing`, {
+ headers: {
+ 'User-Agent': 'Custom-NextJS-Agent/15.0',
+ 'Content-Type': 'text/html',
+ 'X-NextJS-Test': 'nextjs-header-value',
+ Accept: 'text/html, application/xhtml+xml',
+ 'X-Framework': 'Next.js',
+ 'X-Request-ID': 'nextjs-789',
+ },
+ });
+
+ const serverTransaction = await serverTransactionPromise;
+
+ expect(serverTransaction.contexts?.trace?.data).toEqual(
+ expect.objectContaining({
+ 'http.request.header.user_agent': 'Custom-NextJS-Agent/15.0',
+ 'http.request.header.content_type': 'text/html',
+ 'http.request.header.x_nextjs_test': 'nextjs-header-value',
+ 'http.request.header.accept': 'text/html, application/xhtml+xml',
+ 'http.request.header.x_framework': 'Next.js',
+ 'http.request.header.x_request_id': 'nextjs-789',
+ }),
+ );
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-connect/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-connect/tests/transactions.test.ts
index ec02acca77d6..9b06ad052f58 100644
--- a/dev-packages/e2e-tests/test-applications/node-connect/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-connect/tests/transactions.test.ts
@@ -39,6 +39,13 @@ test('Sends an API route transaction', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-transaction',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
diff --git a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/transactions.test.ts
index 86fdffd3b452..048f70a1aba8 100644
--- a/dev-packages/e2e-tests/test-applications/node-express-v5/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-express-v5/tests/transactions.test.ts
@@ -38,6 +38,13 @@ test('Sends an API route transaction', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-transaction',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
diff --git a/dev-packages/e2e-tests/test-applications/node-express/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-express/tests/transactions.test.ts
index b47feebcd728..1ffd9f2e498d 100644
--- a/dev-packages/e2e-tests/test-applications/node-express/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-express/tests/transactions.test.ts
@@ -38,6 +38,13 @@ test('Sends an API route transaction', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-transaction',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
@@ -208,3 +215,34 @@ test('Sends an API route transaction for an errored route', async ({ baseURL })
measurements: {},
});
});
+
+test('Extracts HTTP request headers as span attributes', async ({ baseURL }) => {
+ const transactionEventPromise = waitForTransaction('node-express', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /test-transaction'
+ );
+ });
+
+ await fetch(`${baseURL}/test-transaction`, {
+ headers: {
+ 'User-Agent': 'Custom-Agent/1.0 (Test)',
+ 'Content-Type': 'application/json',
+ 'X-Custom-Header': 'test-value',
+ Accept: 'application/json, text/plain',
+ 'X-Request-ID': 'req-123',
+ },
+ });
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent.contexts?.trace?.data).toEqual(
+ expect.objectContaining({
+ 'http.request.header.user_agent': 'Custom-Agent/1.0 (Test)',
+ 'http.request.header.content_type': 'application/json',
+ 'http.request.header.x_custom_header': 'test-value',
+ 'http.request.header.accept': 'application/json, text/plain',
+ 'http.request.header.x_request_id': 'req-123',
+ }),
+ );
+});
diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/propagation.test.ts
index ee097817bafb..1cdfd67a4851 100644
--- a/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/propagation.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/propagation.test.ts
@@ -32,6 +32,10 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
const outgoingHttpSpanId = outgoingHttpSpan?.span_id;
+ const outgoingHttpSpanData = outgoingHttpSpan?.data || {};
+ // Outgoing span (`http.client`) does not include headers as attributes
+ expect(Object.keys(outgoingHttpSpanData).some(key => key.startsWith('http.request.header.'))).toBe(false);
+
expect(traceId).toEqual(expect.any(String));
// data is passed through from the inbound request, to verify we have the correct headers set
@@ -75,6 +79,13 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-outgoing-http/:id',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
@@ -106,6 +117,10 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-inbound-headers/:id',
+ 'http.request.header.baggage': expect.any(String),
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sentry_trace': expect.stringMatching(/[a-f0-9]{32}-[a-f0-9]{16}-1/),
},
op: 'http.server',
parent_span_id: outgoingHttpSpanId,
@@ -146,6 +161,10 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
const outgoingHttpSpanId = outgoingHttpSpan?.span_id;
+ const outgoingHttpSpanData = outgoingHttpSpan?.data || {};
+ // Outgoing span (`http.client`) does not include headers as attributes
+ expect(Object.keys(outgoingHttpSpanData).some(key => key.startsWith('http.request.header.'))).toBe(false);
+
expect(traceId).toEqual(expect.any(String));
// data is passed through from the inbound request, to verify we have the correct headers set
@@ -189,6 +208,13 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-outgoing-fetch/:id',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
@@ -198,7 +224,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
});
expect(inboundTransaction.contexts?.trace).toEqual({
- data: expect.objectContaining({
+ data: {
'sentry.source': 'route',
'sentry.origin': 'auto.http.otel.http',
'sentry.op': 'http.server',
@@ -219,8 +245,18 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
'net.peer.port': expect.any(Number),
'http.status_code': 200,
'http.status_text': 'OK',
+ 'http.user_agent': 'node',
'http.route': '/test-inbound-headers/:id',
- }),
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.baggage': expect.any(String),
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.sentry_trace': expect.stringMatching(/[a-f0-9]{32}-[a-f0-9]{16}-1/),
+ 'http.request.header.user_agent': 'node',
+ },
op: 'http.server',
parent_span_id: outgoingHttpSpanId,
span_id: expect.stringMatching(/[a-f0-9]{16}/),
diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/transactions.test.ts
index d4c10751f4a5..4bf9b00f127d 100644
--- a/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-fastify-3/tests/transactions.test.ts
@@ -38,6 +38,13 @@ test('Sends an API route transaction', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-transaction',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/propagation.test.ts
index 3746687b92c1..6e6b20b916e8 100644
--- a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/propagation.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/propagation.test.ts
@@ -32,6 +32,10 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
const outgoingHttpSpanId = outgoingHttpSpan?.span_id;
+ const outgoingHttpSpanData = outgoingHttpSpan?.data || {};
+ // Outgoing span (`http.client`) does not include headers as attributes
+ expect(Object.keys(outgoingHttpSpanData).some(key => key.startsWith('http.request.header.'))).toBe(false);
+
expect(traceId).toEqual(expect.any(String));
// data is passed through from the inbound request, to verify we have the correct headers set
@@ -75,6 +79,13 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-outgoing-http/:id',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
@@ -106,6 +117,10 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-inbound-headers/:id',
+ 'http.request.header.baggage': expect.any(String),
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sentry_trace': expect.stringMatching(/[a-f0-9]{32}-[a-f0-9]{16}-1/),
},
op: 'http.server',
parent_span_id: outgoingHttpSpanId,
@@ -146,6 +161,10 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
const outgoingHttpSpanId = outgoingHttpSpan?.span_id;
+ const outgoingHttpSpanData = outgoingHttpSpan?.data || {};
+ // Outgoing span (`http.client`) does not include headers as attributes
+ expect(Object.keys(outgoingHttpSpanData).some(key => key.startsWith('http.request.header.'))).toBe(false);
+
expect(traceId).toEqual(expect.any(String));
// data is passed through from the inbound request, to verify we have the correct headers set
@@ -189,6 +208,13 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-outgoing-fetch/:id',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
@@ -198,7 +224,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
});
expect(inboundTransaction.contexts?.trace).toEqual({
- data: expect.objectContaining({
+ data: {
'sentry.source': 'route',
'sentry.origin': 'auto.http.otel.http',
'sentry.op': 'http.server',
@@ -220,7 +246,17 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-inbound-headers/:id',
- }),
+ 'http.user_agent': 'node',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.baggage': expect.any(String),
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.sentry_trace': expect.stringMatching(/[a-f0-9]{32}-[a-f0-9]{16}-1/),
+ 'http.request.header.user_agent': 'node',
+ },
op: 'http.server',
parent_span_id: outgoingHttpSpanId,
span_id: expect.stringMatching(/[a-f0-9]{16}/),
diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/transactions.test.ts
index 1f049b802bca..eadf89abe7ae 100644
--- a/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-fastify-4/tests/transactions.test.ts
@@ -38,6 +38,13 @@ test('Sends an API route transaction', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-transaction',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/propagation.test.ts
index 6de3b988c3b5..4e903edf05b5 100644
--- a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/propagation.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/propagation.test.ts
@@ -32,6 +32,10 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
const outgoingHttpSpanId = outgoingHttpSpan?.span_id;
+ const outgoingHttpSpanData = outgoingHttpSpan?.data || {};
+ // Outgoing span (`http.client`) does not include headers as attributes
+ expect(Object.keys(outgoingHttpSpanData).some(key => key.startsWith('http.request.header.'))).toBe(false);
+
expect(traceId).toEqual(expect.any(String));
// data is passed through from the inbound request, to verify we have the correct headers set
@@ -75,6 +79,13 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-outgoing-http/:id',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': 'localhost:3030',
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
@@ -106,6 +117,10 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-inbound-headers/:id',
+ 'http.request.header.baggage': expect.any(String),
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sentry_trace': expect.stringMatching(/[a-f0-9]{32}-[a-f0-9]{16}-1/),
},
op: 'http.server',
parent_span_id: outgoingHttpSpanId,
@@ -146,6 +161,10 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
const outgoingHttpSpanId = outgoingHttpSpan?.span_id;
+ const outgoingHttpSpanData = outgoingHttpSpan?.data || {};
+ // Outgoing span (`http.client`) does not include headers as attributes
+ expect(Object.keys(outgoingHttpSpanData).some(key => key.startsWith('http.request.header.'))).toBe(false);
+
expect(traceId).toEqual(expect.any(String));
// data is passed through from the inbound request, to verify we have the correct headers set
@@ -189,6 +208,13 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-outgoing-fetch/:id',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
@@ -198,7 +224,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
});
expect(inboundTransaction.contexts?.trace).toEqual({
- data: expect.objectContaining({
+ data: {
'sentry.source': 'route',
'sentry.origin': 'auto.http.otel.http',
'sentry.op': 'http.server',
@@ -219,8 +245,18 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
'net.peer.port': expect.any(Number),
'http.status_code': 200,
'http.status_text': 'OK',
+ 'http.user_agent': 'node',
'http.route': '/test-inbound-headers/:id',
- }),
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.baggage': expect.any(String),
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.sentry_trace': expect.stringMatching(/[a-f0-9]{32}-[a-f0-9]{16}-1/),
+ 'http.request.header.user_agent': 'node',
+ },
op: 'http.server',
parent_span_id: outgoingHttpSpanId,
span_id: expect.stringMatching(/[a-f0-9]{16}/),
diff --git a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/transactions.test.ts
index e148c8158cd8..3a00e0616f57 100644
--- a/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-fastify-5/tests/transactions.test.ts
@@ -38,6 +38,13 @@ test('Sends an API route transaction', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-transaction',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
diff --git a/dev-packages/e2e-tests/test-applications/node-hapi/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-hapi/tests/transactions.test.ts
index 3f332992d0e7..bd6540b088d3 100644
--- a/dev-packages/e2e-tests/test-applications/node-hapi/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-hapi/tests/transactions.test.ts
@@ -37,6 +37,13 @@ test('Sends successful transaction', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-success',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
@@ -57,6 +64,10 @@ test('Sends successful transaction', async ({ baseURL }) => {
const spans = transactionEvent.spans || [];
+ spans.forEach(span => {
+ expect(Object.keys(span.data).some(key => key.startsWith('http.request.header.'))).toBe(false);
+ });
+
expect(spans).toEqual([
{
data: {
diff --git a/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts b/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts
index f7e8639b7ace..592c5a4717f4 100644
--- a/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-koa/tests/propagation.test.ts
@@ -31,6 +31,10 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
const outgoingHttpSpanId = outgoingHttpSpan?.span_id;
+ const outgoingHttpSpanData = outgoingHttpSpan?.data || {};
+ // Outgoing span (`http.client`) does not include headers as attributes
+ expect(Object.keys(outgoingHttpSpanData).some(key => key.startsWith('http.request.header.'))).toBe(false);
+
expect(traceId).toEqual(expect.any(String));
// data is passed through from the inbound request, to verify we have the correct headers set
@@ -74,6 +78,13 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-outgoing-http/:id',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
@@ -105,6 +116,10 @@ test('Propagates trace for outgoing http requests', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-inbound-headers/:id',
+ 'http.request.header.baggage': expect.stringContaining(traceId!), // we already check if traceId is defined
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sentry_trace': expect.stringMatching(/[a-f0-9]{32}-[a-f0-9]{16}-1/),
},
op: 'http.server',
parent_span_id: outgoingHttpSpanId,
@@ -145,6 +160,10 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
const outgoingHttpSpanId = outgoingHttpSpan?.span_id;
+ const outgoingHttpSpanData = outgoingHttpSpan?.data || {};
+ // Outgoing span (`http.client`) does not include headers as attributes
+ expect(Object.keys(outgoingHttpSpanData).some(key => key.startsWith('http.request.header.'))).toBe(false);
+
expect(traceId).toEqual(expect.any(String));
// data is passed through from the inbound request, to verify we have the correct headers set
@@ -188,6 +207,13 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-outgoing-fetch/:id',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': 'localhost:3030',
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
@@ -197,7 +223,7 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
});
expect(inboundTransaction.contexts?.trace).toEqual({
- data: expect.objectContaining({
+ data: {
'sentry.source': 'route',
'sentry.origin': 'auto.http.otel.http',
'sentry.op': 'http.server',
@@ -219,7 +245,17 @@ test('Propagates trace for outgoing fetch requests', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-inbound-headers/:id',
- }),
+ 'http.user_agent': 'node',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.baggage': expect.stringContaining(traceId!), // we already check if traceId is defined
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.sentry_trace': expect.stringMatching(/[a-f0-9]{32}-[a-f0-9]{16}-1/),
+ 'http.request.header.user_agent': 'node',
+ },
op: 'http.server',
parent_span_id: outgoingHttpSpanId,
span_id: expect.stringMatching(/[a-f0-9]{16}/),
diff --git a/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts
index 966dbc5937e3..53803a8882e6 100644
--- a/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-koa/tests/transactions.test.ts
@@ -38,6 +38,13 @@ test('Sends an API route transaction', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-transaction',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
diff --git a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/tests/sampling.test.ts b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/tests/sampling.test.ts
index 5ca9077634d2..84d783b1d567 100644
--- a/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/tests/sampling.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-otel-custom-sampler/tests/sampling.test.ts
@@ -36,6 +36,13 @@ test('Sends a sampled API route transaction', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/task',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
origin: 'auto.http.otel.http',
op: 'http.server',
diff --git a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/transactions.test.ts
index 3e12007c0d75..a586146eece5 100644
--- a/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-otel-sdk-node/tests/transactions.test.ts
@@ -50,6 +50,13 @@ test('Sends an API route transaction', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-transaction',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
diff --git a/dev-packages/e2e-tests/test-applications/node-otel/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/node-otel/tests/transactions.test.ts
index c6abde474439..1f79fd438719 100644
--- a/dev-packages/e2e-tests/test-applications/node-otel/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/node-otel/tests/transactions.test.ts
@@ -50,6 +50,13 @@ test('Sends an API route transaction', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-transaction',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
diff --git a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.server.test.ts b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.server.test.ts
index f1df13a71ab3..e7b6a5ddfc09 100644
--- a/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nuxt-3/tests/tracing.server.test.ts
@@ -43,3 +43,33 @@ test('does not send transactions for build asset folder "_nuxt"', async ({ page
expect(transactionEvent.transaction).toBe('GET /test-param/:param()');
});
+
+test('extracts HTTP request headers as span attributes', async ({ baseURL }) => {
+ const transactionPromise = waitForTransaction('nuxt-3', transactionEvent => {
+ return transactionEvent.transaction.includes('GET /api/test-param/');
+ });
+
+ await fetch(`${baseURL}/api/test-param/headers-test`, {
+ headers: {
+ 'User-Agent': 'Custom-Nuxt-Agent/3.0',
+ 'Content-Type': 'application/json',
+ 'X-Nuxt-Test': 'nuxt-header-value',
+ Accept: 'application/json, text/html',
+ 'X-Framework': 'Nuxt',
+ 'X-Request-ID': 'nuxt-456',
+ },
+ });
+
+ const transaction = await transactionPromise;
+
+ expect(transaction.contexts?.trace?.data).toEqual(
+ expect.objectContaining({
+ 'http.request.header.user_agent': 'Custom-Nuxt-Agent/3.0',
+ 'http.request.header.content_type': 'application/json',
+ 'http.request.header.x_nuxt_test': 'nuxt-header-value',
+ 'http.request.header.accept': 'application/json, text/html',
+ 'http.request.header.x_framework': 'Nuxt',
+ 'http.request.header.x_request_id': 'nuxt-456',
+ }),
+ );
+});
diff --git a/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/performance.server.test.ts
index 4cc3fb5cef9e..9fd87b052374 100644
--- a/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/performance.server.test.ts
+++ b/dev-packages/e2e-tests/test-applications/sveltekit-2/tests/performance.server.test.ts
@@ -32,3 +32,33 @@ test('server pageload request span has nested request span for sub request', asy
]),
);
});
+
+test('extracts HTTP request headers as span attributes', async ({ page, baseURL }) => {
+ const serverTxnEventPromise = waitForTransaction('sveltekit-2', txnEvent => {
+ return txnEvent?.transaction === 'GET /api/users';
+ });
+
+ await fetch(`${baseURL}/api/users`, {
+ headers: {
+ 'User-Agent': 'Custom-SvelteKit-Agent/1.0',
+ 'Content-Type': 'application/json',
+ 'X-Test-Header': 'sveltekit-test-value',
+ Accept: 'application/json',
+ 'X-Framework': 'SvelteKit',
+ 'X-Request-ID': 'sveltekit-123',
+ },
+ });
+
+ const serverTxnEvent = await serverTxnEventPromise;
+
+ expect(serverTxnEvent.contexts?.trace?.data).toEqual(
+ expect.objectContaining({
+ 'http.request.header.user_agent': 'Custom-SvelteKit-Agent/1.0',
+ 'http.request.header.content_type': 'application/json',
+ 'http.request.header.x_test_header': 'sveltekit-test-value',
+ 'http.request.header.accept': 'application/json',
+ 'http.request.header.x_framework': 'SvelteKit',
+ 'http.request.header.x_request_id': 'sveltekit-123',
+ }),
+ );
+});
diff --git a/dev-packages/e2e-tests/test-applications/tsx-express/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/tsx-express/tests/transactions.test.ts
index 2a0ca499d02c..fca8f1b85528 100644
--- a/dev-packages/e2e-tests/test-applications/tsx-express/tests/transactions.test.ts
+++ b/dev-packages/e2e-tests/test-applications/tsx-express/tests/transactions.test.ts
@@ -38,6 +38,13 @@ test('Sends an API route transaction', async ({ baseURL }) => {
'http.status_code': 200,
'http.status_text': 'OK',
'http.route': '/test-transaction',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
},
op: 'http.server',
span_id: expect.stringMatching(/[a-f0-9]{16}/),
diff --git a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts
index d135ca97ccb1..48fab1be2e9d 100644
--- a/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts
+++ b/dev-packages/node-integration-tests/suites/tracing/httpIntegration/test.ts
@@ -2,6 +2,18 @@ import { afterAll, describe, expect, test } from 'vitest';
import { cleanupChildProcesses, createEsmAndCjsTests, createRunner } from '../../../utils/runner';
import { createTestServer } from '../../../utils/server';
+function getCommonHttpRequestHeaders(): Record {
+ return {
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.accept_language': '*',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.sec_fetch_mode': 'cors',
+ 'http.request.header.user_agent': 'node',
+ };
+}
+
describe('httpIntegration', () => {
afterAll(() => {
cleanupChildProcesses();
@@ -118,6 +130,7 @@ describe('httpIntegration', () => {
'sentry.sample_rate': 1,
'sentry.source': 'route',
url: `http://localhost:${port}/test`,
+ ...getCommonHttpRequestHeaders(),
});
},
})
@@ -159,6 +172,9 @@ describe('httpIntegration', () => {
'sentry.sample_rate': 1,
'sentry.source': 'route',
url: `http://localhost:${port}/test`,
+ 'http.request.header.content_length': '9',
+ 'http.request.header.content_type': 'text/plain;charset=UTF-8',
+ ...getCommonHttpRequestHeaders(),
});
},
})
diff --git a/packages/astro/src/index.server.ts b/packages/astro/src/index.server.ts
index ce9a1b1fa65a..c91d98725d9b 100644
--- a/packages/astro/src/index.server.ts
+++ b/packages/astro/src/index.server.ts
@@ -59,6 +59,8 @@ export {
getSpanStatusFromHttpCode,
getTraceData,
getTraceMetaTags,
+ httpHeadersToSpanAttributes,
+ winterCGHeadersToDict,
graphqlIntegration,
hapiIntegration,
httpIntegration,
diff --git a/packages/astro/src/server/middleware.ts b/packages/astro/src/server/middleware.ts
index e80020ba0913..61f7913cf1b1 100644
--- a/packages/astro/src/server/middleware.ts
+++ b/packages/astro/src/server/middleware.ts
@@ -18,10 +18,12 @@ import {
getClient,
getCurrentScope,
getTraceMetaTags,
+ httpHeadersToSpanAttributes,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
setHttpStatus,
startSpan,
+ winterCGHeadersToDict,
withIsolationScope,
} from '@sentry/node';
import type { APIContext, MiddlewareResponseHandler, RoutePart } from 'astro';
@@ -220,6 +222,10 @@ async function instrumentRequestStartHttpServerSpan(
// This is here for backwards compatibility, we used to set this here before
method,
url: stripUrlQueryAndFragment(ctx.url.href),
+ ...httpHeadersToSpanAttributes(
+ winterCGHeadersToDict(request.headers),
+ getClient()?.getOptions().sendDefaultPii ?? false,
+ ),
};
if (parametrizedRoute) {
diff --git a/packages/astro/test/server/middleware.test.ts b/packages/astro/test/server/middleware.test.ts
index 13e54d1537cb..03933582c846 100644
--- a/packages/astro/test/server/middleware.test.ts
+++ b/packages/astro/test/server/middleware.test.ts
@@ -54,7 +54,7 @@ describe('sentryMiddleware', () => {
} as any;
});
vi.spyOn(SentryNode, 'getActiveSpan').mockImplementation(getSpanMock);
- vi.spyOn(SentryNode, 'getClient').mockImplementation(() => ({}) as Client);
+ vi.spyOn(SentryNode, 'getClient').mockImplementation(() => ({ getOptions: () => ({}) }) as Client);
vi.spyOn(SentryNode, 'getTraceMetaTags').mockImplementation(
() => `
diff --git a/packages/aws-serverless/src/index.ts b/packages/aws-serverless/src/index.ts
index 7d7455d496bb..a041e0a7231f 100644
--- a/packages/aws-serverless/src/index.ts
+++ b/packages/aws-serverless/src/index.ts
@@ -42,6 +42,8 @@ export {
close,
getSentryRelease,
createGetModuleFromFilename,
+ httpHeadersToSpanAttributes,
+ winterCGHeadersToDict,
// eslint-disable-next-line deprecation/deprecation
anrIntegration,
// eslint-disable-next-line deprecation/deprecation
diff --git a/packages/bun/src/index.ts b/packages/bun/src/index.ts
index ec092bcdbbba..e0ce86b1bd23 100644
--- a/packages/bun/src/index.ts
+++ b/packages/bun/src/index.ts
@@ -62,6 +62,8 @@ export {
close,
getSentryRelease,
createGetModuleFromFilename,
+ httpHeadersToSpanAttributes,
+ winterCGHeadersToDict,
// eslint-disable-next-line deprecation/deprecation
anrIntegration,
// eslint-disable-next-line deprecation/deprecation
diff --git a/packages/bun/src/integrations/bunserver.ts b/packages/bun/src/integrations/bunserver.ts
index c31aa0e84ebc..9c235b8bc97c 100644
--- a/packages/bun/src/integrations/bunserver.ts
+++ b/packages/bun/src/integrations/bunserver.ts
@@ -3,6 +3,8 @@ import {
captureException,
continueTrace,
defineIntegration,
+ getClient,
+ httpHeadersToSpanAttributes,
isURLObjectRelative,
parseStringToURLObject,
SEMANTIC_ATTRIBUTE_HTTP_REQUEST_METHOD,
@@ -205,6 +207,10 @@ function wrapRequestHandler(
routeName = route;
}
+ const client = getClient();
+ const sendDefaultPii = client?.getOptions().sendDefaultPii ?? false;
+ Object.assign(attributes, httpHeadersToSpanAttributes(request.headers.toJSON(), sendDefaultPii));
+
isolationScope.setSDKProcessingMetadata({
normalizedRequest: {
url: request.url,
diff --git a/packages/bun/test/integrations/bunserver.test.ts b/packages/bun/test/integrations/bunserver.test.ts
index 77ac2440915b..9792c59c2691 100644
--- a/packages/bun/test/integrations/bunserver.test.ts
+++ b/packages/bun/test/integrations/bunserver.test.ts
@@ -47,6 +47,11 @@ describe('Bun Serve Integration', () => {
'url.port': port.toString(),
'url.scheme': 'http:',
'url.domain': 'localhost',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.user_agent': expect.stringContaining('Bun'),
},
op: 'http.server',
name: 'GET /users',
@@ -81,6 +86,12 @@ describe('Bun Serve Integration', () => {
'url.port': port.toString(),
'url.scheme': 'http:',
'url.domain': 'localhost',
+ 'http.request.header.accept': '*/*',
+ 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.content_length': '0',
+ 'http.request.header.host': expect.any(String),
+ 'http.request.header.user_agent': expect.stringContaining('Bun'),
},
op: 'http.server',
name: 'POST /',
@@ -128,6 +139,59 @@ describe('Bun Serve Integration', () => {
expect(startSpanSpy).toHaveBeenCalledTimes(1);
});
+ test('includes HTTP request headers as span attributes', async () => {
+ const server = Bun.serve({
+ async fetch(_req) {
+ return new Response('Headers test!');
+ },
+ port,
+ });
+
+ // Make request with custom headers
+ await fetch(`http://localhost:${port}/api/test`, {
+ method: 'POST',
+ headers: {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
+ 'Content-Type': 'application/json',
+ 'X-Custom-Header': 'custom-value',
+ Accept: 'application/json, text/plain',
+ Authorization: 'Bearer token123',
+ },
+ body: JSON.stringify({ test: 'data' }),
+ });
+
+ await server.stop();
+
+ // Verify span was created with header attributes
+ expect(startSpanSpy).toHaveBeenCalledTimes(1);
+ expect(startSpanSpy).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ attributes: expect.objectContaining({
+ 'sentry.origin': 'auto.http.bun.serve',
+ 'http.request.method': 'POST',
+ 'sentry.source': 'url',
+ 'url.path': '/api/test',
+ 'url.full': `http://localhost:${port}/api/test`,
+ 'url.port': port.toString(),
+ 'url.scheme': 'http:',
+ 'url.domain': 'localhost',
+ // HTTP headers as span attributes following OpenTelemetry semantic conventions
+ 'http.request.header.user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
+ 'http.request.header.content_type': 'application/json',
+ 'http.request.header.x_custom_header': 'custom-value',
+ 'http.request.header.accept': 'application/json, text/plain',
+ 'http.request.header.accept_encoding': 'gzip, deflate, br, zstd',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.content_length': '15',
+ 'http.request.header.host': expect.any(String),
+ }),
+ op: 'http.server',
+ name: 'POST /api/test',
+ }),
+ expect.any(Function),
+ );
+ });
+
test('skips span creation for OPTIONS and HEAD requests', async () => {
const server = Bun.serve({
async fetch(_req) {
diff --git a/packages/cloudflare/src/request.ts b/packages/cloudflare/src/request.ts
index edc1bccef96f..45fe548696ab 100644
--- a/packages/cloudflare/src/request.ts
+++ b/packages/cloudflare/src/request.ts
@@ -4,10 +4,12 @@ import {
continueTrace,
flush,
getHttpSpanDetailsFromUrlObject,
+ httpHeadersToSpanAttributes,
parseStringToURLObject,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
setHttpStatus,
startSpan,
+ winterCGHeadersToDict,
withIsolationScope,
} from '@sentry/core';
import type { CloudflareOptions } from './client';
@@ -64,6 +66,9 @@ export function wrapRequestHandler(
attributes['user_agent.original'] = userAgentHeader;
}
+ const sendDefaultPii = options.sendDefaultPii ?? false;
+ Object.assign(attributes, httpHeadersToSpanAttributes(winterCGHeadersToDict(request.headers), sendDefaultPii));
+
attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP] = 'http.server';
addCloudResourceContext(isolationScope);
diff --git a/packages/cloudflare/test/request.test.ts b/packages/cloudflare/test/request.test.ts
index eb2989437396..ad323e3c5b5a 100644
--- a/packages/cloudflare/test/request.test.ts
+++ b/packages/cloudflare/test/request.test.ts
@@ -319,6 +319,7 @@ describe('withSentry', () => {
'sentry.sample_rate': 1,
'http.response.status_code': 200,
'http.request.body.size': 10,
+ 'http.request.header.content_length': '10',
},
op: 'http.server',
origin: 'auto.http.cloudflare',
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 4447eea4dae0..ef61364ab3f0 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -92,6 +92,7 @@ export {
httpRequestToRequestData,
extractQueryParamsFromUrl,
headersToDict,
+ httpHeadersToSpanAttributes,
} from './utils/request';
export { DEFAULT_ENVIRONMENT } from './constants';
export { addBreadcrumb } from './breadcrumbs';
diff --git a/packages/core/src/utils/request.ts b/packages/core/src/utils/request.ts
index 04cd1006ba28..ffd60f3e8486 100644
--- a/packages/core/src/utils/request.ts
+++ b/packages/core/src/utils/request.ts
@@ -128,6 +128,47 @@ function getAbsoluteUrl({
return undefined;
}
+// "-user" because otherwise it would match "user-agent"
+const SENSITIVE_HEADER_SNIPPETS = ['auth', 'token', 'secret', 'cookie', '-user', 'password', 'key'];
+
+/**
+ * Converts incoming HTTP request headers to OpenTelemetry span attributes following semantic conventions.
+ * Header names are converted to the format: http.request.header.
+ * where is the header name in lowercase with dashes converted to underscores.
+ *
+ * @see https://opentelemetry.io/docs/specs/semconv/registry/attributes/http/#http-request-header
+ */
+export function httpHeadersToSpanAttributes(
+ headers: Record,
+ sendDefaultPii: boolean = false,
+): Record {
+ const spanAttributes: Record = {};
+
+ try {
+ Object.entries(headers).forEach(([key, value]) => {
+ if (value !== undefined) {
+ const lowerCasedKey = key.toLowerCase();
+
+ if (!sendDefaultPii && SENSITIVE_HEADER_SNIPPETS.some(snippet => lowerCasedKey.includes(snippet))) {
+ return;
+ }
+
+ const normalizedKey = `http.request.header.${lowerCasedKey.replace(/-/g, '_')}`;
+
+ if (Array.isArray(value)) {
+ spanAttributes[normalizedKey] = value.map(v => (v !== null && v !== undefined ? String(v) : v)).join(';');
+ } else if (typeof value === 'string') {
+ spanAttributes[normalizedKey] = value;
+ }
+ }
+ });
+ } catch {
+ // Return empty object if there's an error
+ }
+
+ return spanAttributes;
+}
+
/** Extract the query params from an URL. */
export function extractQueryParamsFromUrl(url: string): string | undefined {
// url is path and query string
diff --git a/packages/core/test/lib/utils/request.test.ts b/packages/core/test/lib/utils/request.test.ts
index fe90578d5392..b37ee860f43f 100644
--- a/packages/core/test/lib/utils/request.test.ts
+++ b/packages/core/test/lib/utils/request.test.ts
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
import {
extractQueryParamsFromUrl,
headersToDict,
+ httpHeadersToSpanAttributes,
httpRequestToRequestData,
winterCGHeadersToDict,
winterCGRequestToRequestData,
@@ -420,4 +421,288 @@ describe('request utils', () => {
expect(extractQueryParamsFromUrl(url)).toEqual(expected);
});
});
+
+ describe('httpHeadersToSpanAttributes', () => {
+ it('works with empty headers object', () => {
+ expect(httpHeadersToSpanAttributes({})).toEqual({});
+ });
+
+ it('converts single string header values to strings', () => {
+ const headers = {
+ 'Content-Type': 'application/json',
+ 'user-agent': 'test-agent',
+ };
+
+ const result = httpHeadersToSpanAttributes(headers);
+
+ expect(result).toEqual({
+ 'http.request.header.content_type': 'application/json',
+ 'http.request.header.user_agent': 'test-agent',
+ });
+ });
+
+ it('handles array header values by joining with semicolons', () => {
+ const headers = {
+ 'custom-header': ['value1', 'value2'],
+ accept: ['application/json', 'text/html'],
+ };
+
+ const result = httpHeadersToSpanAttributes(headers);
+
+ expect(result).toEqual({
+ 'http.request.header.custom_header': 'value1;value2',
+ 'http.request.header.accept': 'application/json;text/html',
+ });
+ });
+
+ it('filters undefined values in arrays when joining', () => {
+ const headers = {
+ 'undefined-values': [undefined, undefined],
+ 'valid-header': 'valid-value',
+ } as any;
+
+ const result = httpHeadersToSpanAttributes(headers);
+
+ expect(result).toEqual({
+ 'http.request.header.valid_header': 'valid-value',
+ 'http.request.header.undefined_values': ';',
+ });
+ });
+
+ it('ignores undefined header values', () => {
+ const headers = {
+ 'valid-header': 'valid-value',
+ 'undefined-header': undefined,
+ };
+
+ const result = httpHeadersToSpanAttributes(headers);
+
+ expect(result).toEqual({
+ 'http.request.header.valid_header': 'valid-value',
+ });
+ });
+
+ it('adds empty array headers as empty string', () => {
+ const headers = {
+ 'empty-header': [],
+ 'valid-header': 'valid-value',
+ } as any;
+
+ const result = httpHeadersToSpanAttributes(headers);
+
+ expect(result).toEqual({
+ 'http.request.header.empty_header': '',
+ 'http.request.header.valid_header': 'valid-value',
+ });
+ });
+
+ it('converts header names to lowercase and replaces dashes with underscores', () => {
+ const headers = {
+ 'Content-Type': 'application/json',
+ 'X-CUSTOM-HEADER': 'custom-value',
+ 'user-Agent': 'test-agent',
+ ACCEPT: 'text/html',
+ };
+
+ const result = httpHeadersToSpanAttributes(headers);
+
+ expect(result).toEqual({
+ 'http.request.header.content_type': 'application/json',
+ 'http.request.header.x_custom_header': 'custom-value',
+ 'http.request.header.user_agent': 'test-agent',
+ 'http.request.header.accept': 'text/html',
+ });
+ });
+
+ it('handles real-world headers', () => {
+ const headers = {
+ Host: 'example.com',
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
+ Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ 'Accept-Language': 'en-US,en;q=0.5',
+ 'Accept-Encoding': 'gzip, deflate',
+ Connection: 'keep-alive',
+ 'Upgrade-Insecure-Requests': '1',
+ 'Cache-Control': 'no-cache',
+ 'X-Forwarded-For': '192.168.1.1',
+ };
+
+ const result = httpHeadersToSpanAttributes(headers);
+
+ expect(result).toEqual({
+ 'http.request.header.host': 'example.com',
+ 'http.request.header.user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
+ 'http.request.header.accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ 'http.request.header.accept_language': 'en-US,en;q=0.5',
+ 'http.request.header.accept_encoding': 'gzip, deflate',
+ 'http.request.header.connection': 'keep-alive',
+ 'http.request.header.upgrade_insecure_requests': '1',
+ 'http.request.header.cache_control': 'no-cache',
+ 'http.request.header.x_forwarded_for': '192.168.1.1',
+ });
+ });
+
+ it('handles multiple values for the same header by joining with semicolons', () => {
+ const headers = {
+ 'x-random-header': ['test=abc123', 'preferences=dark-mode', 'number=three'],
+ Accept: ['application/json', 'text/html'],
+ };
+
+ const result = httpHeadersToSpanAttributes(headers);
+
+ expect(result).toEqual({
+ 'http.request.header.x_random_header': 'test=abc123;preferences=dark-mode;number=three',
+ 'http.request.header.accept': 'application/json;text/html',
+ });
+ });
+
+ it('handles headers with empty string values', () => {
+ const headers = {
+ 'empty-header': '',
+ 'valid-header': 'valid-value',
+ };
+
+ const result = httpHeadersToSpanAttributes(headers);
+
+ expect(result).toEqual({
+ 'http.request.header.empty_header': '',
+ 'http.request.header.valid_header': 'valid-value',
+ });
+ });
+
+ it('returns empty object when processing invalid headers throws error', () => {
+ // Create a headers object that will throw an error when iterated
+ const headers = {};
+ Object.defineProperty(headers, Symbol.iterator, {
+ get() {
+ throw new Error('Test error');
+ },
+ });
+
+ const result = httpHeadersToSpanAttributes(headers);
+
+ expect(result).toEqual({});
+ });
+
+ it('stringifies non-string values (except null) in arrays and joins them', () => {
+ const headers = {
+ 'mixed-types': ['string-value', 123, true, null],
+ } as any;
+
+ const result = httpHeadersToSpanAttributes(headers);
+
+ expect(result).toEqual({
+ 'http.request.header.mixed_types': 'string-value;123;true;',
+ });
+ });
+
+ it('ignores non-string and non-array header values', () => {
+ const headers = {
+ 'string-header': 'valid-value',
+ 'number-header': 123,
+ 'boolean-header': true,
+ 'null-header': null,
+ 'object-header': { key: 'value' },
+ } as any;
+
+ const result = httpHeadersToSpanAttributes(headers);
+
+ expect(result).toEqual({
+ 'http.request.header.string_header': 'valid-value',
+ });
+ });
+
+ describe('PII filtering', () => {
+ it('filters out sensitive headers when sendDefaultPii is false (default)', () => {
+ const headers = {
+ 'Content-Type': 'application/json',
+ 'User-Agent': 'test-agent',
+ Authorization: 'Bearer secret-token',
+ Cookie: 'session=abc123',
+ 'X-API-Key': 'api-key-123',
+ 'X-Auth-Token': 'auth-token-456',
+ };
+
+ const result = httpHeadersToSpanAttributes(headers, false);
+
+ expect(result).toEqual({
+ 'http.request.header.content_type': 'application/json',
+ 'http.request.header.user_agent': 'test-agent',
+ // Sensitive headers should be filtered out
+ });
+ });
+
+ it('includes sensitive headers when sendDefaultPii is true', () => {
+ const headers = {
+ 'Content-Type': 'application/json',
+ 'User-Agent': 'test-agent',
+ Authorization: 'Bearer secret-token',
+ Cookie: 'session=abc123',
+ 'X-API-Key': 'api-key-123',
+ };
+
+ const result = httpHeadersToSpanAttributes(headers, true);
+
+ expect(result).toEqual({
+ 'http.request.header.content_type': 'application/json',
+ 'http.request.header.user_agent': 'test-agent',
+ 'http.request.header.authorization': 'Bearer secret-token',
+ 'http.request.header.cookie': 'session=abc123',
+ 'http.request.header.x_api_key': 'api-key-123',
+ });
+ });
+
+ it('filters sensitive headers case-insensitively', () => {
+ const headers = {
+ AUTHORIZATION: 'Bearer secret-token',
+ Cookie: 'session=abc123',
+ 'x-api-key': 'key-123',
+ 'Content-Type': 'application/json',
+ };
+
+ const result = httpHeadersToSpanAttributes(headers, false);
+
+ expect(result).toEqual({
+ 'http.request.header.content_type': 'application/json',
+ });
+ });
+
+ it('filters comprehensive list of sensitive headers', () => {
+ const headers = {
+ 'Content-Type': 'application/json',
+ 'User-Agent': 'test-agent',
+ Accept: 'application/json',
+ Host: 'example.com',
+
+ // Should be filtered
+ Authorization: 'Bearer token',
+ Cookie: 'session=123',
+ 'Set-Cookie': 'session=456',
+ 'X-API-Key': 'key',
+ 'X-Auth-Token': 'token',
+ 'X-Secret': 'secret',
+ 'x-secret-key': 'another-secret',
+ 'WWW-Authenticate': 'Basic',
+ 'Proxy-Authorization': 'Basic auth',
+ 'X-Access-Token': 'access',
+ 'X-CSRF-Token': 'csrf',
+ 'X-XSRF-Token': 'xsrf',
+ 'X-Session-Token': 'session',
+ 'X-Password': 'password',
+ 'X-Private-Key': 'private',
+ 'X-Forwarded-user': 'user',
+ 'X-Forwarded-authorization': 'auth',
+ };
+
+ const result = httpHeadersToSpanAttributes(headers, false);
+
+ expect(result).toEqual({
+ 'http.request.header.content_type': 'application/json',
+ 'http.request.header.user_agent': 'test-agent',
+ 'http.request.header.accept': 'application/json',
+ 'http.request.header.host': 'example.com',
+ });
+ });
+ });
+ });
});
diff --git a/packages/google-cloud-serverless/src/index.ts b/packages/google-cloud-serverless/src/index.ts
index 0b76f7776772..83292ab11e46 100644
--- a/packages/google-cloud-serverless/src/index.ts
+++ b/packages/google-cloud-serverless/src/index.ts
@@ -42,6 +42,8 @@ export {
close,
getSentryRelease,
createGetModuleFromFilename,
+ httpHeadersToSpanAttributes,
+ winterCGHeadersToDict,
// eslint-disable-next-line deprecation/deprecation
anrIntegration,
// eslint-disable-next-line deprecation/deprecation
diff --git a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts
index ba50778d30ad..a0e4112404cd 100644
--- a/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts
+++ b/packages/nextjs/src/common/pages-router-instrumentation/wrapApiHandlerWithSentry.ts
@@ -15,6 +15,7 @@ import {
} from '@sentry/core';
import type { NextApiRequest } from 'next';
import type { AugmentedNextApiResponse, NextApiHandler } from '../types';
+import { addHeadersAsAttributes } from '../utils/addHeadersAsAttributes';
import { flushSafelyWithTimeout } from '../utils/responseEnd';
import { dropNextjsRootContext, escapeNextjsTracing } from '../utils/tracingUtils';
@@ -87,6 +88,7 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nextjs',
+ ...addHeadersAsAttributes(normalizedRequest.headers || {}),
},
},
async span => {
diff --git a/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts b/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts
new file mode 100644
index 000000000000..4e8cdb3fe7c9
--- /dev/null
+++ b/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts
@@ -0,0 +1,30 @@
+import type { Span, WebFetchHeaders } from '@sentry/core';
+import { getClient, httpHeadersToSpanAttributes, winterCGHeadersToDict } from '@sentry/core';
+
+/**
+ * Extracts HTTP request headers as span attributes and optionally applies them to a span.
+ */
+export function addHeadersAsAttributes(
+ headers: WebFetchHeaders | Headers | Record | undefined,
+ span?: Span,
+): Record {
+ if (!headers) {
+ return {};
+ }
+
+ const client = getClient();
+ const sendDefaultPii = client?.getOptions().sendDefaultPii ?? false;
+
+ const headersDict: Record =
+ headers instanceof Headers || (typeof headers === 'object' && 'get' in headers)
+ ? winterCGHeadersToDict(headers as Headers)
+ : headers;
+
+ const headerAttributes = httpHeadersToSpanAttributes(headersDict, sendDefaultPii);
+
+ if (span) {
+ span.setAttributes(headerAttributes);
+ }
+
+ return headerAttributes;
+}
diff --git a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts
index 2067ebccc245..304066cc2313 100644
--- a/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts
+++ b/packages/nextjs/src/common/wrapGenerationFunctionWithSentry.ts
@@ -22,6 +22,7 @@ import {
import type { GenerationFunctionContext } from '../common/types';
import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils';
import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached';
+import { addHeadersAsAttributes } from './utils/addHeadersAsAttributes';
import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils';
import { getSanitizedRequestUrl } from './utils/urls';
import { maybeExtractSynchronousParamsAndSearchParams } from './utils/wrapperUtils';
@@ -63,6 +64,11 @@ export function wrapGenerationFunctionWithSentry a
const headersDict = headers ? winterCGHeadersToDict(headers) : undefined;
+ if (activeSpan) {
+ const rootSpan = getRootSpan(activeSpan);
+ addHeadersAsAttributes(headers, rootSpan);
+ }
+
let data: Record | undefined = undefined;
if (getClient()?.getOptions().sendDefaultPii) {
const props: unknown = args[0];
diff --git a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts
index 3a9ca786d697..bd84fb4195b7 100644
--- a/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts
+++ b/packages/nextjs/src/common/wrapMiddlewareWithSentry.ts
@@ -13,6 +13,7 @@ import {
winterCGRequestToRequestData,
withIsolationScope,
} from '@sentry/core';
+import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes';
import { flushSafelyWithTimeout } from '../common/utils/responseEnd';
import type { EdgeRouteHandler } from '../edge/types';
@@ -59,6 +60,7 @@ export function wrapMiddlewareWithSentry(
let spanName: string;
let spanSource: TransactionSource;
+ let headerAttributes: Record = {};
if (req instanceof Request) {
isolationScope.setSDKProcessingMetadata({
@@ -66,6 +68,8 @@ export function wrapMiddlewareWithSentry(
});
spanName = `middleware ${req.method} ${new URL(req.url).pathname}`;
spanSource = 'url';
+
+ headerAttributes = addHeadersAsAttributes(req.headers);
} else {
spanName = 'middleware';
spanSource = 'component';
@@ -84,6 +88,7 @@ export function wrapMiddlewareWithSentry(
const rootSpan = getRootSpan(activeSpan);
if (rootSpan) {
setCapturedScopesOnSpan(rootSpan, currentScope, isolationScope);
+ rootSpan.setAttributes(headerAttributes);
}
}
@@ -94,6 +99,7 @@ export function wrapMiddlewareWithSentry(
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: spanSource,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.wrapMiddlewareWithSentry',
+ ...headerAttributes,
},
},
() => {
diff --git a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts
index e1a2238b05a1..e10d51321a0e 100644
--- a/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts
+++ b/packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts
@@ -19,6 +19,7 @@ import {
} from '@sentry/core';
import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils';
import type { RouteHandlerContext } from './types';
+import { addHeadersAsAttributes } from './utils/addHeadersAsAttributes';
import { flushSafelyWithTimeout } from './utils/responseEnd';
import { commonObjectToIsolationScope } from './utils/tracingUtils';
@@ -39,6 +40,10 @@ export function wrapRouteHandlerWithSentry any>(
const activeSpan = getActiveSpan();
const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined;
+ if (rootSpan && process.env.NEXT_RUNTIME !== 'edge') {
+ addHeadersAsAttributes(headers, rootSpan);
+ }
+
let edgeRuntimeIsolationScopeOverride: Scope | undefined;
if (rootSpan && process.env.NEXT_RUNTIME === 'edge') {
const isolationScope = commonObjectToIsolationScope(headers);
@@ -50,6 +55,7 @@ export function wrapRouteHandlerWithSentry any>(
rootSpan.updateName(`${method} ${parameterizedRoute}`);
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server');
+ addHeadersAsAttributes(headers, rootSpan);
}
return withIsolationScope(
diff --git a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts
index 9dd097cb75ae..f0f8e9df8717 100644
--- a/packages/nextjs/src/common/wrapServerComponentWithSentry.ts
+++ b/packages/nextjs/src/common/wrapServerComponentWithSentry.ts
@@ -24,6 +24,7 @@ import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/
import type { ServerComponentContext } from '../common/types';
import { flushSafelyWithTimeout } from '../common/utils/responseEnd';
import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached';
+import { addHeadersAsAttributes } from './utils/addHeadersAsAttributes';
import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils';
import { getSanitizedRequestUrl } from './utils/urls';
import { maybeExtractSynchronousParamsAndSearchParams } from './utils/wrapperUtils';
@@ -61,6 +62,11 @@ export function wrapServerComponentWithSentry any>
const headersDict = context.headers ? winterCGHeadersToDict(context.headers) : undefined;
+ if (activeSpan) {
+ const rootSpan = getRootSpan(activeSpan);
+ addHeadersAsAttributes(context.headers, rootSpan);
+ }
+
let params: Record | undefined = undefined;
if (getClient()?.getOptions().sendDefaultPii) {
diff --git a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts
index 466eb19eb1d1..6002981cfcf4 100644
--- a/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts
+++ b/packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts
@@ -13,6 +13,7 @@ import {
winterCGRequestToRequestData,
withIsolationScope,
} from '@sentry/core';
+import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes';
import { flushSafelyWithTimeout } from '../common/utils/responseEnd';
import type { EdgeRouteHandler } from './types';
@@ -31,11 +32,15 @@ export function wrapApiHandlerWithSentry(
const req: unknown = args[0];
const currentScope = getCurrentScope();
+ let headerAttributes: Record = {};
+
if (req instanceof Request) {
isolationScope.setSDKProcessingMetadata({
normalizedRequest: winterCGRequestToRequestData(req),
});
currentScope.setTransactionName(`${req.method} ${parameterizedRoute}`);
+
+ headerAttributes = addHeadersAsAttributes(req.headers);
} else {
currentScope.setTransactionName(`handler (${parameterizedRoute})`);
}
@@ -58,6 +63,7 @@ export function wrapApiHandlerWithSentry(
rootSpan.setAttributes({
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
+ ...headerAttributes,
});
setCapturedScopesOnSpan(rootSpan, currentScope, isolationScope);
}
@@ -74,6 +80,7 @@ export function wrapApiHandlerWithSentry(
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.wrapApiHandlerWithSentry',
+ ...headerAttributes,
},
},
() => {
diff --git a/packages/nextjs/test/config/wrappers.test.ts b/packages/nextjs/test/config/wrappers.test.ts
index c96184df51cf..c61c92026f60 100644
--- a/packages/nextjs/test/config/wrappers.test.ts
+++ b/packages/nextjs/test/config/wrappers.test.ts
@@ -53,11 +53,14 @@ describe('data-fetching function wrappers should not create manual spans', () =>
test('wrapped function sets route backfill attribute when called within an active span', async () => {
const mockSetAttribute = vi.fn();
+ const mockSetAttributes = vi.fn();
const mockGetActiveSpan = vi.spyOn(SentryCore, 'getActiveSpan').mockReturnValue({
setAttribute: mockSetAttribute,
+ setAttributes: mockSetAttributes,
} as any);
const mockGetRootSpan = vi.spyOn(SentryCore, 'getRootSpan').mockReturnValue({
setAttribute: mockSetAttribute,
+ setAttributes: mockSetAttributes,
} as any);
const origFunction = vi.fn(async () => ({ props: {} }));
@@ -72,11 +75,14 @@ describe('data-fetching function wrappers should not create manual spans', () =>
test('wrapped function does not set route backfill attribute for /_error route', async () => {
const mockSetAttribute = vi.fn();
+ const mockSetAttributes = vi.fn();
const mockGetActiveSpan = vi.spyOn(SentryCore, 'getActiveSpan').mockReturnValue({
setAttribute: mockSetAttribute,
+ setAttributes: mockSetAttributes,
} as any);
const mockGetRootSpan = vi.spyOn(SentryCore, 'getRootSpan').mockReturnValue({
setAttribute: mockSetAttribute,
+ setAttributes: mockSetAttributes,
} as any);
const origFunction = vi.fn(async () => ({ props: {} }));
diff --git a/packages/node-core/src/integrations/http/incoming-requests.ts b/packages/node-core/src/integrations/http/incoming-requests.ts
index 4fddeefef4b5..57588d0ac16e 100644
--- a/packages/node-core/src/integrations/http/incoming-requests.ts
+++ b/packages/node-core/src/integrations/http/incoming-requests.ts
@@ -19,6 +19,7 @@ import {
getCurrentScope,
getIsolationScope,
getSpanStatusFromHttpCode,
+ httpHeadersToSpanAttributes,
httpRequestToRequestData,
parseStringToURLObject,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
@@ -191,6 +192,8 @@ export function instrumentServer(
const tracer = client.tracer;
const scheme = fullUrl.startsWith('https') ? 'https' : 'http';
+ const shouldSendDefaultPii = client?.getOptions().sendDefaultPii ?? false;
+
// We use the plain tracer.startSpan here so we can pass the span kind
const span = tracer.startSpan(bestEffortTransactionName, {
kind: SpanKind.SERVER,
@@ -211,6 +214,7 @@ export function instrumentServer(
'http.flavor': httpVersion,
'net.transport': httpVersion?.toUpperCase() === 'QUIC' ? 'ip_udp' : 'ip_tcp',
...getRequestContentLengthAttribute(request),
+ ...httpHeadersToSpanAttributes(normalizedRequest.headers || {}, shouldSendDefaultPii),
},
});
diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts
index f5f3865feffa..f510ca733d19 100644
--- a/packages/node/src/index.ts
+++ b/packages/node/src/index.ts
@@ -92,6 +92,8 @@ export {
getIsolationScope,
getTraceData,
getTraceMetaTags,
+ httpHeadersToSpanAttributes,
+ winterCGHeadersToDict,
continueTrace,
withScope,
withIsolationScope,
diff --git a/packages/remix/src/server/instrumentServer.ts b/packages/remix/src/server/instrumentServer.ts
index db8322a0c828..109c3e0f3672 100644
--- a/packages/remix/src/server/instrumentServer.ts
+++ b/packages/remix/src/server/instrumentServer.ts
@@ -23,6 +23,7 @@ import {
getRootSpan,
getTraceData,
hasSpansEnabled,
+ httpHeadersToSpanAttributes,
isNodeEnv,
loadModule,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
@@ -31,6 +32,7 @@ import {
setHttpStatus,
spanToJSON,
startSpan,
+ winterCGHeadersToDict,
winterCGRequestToRequestData,
withIsolationScope,
} from '@sentry/core';
@@ -324,6 +326,10 @@ function wrapRequestHandler ServerBuild | Promise
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: source,
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'http.server',
method: request.method,
+ ...httpHeadersToSpanAttributes(
+ winterCGHeadersToDict(request.headers),
+ clientOptions.sendDefaultPii ?? false,
+ ),
},
},
async span => {
diff --git a/packages/sveltekit/src/server-common/handle.ts b/packages/sveltekit/src/server-common/handle.ts
index 3f5797efd211..26872a0f6f24 100644
--- a/packages/sveltekit/src/server-common/handle.ts
+++ b/packages/sveltekit/src/server-common/handle.ts
@@ -3,10 +3,12 @@ import {
continueTrace,
debug,
flushIfServerless,
+ getClient,
getCurrentScope,
getDefaultIsolationScope,
getIsolationScope,
getTraceMetaTags,
+ httpHeadersToSpanAttributes,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
SEMANTIC_ATTRIBUTE_SENTRY_SOURCE,
@@ -14,6 +16,7 @@ import {
spanToJSON,
startSpan,
updateSpanName,
+ winterCGHeadersToDict,
winterCGRequestToRequestData,
withIsolationScope,
} from '@sentry/core';
@@ -176,6 +179,10 @@ async function instrumentHandle(
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeName ? 'route' : 'url',
'sveltekit.tracing.original_name': originalName,
+ ...httpHeadersToSpanAttributes(
+ winterCGHeadersToDict(event.request.headers),
+ getClient()?.getOptions().sendDefaultPii ?? false,
+ ),
});
}
@@ -201,6 +208,10 @@ async function instrumentHandle(
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.sveltekit',
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: routeId ? 'route' : 'url',
'http.method': event.request.method,
+ ...httpHeadersToSpanAttributes(
+ winterCGHeadersToDict(event.request.headers),
+ getClient()?.getOptions().sendDefaultPii ?? false,
+ ),
},
name: routeName,
},