5
5
6
6
import { handle } from '../../../common/event-emitter/handle'
7
7
import { now } from '../../../common/timing/now'
8
- import { getOrSet } from '../../../common/util/get-or-set'
9
- import { wrapRaf , wrapTimer , wrapEvents , wrapXhr } from '../../../common/wrap'
10
- import './debug'
11
8
import { InstrumentBase } from '../../utils/instrument-base'
12
- import { FEATURE_NAME , NR_ERR_PROP } from '../constants'
9
+ import { FEATURE_NAME } from '../constants'
13
10
import { FEATURE_NAMES } from '../../../loaders/features/features'
14
11
import { globalScope } from '../../../common/constants/runtime'
15
12
import { eventListenerOpts } from '../../../common/event-listener/event-listener-opts'
16
- import { getRuntime } from '../../../common/config/config'
17
13
import { stringify } from '../../../common/util/stringify'
14
+ import { UncaughtError } from './uncaught-error'
18
15
19
16
export class Instrument extends InstrumentBase {
20
17
static featureName = FEATURE_NAME
18
+
19
+ #seenErrors = new Set ( )
20
+
21
21
constructor ( agentIdentifier , aggregator , auto = true ) {
22
22
super ( agentIdentifier , aggregator , FEATURE_NAME , auto )
23
- // skipNext counter to keep track of uncaught
24
- // errors that will be the same as caught errors.
25
- this . skipNext = 0
23
+
26
24
try {
27
25
// this try-catch can be removed when IE11 is completely unsupported & gone
28
26
this . removeOnAbort = new AbortController ( )
29
27
} catch ( e ) { }
30
28
31
- const thisInstrument = this
32
- thisInstrument . ee . on ( 'fn-start' , function ( args , obj , methodName ) {
33
- if ( thisInstrument . abortHandler ) thisInstrument . skipNext += 1
34
- } )
35
- thisInstrument . ee . on ( 'fn-err' , function ( args , obj , err ) {
36
- if ( thisInstrument . abortHandler && ! err [ NR_ERR_PROP ] ) {
37
- getOrSet ( err , NR_ERR_PROP , function getVal ( ) {
38
- return true
39
- } )
40
- this . thrown = true
41
- handle ( 'err' , [ err , now ( ) ] , undefined , FEATURE_NAMES . jserrors , thisInstrument . ee )
42
- }
43
- } )
44
- thisInstrument . ee . on ( 'fn-end' , function ( ) {
45
- if ( ! thisInstrument . abortHandler ) return
46
- if ( ! this . thrown && thisInstrument . skipNext > 0 ) thisInstrument . skipNext -= 1
29
+ // Capture function errors early in case the spa feature is loaded
30
+ this . ee . on ( 'fn-err' , ( args , obj , error ) => {
31
+ if ( ! this . abortHandler || this . #seenErrors. has ( error ) ) return
32
+ this . #seenErrors. add ( error )
33
+
34
+ handle ( 'err' , [ this . #castError( error ) , now ( ) ] , undefined , FEATURE_NAMES . jserrors , this . ee )
47
35
} )
48
- thisInstrument . ee . on ( 'internal-error' , function ( e ) {
49
- handle ( 'ierr' , [ e , now ( ) , true ] , undefined , FEATURE_NAMES . jserrors , thisInstrument . ee )
36
+
37
+ this . ee . on ( 'internal-error' , ( error ) => {
38
+ if ( ! this . abortHandler ) return
39
+ handle ( 'ierr' , [ this . #castError( error ) , now ( ) , true ] , undefined , FEATURE_NAMES . jserrors , this . ee )
50
40
} )
51
41
52
- // Replace global error handler with our own.
53
- this . origOnerror = globalScope . onerror
54
- globalScope . onerror = this . onerrorHandler . bind ( this )
42
+ globalScope . addEventListener ( 'unhandledrejection' , ( promiseRejectionEvent ) => {
43
+ if ( ! this . abortHandler ) return
55
44
56
- globalScope . addEventListener ( 'unhandledrejection' , ( e ) => {
57
- /** rejections can contain data of any type -- this is an effort to keep the message human readable */
58
- const err = castReasonToError ( e . reason )
59
- handle ( 'err' , [ err , now ( ) , false , { unhandledPromiseRejection : 1 } ] , undefined , FEATURE_NAMES . jserrors , this . ee )
45
+ handle ( 'err' , [ this . #castPromiseRejectionEvent( promiseRejectionEvent ) , now ( ) , false , { unhandledPromiseRejection : 1 } ] , undefined , FEATURE_NAMES . jserrors , this . ee )
60
46
} , eventListenerOpts ( false , this . removeOnAbort ?. signal ) )
61
47
62
- wrapRaf ( this . ee )
63
- wrapTimer ( this . ee )
64
- wrapEvents ( this . ee )
65
- if ( getRuntime ( agentIdentifier ) . xhrWrappable ) wrapXhr ( this . ee )
48
+ globalScope . addEventListener ( 'error' , ( errorEvent ) => {
49
+ if ( ! this . abortHandler ) return
50
+ if ( this . #seenErrors. has ( errorEvent . error ) ) {
51
+ this . #seenErrors. delete ( errorEvent . error )
52
+ return
53
+ }
54
+
55
+ handle ( 'err' , [ this . #castErrorEvent( errorEvent ) , now ( ) ] , undefined , FEATURE_NAMES . jserrors , this . ee )
56
+ } , eventListenerOpts ( false , this . removeOnAbort ?. signal ) )
66
57
67
58
this . abortHandler = this . #abort // we also use this as a flag to denote that the feature is active or on and handling errors
68
59
this . importAggregator ( )
@@ -71,67 +62,55 @@ export class Instrument extends InstrumentBase {
71
62
/** Restoration and resource release tasks to be done if JS error loader is being aborted. Unwind changes to globals. */
72
63
#abort ( ) {
73
64
this . removeOnAbort ?. abort ( )
65
+ this . #seenErrors. clear ( )
74
66
this . abortHandler = undefined // weakly allow this abort op to run only once
75
67
}
76
68
69
+ #castError ( error ) {
70
+ if ( error instanceof Error ) {
71
+ return error
72
+ }
73
+
74
+ if ( typeof error . message !== 'undefined' ) {
75
+ return new UncaughtError ( error . message , error . filename , error . lineno , error . colno )
76
+ }
77
+
78
+ return new UncaughtError ( error )
79
+ }
80
+
77
81
/**
78
- * FF and Android browsers do not provide error info to the 'error' event callback,
79
- * so we must use window.onerror
80
- * @param {string } message
81
- * @param {string } filename
82
- * @param {number } lineno
83
- * @param {number } column
84
- * @param {Error | * } errorObj
85
- * @returns
82
+ * Attempts to convert a PromiseRejectionEvent object to an Error object
83
+ * @param {PromiseRejectionEvent } unhandledRejectionEvent The unhandled promise rejection event
84
+ * @returns {Error } An Error object with the message as the casted reason
86
85
*/
87
- onerrorHandler ( message , filename , lineno , column , errorObj ) {
88
- if ( typeof this . origOnerror === 'function' ) this . origOnerror ( ...arguments )
89
-
90
- try {
91
- if ( this . skipNext ) this . skipNext -= 1
92
- else handle ( 'err' , [ errorObj || new UncaughtException ( message , filename , lineno ) , now ( ) ] , undefined , FEATURE_NAMES . jserrors , this . ee )
93
- } catch ( e ) {
86
+ #castPromiseRejectionEvent ( promiseRejectionEvent ) {
87
+ let prefix = 'Unhandled Promise Rejection: '
88
+ if ( promiseRejectionEvent ?. reason instanceof Error ) {
94
89
try {
95
- handle ( 'ierr' , [ e , now ( ) , true ] , undefined , FEATURE_NAMES . jserrors , this . ee )
96
- } catch ( err ) {
97
- // do nothing
90
+ promiseRejectionEvent . reason . message = prefix + promiseRejectionEvent . reason . message
91
+ return promiseRejectionEvent . reason
92
+ } catch ( e ) {
93
+ return promiseRejectionEvent . reason
98
94
}
99
95
}
100
- return false // maintain default behavior of the error event of Window
101
- }
102
- }
103
-
104
- /**
105
- *
106
- * @param {string } message
107
- * @param {string } filename
108
- * @param {number } lineno
109
- */
110
- function UncaughtException ( message , filename , lineno ) {
111
- this . message = message || 'Uncaught error with no additional information'
112
- this . sourceURL = filename
113
- this . line = lineno
114
- }
115
-
116
- /**
117
- * Attempts to cast an unhandledPromiseRejection reason (reject(...)) to an Error object
118
- * @param {* } reason - The reason property from an unhandled promise rejection
119
- * @returns {Error } - An Error object with the message as the casted reason
120
- */
121
- function castReasonToError ( reason ) {
122
- let prefix = 'Unhandled Promise Rejection: '
123
- if ( reason instanceof Error ) {
96
+ if ( typeof promiseRejectionEvent . reason === 'undefined' ) return new Error ( prefix )
124
97
try {
125
- reason . message = prefix + reason . message
126
- return reason
98
+ return new Error ( prefix + stringify ( promiseRejectionEvent . reason ) )
127
99
} catch ( e ) {
128
- return reason
100
+ return new Error ( promiseRejectionEvent . reason )
129
101
}
130
102
}
131
- if ( typeof reason === 'undefined' ) return new Error ( prefix )
132
- try {
133
- return new Error ( prefix + stringify ( reason ) )
134
- } catch ( err ) {
135
- return new Error ( prefix )
103
+
104
+ /**
105
+ * Attempts to convert an ErrorEvent object to an Error object
106
+ * @param {ErrorEvent } errorEvent The error event
107
+ * @returns {Error|UncaughtError } The error event converted to an Error object
108
+ */
109
+ #castErrorEvent ( errorEvent ) {
110
+ if ( errorEvent . error instanceof Error ) {
111
+ return errorEvent . error
112
+ }
113
+
114
+ return new UncaughtError ( errorEvent . message , errorEvent . filename , errorEvent . lineno , errorEvent . colno )
136
115
}
137
116
}
0 commit comments