Skip to content

Commit 25542d3

Browse files
author
Luca Forstner
authored
feat(nextjs): Instrument SSR page components (#9346)
1 parent 39ba7d4 commit 25542d3

File tree

7 files changed

+126
-1
lines changed

7 files changed

+126
-1
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react';
2+
3+
export default class Page extends React.Component {
4+
render() {
5+
throw new Error('Pages SSR Error Class');
6+
return <div>Hello world!</div>;
7+
}
8+
}
9+
10+
export function getServerSideProps() {
11+
return {
12+
props: {
13+
foo: 'bar',
14+
},
15+
};
16+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export default function Page() {
2+
throw new Error('Pages SSR Error FC');
3+
return <div>Hello world!</div>;
4+
}
5+
6+
export function getServerSideProps() {
7+
return {
8+
props: {
9+
foo: 'bar',
10+
},
11+
};
12+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { test, expect } from '@playwright/test';
2+
import { waitForError } from '../event-proxy-server';
3+
4+
test('Will capture error for SSR rendering error (Class Component)', async ({ page }) => {
5+
const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => {
6+
return errorEvent?.exception?.values?.[0]?.value === 'Pages SSR Error Class';
7+
});
8+
9+
await page.goto('/pages-router/ssr-error-class');
10+
11+
const errorEvent = await errorEventPromise;
12+
expect(errorEvent).toBeDefined();
13+
});
14+
15+
test('Will capture error for SSR rendering error (Functional Component)', async ({ page }) => {
16+
const errorEventPromise = waitForError('nextjs-13-app-dir', errorEvent => {
17+
return errorEvent?.exception?.values?.[0]?.value === 'Pages SSR Error FC';
18+
});
19+
20+
await page.goto('/pages-router/ssr-error-fc');
21+
22+
const errorEvent = await errorEventPromise;
23+
expect(errorEvent).toBeDefined();
24+
});

packages/nextjs/src/common/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,5 @@ export { wrapRouteHandlerWithSentry } from './wrapRouteHandlerWithSentry';
4141
export { wrapApiHandlerWithSentryVercelCrons } from './wrapApiHandlerWithSentryVercelCrons';
4242

4343
export { wrapMiddlewareWithSentry } from './wrapMiddlewareWithSentry';
44+
45+
export { wrapPageComponentWithSentry } from './wrapPageComponentWithSentry';
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { captureException } from '@sentry/core';
2+
import { addExceptionMechanism } from '@sentry/utils';
3+
4+
interface FunctionComponent {
5+
(...args: unknown[]): unknown;
6+
}
7+
8+
interface ClassComponent {
9+
new (...args: unknown[]): {
10+
render(...args: unknown[]): unknown;
11+
};
12+
}
13+
14+
function isReactClassComponent(target: unknown): target is ClassComponent {
15+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
16+
return typeof target === 'function' && target?.prototype?.isReactComponent;
17+
}
18+
19+
/**
20+
* Wraps a page component with Sentry error instrumentation.
21+
*/
22+
export function wrapPageComponentWithSentry(pageComponent: FunctionComponent | ClassComponent): unknown {
23+
if (isReactClassComponent(pageComponent)) {
24+
return class SentryWrappedPageComponent extends pageComponent {
25+
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+
});
36+
37+
return scope;
38+
});
39+
throw e;
40+
}
41+
}
42+
};
43+
} else if (typeof pageComponent === 'function') {
44+
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+
});
56+
57+
return scope;
58+
});
59+
throw e;
60+
}
61+
},
62+
});
63+
} else {
64+
return pageComponent;
65+
}
66+
}

packages/nextjs/src/config/templates/pageWrapperTemplate.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const getServerSideProps =
4949
? Sentry.wrapGetServerSidePropsWithSentry(origGetServerSideProps, '__ROUTE__')
5050
: undefined;
5151

52-
export default pageComponent;
52+
export default pageComponent ? Sentry.wrapPageComponentWithSentry(pageComponent as unknown) : pageComponent;
5353

5454
// Re-export anything exported by the page module we're wrapping. When processing this code, Rollup is smart enough to
5555
// not include anything whose name matchs something we've explicitly exported above.

packages/nextjs/src/index.types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,8 @@ export declare function wrapApiHandlerWithSentryVercelCrons<F extends (...args:
186186
WrappingTarget: F,
187187
vercelCronsConfig: VercelCronsConfig,
188188
): F;
189+
190+
/**
191+
* Wraps a page component with Sentry error instrumentation.
192+
*/
193+
export declare function wrapPageComponentWithSentry<C>(WrappingTarget: C): C;

0 commit comments

Comments
 (0)