Skip to content

Commit 27b4026

Browse files
committed
add integration tests
1 parent 36107e8 commit 27b4026

File tree

3 files changed

+189
-1
lines changed

3 files changed

+189
-1
lines changed

dev-packages/browser-integration-tests/suites/profiling/legacyMode/subject.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function fibonacci(n) {
1717
return fibonacci(n - 1) + fibonacci(n - 2);
1818
}
1919

20-
await Sentry.startSpanManual({ name: 'root-fibonacci-2', parentSpan: null, forceTransaction: true }, async span => {
20+
await Sentry.startSpanManual({ name: 'root-fibonacci', parentSpan: null, forceTransaction: true }, async span => {
2121
fibonacci(30);
2222

2323
// Timeout to prevent flaky tests. Integration samples every 20ms, if function is too fast it might not get sampled
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import * as Sentry from '@sentry/browser';
2+
import { browserProfilingIntegration } from '@sentry/browser';
3+
4+
window.Sentry = Sentry;
5+
6+
Sentry.init({
7+
dsn: 'https://[email protected]/1337',
8+
integrations: [browserProfilingIntegration()],
9+
tracesSampleRate: 1,
10+
profileSessionSampleRate: 1,
11+
});
12+
13+
function largeSum(amount = 1000000) {
14+
let sum = 0;
15+
for (let i = 0; i < amount; i++) {
16+
sum += Math.sqrt(i) * Math.sin(i);
17+
}
18+
}
19+
20+
function fibonacci(n) {
21+
if (n <= 1) {
22+
return n;
23+
}
24+
return fibonacci(n - 1) + fibonacci(n - 2);
25+
}
26+
27+
let firstSpan;
28+
29+
Sentry.startSpanManual({ name: 'root-largeSum-1', parentSpan: null, forceTransaction: true }, span => {
30+
largeSum();
31+
firstSpan = span;
32+
});
33+
34+
await Sentry.startSpanManual({ name: 'root-fibonacci-2', parentSpan: null, forceTransaction: true }, async span => {
35+
fibonacci(40);
36+
37+
// Timeout to prevent flaky tests. Integration samples every 20ms, if function is too fast it might not get sampled
38+
await new Promise(resolve => setTimeout(resolve, 21));
39+
span.end();
40+
});
41+
42+
await new Promise(r => setTimeout(r, 21));
43+
44+
firstSpan.end();
45+
46+
// Ensure envelope flush
47+
const client = Sentry.getClient();
48+
await client?.flush(5000);
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event, Profile, ProfileChunkEnvelope } from '@sentry/core';
3+
import { sentryTest } from '../../../utils/fixtures';
4+
import {
5+
getMultipleSentryEnvelopeRequests,
6+
properEnvelopeRequestParser,
7+
properFullEnvelopeRequestParser,
8+
shouldSkipTracingTest,
9+
waitForTransactionRequestOnUrl,
10+
} from '../../../utils/helpers';
11+
12+
sentryTest('does not send profile envelope when document-policy is not set', async ({ page, getLocalTestUrl }) => {
13+
if (shouldSkipTracingTest()) {
14+
// Profiling only works when tracing is enabled
15+
sentryTest.skip();
16+
}
17+
18+
const url = await getLocalTestUrl({ testDir: __dirname });
19+
20+
const req = await waitForTransactionRequestOnUrl(page, url);
21+
const transactionEvent = properEnvelopeRequestParser<Event>(req, 0);
22+
const profileEvent = properEnvelopeRequestParser<Profile>(req, 1);
23+
24+
expect(transactionEvent).toBeDefined();
25+
26+
expect(profileEvent).toBeUndefined();
27+
});
28+
29+
sentryTest('sends profile envelope in trace mode', async ({ page, getLocalTestUrl, browserName }) => {
30+
if (shouldSkipTracingTest() || browserName !== 'chromium') {
31+
// Profiling only works when tracing is enabled
32+
sentryTest.skip();
33+
}
34+
35+
const url = await getLocalTestUrl({ testDir: __dirname, responseHeaders: { 'Document-Policy': 'js-profiling' } });
36+
await page.goto(url);
37+
38+
const profileChunkEnvelopePromise = getMultipleSentryEnvelopeRequests<ProfileChunkEnvelope>(
39+
page,
40+
1,
41+
{ envelopeType: 'profile_chunk' },
42+
properFullEnvelopeRequestParser,
43+
);
44+
45+
const profileChunkEnvelopeItem = (await profileChunkEnvelopePromise)[0][1][0];
46+
const envelopeItemHeader = profileChunkEnvelopeItem[0];
47+
const envelopeItemPayload = profileChunkEnvelopeItem[1];
48+
49+
expect(envelopeItemHeader).toHaveProperty('type', 'profile_chunk');
50+
51+
expect(envelopeItemPayload.profile).toBeDefined();
52+
expect(envelopeItemPayload.version).toBe('2');
53+
expect(envelopeItemPayload.platform).toBe('javascript');
54+
55+
const profile = envelopeItemPayload.profile;
56+
57+
expect(profile.samples).toBeDefined();
58+
expect(profile.stacks).toBeDefined();
59+
expect(profile.frames).toBeDefined();
60+
expect(profile.thread_metadata).toBeDefined();
61+
62+
// Samples
63+
expect(profile.samples.length).toBeGreaterThanOrEqual(2);
64+
let previousTimestamp = Number.NEGATIVE_INFINITY;
65+
for (const sample of profile.samples) {
66+
expect(typeof sample.stack_id).toBe('number');
67+
expect(sample.stack_id).toBeGreaterThanOrEqual(0);
68+
expect(sample.stack_id).toBeLessThan(profile.stacks.length);
69+
70+
// In trace lifecycle mode, samples carry a numeric timestamp (ms since epoch or similar clock)
71+
expect(typeof (sample as any).timestamp).toBe('number');
72+
const ts = (sample as any).timestamp as number;
73+
expect(Number.isFinite(ts)).toBe(true);
74+
expect(ts).toBeGreaterThan(0);
75+
// Monotonic non-decreasing timestamps
76+
expect(ts).toBeGreaterThanOrEqual(previousTimestamp);
77+
previousTimestamp = ts;
78+
79+
expect(sample.thread_id).toBe('0'); // Should be main thread
80+
}
81+
82+
// Stacks
83+
expect(profile.stacks.length).toBeGreaterThan(0);
84+
for (const stack of profile.stacks) {
85+
expect(Array.isArray(stack)).toBe(true);
86+
for (const frameIndex of stack) {
87+
expect(typeof frameIndex).toBe('number');
88+
expect(frameIndex).toBeGreaterThanOrEqual(0);
89+
expect(frameIndex).toBeLessThan(profile.frames.length);
90+
}
91+
}
92+
93+
// Frames
94+
expect(profile.frames.length).toBeGreaterThan(0);
95+
for (const frame of profile.frames) {
96+
expect(frame).toHaveProperty('function');
97+
expect(frame).toHaveProperty('abs_path');
98+
expect(frame).toHaveProperty('lineno');
99+
expect(frame).toHaveProperty('colno');
100+
101+
expect(typeof frame.function).toBe('string');
102+
expect(typeof frame.abs_path).toBe('string');
103+
expect(typeof frame.lineno).toBe('number');
104+
expect(typeof frame.colno).toBe('number');
105+
}
106+
107+
const functionNames = profile.frames.map(frame => frame.function).filter(name => name !== '');
108+
109+
if ((process.env.PW_BUNDLE || '').endsWith('min')) {
110+
// In bundled mode, function names are minified
111+
expect(functionNames.length).toBeGreaterThan(0);
112+
expect((functionNames as string[]).every(name => name?.length > 0)).toBe(true); // Just make sure they're not empty strings
113+
} else {
114+
expect(functionNames).toEqual(
115+
expect.arrayContaining([
116+
'_startRootSpan',
117+
'withScope',
118+
'createChildOrRootSpan',
119+
'startSpanManual',
120+
'startJSSelfProfile',
121+
122+
// both functions are captured
123+
'fibonacci',
124+
'largeSum',
125+
]),
126+
);
127+
}
128+
129+
expect(profile.thread_metadata).toHaveProperty('0');
130+
expect(profile.thread_metadata['0']).toHaveProperty('name');
131+
expect(profile.thread_metadata['0'].name).toBe('main');
132+
133+
// Test that profile duration makes sense (should be > 20ms based on test setup)
134+
const startTimeMs = (profile.samples[0] as any).timestamp as number;
135+
const endTimeMs = (profile.samples[profile.samples.length - 1] as any).timestamp as number;
136+
const durationMs = endTimeMs - startTimeMs;
137+
138+
// Should be at least 20ms based on our setTimeout(21) in the test
139+
expect(durationMs).toBeGreaterThan(20);
140+
});

0 commit comments

Comments
 (0)