Skip to content

Commit 74b68c7

Browse files
authored
feat(nextjs): Enable nextjs otel spans pages router (#13960)
1 parent a90d899 commit 74b68c7

File tree

16 files changed

+206
-244
lines changed

16 files changed

+206
-244
lines changed

dev-packages/e2e-tests/test-applications/nextjs-13/instrumentation.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export function register() {
1212
// We are doing a lot of events at once in this test app
1313
bufferSize: 1000,
1414
},
15+
debug: false,
1516
});
1617
}
1718
}

dev-packages/e2e-tests/test-applications/nextjs-13/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"@types/node": "18.11.17",
1818
"@types/react": "18.0.26",
1919
"@types/react-dom": "18.0.9",
20-
"next": "13.2.0",
20+
"next": "13.5.7",
2121
"react": "18.2.0",
2222
"react-dom": "18.2.0",
2323
"typescript": "4.9.5"

dev-packages/e2e-tests/test-applications/nextjs-13/tests/client/pages-dir-pageload.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,38 @@ test('should create a pageload transaction when the `pages` directory is used',
4646
type: 'transaction',
4747
});
4848
});
49+
50+
test('should create a pageload transaction with correct name when an error occurs in getServerSideProps', async ({
51+
page,
52+
}) => {
53+
const transactionPromise = waitForTransaction('nextjs-13', async transactionEvent => {
54+
return (
55+
transactionEvent.transaction === '/[param]/error-getServerSideProps' &&
56+
transactionEvent.contexts?.trace?.op === 'pageload'
57+
);
58+
});
59+
60+
await page.goto(`/something/error-getServerSideProps`, { waitUntil: 'networkidle' });
61+
62+
const transaction = await transactionPromise;
63+
64+
expect(transaction).toMatchObject({
65+
contexts: {
66+
trace: {
67+
data: {
68+
'sentry.op': 'pageload',
69+
'sentry.origin': 'auto.pageload.nextjs.pages_router_instrumentation',
70+
'sentry.source': 'route',
71+
},
72+
op: 'pageload',
73+
origin: 'auto.pageload.nextjs.pages_router_instrumentation',
74+
},
75+
},
76+
transaction: '/[param]/error-getServerSideProps',
77+
transaction_info: { source: 'route' },
78+
type: 'transaction',
79+
});
80+
81+
// Ensure the transaction name is not '/_error'
82+
expect(transaction.transaction).not.toBe('/_error');
83+
});

dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getInitialProps.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ test('should propagate serverside `getInitialProps` trace to client', async ({ p
1111

1212
const serverTransactionPromise = waitForTransaction('nextjs-13', async transactionEvent => {
1313
return (
14-
transactionEvent.transaction === '/[param]/withInitialProps' &&
14+
transactionEvent.transaction === 'GET /[param]/withInitialProps' &&
1515
transactionEvent.contexts?.trace?.op === 'http.server'
1616
);
1717
});
@@ -47,7 +47,7 @@ test('should propagate serverside `getInitialProps` trace to client', async ({ p
4747
status: 'ok',
4848
},
4949
},
50-
transaction: '/[param]/withInitialProps',
50+
transaction: 'GET /[param]/withInitialProps',
5151
transaction_info: {
5252
source: 'route',
5353
},

dev-packages/e2e-tests/test-applications/nextjs-13/tests/isomorphic/getServerSideProps.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ test('Should record performance for getServerSideProps', async ({ page }) => {
1111

1212
const serverTransactionPromise = waitForTransaction('nextjs-13', async transactionEvent => {
1313
return (
14-
transactionEvent.transaction === '/[param]/withServerSideProps' &&
14+
transactionEvent.transaction === 'GET /[param]/withServerSideProps' &&
1515
transactionEvent.contexts?.trace?.op === 'http.server'
1616
);
1717
});
@@ -47,7 +47,7 @@ test('Should record performance for getServerSideProps', async ({ page }) => {
4747
status: 'ok',
4848
},
4949
},
50-
transaction: '/[param]/withServerSideProps',
50+
transaction: 'GET /[param]/withServerSideProps',
5151
transaction_info: {
5252
source: 'route',
5353
},

dev-packages/e2e-tests/test-applications/nextjs-13/tests/server/getServerSideProps.test.ts

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ test('Should report an error event for errors thrown in getServerSideProps', asy
88

99
const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => {
1010
return (
11-
transactionEvent.transaction === '/error-getServerSideProps' &&
11+
transactionEvent.transaction === 'GET /[param]/error-getServerSideProps' &&
1212
transactionEvent.contexts?.trace?.op === 'http.server'
1313
);
1414
});
1515

16-
await page.goto('/error-getServerSideProps');
16+
await page.goto('/dogsaregreat/error-getServerSideProps');
1717

1818
expect(await errorEventPromise).toMatchObject({
1919
contexts: {
@@ -40,7 +40,7 @@ test('Should report an error event for errors thrown in getServerSideProps', asy
4040
url: expect.stringMatching(/^http.*\/error-getServerSideProps/),
4141
},
4242
timestamp: expect.any(Number),
43-
transaction: 'getServerSideProps (/error-getServerSideProps)',
43+
transaction: 'getServerSideProps (/[param]/error-getServerSideProps)',
4444
});
4545

4646
expect(await transactionEventPromise).toMatchObject({
@@ -60,11 +60,11 @@ test('Should report an error event for errors thrown in getServerSideProps', asy
6060
data: {
6161
'http.response.status_code': 500,
6262
'sentry.op': 'http.server',
63-
'sentry.origin': 'auto.function.nextjs',
63+
'sentry.origin': expect.stringMatching(/^(auto|auto\.http\.otel\.http)$/),
6464
'sentry.source': 'route',
6565
},
6666
op: 'http.server',
67-
origin: 'auto.function.nextjs',
67+
origin: expect.stringMatching(/^(auto|auto\.http\.otel\.http)$/),
6868
span_id: expect.any(String),
6969
status: 'internal_error',
7070
trace_id: expect.any(String),
@@ -80,8 +80,9 @@ test('Should report an error event for errors thrown in getServerSideProps', asy
8080
},
8181
start_timestamp: expect.any(Number),
8282
timestamp: expect.any(Number),
83-
transaction: '/error-getServerSideProps',
84-
transaction_info: { source: 'route' },
83+
transaction: 'GET /[param]/error-getServerSideProps',
84+
// TODO: This test fails depending on the next version (next 13: 'custom', next >14: 'route')
85+
// transaction_info: { source: 'custom' },
8586
type: 'transaction',
8687
});
8788
});
@@ -95,11 +96,12 @@ test('Should report an error event for errors thrown in getServerSideProps in pa
9596

9697
const transactionEventPromise = waitForTransaction('nextjs-13', transactionEvent => {
9798
return (
98-
transactionEvent.transaction === '/customPageExtension' && transactionEvent.contexts?.trace?.op === 'http.server'
99+
transactionEvent.transaction === 'GET /[param]/customPageExtension' &&
100+
transactionEvent.contexts?.trace?.op === 'http.server'
99101
);
100102
});
101103

102-
await page.goto('/customPageExtension');
104+
await page.goto('/123/customPageExtension');
103105

104106
expect(await errorEventPromise).toMatchObject({
105107
contexts: {
@@ -126,7 +128,7 @@ test('Should report an error event for errors thrown in getServerSideProps in pa
126128
url: expect.stringMatching(/^http.*\/customPageExtension/),
127129
},
128130
timestamp: expect.any(Number),
129-
transaction: 'getServerSideProps (/customPageExtension)',
131+
transaction: 'getServerSideProps (/[param]/customPageExtension)',
130132
});
131133

132134
expect(await transactionEventPromise).toMatchObject({
@@ -146,11 +148,11 @@ test('Should report an error event for errors thrown in getServerSideProps in pa
146148
data: {
147149
'http.response.status_code': 500,
148150
'sentry.op': 'http.server',
149-
'sentry.origin': 'auto.function.nextjs',
151+
'sentry.origin': expect.stringMatching(/^auto(\.http\.otel\.http)?$/),
150152
'sentry.source': 'route',
151153
},
152154
op: 'http.server',
153-
origin: 'auto.function.nextjs',
155+
origin: expect.stringMatching(/^auto(\.http\.otel\.http)?$/),
154156
span_id: expect.any(String),
155157
status: 'internal_error',
156158
trace_id: expect.any(String),
@@ -166,8 +168,9 @@ test('Should report an error event for errors thrown in getServerSideProps in pa
166168
},
167169
start_timestamp: expect.any(Number),
168170
timestamp: expect.any(Number),
169-
transaction: '/customPageExtension',
170-
transaction_info: { source: 'route' },
171+
transaction: 'GET /[param]/customPageExtension',
172+
// TODO: This test fails depending on the next version (next 13: 'custom', next >14: 'route')
173+
// transaction_info: { source: 'custom' },
171174
type: 'transaction',
172175
});
173176
});

dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/pages-ssr-errors.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ test('Will capture error for SSR rendering error with a connected trace (Class C
88

99
const serverComponentTransaction = waitForTransaction('nextjs-app-dir', async transactionEvent => {
1010
return (
11-
transactionEvent?.transaction === '/pages-router/ssr-error-class' &&
11+
transactionEvent?.transaction === 'GET /pages-router/ssr-error-class' &&
1212
(await errorEventPromise).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id
1313
);
1414
});
@@ -26,7 +26,7 @@ test('Will capture error for SSR rendering error with a connected trace (Functio
2626

2727
const ssrTransactionPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
2828
return (
29-
transactionEvent?.transaction === '/pages-router/ssr-error-fc' &&
29+
transactionEvent?.transaction === 'GET /pages-router/ssr-error-fc' &&
3030
(await errorEventPromise).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id
3131
);
3232
});

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
} from '@sentry/core';
77
import { WINDOW, startBrowserTracingNavigationSpan, startBrowserTracingPageLoadSpan } from '@sentry/react';
88
import type { Client, TransactionSource } from '@sentry/types';
9-
import { browserPerformanceTimeOrigin, logger, stripUrlQueryAndFragment } from '@sentry/utils';
9+
import { browserPerformanceTimeOrigin, logger, parseBaggageHeader, stripUrlQueryAndFragment } from '@sentry/utils';
1010

1111
import type { NEXT_DATA } from 'next/dist/shared/lib/utils';
1212
import RouterImport from 'next/router';
@@ -106,7 +106,15 @@ function extractNextDataTagInformation(): NextDataTagInfo {
106106
*/
107107
export function pagesRouterInstrumentPageLoad(client: Client): void {
108108
const { route, params, sentryTrace, baggage } = extractNextDataTagInformation();
109-
const name = route || globalObject.location.pathname;
109+
const parsedBaggage = parseBaggageHeader(baggage);
110+
let name = route || globalObject.location.pathname;
111+
112+
// /_error is the fallback page for all errors. If there is a transaction name for /_error, use that instead
113+
if (parsedBaggage && parsedBaggage['sentry-transaction'] && name === '/_error') {
114+
name = parsedBaggage['sentry-transaction'];
115+
// Strip any HTTP method from the span name
116+
name = name.replace(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS|TRACE|CONNECT)\s+/i, '');
117+
}
110118

111119
startBrowserTracingPageLoadSpan(
112120
client,

packages/nextjs/src/common/pages-router-instrumentation/wrapGetStaticPropsWithSentry.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ type Props = { [key: string]: unknown };
1414
*/
1515
export function wrapGetStaticPropsWithSentry(
1616
origGetStaticPropsa: GetStaticProps<Props>,
17-
parameterizedRoute: string,
17+
_parameterizedRoute: string,
1818
): GetStaticProps<Props> {
1919
return new Proxy(origGetStaticPropsa, {
2020
apply: async (wrappingTarget, thisArg, args: Parameters<GetStaticProps<Props>>) => {
@@ -23,10 +23,7 @@ export function wrapGetStaticPropsWithSentry(
2323
}
2424

2525
const errorWrappedGetStaticProps = withErrorInstrumentation(wrappingTarget);
26-
return callDataFetcherTraced(errorWrappedGetStaticProps, args, {
27-
parameterizedRoute,
28-
dataFetchingMethodName: 'getStaticProps',
29-
});
26+
return callDataFetcherTraced(errorWrappedGetStaticProps, args);
3027
},
3128
});
3229
}

packages/nextjs/src/common/pages-router-instrumentation/wrapPageComponentWithSentry.ts

Lines changed: 48 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { captureException, getCurrentScope, withIsolationScope } from '@sentry/core';
22
import { extractTraceparentData } from '@sentry/utils';
3-
import { dropNextjsRootContext, escapeNextjsTracing } from '../utils/tracingUtils';
43

54
interface FunctionComponent {
65
(...args: unknown[]): unknown;
@@ -25,70 +24,64 @@ export function wrapPageComponentWithSentry(pageComponent: FunctionComponent | C
2524
if (isReactClassComponent(pageComponent)) {
2625
return class SentryWrappedPageComponent extends pageComponent {
2726
public render(...args: unknown[]): unknown {
28-
dropNextjsRootContext();
29-
return escapeNextjsTracing(() => {
30-
return withIsolationScope(() => {
31-
const scope = getCurrentScope();
32-
// We extract the sentry trace data that is put in the component props by datafetcher wrappers
33-
const sentryTraceData =
34-
typeof this.props === 'object' &&
35-
this.props !== null &&
36-
'_sentryTraceData' in this.props &&
37-
typeof this.props._sentryTraceData === 'string'
38-
? this.props._sentryTraceData
39-
: undefined;
27+
return withIsolationScope(() => {
28+
const scope = getCurrentScope();
29+
// We extract the sentry trace data that is put in the component props by datafetcher wrappers
30+
const sentryTraceData =
31+
typeof this.props === 'object' &&
32+
this.props !== null &&
33+
'_sentryTraceData' in this.props &&
34+
typeof this.props._sentryTraceData === 'string'
35+
? this.props._sentryTraceData
36+
: undefined;
4037

41-
if (sentryTraceData) {
42-
const traceparentData = extractTraceparentData(sentryTraceData);
43-
scope.setContext('trace', {
44-
span_id: traceparentData?.parentSpanId,
45-
trace_id: traceparentData?.traceId,
46-
});
47-
}
38+
if (sentryTraceData) {
39+
const traceparentData = extractTraceparentData(sentryTraceData);
40+
scope.setContext('trace', {
41+
span_id: traceparentData?.parentSpanId,
42+
trace_id: traceparentData?.traceId,
43+
});
44+
}
4845

49-
try {
50-
return super.render(...args);
51-
} catch (e) {
52-
captureException(e, {
53-
mechanism: {
54-
handled: false,
55-
},
56-
});
57-
throw e;
58-
}
59-
});
46+
try {
47+
return super.render(...args);
48+
} catch (e) {
49+
captureException(e, {
50+
mechanism: {
51+
handled: false,
52+
},
53+
});
54+
throw e;
55+
}
6056
});
6157
}
6258
};
6359
} else if (typeof pageComponent === 'function') {
6460
return new Proxy(pageComponent, {
6561
apply(target, thisArg, argArray: [{ _sentryTraceData?: string } | undefined]) {
66-
dropNextjsRootContext();
67-
return escapeNextjsTracing(() => {
68-
return withIsolationScope(() => {
69-
const scope = getCurrentScope();
70-
// We extract the sentry trace data that is put in the component props by datafetcher wrappers
71-
const sentryTraceData = argArray?.[0]?._sentryTraceData;
62+
return withIsolationScope(() => {
63+
const scope = getCurrentScope();
64+
// We extract the sentry trace data that is put in the component props by datafetcher wrappers
65+
const sentryTraceData = argArray?.[0]?._sentryTraceData;
7266

73-
if (sentryTraceData) {
74-
const traceparentData = extractTraceparentData(sentryTraceData);
75-
scope.setContext('trace', {
76-
span_id: traceparentData?.parentSpanId,
77-
trace_id: traceparentData?.traceId,
78-
});
79-
}
67+
if (sentryTraceData) {
68+
const traceparentData = extractTraceparentData(sentryTraceData);
69+
scope.setContext('trace', {
70+
span_id: traceparentData?.parentSpanId,
71+
trace_id: traceparentData?.traceId,
72+
});
73+
}
8074

81-
try {
82-
return target.apply(thisArg, argArray);
83-
} catch (e) {
84-
captureException(e, {
85-
mechanism: {
86-
handled: false,
87-
},
88-
});
89-
throw e;
90-
}
91-
});
75+
try {
76+
return target.apply(thisArg, argArray);
77+
} catch (e) {
78+
captureException(e, {
79+
mechanism: {
80+
handled: false,
81+
},
82+
});
83+
throw e;
84+
}
9285
});
9386
},
9487
});

packages/nextjs/src/common/span-attributes-with-logic-attached.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,5 @@
44
export const TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION = 'sentry.drop_transaction';
55

66
export const TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL = 'sentry.sentry_trace_backfill';
7+
8+
export const TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL = 'sentry.route_backfill';

0 commit comments

Comments
 (0)