Skip to content

Commit 0704d38

Browse files
mydeaanonrig
authored andcommitted
fix(core): Ensure withScope sets current scope correctly with async callbacks (#9974)
Oops, I noticed that our current `withScope` implementation does not actually wait for async callbacks for setting the current scope 😬 Related to #9958, which I copied there now!
1 parent cd1e3bc commit 0704d38

File tree

2 files changed

+109
-4
lines changed

2 files changed

+109
-4
lines changed

packages/core/src/hub.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,15 @@ import type {
2020
TransactionContext,
2121
User,
2222
} from '@sentry/types';
23-
import { GLOBAL_OBJ, consoleSandbox, dateTimestampInSeconds, getGlobalSingleton, logger, uuid4 } from '@sentry/utils';
23+
import {
24+
GLOBAL_OBJ,
25+
consoleSandbox,
26+
dateTimestampInSeconds,
27+
getGlobalSingleton,
28+
isThenable,
29+
logger,
30+
uuid4,
31+
} from '@sentry/utils';
2432

2533
import { DEFAULT_ENVIRONMENT } from './constants';
2634
import { DEBUG_BUILD } from './debug-build';
@@ -177,12 +185,35 @@ export class Hub implements HubInterface {
177185
public withScope<T>(callback: (scope: Scope) => T): T {
178186
// eslint-disable-next-line deprecation/deprecation
179187
const scope = this.pushScope();
188+
189+
let maybePromiseResult: T;
180190
try {
181-
return callback(scope);
182-
} finally {
191+
maybePromiseResult = callback(scope);
192+
} catch (e) {
183193
// eslint-disable-next-line deprecation/deprecation
184194
this.popScope();
195+
throw e;
185196
}
197+
198+
if (isThenable(maybePromiseResult)) {
199+
// @ts-expect-error - isThenable returns the wrong type
200+
return maybePromiseResult.then(
201+
res => {
202+
// eslint-disable-next-line deprecation/deprecation
203+
this.popScope();
204+
return res;
205+
},
206+
e => {
207+
// eslint-disable-next-line deprecation/deprecation
208+
this.popScope();
209+
throw e;
210+
},
211+
);
212+
}
213+
214+
// eslint-disable-next-line deprecation/deprecation
215+
this.popScope();
216+
return maybePromiseResult;
186217
}
187218

188219
/**

packages/core/test/lib/exports.test.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,86 @@ describe('withScope', () => {
4242
expect(res).toBe('foo');
4343
});
4444

45-
it('works with an async function', async () => {
45+
it('works with an async function return value', async () => {
4646
const res = withScope(async scope => {
4747
return 'foo';
4848
});
4949

5050
expect(res).toBeInstanceOf(Promise);
5151
expect(await res).toBe('foo');
5252
});
53+
54+
it('correctly sets & resets the current scope', () => {
55+
const scope1 = getCurrentScope();
56+
57+
withScope(scope2 => {
58+
expect(scope2).not.toBe(scope1);
59+
expect(getCurrentScope()).toBe(scope2);
60+
});
61+
62+
withScope(scope3 => {
63+
expect(scope3).not.toBe(scope1);
64+
expect(getCurrentScope()).toBe(scope3);
65+
});
66+
67+
expect(getCurrentScope()).toBe(scope1);
68+
});
69+
70+
it('correctly sets & resets the current scope with async functions', async () => {
71+
const scope1 = getCurrentScope();
72+
73+
await withScope(async scope2 => {
74+
expect(scope2).not.toBe(scope1);
75+
expect(getCurrentScope()).toBe(scope2);
76+
77+
await new Promise(resolve => setTimeout(resolve, 10));
78+
79+
expect(getCurrentScope()).toBe(scope2);
80+
});
81+
82+
await withScope(async scope3 => {
83+
expect(scope3).not.toBe(scope1);
84+
expect(getCurrentScope()).toBe(scope3);
85+
86+
await new Promise(resolve => setTimeout(resolve, 10));
87+
88+
expect(getCurrentScope()).toBe(scope3);
89+
});
90+
91+
expect(getCurrentScope()).toBe(scope1);
92+
});
93+
94+
it('correctly sets & resets the current scope when an error happens', () => {
95+
const scope1 = getCurrentScope();
96+
97+
const error = new Error('foo');
98+
99+
expect(() =>
100+
withScope(scope2 => {
101+
expect(scope2).not.toBe(scope1);
102+
expect(getCurrentScope()).toBe(scope2);
103+
104+
throw error;
105+
}),
106+
).toThrow(error);
107+
108+
expect(getCurrentScope()).toBe(scope1);
109+
});
110+
111+
it('correctly sets & resets the current scope with async functions & errors', async () => {
112+
const scope1 = getCurrentScope();
113+
114+
const error = new Error('foo');
115+
116+
await expect(
117+
withScope(async scope2 => {
118+
expect(scope2).not.toBe(scope1);
119+
expect(getCurrentScope()).toBe(scope2);
120+
121+
throw error;
122+
}),
123+
).rejects.toBe(error);
124+
125+
expect(getCurrentScope()).toBe(scope1);
126+
});
53127
});

0 commit comments

Comments
 (0)