Skip to content

Commit d58e520

Browse files
authored
feat(plugin-cloudflare-workers): add basic cloudflare workers plugin (#2643)
* feat(plugin-cloudflare-workers): add basic cloudflare workers plugin * refactor(plugin-cloudflare-workers): rework request metadata handling * refactor(plugin-cloudflare-workers): rework type declarations * test(plugin-cloudflare-workers): rework unit tests * docs(plugin-cloudflare-workers): add changelog entry # Conflicts: # CHANGELOG.md
1 parent 731ccb7 commit d58e520

10 files changed

Lines changed: 558 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## [Unreleased]
44

5+
### Added
6+
7+
(plugin-cloudflare-workers): Add initial support for Cloudflare Workers [#2643](https://github.com/bugsnag/bugsnag-js/pull/2643)
8+
59
### Fixed
610

711
(plugin-server-session) Delay session tracker initialization until first use [#2642](https://github.com/bugsnag/bugsnag-js/pull/2642)

jest.config.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,10 @@ module.exports = {
131131
clearMocks: true,
132132
modulePathIgnorePatterns: ['.verdaccio', 'fixtures']
133133
}),
134-
project('react native cli', ['react-native-cli'], { testEnvironment: 'node' })
134+
project('react native cli', ['react-native-cli'], { testEnvironment: 'node' }),
135+
project('cloudflare-workers', ['plugin-cloudflare-workers'], {
136+
testEnvironment: 'node',
137+
setupFilesAfterEnv: ['<rootDir>/packages/plugin-cloudflare-workers/test/setup.ts']
138+
})
135139
]
136140
}

package-lock.json

Lines changed: 42 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) Bugsnag, https://www.bugsnag.com/
2+
3+
Permission is hereby granted, free of charge, to any person obtaining
4+
a copy of this software and associated documentation files (the "Software"),
5+
to deal in the Software without restriction, including without limitation
6+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
7+
and/or sell copies of the Software, and to permit persons to whom the Software
8+
is furnished to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in
11+
all copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# @bugsnag/plugin-cloudflare-workers
2+
3+
A [@bugsnag/js](https://github.com/bugsnag/bugsnag-js) plugin for capturing errors in Cloudflare Workers.
4+
5+
## License
6+
7+
This package is free software released under the MIT License. See [LICENSE.txt](./LICENSE.txt) for details.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"name": "@bugsnag/plugin-cloudflare-workers",
3+
"version": "8.6.0",
4+
"main": "src/index.js",
5+
"types": "types/bugsnag-plugin-cloudflare-workers.d.ts",
6+
"description": "Cloudflare Workers support for @bugsnag/js",
7+
"homepage": "https://www.bugsnag.com/",
8+
"repository": {
9+
"type": "git",
10+
"url": "git@github.com:bugsnag/bugsnag-js.git"
11+
},
12+
"publishConfig": {
13+
"access": "public"
14+
},
15+
"files": [
16+
"src",
17+
"types"
18+
],
19+
"author": "Bugsnag",
20+
"license": "MIT",
21+
"dependencies": {
22+
"@bugsnag/in-flight": "^8.6.0",
23+
"@bugsnag/plugin-browser-session": "^8.6.0"
24+
},
25+
"devDependencies": {
26+
"@bugsnag/core": "^8.6.0",
27+
"@cloudflare/workers-types": "^4.20251213.0"
28+
},
29+
"peerDependencies": {
30+
"@bugsnag/core": "^8.0.0"
31+
}
32+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
const bugsnagInFlight = require('@bugsnag/in-flight')
2+
const BugsnagPluginBrowserSession = require('@bugsnag/plugin-browser-session')
3+
4+
const SERVER_PLUGIN_NAMES = ['express', 'koa', 'restify', 'hono']
5+
const isServerPluginLoaded = client => SERVER_PLUGIN_NAMES.some(name => client.getPlugin(name))
6+
7+
const extractRequestInfo = (request) => {
8+
if (!request) return {}
9+
10+
const url = new URL(request.url)
11+
12+
const info = {
13+
url: request.url,
14+
path: url.pathname,
15+
httpMethod: request.method,
16+
headers: Object.fromEntries(request.headers),
17+
query: url.searchParams.size > 0 ? Object.fromEntries(url.searchParams) : undefined,
18+
clientIp: request.headers.get('Cf-Connecting-IP') || request.headers.get('X-Forwarded-For') || undefined
19+
}
20+
21+
return info
22+
}
23+
24+
const getRequestAndMetadataFromReq = (request) => {
25+
const requestInfo = extractRequestInfo(request)
26+
27+
return {
28+
metadata: requestInfo,
29+
request: {
30+
clientIp: requestInfo.clientIp,
31+
headers: requestInfo.headers,
32+
httpMethod: requestInfo.httpMethod,
33+
url: requestInfo.url
34+
}
35+
}
36+
}
37+
38+
const BugsnagPluginCloudflareWorkers = {
39+
name: 'cloudflareWorkers',
40+
41+
load (client) {
42+
bugsnagInFlight.trackInFlight(client)
43+
client._loadPlugin(BugsnagPluginBrowserSession)
44+
45+
// Reset the app duration between invocations, if the plugin is loaded
46+
const appDurationPlugin = client.getPlugin('appDuration')
47+
48+
if (appDurationPlugin) {
49+
appDurationPlugin.reset()
50+
}
51+
52+
return {
53+
createHandler ({ flushTimeoutMs = 2000 } = {}) {
54+
return wrapHandler.bind(null, client, flushTimeoutMs)
55+
}
56+
}
57+
}
58+
}
59+
60+
function wrapHandler (client, flushTimeoutMs, handler) {
61+
return async function (request, env, ctx) {
62+
// Add request metadata via onError callback so server plugins can override
63+
// Only add metadata if no server plugin is loaded
64+
if (!isServerPluginLoaded(client)) {
65+
client.addOnError((event) => {
66+
const { metadata, request: requestData } = getRequestAndMetadataFromReq(request)
67+
event.request = { ...event.request, ...requestData }
68+
event.addMetadata('request', metadata)
69+
}, true)
70+
}
71+
72+
// Track sessions if autoTrackSessions is enabled and no server plugin is loaded
73+
if (client._config.autoTrackSessions && !isServerPluginLoaded(client)) {
74+
client.startSession()
75+
}
76+
77+
try {
78+
return await handler(request, env, ctx)
79+
} catch (err) {
80+
if (client._config.autoDetectErrors && client._config.enabledErrorTypes.unhandledExceptions) {
81+
const handledState = {
82+
severity: 'error',
83+
unhandled: true,
84+
severityReason: { type: 'unhandledException' }
85+
}
86+
87+
const event = client.Event.create(err, true, handledState, 'cloudflare workers plugin', 1)
88+
89+
client._notify(event)
90+
}
91+
92+
throw err
93+
} finally {
94+
// Use ctx.waitUntil to ensure flush completes even after response is returned
95+
// This is critical for Cloudflare Workers as they can terminate immediately
96+
if (ctx && typeof ctx.waitUntil === 'function') {
97+
ctx.waitUntil(
98+
bugsnagInFlight.flush(flushTimeoutMs).catch(err => {
99+
client._logger.error(`Delivery may be unsuccessful: ${err.message}`)
100+
})
101+
)
102+
} else {
103+
try {
104+
await bugsnagInFlight.flush(flushTimeoutMs)
105+
} catch (err) {
106+
client._logger.error(`Delivery may be unsuccessful: ${err.message}`)
107+
}
108+
}
109+
}
110+
}
111+
}
112+
113+
module.exports = BugsnagPluginCloudflareWorkers
114+
115+
// add a default export for ESM modules without interop
116+
module.exports.default = module.exports

0 commit comments

Comments
 (0)