Skip to content

Commit ba6f6b1

Browse files
committed
Add unit tests for node-core/light
1 parent 6d9270e commit ba6f6b1

File tree

4 files changed

+725
-0
lines changed

4 files changed

+725
-0
lines changed
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { createTransport, getCurrentScope, getGlobalScope, getIsolationScope, resolvedSyncPromise } from '@sentry/core';
2+
import { init } from '../../src/light/sdk';
3+
import type { NodeClientOptions } from '../../src/types';
4+
5+
const PUBLIC_DSN = 'https://username@domain/123';
6+
7+
export function resetGlobals(): void {
8+
getCurrentScope().clear();
9+
getCurrentScope().setClient(undefined);
10+
getIsolationScope().clear();
11+
getGlobalScope().clear();
12+
}
13+
14+
export function mockLightSdkInit(options?: Partial<NodeClientOptions>) {
15+
resetGlobals();
16+
const client = init({
17+
dsn: PUBLIC_DSN,
18+
defaultIntegrations: false,
19+
// We are disabling client reports because we would be acquiring resources with every init call and that would leak
20+
// memory every time we call init in the tests
21+
sendClientReports: false,
22+
// Use a mock transport to prevent network calls
23+
transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})),
24+
...options,
25+
});
26+
27+
return client;
28+
}
29+
30+
export function cleanupLightSdk(): void {
31+
resetGlobals();
32+
}
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
import { afterEach, describe, expect, it } from 'vitest';
2+
import * as Sentry from '../../src/light';
3+
import { cleanupLightSdk, mockLightSdkInit, resetGlobals } from '../helpers/mockLightSdkInit';
4+
5+
describe('Light Mode | AsyncLocalStorage Strategy', () => {
6+
afterEach(() => {
7+
cleanupLightSdk();
8+
});
9+
10+
describe('scope isolation with setTimeout', () => {
11+
it('maintains scope across setTimeout', async () => {
12+
mockLightSdkInit();
13+
14+
const result = await new Promise<string>(resolve => {
15+
Sentry.withScope(scope => {
16+
scope.setTag('asyncTag', 'asyncValue');
17+
18+
setTimeout(() => {
19+
const tag = Sentry.getCurrentScope().getScopeData().tags?.asyncTag;
20+
resolve(tag as string);
21+
}, 10);
22+
});
23+
});
24+
25+
expect(result).toBe('asyncValue');
26+
});
27+
28+
it('isolates scopes across concurrent setTimeout calls', async () => {
29+
mockLightSdkInit();
30+
31+
const results = await Promise.all([
32+
new Promise<string>(resolve => {
33+
Sentry.withScope(scope => {
34+
scope.setTag('id', 'first');
35+
setTimeout(() => {
36+
resolve(Sentry.getCurrentScope().getScopeData().tags?.id as string);
37+
}, 20);
38+
});
39+
}),
40+
new Promise<string>(resolve => {
41+
Sentry.withScope(scope => {
42+
scope.setTag('id', 'second');
43+
setTimeout(() => {
44+
resolve(Sentry.getCurrentScope().getScopeData().tags?.id as string);
45+
}, 10);
46+
});
47+
}),
48+
]);
49+
50+
expect(results).toEqual(['first', 'second']);
51+
});
52+
});
53+
54+
describe('scope isolation with Promises', () => {
55+
it('maintains scope across Promise chains', async () => {
56+
mockLightSdkInit();
57+
58+
const result = await Sentry.withScope(async scope => {
59+
scope.setTag('promiseTag', 'promiseValue');
60+
61+
await Promise.resolve();
62+
63+
return Sentry.getCurrentScope().getScopeData().tags?.promiseTag;
64+
});
65+
66+
expect(result).toBe('promiseValue');
67+
});
68+
69+
it('isolates scopes across concurrent Promise.all', async () => {
70+
mockLightSdkInit();
71+
72+
const results = await Promise.all(
73+
[1, 2, 3].map(id =>
74+
Sentry.withScope(async scope => {
75+
scope.setTag('id', `value-${id}`);
76+
77+
// Simulate async work
78+
await new Promise(resolve => setTimeout(resolve, Math.random() * 20));
79+
80+
return Sentry.getCurrentScope().getScopeData().tags?.id;
81+
}),
82+
),
83+
);
84+
85+
expect(results).toEqual(['value-1', 'value-2', 'value-3']);
86+
});
87+
});
88+
89+
describe('scope isolation with async/await', () => {
90+
it('maintains isolation scope across async/await', async () => {
91+
mockLightSdkInit();
92+
93+
const result = await Sentry.withIsolationScope(async isolationScope => {
94+
isolationScope.setUser({ id: 'async-user' });
95+
96+
await Promise.resolve();
97+
98+
return Sentry.getIsolationScope().getScopeData().user?.id;
99+
});
100+
101+
expect(result).toBe('async-user');
102+
});
103+
104+
it('maintains both current and isolation scope across async boundaries', async () => {
105+
mockLightSdkInit();
106+
107+
const result = await Sentry.withIsolationScope(async isolationScope => {
108+
isolationScope.setTag('isolationTag', 'isolationValue');
109+
110+
return Sentry.withScope(async currentScope => {
111+
currentScope.setTag('currentTag', 'currentValue');
112+
113+
await new Promise(resolve => setTimeout(resolve, 10));
114+
115+
return {
116+
isolationTag: Sentry.getIsolationScope().getScopeData().tags?.isolationTag,
117+
currentTag: Sentry.getCurrentScope().getScopeData().tags?.currentTag,
118+
};
119+
});
120+
});
121+
122+
expect(result).toEqual({
123+
isolationTag: 'isolationValue',
124+
currentTag: 'currentValue',
125+
});
126+
});
127+
});
128+
129+
describe('suppressTracing', () => {
130+
it('sets suppression metadata on scope', () => {
131+
mockLightSdkInit();
132+
133+
Sentry.suppressTracing(() => {
134+
const metadata = Sentry.getCurrentScope().getScopeData().sdkProcessingMetadata;
135+
expect(metadata?.__SENTRY_SUPPRESS_TRACING__).toBe(true);
136+
});
137+
});
138+
139+
it('does not affect outer scope', () => {
140+
mockLightSdkInit();
141+
142+
Sentry.suppressTracing(() => {
143+
// Inside suppressTracing
144+
});
145+
146+
const metadata = Sentry.getCurrentScope().getScopeData().sdkProcessingMetadata;
147+
expect(metadata?.__SENTRY_SUPPRESS_TRACING__).toBeUndefined();
148+
});
149+
});
150+
151+
describe('nested withScope and withIsolationScope', () => {
152+
it('correctly nests isolation and current scopes', async () => {
153+
mockLightSdkInit();
154+
155+
const initialIsolationScope = Sentry.getIsolationScope();
156+
const initialCurrentScope = Sentry.getCurrentScope();
157+
158+
await Sentry.withIsolationScope(async isolationScope1 => {
159+
expect(Sentry.getIsolationScope()).toBe(isolationScope1);
160+
expect(Sentry.getIsolationScope()).not.toBe(initialIsolationScope);
161+
// Current scope should also be forked
162+
expect(Sentry.getCurrentScope()).not.toBe(initialCurrentScope);
163+
164+
isolationScope1.setTag('level', '1');
165+
166+
await Sentry.withScope(async currentScope1 => {
167+
expect(Sentry.getCurrentScope()).toBe(currentScope1);
168+
currentScope1.setTag('current', '1');
169+
170+
await Sentry.withIsolationScope(async isolationScope2 => {
171+
expect(Sentry.getIsolationScope()).toBe(isolationScope2);
172+
expect(Sentry.getIsolationScope()).not.toBe(isolationScope1);
173+
174+
// Should inherit from parent isolation scope
175+
expect(isolationScope2.getScopeData().tags?.level).toBe('1');
176+
isolationScope2.setTag('level', '2');
177+
178+
// Parent should be unchanged
179+
expect(isolationScope1.getScopeData().tags?.level).toBe('1');
180+
});
181+
182+
// After exiting nested isolation scope, we should be back to original
183+
expect(Sentry.getIsolationScope()).toBe(isolationScope1);
184+
});
185+
});
186+
187+
// After exiting all scopes, we should be back to initial
188+
expect(Sentry.getIsolationScope()).toBe(initialIsolationScope);
189+
expect(Sentry.getCurrentScope()).toBe(initialCurrentScope);
190+
});
191+
});
192+
193+
describe('fallback behavior', () => {
194+
it('returns default scopes when AsyncLocalStorage store is empty', () => {
195+
resetGlobals();
196+
// Before init, should still return valid scopes
197+
const currentScope = Sentry.getCurrentScope();
198+
const isolationScope = Sentry.getIsolationScope();
199+
200+
expect(currentScope).toBeDefined();
201+
expect(isolationScope).toBeDefined();
202+
203+
// Should be able to set data on them
204+
currentScope.setTag('test', 'value');
205+
expect(currentScope.getScopeData().tags?.test).toBe('value');
206+
});
207+
});
208+
});

0 commit comments

Comments
 (0)