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,

0 commit comments

Comments
 (0)