Skip to content

Commit 4fe3ec8

Browse files
committed
migrate
1 parent fb78a25 commit 4fe3ec8

7 files changed

Lines changed: 199 additions & 6 deletions

packages/nextjs/src/client/routing/pagesRouterRoutingInstrumentation.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Client, TransactionSource } from '@sentry/core';
22
import {
3+
_INTERNAL_filterKeyValueData,
34
browserPerformanceTimeOrigin,
45
debug,
56
parseBaggageHeader,
@@ -129,7 +130,7 @@ export function pagesRouterInstrumentPageLoad(client: Client): void {
129130
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'pageload',
130131
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.pageload.nextjs.pages_router_instrumentation',
131132
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: route ? 'route' : 'url',
132-
...(params && client.getOptions().sendDefaultPii && { ...params }),
133+
...(params && getFilteredRouteParams(params, client)),
133134
},
134135
},
135136
{ sentryTrace, baggage },
@@ -185,6 +186,17 @@ function getNextRouteFromPathname(pathname: string): string | undefined {
185186
});
186187
}
187188

189+
function getFilteredRouteParams(params: ParsedUrlQuery, client: Client): Record<string, string> {
190+
const { queryParams } = client.getDataCollectionOptions();
191+
const stringParams: Record<string, string> = {};
192+
for (const [key, value] of Object.entries(params)) {
193+
if (typeof value === 'string') {
194+
stringParams[key] = value;
195+
}
196+
}
197+
return _INTERNAL_filterKeyValueData(stringParams, queryParams);
198+
}
199+
188200
/**
189201
* Converts a Next.js style route to a regular expression that matches on pathnames (no query params or URL fragments).
190202
*

packages/nextjs/src/common/utils/addHeadersAsAttributes.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,19 @@ export function addHeadersAsAttributes(
1212
return {};
1313
}
1414

15+
const client = getClient();
16+
const { httpHeaders } = client?.getDataCollectionOptions() ?? { httpHeaders: { request: false, response: false } };
17+
18+
if (httpHeaders.request === false) {
19+
return {};
20+
}
21+
1522
const headersDict: Record<string, string | string[] | undefined> =
1623
headers instanceof Headers || (typeof headers === 'object' && 'get' in headers)
1724
? winterCGHeadersToDict(headers as Headers)
1825
: headers;
1926

20-
const headerAttributes = httpHeadersToSpanAttributes(headersDict, getClient()?.getOptions().sendDefaultPii ?? false);
27+
const headerAttributes = httpHeadersToSpanAttributes(headersDict, httpHeaders.request === true);
2128

2229
if (span) {
2330
span.setAttributes(headerAttributes);

packages/nextjs/src/common/utils/setUrlProcessingMetadata.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@ export function setUrlProcessingMetadata(event: Event): void {
1111
return;
1212
}
1313

14-
// Only add URL if sendDefaultPii is enabled, as URLs may contain PII
1514
const client = getClient();
16-
if (!client?.getOptions().sendDefaultPii) {
15+
if (!client) {
16+
return;
17+
}
18+
19+
// todo(v11): Replace with a dataCollection gate once URL collection is covered by the spec.
20+
if (!client.getOptions().sendDefaultPii) {
1721
return;
1822
}
1923

packages/nextjs/src/common/withServerActionInstrumentation.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ async function withServerActionInstrumentationImplementation<A extends (...args:
7070
callback: A,
7171
): Promise<ReturnType<A>> {
7272
return withIsolationScope(async isolationScope => {
73-
const sendDefaultPii = getClient()?.getOptions().sendDefaultPii;
73+
const shouldRecordResponse = getClient()?.getDataCollectionOptions().httpBodies.includes('outgoingResponse');
7474

7575
let sentryTraceHeader;
7676
let baggageHeader;
@@ -138,7 +138,7 @@ async function withServerActionInstrumentationImplementation<A extends (...args:
138138
}
139139
});
140140

141-
if (options.recordResponse !== undefined ? options.recordResponse : sendDefaultPii) {
141+
if (options.recordResponse !== undefined ? options.recordResponse : shouldRecordResponse) {
142142
getIsolationScope().setExtra('server_action_result', result);
143143
}
144144

packages/nextjs/test/performance/pagesRouterInstrumentation.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,9 @@ describe('pagesRouterInstrumentPageLoad', () => {
128128
'sentry.op': 'pageload',
129129
'sentry.origin': 'auto.pageload.nextjs.pages_router_instrumentation',
130130
'sentry.source': 'route',
131+
user: 'lforst',
132+
id: '1337',
133+
q: '42',
131134
},
132135
},
133136
],
@@ -190,6 +193,9 @@ describe('pagesRouterInstrumentPageLoad', () => {
190193
const client = {
191194
emit,
192195
getOptions: () => ({}),
196+
getDataCollectionOptions: () => ({
197+
queryParams: { deny: [] },
198+
}),
193199
} as unknown as Client;
194200

195201
pagesRouterInstrumentPageLoad(client);
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import * as SentryCore from '@sentry/core';
2+
import { describe, expect, it, vi } from 'vitest';
3+
import { addHeadersAsAttributes } from '../../src/common/utils/addHeadersAsAttributes';
4+
5+
describe('addHeadersAsAttributes', () => {
6+
it('returns empty object when headers are undefined', () => {
7+
expect(addHeadersAsAttributes(undefined)).toEqual({});
8+
});
9+
10+
it('returns empty object when httpHeaders.request is false', () => {
11+
vi.spyOn(SentryCore, 'getClient').mockReturnValue({
12+
getDataCollectionOptions: () => ({
13+
httpHeaders: { request: false, response: true },
14+
}),
15+
} as unknown as SentryCore.Client);
16+
17+
const result = addHeadersAsAttributes({ 'content-type': 'application/json' });
18+
expect(result).toEqual({});
19+
});
20+
21+
it('includes all headers with sensitive filtering when httpHeaders.request is true', () => {
22+
vi.spyOn(SentryCore, 'getClient').mockReturnValue({
23+
getDataCollectionOptions: () => ({
24+
httpHeaders: { request: true, response: true },
25+
}),
26+
} as unknown as SentryCore.Client);
27+
28+
const result = addHeadersAsAttributes({
29+
'content-type': 'application/json',
30+
accept: 'text/html',
31+
});
32+
33+
expect(result).toMatchObject({
34+
'http.request.header.content_type': 'application/json',
35+
'http.request.header.accept': 'text/html',
36+
});
37+
});
38+
39+
it('applies stricter PII filtering when httpHeaders.request is a deny list', () => {
40+
vi.spyOn(SentryCore, 'getClient').mockReturnValue({
41+
getDataCollectionOptions: () => ({
42+
httpHeaders: { request: { deny: [] }, response: true },
43+
}),
44+
} as unknown as SentryCore.Client);
45+
46+
const result = addHeadersAsAttributes({
47+
'content-type': 'application/json',
48+
accept: 'text/html',
49+
});
50+
51+
expect(result).toMatchObject({
52+
'http.request.header.content_type': 'application/json',
53+
'http.request.header.accept': 'text/html',
54+
});
55+
});
56+
57+
it('returns empty object when no client is available', () => {
58+
vi.spyOn(SentryCore, 'getClient').mockReturnValue(undefined);
59+
60+
const result = addHeadersAsAttributes({ 'content-type': 'application/json' });
61+
expect(result).toEqual({});
62+
});
63+
});
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import type { Event } from '@sentry/core';
2+
import * as SentryCore from '@sentry/core';
3+
import { describe, expect, it, vi } from 'vitest';
4+
import { setUrlProcessingMetadata } from '../../src/common/utils/setUrlProcessingMetadata';
5+
6+
function makeTransactionEvent(overrides?: Partial<Event>): Event {
7+
return {
8+
type: 'transaction',
9+
contexts: {
10+
trace: {
11+
op: 'http.server',
12+
data: {
13+
'next.route': '/api/users/[id]',
14+
'http.target': '/api/users/123',
15+
},
16+
},
17+
},
18+
sdkProcessingMetadata: {
19+
capturedSpanIsolationScope: {
20+
getScopeData: () => ({
21+
sdkProcessingMetadata: {
22+
normalizedRequest: {
23+
headers: {
24+
'x-forwarded-proto': 'https',
25+
host: 'example.com',
26+
},
27+
},
28+
},
29+
}),
30+
},
31+
},
32+
...overrides,
33+
};
34+
}
35+
36+
describe('setUrlProcessingMetadata', () => {
37+
it('skips non-transaction events', () => {
38+
const event = makeTransactionEvent({ type: undefined });
39+
setUrlProcessingMetadata(event);
40+
// No error thrown, nothing changed
41+
});
42+
43+
it('skips when sendDefaultPii is false', () => {
44+
vi.spyOn(SentryCore, 'getClient').mockReturnValue({
45+
getOptions: () => ({ sendDefaultPii: false }),
46+
} as unknown as SentryCore.Client);
47+
48+
const scopeData = {
49+
sdkProcessingMetadata: {
50+
normalizedRequest: {
51+
headers: { host: 'example.com' },
52+
},
53+
},
54+
};
55+
56+
const event = makeTransactionEvent({
57+
sdkProcessingMetadata: {
58+
capturedSpanIsolationScope: { getScopeData: () => scopeData },
59+
},
60+
});
61+
62+
setUrlProcessingMetadata(event);
63+
expect(scopeData.sdkProcessingMetadata.normalizedRequest).not.toHaveProperty('url');
64+
});
65+
66+
it('adds URL when sendDefaultPii is true', () => {
67+
vi.spyOn(SentryCore, 'getClient').mockReturnValue({
68+
getOptions: () => ({ sendDefaultPii: true }),
69+
} as unknown as SentryCore.Client);
70+
71+
const scopeData = {
72+
sdkProcessingMetadata: {
73+
normalizedRequest: {
74+
headers: {
75+
'x-forwarded-proto': 'https',
76+
host: 'example.com',
77+
},
78+
},
79+
},
80+
};
81+
82+
const event: Event = {
83+
type: 'transaction',
84+
contexts: {
85+
trace: {
86+
op: 'http.server',
87+
data: {
88+
'next.route': '/api/users/[id]',
89+
'http.target': '/api/users/123',
90+
},
91+
},
92+
},
93+
sdkProcessingMetadata: {
94+
capturedSpanIsolationScope: { getScopeData: () => scopeData },
95+
},
96+
};
97+
98+
setUrlProcessingMetadata(event);
99+
expect(scopeData.sdkProcessingMetadata.normalizedRequest.url).toBe('https://example.com/api/users/123');
100+
});
101+
});

0 commit comments

Comments
 (0)