diff --git a/dev-packages/e2e-tests/test-applications/nextjs-16/tests/turbopack-dev-in-app.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/turbopack-dev-in-app.test.ts new file mode 100644 index 000000000000..c7967705963f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nextjs-16/tests/turbopack-dev-in-app.test.ts @@ -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__.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); + } +}); diff --git a/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts b/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts index 7e7d221b9bba..f70990a173e8 100644 --- a/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts +++ b/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts @@ -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__.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, lineno: resolvedFrame.originalStackFrame.lineNumber || resolvedFrame.originalStackFrame.line1 || undefined, colno: resolvedFrame.originalStackFrame.column || resolvedFrame.originalStackFrame.column1 || undefined,