diff --git a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts index a1bec4eac04a..0b9146b26f5b 100644 --- a/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts +++ b/packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts @@ -129,7 +129,7 @@ export function pagesRouterInstrumentPageLoad(client: Client): void { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.nextjs.pages_router_instrumentation', [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: route ? 'route' : 'url', - ...(params && client.getOptions().sendDefaultPii && { ...params }), + ...(params && { ...params }), }, }, { sentryTrace, baggage }, diff --git a/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts b/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts index 8d4b0eca3724..162828c0ac2a 100644 --- a/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts +++ b/packages/nextjs/src/common/utils/addHeadersAsAttributes.ts @@ -12,12 +12,19 @@ export function addHeadersAsAttributes( return {}; } + const client = getClient(); + const { httpHeaders } = client?.getDataCollectionOptions() ?? { httpHeaders: { request: false, response: false } }; + + if (httpHeaders.request === false) { + return {}; + } + const headersDict: Record = headers instanceof Headers || (typeof headers === 'object' && 'get' in headers) ? winterCGHeadersToDict(headers as Headers) : headers; - const headerAttributes = httpHeadersToSpanAttributes(headersDict, getClient()?.getOptions().sendDefaultPii ?? false); + const headerAttributes = httpHeadersToSpanAttributes(headersDict, httpHeaders.request === true); if (span) { span.setAttributes(headerAttributes); diff --git a/packages/nextjs/src/common/utils/setUrlProcessingMetadata.ts b/packages/nextjs/src/common/utils/setUrlProcessingMetadata.ts index 0c7e0c3b33f2..61add752008a 100644 --- a/packages/nextjs/src/common/utils/setUrlProcessingMetadata.ts +++ b/packages/nextjs/src/common/utils/setUrlProcessingMetadata.ts @@ -11,9 +11,8 @@ export function setUrlProcessingMetadata(event: Event): void { return; } - // Only add URL if sendDefaultPii is enabled, as URLs may contain PII const client = getClient(); - if (!client?.getOptions().sendDefaultPii) { + if (!client) { return; } diff --git a/packages/nextjs/src/common/withServerActionInstrumentation.ts b/packages/nextjs/src/common/withServerActionInstrumentation.ts index 5a2c884a8f85..a1ff9ac12ffe 100644 --- a/packages/nextjs/src/common/withServerActionInstrumentation.ts +++ b/packages/nextjs/src/common/withServerActionInstrumentation.ts @@ -70,7 +70,7 @@ async function withServerActionInstrumentationImplementation> { return withIsolationScope(async isolationScope => { - const sendDefaultPii = getClient()?.getOptions().sendDefaultPii; + const shouldRecordResponse = getClient()?.getDataCollectionOptions().httpBodies.includes('outgoingResponse'); let sentryTraceHeader; let baggageHeader; @@ -138,7 +138,7 @@ async function withServerActionInstrumentationImplementation { it.each([ [ - 'https://example.com/lforst/posts/1337?q=42', + 'https://example.com/chargome/posts/1337?q=42', '/[user]/posts/[id]', - { user: 'lforst', id: '1337', q: '42' }, + { user: 'chargome', id: '1337', q: '42' }, { pageProps: { _sentryTraceData: 'c82b8554881b4d28ad977de04a4fb40a-a755953cd3394d5f-1', @@ -128,6 +128,9 @@ describe('pagesRouterInstrumentPageLoad', () => { 'sentry.op': 'pageload', 'sentry.origin': 'auto.pageload.nextjs.pages_router_instrumentation', 'sentry.source': 'route', + user: 'chargome', + id: '1337', + q: '42', }, }, ], diff --git a/packages/nextjs/test/utils/addHeadersAsAttributes.test.ts b/packages/nextjs/test/utils/addHeadersAsAttributes.test.ts new file mode 100644 index 000000000000..639a08a23b52 --- /dev/null +++ b/packages/nextjs/test/utils/addHeadersAsAttributes.test.ts @@ -0,0 +1,63 @@ +import * as SentryCore from '@sentry/core'; +import { describe, expect, it, vi } from 'vitest'; +import { addHeadersAsAttributes } from '../../src/common/utils/addHeadersAsAttributes'; + +describe('addHeadersAsAttributes', () => { + it('returns empty object when headers are undefined', () => { + expect(addHeadersAsAttributes(undefined)).toEqual({}); + }); + + it('returns empty object when httpHeaders.request is false', () => { + vi.spyOn(SentryCore, 'getClient').mockReturnValue({ + getDataCollectionOptions: () => ({ + httpHeaders: { request: false, response: true }, + }), + } as unknown as SentryCore.Client); + + const result = addHeadersAsAttributes({ 'content-type': 'application/json' }); + expect(result).toEqual({}); + }); + + it('includes all headers with sensitive filtering when httpHeaders.request is true', () => { + vi.spyOn(SentryCore, 'getClient').mockReturnValue({ + getDataCollectionOptions: () => ({ + httpHeaders: { request: true, response: true }, + }), + } as unknown as SentryCore.Client); + + const result = addHeadersAsAttributes({ + 'content-type': 'application/json', + accept: 'text/html', + }); + + expect(result).toMatchObject({ + 'http.request.header.content_type': 'application/json', + 'http.request.header.accept': 'text/html', + }); + }); + + it('applies stricter PII filtering when httpHeaders.request is a deny list', () => { + vi.spyOn(SentryCore, 'getClient').mockReturnValue({ + getDataCollectionOptions: () => ({ + httpHeaders: { request: { deny: [] }, response: true }, + }), + } as unknown as SentryCore.Client); + + const result = addHeadersAsAttributes({ + 'content-type': 'application/json', + accept: 'text/html', + }); + + expect(result).toMatchObject({ + 'http.request.header.content_type': 'application/json', + 'http.request.header.accept': 'text/html', + }); + }); + + it('returns empty object when no client is available', () => { + vi.spyOn(SentryCore, 'getClient').mockReturnValue(undefined); + + const result = addHeadersAsAttributes({ 'content-type': 'application/json' }); + expect(result).toEqual({}); + }); +}); diff --git a/packages/nextjs/test/utils/setUrlProcessingMetadata.test.ts b/packages/nextjs/test/utils/setUrlProcessingMetadata.test.ts new file mode 100644 index 000000000000..a170fbaa8a71 --- /dev/null +++ b/packages/nextjs/test/utils/setUrlProcessingMetadata.test.ts @@ -0,0 +1,58 @@ +import type { Event } from '@sentry/core'; +import * as SentryCore from '@sentry/core'; +import { describe, expect, it, vi } from 'vitest'; +import { setUrlProcessingMetadata } from '../../src/common/utils/setUrlProcessingMetadata'; + +describe('setUrlProcessingMetadata', () => { + it('skips non-transaction events', () => { + const event: Event = { type: undefined }; + setUrlProcessingMetadata(event); + }); + + it('adds URL without sendDefaultPii', () => { + vi.spyOn(SentryCore, 'getClient').mockReturnValue({ + getOptions: () => ({ sendDefaultPii: false }), + } as unknown as SentryCore.Client); + + const scopeData = { + sdkProcessingMetadata: { + normalizedRequest: { + headers: { + 'x-forwarded-proto': 'https', + host: 'example.com', + }, + }, + }, + }; + + const event: Event = { + type: 'transaction', + contexts: { + trace: { + op: 'http.server', + data: { + 'next.route': '/api/users/[id]', + 'http.target': '/api/users/123', + }, + }, + }, + sdkProcessingMetadata: { + capturedSpanIsolationScope: { getScopeData: () => scopeData }, + }, + }; + + setUrlProcessingMetadata(event); + expect(scopeData.sdkProcessingMetadata.normalizedRequest.url).toBe('https://example.com/api/users/123'); + }); + + it('skips when no client is available', () => { + vi.spyOn(SentryCore, 'getClient').mockReturnValue(undefined); + + const event: Event = { + type: 'transaction', + contexts: { trace: { op: 'http.server', data: { 'next.route': '/test' } } }, + }; + + setUrlProcessingMetadata(event); + }); +});