Skip to content

Commit 78e61ff

Browse files
author
Luca Forstner
authored
feat(nextjs): Trace errors in page component SSR (#9388)
1 parent b72b5f6 commit 78e61ff

File tree

2 files changed

+86
-36
lines changed

2 files changed

+86
-36
lines changed
Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,38 @@
11
import { test, expect } from '@playwright/test';
2-
import { waitForError } from '../event-proxy-server';
2+
import { waitForError, waitForTransaction } from '../event-proxy-server';
33

4-
test('Will capture error for SSR rendering error (Class Component)', async ({ page }) => {
4+
test('Will capture error for SSR rendering error with a connected trace (Class Component)', async ({ page }) => {
55
const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => {
66
return errorEvent?.exception?.values?.[0]?.value === 'Pages SSR Error Class';
77
});
88

9+
const serverComponentTransaction = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
10+
return (
11+
transactionEvent?.transaction === '/pages-router/ssr-error-class' &&
12+
(await errorEventPromise).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id
13+
);
14+
});
15+
916
await page.goto('/pages-router/ssr-error-class');
1017

11-
const errorEvent = await errorEventPromise;
12-
expect(errorEvent).toBeDefined();
18+
expect(await errorEventPromise).toBeDefined();
19+
expect(await serverComponentTransaction).toBeDefined();
1320
});
1421

15-
test('Will capture error for SSR rendering error (Functional Component)', async ({ page }) => {
22+
test('Will capture error for SSR rendering error with a connected trace (Functional Component)', async ({ page }) => {
1623
const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => {
1724
return errorEvent?.exception?.values?.[0]?.value === 'Pages SSR Error FC';
1825
});
1926

27+
const serverComponentTransaction = waitForTransaction('nextjs-13-app-dir', async transactionEvent => {
28+
return (
29+
transactionEvent?.transaction === '/pages-router/ssr-error-fc' &&
30+
(await errorEventPromise).contexts?.trace?.trace_id === transactionEvent.contexts?.trace?.trace_id
31+
);
32+
});
33+
2034
await page.goto('/pages-router/ssr-error-fc');
2135

22-
const errorEvent = await errorEventPromise;
23-
expect(errorEvent).toBeDefined();
36+
expect(await errorEventPromise).toBeDefined();
37+
expect(await serverComponentTransaction).toBeDefined();
2438
});

packages/nextjs/src/common/wrapPageComponentWithSentry.ts

Lines changed: 65 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { captureException } from '@sentry/core';
2-
import { addExceptionMechanism } from '@sentry/utils';
1+
import { captureException, configureScope, runWithAsyncContext } from '@sentry/core';
2+
import { addExceptionMechanism, extractTraceparentData } from '@sentry/utils';
33

44
interface FunctionComponent {
55
(...args: unknown[]): unknown;
66
}
77

88
interface ClassComponent {
99
new (...args: unknown[]): {
10+
props?: unknown;
1011
render(...args: unknown[]): unknown;
1112
};
1213
}
@@ -23,41 +24,76 @@ export function wrapPageComponentWithSentry(pageComponent: FunctionComponent | C
2324
if (isReactClassComponent(pageComponent)) {
2425
return class SentryWrappedPageComponent extends pageComponent {
2526
public render(...args: unknown[]): unknown {
26-
try {
27-
return super.render(...args);
28-
} catch (e) {
29-
captureException(e, scope => {
30-
scope.addEventProcessor(event => {
31-
addExceptionMechanism(event, {
32-
handled: false,
33-
});
34-
return event;
35-
});
27+
return runWithAsyncContext(() => {
28+
configureScope(scope => {
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;
3637

37-
return scope;
38+
if (sentryTraceData) {
39+
const traceparentData = extractTraceparentData(sentryTraceData);
40+
scope.setContext('trace', {
41+
span_id: traceparentData?.parentSpanId,
42+
trace_id: traceparentData?.traceId,
43+
});
44+
}
3845
});
39-
throw e;
40-
}
46+
47+
try {
48+
return super.render(...args);
49+
} catch (e) {
50+
captureException(e, scope => {
51+
scope.addEventProcessor(event => {
52+
addExceptionMechanism(event, {
53+
handled: false,
54+
});
55+
return event;
56+
});
57+
58+
return scope;
59+
});
60+
throw e;
61+
}
62+
});
4163
}
4264
};
4365
} else if (typeof pageComponent === 'function') {
4466
return new Proxy(pageComponent, {
45-
apply(target, thisArg, argArray) {
46-
try {
47-
return target.apply(thisArg, argArray);
48-
} catch (e) {
49-
captureException(e, scope => {
50-
scope.addEventProcessor(event => {
51-
addExceptionMechanism(event, {
52-
handled: false,
53-
});
54-
return event;
55-
});
67+
apply(target, thisArg, argArray: [{ _sentryTraceData?: string } | undefined]) {
68+
return runWithAsyncContext(() => {
69+
configureScope(scope => {
70+
// We extract the sentry trace data that is put in the component props by datafetcher wrappers
71+
const sentryTraceData = argArray?.[0]?._sentryTraceData;
5672

57-
return scope;
73+
if (sentryTraceData) {
74+
const traceparentData = extractTraceparentData(sentryTraceData);
75+
scope.setContext('trace', {
76+
span_id: traceparentData?.parentSpanId,
77+
trace_id: traceparentData?.traceId,
78+
});
79+
}
5880
});
59-
throw e;
60-
}
81+
try {
82+
return target.apply(thisArg, argArray);
83+
} catch (e) {
84+
captureException(e, scope => {
85+
scope.addEventProcessor(event => {
86+
addExceptionMechanism(event, {
87+
handled: false,
88+
});
89+
return event;
90+
});
91+
92+
return scope;
93+
});
94+
throw e;
95+
}
96+
});
6197
},
6298
});
6399
} else {

0 commit comments

Comments
 (0)