11import { clocksNow } from '@datadog/js-core/time'
22import type { StackTrace } from '../../tools/stackTrace/computeStackTrace'
3- import { registerCleanupTask } from '../../../test'
4- import {
5- computeRawError ,
6- getFileFromStackTraceString ,
7- flattenErrorCauses ,
8- isError ,
9- NO_ERROR_STACK_PRESENT_MESSAGE ,
10- } from './error'
3+ import { mockSourceCodeContext , registerCleanupTask } from '../../../test'
4+ import { computeRawError , getFileFromStackTraceString , isError , NO_ERROR_STACK_PRESENT_MESSAGE } from './error'
115import type { RawErrorCause , ErrorWithCause } from './error.types'
126import { ErrorHandling , ErrorSource , NonErrorPrefix } from './error.types'
137
@@ -77,7 +71,7 @@ describe('computeRawError', () => {
7771 expect ( formatted . stack ) . toEqual ( NO_ERROR_STACK_PRESENT_MESSAGE )
7872 } )
7973
80- it ( 'does not define the stack if useFallbackStack is false' , ( ) => {
74+ it ( 'should not define the stack if useFallbackStack is false' , ( ) => {
8175 const error = 'foo is undefined'
8276
8377 const formatted = computeRawError ( {
@@ -90,7 +84,7 @@ describe('computeRawError', () => {
9084 expect ( formatted . stack ) . toEqual ( undefined )
9185 } )
9286
93- it ( 'uses the provided stack trace object' , ( ) => {
87+ it ( 'should use the provided stack trace object' , ( ) => {
9488 const stackTrace : StackTrace = {
9589 message : 'oh snap!' ,
9690 name : 'TypeError' ,
@@ -154,42 +148,30 @@ describe('computeRawError', () => {
154148 ) . toEqual ( ErrorHandling . UNHANDLED )
155149 } )
156150
157- it ( 'should compute an object error with causes ' , ( ) => {
158- const error = new Error ( 'foo: bar' ) as ErrorWithCause
159- error . stack = ' Error: foo: bar \n at <anonymous> :1:15'
151+ it ( 'should attach debug IDs for stack frame URLs present in the source code context ' , ( ) => {
152+ const url = 'http://path/to/debug-id.js'
153+ mockSourceCodeContext ( { [ ` Error: ctx \n at fn ( ${ url } :1:1)` ] : { service : 'svc' , ddDebugId : 'debug-id-1' } } )
160154
161- const nestedError = new Error ( 'biz: buz' ) as ErrorWithCause
162- nestedError . stack = 'NestedError: biz: buz\n at <anonymous>:2:15'
163-
164- const deepNestedError = new TypeError ( 'fiz: buz' ) as ErrorWithCause
165- deepNestedError . stack = 'NestedError: fiz: buz\n at <anonymous>:3:15'
166-
167- error . cause = nestedError
168- nestedError . cause = deepNestedError
155+ const error = new Error ( 'oh snap!' )
156+ error . stack = `Error: oh snap!\n at foo (${ url } :52:15)`
169157
170158 const formatted = computeRawError ( {
171159 ...DEFAULT_RAW_ERROR_PARAMS ,
172160 originalError : error ,
173161 handling : ErrorHandling . HANDLED ,
174- source : ErrorSource . SOURCE ,
175162 } )
176163
177- expect ( formatted . type ) . toEqual ( 'Error' )
178- expect ( formatted . message ) . toEqual ( 'foo: bar' )
179- expect ( formatted . causes ! . length ) . toBe ( 2 )
180- expect ( formatted . stack ) . toContain ( 'Error: foo: bar' )
181-
182- const causes = formatted . causes as RawErrorCause [ ]
164+ expect ( formatted . debugIds ) . toEqual ( { [ url ] : 'debug-id-1' } )
165+ } )
183166
184- expect ( causes [ 0 ] . message ) . toContain ( nestedError . message )
185- expect ( causes [ 0 ] . source ) . toContain ( ErrorSource . SOURCE )
186- expect ( causes [ 0 ] . type ) . toEqual ( nestedError . name )
187- expect ( causes [ 0 ] . stack ) . toContain ( 'Error: biz: buz' )
167+ it ( 'should not attach debug IDs when the error has no stack' , ( ) => {
168+ const formatted = computeRawError ( {
169+ ...DEFAULT_RAW_ERROR_PARAMS ,
170+ originalError : 'oh snap!' ,
171+ handling : ErrorHandling . HANDLED ,
172+ } )
188173
189- expect ( causes [ 1 ] . message ) . toContain ( deepNestedError . message )
190- expect ( causes [ 1 ] . source ) . toContain ( ErrorSource . SOURCE )
191- expect ( causes [ 1 ] . type ) . toEqual ( deepNestedError . name )
192- expect ( causes [ 1 ] . stack ) . toContain ( 'Error: fiz: buz' )
174+ expect ( formatted . debugIds ) . toBeUndefined ( )
193175 } )
194176
195177 it ( 'should propagate the original error without modifications' , ( ) => {
@@ -210,105 +192,156 @@ describe('computeRawError', () => {
210192 expect ( formattedWithStackTrace . originalError ) . toBe ( error )
211193 expect ( formattedWithoutStackTrace . originalError ) . toBe ( error )
212194 } )
213- } )
214195
215- describe ( 'getFileFromStackTraceString' , ( ) => {
216- it ( 'should get the first source file of the stack' , ( ) => {
217- expect (
218- getFileFromStackTraceString ( `TypeError: oh snap!
219- at foo(1, bar) @ http://path/to/file.js:52:15
220- at <anonymous> @ http://path/to/file.js:12
221- at <anonymous>(baz) @ http://path/to/file.js` )
222- ) . toEqual ( 'http://path/to/file.js:52:15' )
223- } )
196+ describe ( 'causes' , ( ) => {
197+ it ( 'should compute an object error with causes' , ( ) => {
198+ const error = new Error ( 'foo: bar' ) as ErrorWithCause
199+ error . stack = 'Error: foo: bar\n at <anonymous>:1:15'
224200
225- it ( 'should get undefined if no source file is in the stack' , ( ) => {
226- expect ( getFileFromStackTraceString ( 'TypeError: oh snap!' ) ) . not . toBeDefined ( )
227- } )
228- } )
201+ const nestedError = new Error ( 'biz: buz' ) as ErrorWithCause
202+ nestedError . stack = 'NestedError: biz: buz\n at <anonymous>:2:15'
229203
230- describe ( 'flattenErrorCauses' , ( ) => {
231- it ( 'should return undefined if no cause found' , ( ) => {
232- const error = new Error ( 'foo' ) as ErrorWithCause
233- const errorCauses = flattenErrorCauses ( error , ErrorSource . LOGGER )
234- expect ( errorCauses ) . toEqual ( undefined )
235- } )
204+ const deepNestedError = new TypeError ( 'fiz: buz' ) as ErrorWithCause
205+ deepNestedError . stack = 'NestedError: fiz: buz\n at <anonymous>:3:15'
206+
207+ error . cause = nestedError
208+ nestedError . cause = deepNestedError
209+
210+ const formatted = computeRawError ( {
211+ ...DEFAULT_RAW_ERROR_PARAMS ,
212+ originalError : error ,
213+ handling : ErrorHandling . HANDLED ,
214+ source : ErrorSource . SOURCE ,
215+ } )
236216
237- it ( 'should use error to extract stack trace' , ( ) => {
238- const error = new Error ( 'foo' ) as ErrorWithCause
217+ expect ( formatted . type ) . toEqual ( 'Error' )
218+ expect ( formatted . message ) . toEqual ( 'foo: bar' )
219+ expect ( formatted . causes ! . length ) . toBe ( 2 )
220+ expect ( formatted . stack ) . toContain ( 'Error: foo: bar' )
239221
240- error . cause = new Error ( 'bar' )
222+ const causes = formatted . causes as RawErrorCause [ ]
241223
242- const errorCauses = flattenErrorCauses ( error , ErrorSource . LOGGER )
243- expect ( errorCauses ?. [ 0 ] . type ) . toEqual ( 'Error' )
244- } )
224+ expect ( causes [ 0 ] . message ) . toContain ( nestedError . message )
225+ expect ( causes [ 0 ] . source ) . toContain ( ErrorSource . SOURCE )
226+ expect ( causes [ 0 ] . type ) . toEqual ( nestedError . name )
227+ expect ( causes [ 0 ] . stack ) . toContain ( 'Error: biz: buz' )
245228
246- it ( 'should only return the first 10 errors if nested chain is longer' , ( ) => {
247- const error = new Error ( 'foo' ) as ErrorWithCause
248- error . cause = error
249- const errorCauses = flattenErrorCauses ( error , ErrorSource . LOGGER )
250- expect ( errorCauses ?. length ) . toEqual ( 10 )
251- } )
229+ expect ( causes [ 1 ] . message ) . toContain ( deepNestedError . message )
230+ expect ( causes [ 1 ] . source ) . toContain ( ErrorSource . SOURCE )
231+ expect ( causes [ 1 ] . type ) . toEqual ( deepNestedError . name )
232+ expect ( causes [ 1 ] . stack ) . toContain ( 'Error: fiz: buz' )
233+ } )
234+
235+ it ( 'should attach debug IDs for stack frame URLs coming from a cause in another bundle' , ( ) => {
236+ const url = 'http://path/to/debug-id.js'
237+ const causeUrl = 'http://path/to/cause-bundle.js'
238+ mockSourceCodeContext ( {
239+ [ `Error: ctx\n at fn (${ url } :1:1)` ] : { ddDebugId : 'debug-id-1' } ,
240+ [ `Error: ctx\n at fn (${ causeUrl } :1:1)` ] : { ddDebugId : 'debug-id-cause' } ,
241+ } )
242+
243+ const cause = new Error ( 'root cause' )
244+ cause . stack = `Error: root cause\n at bar (${ causeUrl } :11:7)`
245+ const error = new Error ( 'oh snap!' ) as ErrorWithCause
246+ error . stack = `Error: oh snap!\n at foo (${ url } :52:15)`
247+ error . cause = cause
248+
249+ const formatted = computeRawError ( {
250+ ...DEFAULT_RAW_ERROR_PARAMS ,
251+ originalError : error ,
252+ handling : ErrorHandling . HANDLED ,
253+ } )
254+
255+ expect ( formatted . debugIds ) . toEqual ( { [ url ] : 'debug-id-1' , [ causeUrl ] : 'debug-id-cause' } )
256+ } )
252257
253- describe ( 'with non-Error values' , ( ) => {
254- it ( 'should handle string cause with consistent structure' , ( ) => {
258+ it ( 'should return undefined causes when error has no cause' , ( ) => {
259+ const error = new Error ( 'main' )
260+ const formatted = computeRawError ( {
261+ ...DEFAULT_RAW_ERROR_PARAMS ,
262+ originalError : error ,
263+ handling : ErrorHandling . HANDLED ,
264+ } )
265+ expect ( formatted . causes ) . toBeUndefined ( )
266+ } )
267+
268+ it ( 'should cap causes at 10 for a circular reference' , ( ) => {
269+ const error = new Error ( 'foo' ) as ErrorWithCause
270+ error . cause = error
271+ const formatted = computeRawError ( {
272+ ...DEFAULT_RAW_ERROR_PARAMS ,
273+ originalError : error ,
274+ handling : ErrorHandling . HANDLED ,
275+ } )
276+ expect ( formatted . causes ?. length ) . toBe ( 10 )
277+ } )
278+
279+ it ( 'should handle string cause' , ( ) => {
255280 const error = new Error ( 'main' ) as ErrorWithCause
256281 error . cause = 'string cause'
257-
258- const causes = flattenErrorCauses ( error , ErrorSource . CUSTOM )
259- expect ( causes ?. length ) . toBe ( 1 )
260- expect ( causes ?. [ 0 ] ) . toEqual ( {
261- message : '"string cause"' , // JSON stringified
282+ const formatted = computeRawError ( {
283+ ...DEFAULT_RAW_ERROR_PARAMS ,
284+ originalError : error ,
285+ handling : ErrorHandling . HANDLED ,
286+ } )
287+ expect ( formatted . causes ?. length ) . toBe ( 1 )
288+ expect ( formatted . causes ?. [ 0 ] ) . toEqual ( {
289+ message : '"string cause"' ,
262290 source : ErrorSource . CUSTOM ,
263291 type : undefined ,
264292 stack : undefined ,
265293 } )
266294 } )
267295
268- it ( 'should handle object cause with consistent structure ' , ( ) => {
296+ it ( 'should handle object cause' , ( ) => {
269297 const error = new Error ( 'main' ) as ErrorWithCause
270298 error . cause = { code : 'ERR_001' , details : 'Invalid input' }
271-
272- const causes = flattenErrorCauses ( error , ErrorSource . CUSTOM )
273- expect ( causes ?. length ) . toBe ( 1 )
274- expect ( causes ?. [ 0 ] ) . toEqual ( {
299+ const formatted = computeRawError ( {
300+ ...DEFAULT_RAW_ERROR_PARAMS ,
301+ originalError : error ,
302+ handling : ErrorHandling . HANDLED ,
303+ } )
304+ expect ( formatted . causes ?. length ) . toBe ( 1 )
305+ expect ( formatted . causes ?. [ 0 ] ) . toEqual ( {
275306 message : '{"code":"ERR_001","details":"Invalid input"}' ,
276307 source : ErrorSource . CUSTOM ,
277308 type : undefined ,
278309 stack : undefined ,
279310 } )
280311 } )
281312
282- it ( 'should handle number cause with consistent structure ' , ( ) => {
313+ it ( 'should handle number cause' , ( ) => {
283314 const error = new Error ( 'main' ) as ErrorWithCause
284315 error . cause = 42
285-
286- const causes = flattenErrorCauses ( error , ErrorSource . CUSTOM )
287- expect ( causes ?. length ) . toBe ( 1 )
288- expect ( causes ?. [ 0 ] ) . toEqual ( {
316+ const formatted = computeRawError ( {
317+ ...DEFAULT_RAW_ERROR_PARAMS ,
318+ originalError : error ,
319+ handling : ErrorHandling . HANDLED ,
320+ } )
321+ expect ( formatted . causes ?. length ) . toBe ( 1 )
322+ expect ( formatted . causes ?. [ 0 ] ) . toEqual ( {
289323 message : '42' ,
290324 source : ErrorSource . CUSTOM ,
291325 type : undefined ,
292326 stack : undefined ,
293327 } )
294328 } )
295329
296- it ( 'should handle mixed Error and non-Error chain' , ( ) => {
330+ it ( 'should handle mixed Error and non-Error cause chain' , ( ) => {
297331 const error1 = new Error ( 'first' ) as ErrorWithCause
298332 const error2 = new Error ( 'second' ) as ErrorWithCause
299333 error1 . cause = error2
300334 error2 . cause = { code : 'ERR_ROOT' }
301-
302- const causes = flattenErrorCauses ( error1 , ErrorSource . CUSTOM )
303- expect ( causes ?. length ) . toBe ( 2 )
304-
305- // First cause: Error with full structure
306- expect ( causes ?. [ 0 ] . message ) . toBe ( 'second' )
307- expect ( causes ?. [ 0 ] . type ) . toBe ( 'Error' )
308- expect ( causes ?. [ 0 ] . stack ) . toContain ( 'Error' )
309-
310- // Second cause: Object with normalized structure
311- expect ( causes ?. [ 1 ] ) . toEqual ( {
335+ const formatted = computeRawError ( {
336+ ...DEFAULT_RAW_ERROR_PARAMS ,
337+ originalError : error1 ,
338+ handling : ErrorHandling . HANDLED ,
339+ } )
340+ expect ( formatted . causes ?. length ) . toBe ( 2 )
341+ expect ( formatted . causes ?. [ 0 ] . message ) . toBe ( 'second' )
342+ expect ( formatted . causes ?. [ 0 ] . type ) . toBe ( 'Error' )
343+ expect ( formatted . causes ?. [ 0 ] . stack ) . toContain ( 'Error' )
344+ expect ( formatted . causes ?. [ 1 ] ) . toEqual ( {
312345 message : '{"code":"ERR_ROOT"}' ,
313346 source : ErrorSource . CUSTOM ,
314347 type : undefined ,
@@ -319,28 +352,55 @@ describe('flattenErrorCauses', () => {
319352 it ( 'should stop chain after non-Error cause' , ( ) => {
320353 const error = new Error ( 'main' ) as ErrorWithCause
321354 error . cause = { value : 'data' , cause : new Error ( 'ignored' ) }
322-
323- const causes = flattenErrorCauses ( error , ErrorSource . CUSTOM )
324- expect ( causes ?. length ) . toBe ( 1 )
325- // The entire object is captured, nested cause is sanitized
326- expect ( causes ?. [ 0 ] . message ) . toContain ( '"value":"data"' )
327- expect ( causes ?. [ 0 ] . type ) . toBeUndefined ( )
355+ const formatted = computeRawError ( {
356+ ...DEFAULT_RAW_ERROR_PARAMS ,
357+ originalError : error ,
358+ handling : ErrorHandling . HANDLED ,
359+ } )
360+ expect ( formatted . causes ?. length ) . toBe ( 1 )
361+ expect ( formatted . causes ?. [ 0 ] . message ) . toContain ( '"value":"data"' )
362+ expect ( formatted . causes ?. [ 0 ] . type ) . toBeUndefined ( )
328363 } )
329364
330365 it ( 'should handle null cause' , ( ) => {
331366 const error = new Error ( 'main' ) as ErrorWithCause
332367 error . cause = null
333- expect ( flattenErrorCauses ( error , ErrorSource . CUSTOM ) ) . toBeUndefined ( )
368+ const formatted = computeRawError ( {
369+ ...DEFAULT_RAW_ERROR_PARAMS ,
370+ originalError : error ,
371+ handling : ErrorHandling . HANDLED ,
372+ } )
373+ expect ( formatted . causes ) . toBeUndefined ( )
334374 } )
335375
336376 it ( 'should handle undefined cause' , ( ) => {
337377 const error = new Error ( 'main' ) as ErrorWithCause
338378 error . cause = undefined
339- expect ( flattenErrorCauses ( error , ErrorSource . CUSTOM ) ) . toBeUndefined ( )
379+ const formatted = computeRawError ( {
380+ ...DEFAULT_RAW_ERROR_PARAMS ,
381+ originalError : error ,
382+ handling : ErrorHandling . HANDLED ,
383+ } )
384+ expect ( formatted . causes ) . toBeUndefined ( )
340385 } )
341386 } )
342387} )
343388
389+ describe ( 'getFileFromStackTraceString' , ( ) => {
390+ it ( 'should get the first source file of the stack' , ( ) => {
391+ expect (
392+ getFileFromStackTraceString ( `TypeError: oh snap!
393+ at foo(1, bar) @ http://path/to/file.js:52:15
394+ at <anonymous> @ http://path/to/file.js:12
395+ at <anonymous>(baz) @ http://path/to/file.js` )
396+ ) . toEqual ( 'http://path/to/file.js:52:15' )
397+ } )
398+
399+ it ( 'should get undefined if no source file is in the stack' , ( ) => {
400+ expect ( getFileFromStackTraceString ( 'TypeError: oh snap!' ) ) . not . toBeDefined ( )
401+ } )
402+ } )
403+
344404describe ( 'isError' , ( ) => {
345405 it ( 'should correctly identify an error object from a different window context' , ( ) => {
346406 const iframe = document . createElement ( 'iframe' )
0 commit comments