11import type { Event , EventHint } from '@sentry/core' ;
2+ import { parseSemver } from '@sentry/core' ;
23import { GLOBAL_OBJ , suppressTracing } from '@sentry/core' ;
4+ import { logger } from '@sentry/core' ;
35import type { StackFrame } from 'stacktrace-parser' ;
46import * as stackTraceParser from 'stacktrace-parser' ;
7+ import { DEBUG_BUILD } from './debug-build' ;
58
69type OriginalStackFrameResponse = {
710 originalStackFrame : StackFrame ;
@@ -11,8 +14,92 @@ type OriginalStackFrameResponse = {
1114
1215const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
1316 _sentryBasePath ?: string ;
17+ next ?: {
18+ version ?: string ;
19+ } ;
1420} ;
1521
22+ /**
23+ * Event processor that will symbolicate errors by using the webpack/nextjs dev server that is used to show stack traces
24+ * in the dev overlay.
25+ */
26+ export async function devErrorSymbolicationEventProcessor ( event : Event , hint : EventHint ) : Promise < Event | null > {
27+ // Filter out spans for requests resolving source maps for stack frames in dev mode
28+ if ( event . type === 'transaction' ) {
29+ event . spans = event . spans ?. filter ( span => {
30+ const httpUrlAttribute : unknown = span . data ?. [ 'http.url' ] ;
31+ if ( typeof httpUrlAttribute === 'string' ) {
32+ return ! httpUrlAttribute . includes ( '__nextjs_original-stack-frame' ) ; // could also be __nextjs_original-stack-frames (plural)
33+ }
34+
35+ return true ;
36+ } ) ;
37+ }
38+
39+ // Due to changes across Next.js versions, there are a million things that can go wrong here so we just try-catch the
40+ // entire event processor. Symbolicated stack traces are just a nice to have.
41+ try {
42+ if ( hint . originalException && hint . originalException instanceof Error && hint . originalException . stack ) {
43+ const frames = stackTraceParser . parse ( hint . originalException . stack ) ;
44+
45+ const nextjsVersion = globalWithInjectedValues . next ?. version || '0.0.0' ;
46+ const parsedNextjsVersion = nextjsVersion ? parseSemver ( nextjsVersion ) : { } ;
47+
48+ let resolvedFrames : ( {
49+ originalCodeFrame : string | null ;
50+ originalStackFrame : StackFrame | null ;
51+ } | null ) [ ] ;
52+
53+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
54+ if ( parsedNextjsVersion . major ! > 15 || ( parsedNextjsVersion . major === 15 && parsedNextjsVersion . minor ! >= 2 ) ) {
55+ const r = await resolveStackFrames ( frames ) ;
56+ if ( r === null ) {
57+ return event ;
58+ }
59+ resolvedFrames = r ;
60+ } else {
61+ resolvedFrames = await Promise . all (
62+ frames . map ( frame => resolveStackFrame ( frame , hint . originalException as Error ) ) ,
63+ ) ;
64+ }
65+
66+ if ( event . exception ?. values ?. [ 0 ] ?. stacktrace ?. frames ) {
67+ event . exception . values [ 0 ] . stacktrace . frames = event . exception . values [ 0 ] . stacktrace . frames . map (
68+ ( frame , i , frames ) => {
69+ const resolvedFrame = resolvedFrames [ frames . length - 1 - i ] ;
70+ if ( ! resolvedFrame ?. originalStackFrame || ! resolvedFrame . originalCodeFrame ) {
71+ return {
72+ ...frame ,
73+ platform : frame . filename ?. startsWith ( 'node:internal' ) ? 'nodejs' : undefined , // simple hack that will prevent a source mapping error from showing up
74+ in_app : false ,
75+ } ;
76+ }
77+
78+ const { contextLine, preContextLines, postContextLines } = parseOriginalCodeFrame (
79+ resolvedFrame . originalCodeFrame ,
80+ ) ;
81+
82+ return {
83+ ...frame ,
84+ pre_context : preContextLines ,
85+ context_line : contextLine ,
86+ post_context : postContextLines ,
87+ function : resolvedFrame . originalStackFrame . methodName ,
88+ filename : resolvedFrame . originalStackFrame . file || undefined ,
89+ lineno : resolvedFrame . originalStackFrame . lineNumber || undefined ,
90+ colno : resolvedFrame . originalStackFrame . column || undefined ,
91+ } ;
92+ } ,
93+ ) ;
94+ }
95+ }
96+ } catch ( e ) {
97+ return event ;
98+ }
99+
100+ return event ;
101+ }
102+
16103async function resolveStackFrame (
17104 frame : StackFrame ,
18105 error : Error ,
@@ -65,6 +152,79 @@ async function resolveStackFrame(
65152 originalStackFrame : body . originalStackFrame ,
66153 } ;
67154 } catch ( e ) {
155+ DEBUG_BUILD && logger . error ( 'Failed to symbolicate event with Next.js dev server' , e ) ;
156+ return null ;
157+ }
158+ }
159+
160+ async function resolveStackFrames (
161+ frames : StackFrame [ ] ,
162+ ) : Promise < { originalCodeFrame : string | null ; originalStackFrame : StackFrame | null } [ ] | null > {
163+ try {
164+ const postBody = {
165+ frames : frames
166+ . filter ( frame => {
167+ return ! ! frame . file ;
168+ } )
169+ . map ( frame => {
170+ // https://github.com/vercel/next.js/blob/df0573a478baa8b55478a7963c473dddd59a5e40/packages/next/src/client/components/react-dev-overlay/server/middleware-turbopack.ts#L129
171+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
172+ frame . file = frame . file ! . replace ( / ^ r s c : \/ \/ R e a c t \/ [ ^ / ] + \/ / , '' ) . replace ( / \? \d + $ / , '' ) ;
173+
174+ return {
175+ file : frame . file ,
176+ methodName : frame . methodName ?? '<unknown>' ,
177+ arguments : [ ] ,
178+ lineNumber : frame . lineNumber ?? 0 ,
179+ column : frame . column ?? 0 ,
180+ } ;
181+ } ) ,
182+ isServer : false ,
183+ isEdgeServer : false ,
184+ isAppDirectory : true ,
185+ } ;
186+
187+ let basePath = process . env . _sentryBasePath ?? globalWithInjectedValues . _sentryBasePath ?? '' ;
188+
189+ // Prefix the basepath with a slash if it doesn't have one
190+ if ( basePath !== '' && ! basePath . match ( / ^ \/ / ) ) {
191+ basePath = `/${ basePath } ` ;
192+ }
193+
194+ const controller = new AbortController ( ) ;
195+ const timer = setTimeout ( ( ) => controller . abort ( ) , 3000 ) ;
196+
197+ const res = await fetch (
198+ `${
199+ // eslint-disable-next-line no-restricted-globals
200+ typeof window === 'undefined' ? 'http://localhost:3000' : '' // TODO: handle the case where users define a different port
201+ } ${ basePath } /__nextjs_original-stack-frames`,
202+ {
203+ method : 'POST' ,
204+ headers : {
205+ 'Content-Type' : 'application/json' ,
206+ } ,
207+ signal : controller . signal ,
208+ body : JSON . stringify ( postBody ) ,
209+ } ,
210+ ) . finally ( ( ) => {
211+ clearTimeout ( timer ) ;
212+ } ) ;
213+
214+ if ( ! res . ok || res . status === 204 ) {
215+ return null ;
216+ }
217+
218+ const body : { value : OriginalStackFrameResponse } [ ] = await res . json ( ) ;
219+
220+ return body . map ( frame => {
221+ return {
222+ originalCodeFrame : frame . value . originalCodeFrame ,
223+ originalStackFrame : frame . value . originalStackFrame ,
224+ } ;
225+ } ) ;
226+ } catch ( e ) {
227+ DEBUG_BUILD && logger . error ( 'Failed to symbolicate event with Next.js dev server' , e ) ;
68228 return null ;
69229 }
70230}
@@ -118,66 +278,3 @@ function parseOriginalCodeFrame(codeFrame: string): {
118278 postContextLines,
119279 } ;
120280}
121-
122- /**
123- * Event processor that will symbolicate errors by using the webpack/nextjs dev server that is used to show stack traces
124- * in the dev overlay.
125- */
126- export async function devErrorSymbolicationEventProcessor ( event : Event , hint : EventHint ) : Promise < Event | null > {
127- // Filter out spans for requests resolving source maps for stack frames in dev mode
128- if ( event . type === 'transaction' ) {
129- event . spans = event . spans ?. filter ( span => {
130- const httpUrlAttribute : unknown = span . data ?. [ 'http.url' ] ;
131- if ( typeof httpUrlAttribute === 'string' ) {
132- return ! httpUrlAttribute . includes ( '__nextjs_original-stack-frame' ) ;
133- }
134-
135- return true ;
136- } ) ;
137- }
138-
139- // Due to changes across Next.js versions, there are a million things that can go wrong here so we just try-catch the // entire event processor.Symbolicated stack traces are just a nice to have.
140- try {
141- if ( hint . originalException && hint . originalException instanceof Error && hint . originalException . stack ) {
142- const frames = stackTraceParser . parse ( hint . originalException . stack ) ;
143-
144- const resolvedFrames = await Promise . all (
145- frames . map ( frame => resolveStackFrame ( frame , hint . originalException as Error ) ) ,
146- ) ;
147-
148- if ( event . exception ?. values ?. [ 0 ] ?. stacktrace ?. frames ) {
149- event . exception . values [ 0 ] . stacktrace . frames = event . exception . values [ 0 ] . stacktrace . frames . map (
150- ( frame , i , frames ) => {
151- const resolvedFrame = resolvedFrames [ frames . length - 1 - i ] ;
152- if ( ! resolvedFrame ?. originalStackFrame || ! resolvedFrame . originalCodeFrame ) {
153- return {
154- ...frame ,
155- platform : frame . filename ?. startsWith ( 'node:internal' ) ? 'nodejs' : undefined , // simple hack that will prevent a source mapping error from showing up
156- in_app : false ,
157- } ;
158- }
159-
160- const { contextLine, preContextLines, postContextLines } = parseOriginalCodeFrame (
161- resolvedFrame . originalCodeFrame ,
162- ) ;
163-
164- return {
165- ...frame ,
166- pre_context : preContextLines ,
167- context_line : contextLine ,
168- post_context : postContextLines ,
169- function : resolvedFrame . originalStackFrame . methodName ,
170- filename : resolvedFrame . originalStackFrame . file || undefined ,
171- lineno : resolvedFrame . originalStackFrame . lineNumber || undefined ,
172- colno : resolvedFrame . originalStackFrame . column || undefined ,
173- } ;
174- } ,
175- ) ;
176- }
177- }
178- } catch ( e ) {
179- return event ;
180- }
181-
182- return event ;
183- }
0 commit comments