1
1
import type { Event , EventHint } from '@sentry/core' ;
2
+ import { parseSemver } from '@sentry/core' ;
2
3
import { GLOBAL_OBJ , suppressTracing } from '@sentry/core' ;
4
+ import { logger } from '@sentry/core' ;
3
5
import type { StackFrame } from 'stacktrace-parser' ;
4
6
import * as stackTraceParser from 'stacktrace-parser' ;
7
+ import { DEBUG_BUILD } from './debug-build' ;
5
8
6
9
type OriginalStackFrameResponse = {
7
10
originalStackFrame : StackFrame ;
@@ -11,8 +14,92 @@ type OriginalStackFrameResponse = {
11
14
12
15
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
13
16
_sentryBasePath ?: string ;
17
+ next ?: {
18
+ version ?: string ;
19
+ } ;
14
20
} ;
15
21
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
+
16
103
async function resolveStackFrame (
17
104
frame : StackFrame ,
18
105
error : Error ,
@@ -65,6 +152,79 @@ async function resolveStackFrame(
65
152
originalStackFrame : body . originalStackFrame ,
66
153
} ;
67
154
} 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 ) ;
68
228
return null ;
69
229
}
70
230
}
@@ -118,66 +278,3 @@ function parseOriginalCodeFrame(codeFrame: string): {
118
278
postContextLines,
119
279
} ;
120
280
}
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