Skip to content

Commit 08a2bd8

Browse files
authoredAug 7, 2023
Merge pull request #1927 from bugsnag/node-breadcrumbs
enable breadcrumbs and context-scoped calls for node
2 parents 28d9670 + 036f3da commit 08a2bd8

21 files changed

+1482
-145
lines changed
 

‎CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
### Changed
1010

11+
- (plugin-navigation-breadcrumbs) calling `pushState` or `replaceState` no longer triggers a new session when `autoTrackSessions` is enabled [#1820](https://github.com/bugsnag/bugsnag-js/pull/1820)
12+
- (plugin-contextualize) reimplement without relying on the deprecated node Domain API. From Node 16+ unhandled promise rejections are also supported [#1924](https://github.com/bugsnag/bugsnag-js/pull/1924)
13+
- (node) enable breadcrumbs and context-scoped calls [#1927](https://github.com/bugsnag/bugsnag-js/pull/1927)
1114
- (plugin-navigation-breadcrumbs) Calling `pushState` or `replaceState` no longer triggers a new session when `autoTrackSessions` is enabled [#1820](https://github.com/bugsnag/bugsnag-js/pull/1820)
1215
- (plugin-contextualize) Reimplement without relying on the deprecated node Domain API. From Node 16+ unhandled promise rejections are also supported [#1924](https://github.com/bugsnag/bugsnag-js/pull/1924)
1316
- (plugin-network-breadcrumbs, plugin-electron-net-breadcrumbs) *Breaking change*: The `request` metadata field in network breadcrumbs has been renamed to `url` and is no longer pre-pended with the HTTP method [#1988](https://github.com/bugsnag/bugsnag-js/pull/1988)

‎jest.config.js

+1
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ module.exports = {
9393
], {
9494
testEnvironment: 'node',
9595
testMatch: [
96+
'<rootDir>/packages/node/test/**/*.test.[jt]s',
9697
'<rootDir>/packages/node/test/integration/**/*.test.[jt]s'
9798
]
9899
}),

‎packages/node/src/notifier.js

+8-13
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@ const delivery = require('@bugsnag/delivery-node')
1616
// extend the base config schema with some node-specific options
1717
const schema = { ...require('@bugsnag/core/config').schema, ...require('./config') }
1818

19-
// remove enabledBreadcrumbTypes from the config schema
20-
delete schema.enabledBreadcrumbTypes
21-
2219
const pluginApp = require('@bugsnag/plugin-app-duration')
2320
const pluginSurroundingCode = require('@bugsnag/plugin-node-surrounding-code')
2421
const pluginInProject = require('@bugsnag/plugin-node-in-project')
@@ -63,12 +60,6 @@ const Bugsnag = {
6360

6461
bugsnag._logger.debug('Loaded!')
6562

66-
bugsnag.leaveBreadcrumb = function () {
67-
bugsnag._logger.warn('Breadcrumbs are not supported in Node.js yet')
68-
}
69-
70-
bugsnag._config.enabledBreadcrumbTypes = []
71-
7263
return bugsnag
7364
},
7465
start: (opts) => {
@@ -87,10 +78,14 @@ const Bugsnag = {
8778
Object.keys(Client.prototype).forEach((m) => {
8879
if (/^_/.test(m)) return
8980
Bugsnag[m] = function () {
90-
if (!Bugsnag._client) return console.error(`Bugsnag.${m}() was called before Bugsnag.start()`)
91-
Bugsnag._client._depth += 1
92-
const ret = Bugsnag._client[m].apply(Bugsnag._client, arguments)
93-
Bugsnag._client._depth -= 1
81+
// if we are in an async context, use the client from that context
82+
const client = Bugsnag._client._clientContext.getStore() || Bugsnag._client
83+
84+
if (!client) return console.error(`Bugsnag.${m}() was called before Bugsnag.start()`)
85+
86+
client._depth += 1
87+
const ret = client[m].apply(client, arguments)
88+
client._depth -= 1
9489
return ret
9590
}
9691
})

‎packages/node/test/notifier.test.ts

+281-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import Bugsnag from '..'
1+
import Bugsnag from '../src/notifier'
22

33
describe('node notifier', () => {
44
beforeAll(() => {
@@ -7,7 +7,7 @@ describe('node notifier', () => {
77
})
88

99
beforeEach(() => {
10-
// @ts-ignore:
10+
// @ts-ignore
1111
Bugsnag._client = null
1212
})
1313

@@ -21,4 +21,283 @@ describe('node notifier', () => {
2121
expect(Bugsnag.isStarted()).toBe(true)
2222
})
2323
})
24+
25+
describe('addMetadata()', () => {
26+
it('adds metadata to the client', () => {
27+
Bugsnag.start('abcd12abcd12abcd12abcd12abcd12abcd')
28+
Bugsnag.addMetadata('test', { meta: 'data' })
29+
// @ts-ignore
30+
expect(Bugsnag._client._metadata).toStrictEqual({ test: { meta: 'data' } })
31+
})
32+
33+
describe('when in an async context', () => {
34+
it('adds meta data to the cloned client not not the base client', () => {
35+
Bugsnag.start('abcd12abcd12abcd12abcd12abcd12abcd')
36+
const contextualize = Bugsnag.getPlugin('contextualize')
37+
38+
contextualize(() => {
39+
Bugsnag.addMetadata('test', { meta: 'data' })
40+
// @ts-ignore
41+
expect(Bugsnag._client._clientContext.getStore()._metadata).toStrictEqual({ test: { meta: 'data' } })
42+
})
43+
44+
// @ts-ignore
45+
expect(Bugsnag._client._metadata).toStrictEqual({})
46+
})
47+
})
48+
})
49+
50+
describe('getMetadata()', () => {
51+
it('retrieves metadata previously set on the client', () => {
52+
Bugsnag.start('abcd12abcd12abcd12abcd12abcd12abcd')
53+
Bugsnag.addMetadata('test', { meta: 'data' })
54+
// @ts-ignore
55+
expect(Bugsnag._client._metadata).toStrictEqual({ test: { meta: 'data' } })
56+
57+
expect(Bugsnag.getMetadata('test')).toStrictEqual({ meta: 'data' })
58+
})
59+
60+
describe('when in an async context', () => {
61+
it('retrieves metadata previously set on the cloned client not not the base client', () => {
62+
Bugsnag.start('abcd12abcd12abcd12abcd12abcd12abcd')
63+
const contextualize = Bugsnag.getPlugin('contextualize')
64+
65+
contextualize(() => {
66+
Bugsnag.addMetadata('test', { meta: 'data' })
67+
// @ts-ignore
68+
expect(Bugsnag._client._clientContext.getStore()._metadata).toStrictEqual({ test: { meta: 'data' } })
69+
70+
expect(Bugsnag.getMetadata('test')).toStrictEqual({ meta: 'data' })
71+
})
72+
73+
// @ts-ignore
74+
expect(Bugsnag._client._metadata).toStrictEqual({})
75+
expect(Bugsnag.getMetadata('test')).toBeUndefined()
76+
})
77+
})
78+
})
79+
80+
describe('clearMetadata()', () => {
81+
it('clears metadata previously set on the client', () => {
82+
Bugsnag.start('abcd12abcd12abcd12abcd12abcd12abcd')
83+
Bugsnag.addMetadata('test', { meta: 'data' })
84+
Bugsnag.clearMetadata('test')
85+
86+
// @ts-ignore
87+
expect(Bugsnag._client._metadata).toStrictEqual({})
88+
})
89+
90+
describe('when in an async context', () => {
91+
it('clears metadata previously set on the cloned client not not the base client', () => {
92+
Bugsnag.start('abcd12abcd12abcd12abcd12abcd12abcd')
93+
Bugsnag.addMetadata('test', { meta: 'data' })
94+
const contextualize = Bugsnag.getPlugin('contextualize')
95+
96+
contextualize(() => {
97+
Bugsnag.addMetadata('test', { meta: 'data' })
98+
Bugsnag.clearMetadata('test')
99+
// @ts-ignore
100+
expect(Bugsnag._client._clientContext.getStore()._metadata).toStrictEqual({})
101+
})
102+
103+
// @ts-ignore
104+
expect(Bugsnag._client._metadata).toStrictEqual({ test: { meta: 'data' } })
105+
})
106+
})
107+
})
108+
109+
describe('addFeatureFlag()', () => {
110+
it('adds a feature flag to the client', () => {
111+
Bugsnag.start('abcd12abcd12abcd12abcd12abcd12abcd')
112+
Bugsnag.addFeatureFlag('test')
113+
// @ts-ignore
114+
expect(Bugsnag._client._features[0].name).toBe('test')
115+
})
116+
117+
describe('when in an async context', () => {
118+
it('adds a feature flag to the cloned client not not the base client', () => {
119+
Bugsnag.start('abcd12abcd12abcd12abcd12abcd12abcd')
120+
const contextualize = Bugsnag.getPlugin('contextualize')
121+
122+
contextualize(() => {
123+
Bugsnag.addFeatureFlag('test')
124+
// @ts-ignore
125+
expect(Bugsnag._client._clientContext.getStore()._features[0].name).toBe('test')
126+
})
127+
128+
// @ts-ignore
129+
expect(Bugsnag._client._features.length).toBe(0)
130+
})
131+
})
132+
})
133+
134+
describe('addFeatureFlags()', () => {
135+
it('adds feature flags to the client', () => {
136+
Bugsnag.start('abcd12abcd12abcd12abcd12abcd12abcd')
137+
Bugsnag.addFeatureFlags([{ name: 'test' }, { name: 'other' }])
138+
// @ts-ignore
139+
expect(Bugsnag._client._features).toStrictEqual([
140+
{ name: 'test', variant: null },
141+
{ name: 'other', variant: null }
142+
])
143+
})
144+
145+
describe('when in an async context', () => {
146+
it('adds feature flags to the cloned client not not the base client', () => {
147+
Bugsnag.start('abcd12abcd12abcd12abcd12abcd12abcd')
148+
const contextualize = Bugsnag.getPlugin('contextualize')
149+
150+
contextualize(() => {
151+
Bugsnag.addFeatureFlags([{ name: 'test' }, { name: 'other' }])
152+
// @ts-ignore
153+
expect(Bugsnag._client._clientContext.getStore()._features).toStrictEqual([
154+
{ name: 'test', variant: null },
155+
{ name: 'other', variant: null }
156+
])
157+
})
158+
159+
// @ts-ignore
160+
expect(Bugsnag._client._features).toStrictEqual([])
161+
})
162+
})
163+
})
164+
165+
describe('clearFeatureFlag()', () => {
166+
it('clears a feature flag set on the client', () => {
167+
Bugsnag.start('abcd12abcd12abcd12abcd12abcd12abcd')
168+
Bugsnag.addFeatureFlags([{ name: 'test' }, { name: 'other' }])
169+
Bugsnag.clearFeatureFlag('test')
170+
// @ts-ignore
171+
expect(Bugsnag._client._features).toStrictEqual([
172+
null,
173+
{ name: 'other', variant: null }
174+
])
175+
})
176+
177+
describe('when in an async context', () => {
178+
it('clears a feature flag previously set on the cloned client not not the base client', () => {
179+
Bugsnag.start('abcd12abcd12abcd12abcd12abcd12abcd')
180+
Bugsnag.addFeatureFlags([{ name: 'test' }, { name: 'other' }])
181+
const contextualize = Bugsnag.getPlugin('contextualize')
182+
183+
contextualize(() => {
184+
Bugsnag.addFeatureFlags([{ name: 'test' }, { name: 'other' }])
185+
Bugsnag.clearFeatureFlag('test')
186+
// @ts-ignore
187+
expect(Bugsnag._client._clientContext.getStore()._features).toStrictEqual([
188+
null,
189+
{ name: 'other', variant: null }
190+
])
191+
})
192+
193+
// @ts-ignore
194+
expect(Bugsnag._client._features).toStrictEqual([
195+
{ name: 'test', variant: null },
196+
{ name: 'other', variant: null }
197+
])
198+
})
199+
})
200+
})
201+
202+
describe('clearFeatureFlags()', () => {
203+
it('clears feature flags set on the client', () => {
204+
Bugsnag.start('abcd12abcd12abcd12abcd12abcd12abcd')
205+
Bugsnag.addFeatureFlags([{ name: 'test' }, { name: 'other' }])
206+
Bugsnag.clearFeatureFlags()
207+
// @ts-ignore
208+
expect(Bugsnag._client._features).toStrictEqual([])
209+
})
210+
211+
describe('when in an async context', () => {
212+
it('clears feature flags previously set on the cloned client not not the base client', () => {
213+
Bugsnag.start('abcd12abcd12abcd12abcd12abcd12abcd')
214+
Bugsnag.addFeatureFlags([{ name: 'test' }, { name: 'other' }])
215+
const contextualize = Bugsnag.getPlugin('contextualize')
216+
217+
contextualize(() => {
218+
Bugsnag.addFeatureFlags([{ name: 'test' }, { name: 'other' }])
219+
Bugsnag.clearFeatureFlags()
220+
// @ts-ignore
221+
expect(Bugsnag._client._clientContext.getStore()._features).toStrictEqual([])
222+
})
223+
224+
// @ts-ignore
225+
expect(Bugsnag._client._features).toStrictEqual([
226+
{ name: 'test', variant: null },
227+
{ name: 'other', variant: null }
228+
])
229+
})
230+
})
231+
})
232+
233+
describe('setContext() and getContext()', () => {
234+
it('sets the context on the client', () => {
235+
Bugsnag.start('abcd12abcd12abcd12abcd12abcd12abcd')
236+
expect(Bugsnag.getContext()).toBeUndefined()
237+
Bugsnag.setContext('my context')
238+
expect(Bugsnag.getContext()).toBe('my context')
239+
})
240+
241+
describe('when in an async context', () => {
242+
it('sets the context on the cloned client not not the base client', () => {
243+
Bugsnag.start('abcd12abcd12abcd12abcd12abcd12abcd')
244+
const contextualize = Bugsnag.getPlugin('contextualize')
245+
246+
contextualize(() => {
247+
Bugsnag.setContext('my context')
248+
expect(Bugsnag.getContext()).toBe('my context')
249+
})
250+
251+
expect(Bugsnag.getContext()).toBeUndefined()
252+
})
253+
})
254+
})
255+
256+
describe('setUser() and getUser()', () => {
257+
it('sets the context on the client', () => {
258+
Bugsnag.start('abcd12abcd12abcd12abcd12abcd12abcd')
259+
expect(Bugsnag.getUser()).toStrictEqual({})
260+
Bugsnag.setUser('my user id')
261+
expect(Bugsnag.getUser()).toStrictEqual({ id: 'my user id', email: undefined, name: undefined })
262+
})
263+
264+
describe('when in an async context', () => {
265+
it('sets the context on the cloned client not not the base client', () => {
266+
Bugsnag.start('abcd12abcd12abcd12abcd12abcd12abcd')
267+
const contextualize = Bugsnag.getPlugin('contextualize')
268+
269+
contextualize(() => {
270+
Bugsnag.setUser('my user id')
271+
expect(Bugsnag.getUser()).toStrictEqual({ id: 'my user id', email: undefined, name: undefined })
272+
})
273+
274+
expect(Bugsnag.getUser()).toStrictEqual({})
275+
})
276+
})
277+
})
278+
279+
describe('leaveBreadcrumb()', () => {
280+
it('adds a breadcrumb to the client', () => {
281+
Bugsnag.start('abcd12abcd12abcd12abcd12abcd12abcd')
282+
Bugsnag.leaveBreadcrumb('test')
283+
// @ts-ignore
284+
expect(Bugsnag._client._breadcrumbs[0].message).toBe('test')
285+
})
286+
287+
describe('when in an async context', () => {
288+
it('adds a breadcrumb to the cloned client not not the base client', () => {
289+
Bugsnag.start('abcd12abcd12abcd12abcd12abcd12abcd')
290+
const contextualize = Bugsnag.getPlugin('contextualize')
291+
292+
contextualize(() => {
293+
Bugsnag.leaveBreadcrumb('test')
294+
// @ts-ignore
295+
expect(Bugsnag._client._clientContext.getStore()._breadcrumbs[0].message).toBe('test')
296+
})
297+
298+
// @ts-ignore
299+
expect(Bugsnag._client._breadcrumbs.length).toBe(0)
300+
})
301+
})
302+
})
24303
})

‎packages/plugin-express/src/express.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ module.exports = {
5555
next(err)
5656
}
5757

58-
return { requestHandler, errorHandler }
58+
const runInContext = (req, res, next) => {
59+
client._clientContext.run(req.bugsnag, next)
60+
}
61+
62+
return { requestHandler, errorHandler, runInContext }
5963
}
6064
}
6165

‎packages/plugin-express/types/bugsnag-express.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export default bugsnagPluginExpress
77
interface BugsnagPluginExpressResult {
88
errorHandler: express.ErrorRequestHandler
99
requestHandler: express.RequestHandler
10+
runInContext: express.RequestHandler
1011
}
1112

1213
// add a new call signature for the getPlugin() method that types the plugin result

0 commit comments

Comments
 (0)