Skip to content

Commit 538c3a6

Browse files
author
Luca Forstner
authored
feat(nextjs): Improve client stack traces (#7097)
1 parent fc7b716 commit 538c3a6

File tree

3 files changed

+70
-1
lines changed

3 files changed

+70
-1
lines changed

packages/nextjs/src/client/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,22 @@ function addClientIntegrations(options: BrowserOptions): void {
6969
// Filename wasn't a properly formed URL, so there's nothing we can do
7070
}
7171

72+
if (frame.filename && frame.filename.startsWith('app:///_next')) {
73+
// We need to URI-decode the filename because Next.js has wildcard routes like "/users/[id].js" which show up as "/users/%5id%5.js" in Error stacktraces.
74+
// The corresponding sources that Next.js generates have proper brackets so we also need proper brackets in the frame so that source map resolving works.
75+
frame.filename = decodeURI(frame.filename);
76+
}
77+
78+
if (
79+
frame.filename &&
80+
frame.filename.match(
81+
/^app:\/\/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/,
82+
)
83+
) {
84+
// We don't care about these frames. It's Next.js internal code.
85+
frame.in_app = false;
86+
}
87+
7288
return frame;
7389
},
7490
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
const ButtonPage = (): JSX.Element => (
2+
<button
3+
onClick={() => {
4+
throw new Error('Sentry Frontend Error');
5+
}}
6+
>
7+
Throw Error
8+
</button>
9+
);
10+
11+
export default ButtonPage;

packages/nextjs/test/integration/test/client/errorClick.test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Event } from '@sentry/types';
55
test('should capture error triggered on click', async ({ page }) => {
66
await page.goto('/errorClick');
77

8-
const [_, events] = await Promise.all([
8+
const [, events] = await Promise.all([
99
page.click('button'),
1010
getMultipleSentryEnvelopeRequests<Event>(page, 1, { envelopeType: 'event' }),
1111
]);
@@ -15,3 +15,45 @@ test('should capture error triggered on click', async ({ page }) => {
1515
value: 'Sentry Frontend Error',
1616
});
1717
});
18+
19+
test('should have a non-url-encoded top frame in route with parameter', async ({ page }) => {
20+
await page.goto('/some-param/errorClick');
21+
22+
const [, events] = await Promise.all([
23+
page.click('button'),
24+
getMultipleSentryEnvelopeRequests<Event>(page, 1, { envelopeType: 'event' }),
25+
]);
26+
27+
const frames = events[0]?.exception?.values?.[0].stacktrace?.frames;
28+
29+
expect(frames?.[frames.length - 1].filename).toMatch(/\/\[id\]\/errorClick-[a-f0-9]+\.js$/);
30+
});
31+
32+
test('should mark nextjs internal frames as `in_app`: false', async ({ page }) => {
33+
await page.goto('/some-param/errorClick');
34+
35+
const [, events] = await Promise.all([
36+
page.click('button'),
37+
getMultipleSentryEnvelopeRequests<Event>(page, 1, { envelopeType: 'event' }),
38+
]);
39+
40+
const frames = events[0]?.exception?.values?.[0].stacktrace?.frames;
41+
42+
expect(frames).toContainEqual(
43+
expect.objectContaining({
44+
filename: expect.stringMatching(
45+
/^app:\/\/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/,
46+
),
47+
in_app: false,
48+
}),
49+
);
50+
51+
expect(frames).not.toContainEqual(
52+
expect.objectContaining({
53+
filename: expect.stringMatching(
54+
/^app:\/\/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/,
55+
),
56+
in_app: true,
57+
}),
58+
);
59+
});

0 commit comments

Comments
 (0)