Skip to content

Commit e393c96

Browse files
feat: reduce jserrors wrapping and remove onerror use (#614)
1 parent 8d0a5ff commit e393c96

File tree

12 files changed

+92
-138
lines changed

12 files changed

+92
-138
lines changed

src/features/jserrors/aggregate/compute-stack-trace.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ function computeStackTraceBySourceAndLine (ex) {
233233
mode: 'sourceline',
234234
name: className,
235235
message: ex.message,
236-
stackString: getClassName(ex) + ': ' + ex.message + '\n in evaluated code',
236+
stackString: className + ': ' + ex.message + '\n in evaluated code',
237237
frames: [{
238238
func: 'evaluated code'
239239
}]

src/features/jserrors/instrument/debug.js

-36
This file was deleted.

src/features/jserrors/instrument/index.js

+66-87
Original file line numberDiff line numberDiff line change
@@ -5,64 +5,55 @@
55

66
import { handle } from '../../../common/event-emitter/handle'
77
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'
118
import { InstrumentBase } from '../../utils/instrument-base'
12-
import { FEATURE_NAME, NR_ERR_PROP } from '../constants'
9+
import { FEATURE_NAME } from '../constants'
1310
import { FEATURE_NAMES } from '../../../loaders/features/features'
1411
import { globalScope } from '../../../common/constants/runtime'
1512
import { eventListenerOpts } from '../../../common/event-listener/event-listener-opts'
16-
import { getRuntime } from '../../../common/config/config'
1713
import { stringify } from '../../../common/util/stringify'
14+
import { UncaughtError } from './uncaught-error'
1815

1916
export class Instrument extends InstrumentBase {
2017
static featureName = FEATURE_NAME
18+
19+
#seenErrors = new Set()
20+
2121
constructor (agentIdentifier, aggregator, auto = true) {
2222
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+
2624
try {
2725
// this try-catch can be removed when IE11 is completely unsupported & gone
2826
this.removeOnAbort = new AbortController()
2927
} catch (e) {}
3028

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)
4735
})
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)
5040
})
5141

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
5544

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)
6046
}, eventListenerOpts(false, this.removeOnAbort?.signal))
6147

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))
6657

6758
this.abortHandler = this.#abort // we also use this as a flag to denote that the feature is active or on and handling errors
6859
this.importAggregator()
@@ -71,67 +62,55 @@ export class Instrument extends InstrumentBase {
7162
/** Restoration and resource release tasks to be done if JS error loader is being aborted. Unwind changes to globals. */
7263
#abort () {
7364
this.removeOnAbort?.abort()
65+
this.#seenErrors.clear()
7466
this.abortHandler = undefined // weakly allow this abort op to run only once
7567
}
7668

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+
7781
/**
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
8685
*/
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) {
9489
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
9894
}
9995
}
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)
12497
try {
125-
reason.message = prefix + reason.message
126-
return reason
98+
return new Error(prefix + stringify(promiseRejectionEvent.reason))
12799
} catch (e) {
128-
return reason
100+
return new Error(promiseRejectionEvent.reason)
129101
}
130102
}
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)
136115
}
137116
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Represents an uncaught non Error type error. This class does
3+
* not extend the Error class to prevent an invalid stack trace
4+
* from being created. Use this class to cast thrown errors that
5+
* do not use the Error class (strings, etc) to an object.
6+
*/
7+
export class UncaughtError {
8+
constructor (message, filename, lineno, colno) {
9+
this.name = 'UncaughtError'
10+
this.message = message
11+
this.sourceURL = filename
12+
this.line = lineno
13+
this.column = colno
14+
}
15+
}

src/loaders/api/api.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ export function setAPI (agentIdentifier, forceDrain) {
127127
try {
128128
return cb.apply(this, arguments)
129129
} catch (err) {
130-
tracerEE.emit('fn-err', [arguments, this, typeof err == 'string' ? new Error(err) : err], contextStore)
130+
tracerEE.emit('fn-err', [arguments, this, err], contextStore)
131131
// the error came from outside the agent, so don't swallow
132132
throw err
133133
} finally {

tests/functional/err/uncaught.test.js

+2-3
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ testDriver.test('reporting uncaught errors', supported, function (t, browser, ro
3030
const expectedErrorMessages = [
3131
{ message: 'original onerror', tested: false },
3232
{ message: 'uncaught error', tested: false },
33-
{ message: 'fake', tested: false },
34-
{ message: 'original return false', tested: false }
33+
{ message: 'original return abc', tested: false }
3534
]
3635
actualErrors.forEach(err => {
3736
const targetError = expectedErrorMessages.find(x => x.message === err.params.message)
@@ -43,7 +42,7 @@ testDriver.test('reporting uncaught errors', supported, function (t, browser, ro
4342
if (err.params.message === 'fake') t.ok(err.params.exceptionClass !== 'Error', `fake error has correct exceptionClass (${err.params.exceptionClass})`)
4443
else t.ok(err.params.exceptionClass === 'Error', `error has correct exceptionClass (${err.params.exceptionClass})`)
4544
})
46-
t.ok(expectedErrorMessages.every(x => x.tested), 'All expected error messages were found')
45+
t.ok(expectedErrorMessages.every(x => x.tested), `All expected error messages were found ${JSON.stringify(expectedErrorMessages.filter(x => !x.tested))}`)
4746
t.end()
4847
}).catch(fail)
4948

tests/functional/err/unhandled-rejection-readable.test.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ testDriver.test('unhandledPromiseRejections are caught and are readable', suppor
4949
t.ok(!!err.params.stack_trace, 'stack_trace exists')
5050
t.ok(!!err.params.stackHash, 'stackHash exists')
5151
})
52-
t.ok(expectedErrorMessages.every(x => x.tested), `All expected error messages were found ${expectedErrorMessages.filter(x => !x.tested)}`)
52+
t.ok(expectedErrorMessages.every(x => x.tested), `All expected error messages were found ${JSON.stringify(expectedErrorMessages.filter(x => !x.tested))}`)
5353
t.end()
5454
}).catch(fail)
5555

tests/functional/spa/errors.test.js

+2-5
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,6 @@ testDriver.test('error in custom tracer', function (t, browser, router) {
160160
})
161161

162162
testDriver.test('string error in custom tracer', function (t, browser, router) {
163-
// This tests throwing a string inside a custom tracer. It shows that in a specific case, the
164-
// agent will double count the error because the error is first caught in the custom node, re-thrown, and caught again in the click event listener.
165-
// This behavior only happens in ie11, other browsers ignore the string error and only generate 1 error.
166163
waitForPageLoadAnInitialCalls(browser, router, 'spa/errors/captured-custom-string.html')
167164
.then(() => {
168165
return clickPageAndWaitForEventsAndErrors(t, browser, router)
@@ -171,8 +168,7 @@ testDriver.test('string error in custom tracer', function (t, browser, router) {
171168
// check that errors payload did not include the error
172169
const errors = getErrorsFromResponse(errorData, browser)
173170

174-
if (browser.match('ie@>=11')) t.equal(errors.length, 2, 'should have 2 errors (1 String Class, 1 Error Class)')
175-
else t.equal(errors.length, 1, 'should have 1 errors')
171+
t.equal(errors.length, 1, 'should have 1 errors')
176172

177173
let { body, query } = eventData
178174
let interactionTree = (body && body.length ? body : querypack.decode(query.e))[0]
@@ -183,6 +179,7 @@ testDriver.test('string error in custom tracer', function (t, browser, router) {
183179
var nodeId = interactionTree.children[0].nodeId
184180

185181
var error = errors[0]
182+
console.log(JSON.stringify(error))
186183
t.equal(error.params.message, 'some error')
187184
t.equal(error.params.browserInteractionId, interactionId, 'should have the correct interaction id')
188185
t.equal(error.params.parentNodeId, nodeId, 'has the correct node id')

tools/test-builds/browser-agent-wrapper/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@
99
"author": "",
1010
"license": "ISC",
1111
"dependencies": {
12-
"@newrelic/browser-agent": "file:../../../temp/newrelic-browser-agent-1.235.0.tgz"
12+
"@newrelic/browser-agent": "file:../../../temp/newrelic-browser-agent-1.236.0.tgz"
1313
}
1414
}

tools/test-builds/browser-tests/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@
99
"author": "",
1010
"license": "ISC",
1111
"dependencies": {
12-
"@newrelic/browser-agent": "file:../../../temp/newrelic-browser-agent-1.235.0.tgz"
12+
"@newrelic/browser-agent": "file:../../../temp/newrelic-browser-agent-1.236.0.tgz"
1313
}
1414
}

tools/test-builds/raw-src-wrapper/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@
99
"author": "",
1010
"license": "ISC",
1111
"dependencies": {
12-
"@newrelic/browser-agent": "file:../../../temp/newrelic-browser-agent-1.235.0.tgz"
12+
"@newrelic/browser-agent": "file:../../../temp/newrelic-browser-agent-1.236.0.tgz"
1313
}
1414
}

tools/test-builds/vite-react-wrapper/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"main": "index.js",
55
"license": "MIT",
66
"dependencies": {
7-
"@newrelic/browser-agent": "file:../../../temp/newrelic-browser-agent-1.235.0.tgz",
7+
"@newrelic/browser-agent": "file:../../../temp/newrelic-browser-agent-1.236.0.tgz",
88
"react": "18.2.0",
99
"react-dom": "18.2.0"
1010
},

0 commit comments

Comments
 (0)