-
Notifications
You must be signed in to change notification settings - Fork 253
/
index.js
178 lines (146 loc) · 5.91 KB
/
index.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
const bugsnagInFlight = require('@bugsnag/in-flight')
const BugsnagPluginBrowserSession = require('@bugsnag/plugin-browser-session')
const LambdaTimeoutApproaching = require('./lambda-timeout-approaching')
// JS timers use a signed 32 bit integer for the millisecond parameter. SAM's
// "local invoke" has a bug that means it exceeds this amount, resulting in
// warnings. See https://github.com/aws/aws-sam-cli/issues/2519
const MAX_TIMER_VALUE = Math.pow(2, 31) - 1
const SERVER_PLUGIN_NAMES = ['express', 'koa', 'restify']
const isServerPluginLoaded = client => SERVER_PLUGIN_NAMES.some(name => client.getPlugin(name))
const BugsnagPluginAwsLambda = {
name: 'awsLambda',
load (client) {
bugsnagInFlight.trackInFlight(client)
client._loadPlugin(BugsnagPluginBrowserSession)
// Reset the app duration between invocations, if the plugin is loaded
const appDurationPlugin = client.getPlugin('appDuration')
if (appDurationPlugin) {
appDurationPlugin.reset()
}
// AWS add a default unhandledRejection listener that forcefully exits the
// process. This breaks reporting of unhandled rejections, so we have to
// remove all existing listeners and call them after we handle the rejection
if (client._config.autoDetectErrors && client._config.enabledErrorTypes.unhandledRejections) {
const listeners = process.listeners('unhandledRejection')
process.removeAllListeners('unhandledRejection')
// This relies on our unhandled rejection plugin adding its listener first
// using process.prependListener, so we can call it first instead of AWS'
process.on('unhandledRejection', async (reason, promise) => {
for (const listener of listeners) {
await listener.call(process, reason, promise)
}
})
}
// same for uncaught exceptions
if (client._config.autoDetectErrors && client._config.enabledErrorTypes.unhandledExceptions) {
const listeners = process.listeners('uncaughtException')
process.removeAllListeners('uncaughtException')
// This relies on our unhandled rejection plugin adding its listener first
// using process.prependListener, so we can call it first instead of AWS'
process.on('uncaughtException', async (err, origin) => {
for (const listener of listeners) {
await listener.call(process, err, origin)
}
})
}
return {
createHandler ({ flushTimeoutMs = 2000, lambdaTimeoutNotifyMs = 1000 } = {}) {
return wrapHandler.bind(null, client, flushTimeoutMs, lambdaTimeoutNotifyMs)
}
}
}
}
function wrapHandler (client, flushTimeoutMs, lambdaTimeoutNotifyMs, handler) {
let _handler = handler
if (handler.length > 2) {
// This is a handler expecting a 'callback' argument, so we convert
// it to return a Promise so '_handler' always has the same API
_handler = promisifyHandler(handler)
}
return async function (event, context) {
let lambdaTimeout
// Guard against the "getRemainingTimeInMillis" being missing. This should
// never happen but could when unit testing
if (typeof context.getRemainingTimeInMillis === 'function' &&
lambdaTimeoutNotifyMs > 0
) {
const timeoutMs = context.getRemainingTimeInMillis() - lambdaTimeoutNotifyMs
if (timeoutMs <= MAX_TIMER_VALUE) {
lambdaTimeout = setTimeout(function () {
const handledState = {
severity: 'warning',
unhandled: true,
severityReason: { type: 'log' }
}
const event = client.Event.create(
new LambdaTimeoutApproaching(context.getRemainingTimeInMillis()),
true,
handledState,
'aws lambda plugin',
0
)
event.context = context.functionName || 'Lambda timeout approaching'
client._notify(event)
}, timeoutMs)
}
}
client.addMetadata('AWS Lambda context', context)
// track sessions if autoTrackSessions is enabled and no server plugin is
// loaded - the server plugins handle starting sessions automatically, so
// we don't need to start one as well
if (client._config.autoTrackSessions && !isServerPluginLoaded(client)) {
client.startSession()
}
try {
return await _handler(event, context)
} catch (err) {
if (client._config.autoDetectErrors && client._config.enabledErrorTypes.unhandledExceptions) {
const handledState = {
severity: 'error',
unhandled: true,
severityReason: { type: 'unhandledException' }
}
const event = client.Event.create(err, true, handledState, 'aws lambda plugin', 1)
client._notify(event)
}
throw err
} finally {
if (lambdaTimeout) {
clearTimeout(lambdaTimeout)
}
try {
await bugsnagInFlight.flush(flushTimeoutMs)
} catch (err) {
client._logger.error(`Delivery may be unsuccessful: ${err.message}`)
}
}
}
}
// Convert a handler that uses callbacks to an async handler
function promisifyHandler (handler) {
return function (event, context) {
return new Promise(function (resolve, reject) {
const result = handler(event, context, function (err, response) {
if (err) {
reject(err)
return
}
resolve(response)
})
// Handle an edge case where the passed handler has the callback parameter
// but actually returns a promise. In this case we need to resolve/reject
// based on the returned promise instead of in the callback
if (isPromise(result)) {
result.then(resolve).catch(reject)
}
})
}
}
function isPromise (value) {
return (typeof value === 'object' || typeof value === 'function') &&
typeof value.then === 'function' &&
typeof value.catch === 'function'
}
module.exports = BugsnagPluginAwsLambda
// add a default export for ESM modules without interop
module.exports.default = module.exports