Skip to content

Commit 3c6ed94

Browse files
committed
add unit tests
1 parent 2e8dc1d commit 3c6ed94

File tree

1 file changed

+127
-1
lines changed

1 file changed

+127
-1
lines changed

packages/browser/test/profiling/traceLifecycle.test.ts

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
import * as Sentry from '@sentry/browser';
6-
import { afterEach, describe, expect, it, vi } from 'vitest';
6+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
77

88
describe('Browser Profiling v2 trace lifecycle', () => {
99
afterEach(async () => {
@@ -63,4 +63,130 @@ describe('Browser Profiling v2 trace lifecycle', () => {
6363
expect(send).not.toHaveBeenCalled();
6464
warn.mockRestore();
6565
});
66+
67+
describe('profiling lifecycle behavior', () => {
68+
beforeEach(() => {
69+
vi.useFakeTimers();
70+
});
71+
72+
afterEach(() => {
73+
vi.useRealTimers();
74+
});
75+
76+
it('starts on first sampled root span and sends a chunk on stop', async () => {
77+
const { stop, mockConstructor } = mockProfiler();
78+
const send = vi.fn().mockResolvedValue(undefined);
79+
80+
Sentry.init({
81+
dsn: 'https://[email protected]/1',
82+
tracesSampleRate: 1,
83+
profileSessionSampleRate: 1,
84+
profileLifecycle: 'trace',
85+
integrations: [Sentry.browserProfilingIntegration()],
86+
transport: () => ({ flush: vi.fn().mockResolvedValue(true), send }),
87+
});
88+
89+
let spanRef: any;
90+
Sentry.startSpanManual({ name: 'root-1', parentSpan: null, forceTransaction: true }, span => {
91+
spanRef = span;
92+
});
93+
94+
expect(mockConstructor).toHaveBeenCalledTimes(1);
95+
// Ending the only root span should flush one chunk immediately
96+
spanRef.end();
97+
98+
// Resolve any pending microtasks
99+
await Promise.resolve();
100+
101+
expect(stop).toHaveBeenCalledTimes(1);
102+
expect(send).toHaveBeenCalledTimes(2); // one for transaction, one for profile_chunk
103+
const transactionEnvelopeHeader = send.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0];
104+
const profileChunkEnvelopeHeader = send.mock.calls?.[1]?.[0]?.[1]?.[0]?.[0];
105+
expect(profileChunkEnvelopeHeader?.type).toBe('profile_chunk');
106+
expect(transactionEnvelopeHeader?.type).toBe('transaction');
107+
});
108+
109+
it('continues while any sampled root span is active; stops after last ends', async () => {
110+
const { stop, mockConstructor } = mockProfiler();
111+
const send = vi.fn().mockResolvedValue(undefined);
112+
113+
Sentry.init({
114+
dsn: 'https://[email protected]/1',
115+
tracesSampleRate: 1,
116+
profileSessionSampleRate: 1,
117+
profileLifecycle: 'trace',
118+
integrations: [Sentry.browserProfilingIntegration()],
119+
transport: () => ({ flush: vi.fn().mockResolvedValue(true), send }),
120+
});
121+
122+
let spanA: any;
123+
Sentry.startSpanManual({ name: 'root-A', parentSpan: null, forceTransaction: true }, span => {
124+
spanA = span;
125+
});
126+
127+
let spanB: any;
128+
Sentry.startSpanManual({ name: 'root-B', parentSpan: null, forceTransaction: true }, span => {
129+
spanB = span;
130+
});
131+
132+
expect(mockConstructor).toHaveBeenCalledTimes(1);
133+
134+
// End first root span -> still one active sampled root span; no send yet
135+
spanA.end();
136+
await Promise.resolve();
137+
expect(stop).toHaveBeenCalledTimes(0);
138+
expect(send).toHaveBeenCalledTimes(1); // only transaction so far
139+
const envelopeHeadersTxn = send.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0];
140+
expect(envelopeHeadersTxn?.type).toBe('transaction');
141+
142+
// End last root span -> should flush one chunk
143+
spanB.end();
144+
await Promise.resolve();
145+
expect(stop).toHaveBeenCalledTimes(1);
146+
expect(send).toHaveBeenCalledTimes(3);
147+
const envelopeHeadersTxn1 = send.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0];
148+
const envelopeHeadersTxn2 = send.mock.calls?.[1]?.[0]?.[1]?.[0]?.[0];
149+
const envelopeHeadersProfile = send.mock.calls?.[2]?.[0]?.[1]?.[0]?.[0];
150+
151+
expect(envelopeHeadersTxn1?.type).toBe('transaction');
152+
expect(envelopeHeadersTxn2?.type).toBe('transaction');
153+
expect(envelopeHeadersProfile?.type).toBe('profile_chunk');
154+
});
155+
156+
it('sends a periodic chunk every 60s while running and restarts profiler', async () => {
157+
const { stop, mockConstructor } = mockProfiler();
158+
const send = vi.fn().mockResolvedValue(undefined);
159+
160+
Sentry.init({
161+
dsn: 'https://[email protected]/1',
162+
tracesSampleRate: 1,
163+
profileSessionSampleRate: 1,
164+
profileLifecycle: 'trace',
165+
integrations: [Sentry.browserProfilingIntegration()],
166+
transport: () => ({ flush: vi.fn().mockResolvedValue(true), send }),
167+
});
168+
169+
let spanRef: any;
170+
Sentry.startSpanManual({ name: 'root-interval', parentSpan: null, forceTransaction: true }, span => {
171+
spanRef = span;
172+
});
173+
174+
expect(mockConstructor).toHaveBeenCalledTimes(1);
175+
176+
// Advance timers by 60s to trigger scheduled chunk collection
177+
vi.advanceTimersByTime(60_000);
178+
await Promise.resolve();
179+
180+
// One chunk sent and profiler restarted (second constructor call)
181+
expect(stop).toHaveBeenCalledTimes(1);
182+
expect(send).toHaveBeenCalledTimes(1);
183+
expect(mockConstructor).toHaveBeenCalledTimes(2);
184+
const envelopeHeaders = send.mock.calls?.[0]?.[0]?.[1]?.[0]?.[0];
185+
expect(envelopeHeaders?.type).toBe('profile_chunk');
186+
187+
// Clean up
188+
spanRef.end();
189+
await Promise.resolve();
190+
});
191+
});
66192
});

0 commit comments

Comments
 (0)