-
Notifications
You must be signed in to change notification settings - Fork 254
/
Copy pathevent.js
321 lines (281 loc) · 10.1 KB
/
event.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
const ErrorStackParser = require('./lib/error-stack-parser')
const StackGenerator = require('stack-generator')
const hasStack = require('./lib/has-stack')
const map = require('./lib/es-utils/map')
const reduce = require('./lib/es-utils/reduce')
const filter = require('./lib/es-utils/filter')
const assign = require('./lib/es-utils/assign')
const metadataDelegate = require('./lib/metadata-delegate')
const featureFlagDelegate = require('./lib/feature-flag-delegate')
const isError = require('./lib/iserror')
class Event {
constructor (errorClass, errorMessage, stacktrace = [], handledState = defaultHandledState(), originalError) {
this.apiKey = undefined
this.context = undefined
this.groupingHash = undefined
this.originalError = originalError
this._handledState = handledState
this.severity = this._handledState.severity
this.unhandled = this._handledState.unhandled
this.app = {}
this.device = {}
this.request = {}
this.breadcrumbs = []
this.threads = []
this._metadata = {}
this._features = []
this._featuresIndex = {}
this._user = {}
this._session = undefined
this._correlation = undefined
this.errors = [
createBugsnagError(errorClass, errorMessage, Event.__type, stacktrace)
]
// Flags.
// Note these are not initialised unless they are used
// to save unnecessary bytes in the browser bundle
/* this.attemptImmediateDelivery, default: true */
}
addMetadata (section, keyOrObj, maybeVal) {
return metadataDelegate.add(this._metadata, section, keyOrObj, maybeVal)
}
/**
* Associate this event with a specific trace. This is usually done automatically when
* using bugsnag-js-performance, but can also be set manually if required.
*
* @param traceId the ID of the trace the event occurred within
* @param spanId the ID of the span that the event occurred within
*/
setTraceCorrelation (traceId, spanId) {
if (typeof traceId === 'string') {
this._correlation = { traceId, ...typeof spanId === 'string' ? { spanId } : { } }
}
}
getMetadata (section, key) {
return metadataDelegate.get(this._metadata, section, key)
}
clearMetadata (section, key) {
return metadataDelegate.clear(this._metadata, section, key)
}
addFeatureFlag (name, variant = null) {
featureFlagDelegate.add(this._features, this._featuresIndex, name, variant)
}
addFeatureFlags (featureFlags) {
featureFlagDelegate.merge(this._features, featureFlags, this._featuresIndex)
}
getFeatureFlags () {
return featureFlagDelegate.toEventApi(this._features)
}
clearFeatureFlag (name) {
featureFlagDelegate.clear(this._features, this._featuresIndex, name)
}
clearFeatureFlags () {
this._features = []
this._featuresIndex = {}
}
getUser () {
return this._user
}
setUser (id, email, name) {
this._user = { id, email, name }
}
toJSON () {
return {
payloadVersion: '4',
exceptions: map(this.errors, er => assign({}, er, { message: er.errorMessage })),
severity: this.severity,
unhandled: this._handledState.unhandled,
severityReason: this._handledState.severityReason,
app: this.app,
device: this.device,
request: this.request,
breadcrumbs: this.breadcrumbs,
context: this.context,
groupingHash: this.groupingHash,
metaData: this._metadata,
user: this._user,
session: this._session,
featureFlags: this.getFeatureFlags(),
correlation: this._correlation
}
}
}
// takes a stacktrace.js style stackframe (https://github.com/stacktracejs/stackframe)
// and returns a Bugsnag compatible stackframe (https://docs.bugsnag.com/api/error-reporting/#json-payload)
const formatStackframe = frame => {
const f = {
file: frame.fileName,
method: normaliseFunctionName(frame.functionName),
lineNumber: frame.lineNumber,
columnNumber: frame.columnNumber,
code: undefined,
inProject: undefined
}
// Some instances result in no file:
// - calling notify() from chrome's terminal results in no file/method.
// - non-error exception thrown from global code in FF
// This adds one.
if (f.lineNumber > -1 && !f.file && !f.method) {
f.file = 'global code'
}
return f
}
const normaliseFunctionName = name => /^global code$/i.test(name) ? 'global code' : name
const defaultHandledState = () => ({
unhandled: false,
severity: 'warning',
severityReason: { type: 'handledException' }
})
const ensureString = (str) => typeof str === 'string' ? str : ''
function createBugsnagError (errorClass, errorMessage, type, stacktrace) {
return {
errorClass: ensureString(errorClass),
errorMessage: ensureString(errorMessage),
type,
stacktrace: reduce(stacktrace, (accum, frame) => {
const f = formatStackframe(frame)
// don't include a stackframe if none of its properties are defined
try {
if (JSON.stringify(f) === '{}') return accum
return accum.concat(f)
} catch (e) {
return accum
}
}, [])
}
}
function getCauseStack (error) {
if (error.cause) {
return [error, ...getCauseStack(error.cause)]
} else {
return [error]
}
}
// Helpers
Event.getStacktrace = function (error, errorFramesToSkip, backtraceFramesToSkip) {
if (hasStack(error)) return ErrorStackParser.parse(error).slice(errorFramesToSkip)
// error wasn't provided or didn't have a stacktrace so try to walk the callstack
try {
return filter(StackGenerator.backtrace(), frame =>
(frame.functionName || '').indexOf('StackGenerator$$') === -1
).slice(1 + backtraceFramesToSkip)
} catch (e) {
return []
}
}
Event.create = function (maybeError, tolerateNonErrors, handledState, component, errorFramesToSkip = 0, logger) {
const [error, internalFrames] = normaliseError(maybeError, tolerateNonErrors, component, logger)
let event
try {
const stacktrace = Event.getStacktrace(
error,
// if an error was created/throw in the normaliseError() function, we need to
// tell the getStacktrace() function to skip the number of frames we know will
// be from our own functions. This is added to the number of frames deep we
// were told about
internalFrames > 0 ? 1 + internalFrames + errorFramesToSkip : 0,
// if there's no stacktrace, the callstack may be walked to generated one.
// this is how many frames should be removed because they come from our library
1 + errorFramesToSkip
)
event = new Event(error.name, error.message, stacktrace, handledState, maybeError)
} catch (e) {
event = new Event(error.name, error.message, [], handledState, maybeError)
}
if (error.name === 'InvalidError') {
event.addMetadata(`${component}`, 'non-error parameter', makeSerialisable(maybeError))
}
if (error.cause) {
const causes = getCauseStack(error).slice(1)
const normalisedCauses = map(causes, (cause) => {
// Only get stacktrace for error causes that are a valid JS Error and already have a stack
const stacktrace = (isError(cause) && hasStack(cause)) ? ErrorStackParser.parse(cause) : []
const [error] = normaliseError(cause, true, 'error cause')
if (error.name === 'InvalidError') event.addMetadata('error cause', makeSerialisable(cause))
return createBugsnagError(error.name, error.message, Event.__type, stacktrace)
})
event.errors.push(...normalisedCauses)
}
return event
}
const makeSerialisable = (err) => {
if (err === null) return 'null'
if (err === undefined) return 'undefined'
return err
}
const normaliseError = (maybeError, tolerateNonErrors, component, logger) => {
let error
let internalFrames = 0
const createAndLogInputError = (reason) => {
const verb = (component === 'error cause' ? 'was' : 'received')
if (logger) logger.warn(`${component} ${verb} a non-error: "${reason}"`)
const err = new Error(`${component} ${verb} a non-error. See "${component}" tab for more detail.`)
err.name = 'InvalidError'
return err
}
// In some cases:
//
// - the promise rejection handler (both in the browser and node)
// - the node uncaughtException handler
//
// We are really limited in what we can do to get a stacktrace. So we use the
// tolerateNonErrors option to ensure that the resulting error communicates as
// such.
if (!tolerateNonErrors) {
if (isError(maybeError)) {
error = maybeError
} else {
error = createAndLogInputError(typeof maybeError)
internalFrames += 2
}
} else {
switch (typeof maybeError) {
case 'string':
case 'number':
case 'boolean':
error = new Error(String(maybeError))
internalFrames += 1
break
case 'function':
error = createAndLogInputError('function')
internalFrames += 2
break
case 'object':
if (maybeError !== null && isError(maybeError)) {
error = maybeError
} else if (maybeError !== null && hasNecessaryFields(maybeError)) {
error = new Error(maybeError.message || maybeError.errorMessage)
error.name = maybeError.name || maybeError.errorClass
internalFrames += 1
} else {
error = createAndLogInputError(maybeError === null ? 'null' : 'unsupported object')
internalFrames += 2
}
break
default:
error = createAndLogInputError('nothing')
internalFrames += 2
}
}
if (!hasStack(error)) {
// in IE10/11 a new Error() doesn't have a stacktrace until you throw it, so try that here
try {
throw error
} catch (e) {
if (hasStack(e)) {
error = e
// if the error only got a stacktrace after we threw it here, we know it
// will only have one extra internal frame from this function, regardless
// of whether it went through createAndLogInputError() or not
internalFrames = 1
}
}
}
return [error, internalFrames]
}
// default value for stacktrace.type
Event.__type = 'browserjs'
const hasNecessaryFields = error =>
(typeof error.name === 'string' || typeof error.errorClass === 'string') &&
(typeof error.message === 'string' || typeof error.errorMessage === 'string')
module.exports = Event