Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import test, { expect } from '@playwright/test';
import { waitForError } from '@sentry-internal/test-utils';
import { isTurbopackDevMode } from './isDevMode';

// Regression test for https://github.com/getsentry/sentry-javascript/issues/23176
//
// Under `next dev` with Turbopack, server stack frames get their `in_app` classification inverted:
// Turbopack names vendor chunks `node_modules_<pkg>_<hash>.js` (underscores, no `node_modules/`
// segment) and prefixes app modules with `[project]/`. `filenameIsInApp` keys off a literal
// `node_modules/` substring and treats bracket-prefixed paths as internal, so vendor code is
// marked `in_app: true` and the real app crash site is marked `in_app: false`.
test('Turbopack dev: server stack frames are classified in_app correctly', async ({ page }) => {
test.skip(
!isTurbopackDevMode,
'Turbopack chunk naming (node_modules_ / [project]) only occurs in Turbopack dev mode',
);

const errorPromise = waitForError('nextjs-16', errorEvent => {
return !!errorEvent?.exception?.values?.some(value => value.value?.includes('Tool call failed'));
});

await page.goto('/ai-error-test');

const errorEvent = await errorPromise;

const frames = errorEvent.exception?.values?.flatMap(value => value.stacktrace?.frames ?? []) ?? [];
expect(frames.length).toBeGreaterThan(0);

// The crash site is app code: the `execute` tool callback defined in `app/ai-error-test/page.tsx`.
// It must be `in_app` so the issue leads with the real crash frame instead of vendor code.
const appFrame = frames.find(frame => frame.filename?.includes('ai-error-test'));
expect(
appFrame,
`expected an app frame for the route file, got: ${JSON.stringify(frames.map(frame => frame.filename))}`,
).toBeDefined();
expect(appFrame?.in_app).toBe(true);

// Vendor frames from the `ai` package (Turbopack: `...node_modules_ai_...`) must not be `in_app`.
const vendorFrames = frames.filter(frame => frame.filename?.includes('node_modules'));
for (const vendorFrame of vendorFrames) {
expect(vendorFrame.in_app, `vendor frame ${vendorFrame.filename} should not be in_app`).toBe(false);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Vacuous vendor in_app assertion

Low Severity

The vendor in_app checks can pass without verifying any frames: vendorFrames is filtered and asserted in a loop, but nothing requires that filter to match. That weakens coverage for the half of this regression where vendor frames were wrongly marked in_app. Sibling tests in this app assert filtered collections are non-empty before checking their contents. Flagged because the PR review rules ask for thorough assertions of newly added behavior in fix tests.

Fix in Cursor Fix in Web

Triggered by project rule: PR Review Guidelines for Cursor Bot

Reviewed by Cursor Bugbot for commit e0d8aed. Configure here.

});
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,20 @@ export async function devErrorSymbolicationEventProcessor(event: Event, hint: Ev
resolvedFrame.originalCodeFrame,
);

const resolvedFilename = resolvedFrame.originalStackFrame.file
? stripWebpackInternalPrefix(resolvedFrame.originalStackFrame.file)
: undefined;

return {
...frame,
pre_context: preContextLines,
context_line: contextLine,
post_context: postContextLines,
function: resolvedFrame.originalStackFrame.methodName,
filename: resolvedFrame.originalStackFrame.file
? stripWebpackInternalPrefix(resolvedFrame.originalStackFrame.file)
: undefined,
filename: resolvedFilename,
// The parse-time `in_app` is derived from Turbopack's dev chunk names (`node_modules_<pkg>_<hash>.js`,
// `[project]/…`), which invert the classification. Re-derive it from the resolved original source path.
in_app: resolvedFilename ? !resolvedFilename.includes('node_modules') : frame.in_app,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the filenames always starting with this string? Then we could tighten this to .startsWith('node_modules_') 🤔

@logaretm logaretm Aug 11, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want to make that assumption since it may rely on the project being in a workspace or something so the starting path wouldn't be node_modules_[...]

Also noticed that npm and yarn would yield absolute paths (/Users/...node_modules) at least from one test I tried. The downside is if someone does have a file called node_modules then it would be incorrectly tagged but I wonder how common is that.

lineno:
resolvedFrame.originalStackFrame.lineNumber || resolvedFrame.originalStackFrame.line1 || undefined,
colno: resolvedFrame.originalStackFrame.column || resolvedFrame.originalStackFrame.column1 || undefined,
Expand Down
Loading