Skip to content

Commit e379e7b

Browse files
authored
feat: ✨ return callback value from contextualize plugin (#2654)
2 parents cd8fcba + e521d74 commit e379e7b

File tree

3 files changed

+157
-1
lines changed

3 files changed

+157
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Added
66

77
(plugin-cloudflare-workers): Add initial support for Cloudflare Workers [#2643](https://github.com/bugsnag/bugsnag-js/pull/2643) [#2644](https://github.com/bugsnag/bugsnag-js/pull/2644)
8+
(plugin-contextualize) Return callback value from contextualize plugin [#2654](https://github.com/bugsnag/bugsnag-js/pull/2654)
89

910
### Fixed
1011

packages/plugin-contextualize/contextualize.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ module.exports = {
1717

1818
clonedClient.addOnError(onError)
1919

20-
client._clientContext.run(clonedClient, fn)
20+
return client._clientContext.run(clonedClient, fn)
2121
}
2222

2323
return contextualize
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
const Client = require('@bugsnag/core/client')
2+
const plugin = require('../contextualize')
3+
const { AsyncLocalStorage } = require('async_hooks')
4+
5+
describe('plugin: contextualize', () => {
6+
describe('client cloning', () => {
7+
it('creates a cloned client for the contextualized scope', () => {
8+
const c = new Client({ apiKey: 'api_key', plugins: [plugin] })
9+
c._clientContext = new AsyncLocalStorage()
10+
const contextualize = c.getPlugin('contextualize')
11+
12+
let capturedClient = null
13+
contextualize(() => {
14+
capturedClient = c._clientContext.getStore()
15+
}, (event) => {})
16+
17+
expect(capturedClient).not.toBe(c)
18+
expect(capturedClient).toBeTruthy()
19+
})
20+
21+
it('sets fallbackStack on the cloned client', () => {
22+
const c = new Client({ apiKey: 'api_key', plugins: [plugin] })
23+
c._clientContext = new AsyncLocalStorage()
24+
const contextualize = c.getPlugin('contextualize')
25+
26+
let capturedClient = null
27+
contextualize(() => {
28+
capturedClient = c._clientContext.getStore()
29+
}, (event) => {})
30+
31+
expect(capturedClient.fallbackStack).toBeDefined()
32+
expect(typeof capturedClient.fallbackStack).toBe('string')
33+
})
34+
})
35+
36+
describe('onError callback', () => {
37+
it('adds the onError callback to the cloned client', () => {
38+
const c = new Client({ apiKey: 'api_key', plugins: [plugin] })
39+
c._clientContext = new AsyncLocalStorage()
40+
const contextualize = c.getPlugin('contextualize')
41+
42+
const onError = jest.fn()
43+
let capturedClient = null
44+
45+
contextualize(() => {
46+
capturedClient = c._clientContext.getStore()
47+
}, onError)
48+
49+
expect(capturedClient._cbs.e.length).toBeGreaterThan(0)
50+
})
51+
52+
it('does not add onError to the parent client', () => {
53+
const c = new Client({ apiKey: 'api_key', plugins: [plugin] })
54+
c._clientContext = new AsyncLocalStorage()
55+
const contextualize = c.getPlugin('contextualize')
56+
57+
const initialCallbackCount = c._cbs.e.length
58+
59+
contextualize(() => {}, (event) => {
60+
event.addMetadata('test', { key: 'value' })
61+
})
62+
63+
expect(c._cbs.e.length).toBe(initialCallbackCount)
64+
})
65+
})
66+
67+
describe('context isolation', () => {
68+
it('isolates metadata changes within the contextualized scope', () => {
69+
const c = new Client({ apiKey: 'api_key', plugins: [plugin] })
70+
c._clientContext = new AsyncLocalStorage()
71+
const contextualize = c.getPlugin('contextualize')
72+
73+
let clonedClient = null
74+
contextualize(() => {
75+
clonedClient = c._clientContext.getStore()
76+
clonedClient.addMetadata('custom', { foo: 'bar' })
77+
}, (event) => {})
78+
79+
expect(c.getMetadata('custom')).toBeUndefined()
80+
expect(clonedClient.getMetadata('custom')).toEqual({ foo: 'bar' })
81+
})
82+
})
83+
84+
describe('async execution', () => {
85+
it('maintains context in async operations', async () => {
86+
const c = new Client({ apiKey: 'api_key', plugins: [plugin] })
87+
c._clientContext = new AsyncLocalStorage()
88+
const contextualize = c.getPlugin('contextualize')
89+
90+
let capturedClient = null
91+
await contextualize(async () => {
92+
await new Promise(resolve => setTimeout(resolve, 10))
93+
capturedClient = c._clientContext.getStore()
94+
}, (event) => {})
95+
96+
expect(capturedClient).toBeTruthy()
97+
expect(capturedClient.fallbackStack).toBeDefined()
98+
})
99+
100+
it('returns a promise when callback is async', async () => {
101+
const c = new Client({ apiKey: 'api_key', plugins: [plugin] })
102+
c._clientContext = new AsyncLocalStorage()
103+
const contextualize = c.getPlugin('contextualize')
104+
105+
const result = contextualize(async () => {
106+
return 'async value'
107+
}, (event) => {})
108+
109+
expect(result).toBeInstanceOf(Promise)
110+
await expect(result).resolves.toBe('async value')
111+
})
112+
})
113+
114+
describe('callback execution', () => {
115+
it('executes the callback function', () => {
116+
const c = new Client({ apiKey: 'api_key', plugins: [plugin] })
117+
c._clientContext = new AsyncLocalStorage()
118+
const contextualize = c.getPlugin('contextualize')
119+
120+
const callback = jest.fn(() => 'result')
121+
contextualize(callback, (event) => {})
122+
123+
expect(callback).toHaveBeenCalledTimes(1)
124+
})
125+
126+
it('passes through exceptions from the callback', () => {
127+
const c = new Client({ apiKey: 'api_key', plugins: [plugin] })
128+
c._clientContext = new AsyncLocalStorage()
129+
const contextualize = c.getPlugin('contextualize')
130+
131+
const error = new Error('test error')
132+
expect(() => {
133+
contextualize(() => {
134+
throw error
135+
}, (event) => {})
136+
}).toThrow(error)
137+
})
138+
})
139+
140+
describe('return value', () => {
141+
it('returns the value returned by the callback function', () => {
142+
const c = new Client({ apiKey: 'api_key', plugins: [plugin] })
143+
c._clientContext = new AsyncLocalStorage()
144+
const contextualize = c.getPlugin('contextualize')
145+
146+
const result = contextualize(() => {
147+
return 'test value'
148+
}, (event) => {
149+
event.addMetadata('test', { key: 'value' })
150+
})
151+
152+
expect(result).toBe('test value')
153+
})
154+
})
155+
})

0 commit comments

Comments
 (0)