Skip to content

Commit 0ea9d06

Browse files
authored
Merge branch 'develop' into support-vercel-and-cloudflare
2 parents 799dc56 + 40bcc3d commit 0ea9d06

File tree

21 files changed

+2027
-148
lines changed

21 files changed

+2027
-148
lines changed

.size-limit.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ module.exports = [
4040
gzip: true,
4141
limit: '41 KB',
4242
},
43+
{
44+
name: '@sentry/browser (incl. Tracing, Profiling)',
45+
path: 'packages/browser/build/npm/esm/index.js',
46+
import: createImport('init', 'browserTracingIntegration', 'browserProfilingIntegration'),
47+
gzip: true,
48+
limit: '48 KB',
49+
},
4350
{
4451
name: '@sentry/browser (incl. Tracing, Replay)',
4552
path: 'packages/browser/build/npm/esm/index.js',

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

dev-packages/browser-integration-tests/suites/profiling/legacyMode/test.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -73,14 +73,16 @@ sentryTest('sends profile envelope in legacy mode', async ({ page, getLocalTestU
7373
expect(profile.frames.length).toBeGreaterThan(0);
7474
for (const frame of profile.frames) {
7575
expect(frame).toHaveProperty('function');
76-
expect(frame).toHaveProperty('abs_path');
77-
expect(frame).toHaveProperty('lineno');
78-
expect(frame).toHaveProperty('colno');
79-
8076
expect(typeof frame.function).toBe('string');
81-
expect(typeof frame.abs_path).toBe('string');
82-
expect(typeof frame.lineno).toBe('number');
83-
expect(typeof frame.colno).toBe('number');
77+
78+
if (frame.function !== 'fetch' && frame.function !== 'setTimeout') {
79+
expect(frame).toHaveProperty('abs_path');
80+
expect(frame).toHaveProperty('lineno');
81+
expect(frame).toHaveProperty('colno');
82+
expect(typeof frame.abs_path).toBe('string');
83+
expect(typeof frame.lineno).toBe('number');
84+
expect(typeof frame.colno).toBe('number');
85+
}
8486
}
8587

8688
const functionNames = profile.frames.map(frame => frame.function).filter(name => name !== '');
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+
profileLifecycle: 'trace',
12+
});
13+
14+
function largeSum(amount = 1000000) {
15+
let sum = 0;
16+
for (let i = 0; i < amount; i++) {
17+
sum += Math.sqrt(i) * Math.sin(i);
18+
}
19+
}
20+
21+
function fibonacci(n) {
22+
if (n <= 1) {
23+
return n;
24+
}
25+
return fibonacci(n - 1) + fibonacci(n - 2);
26+
}
27+
28+
// Create two NON-overlapping root spans so that the profiler stops and emits a chunk
29+
// after each span (since active root span count returns to 0 between them).
30+
await Sentry.startSpanManual({ name: 'root-fibonacci-1', parentSpan: null, forceTransaction: true }, async span => {
31+
fibonacci(40);
32+
// Ensure we cross the sampling interval to avoid flakes
33+
await new Promise(resolve => setTimeout(resolve, 25));
34+
span.end();
35+
});
36+
37+
// Small delay to ensure the first chunk is collected and sent
38+
await new Promise(r => setTimeout(r, 25));
39+
40+
await Sentry.startSpanManual({ name: 'root-largeSum-2', parentSpan: null, forceTransaction: true }, async span => {
41+
largeSum();
42+
// Ensure we cross the sampling interval to avoid flakes
43+
await new Promise(resolve => setTimeout(resolve, 25));
44+
span.end();
45+
});
46+
47+
const client = Sentry.getClient();
48+
await client?.flush(5000);
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import { expect } from '@playwright/test';
2+
import type { ProfileChunkEnvelope } from '@sentry/core';
3+
import { sentryTest } from '../../../utils/fixtures';
4+
import {
5+
countEnvelopes,
6+
getMultipleSentryEnvelopeRequests,
7+
properFullEnvelopeRequestParser,
8+
shouldSkipTracingTest,
9+
} from '../../../utils/helpers';
10+
11+
sentryTest(
12+
'does not send profile envelope when document-policy is not set',
13+
async ({ page, getLocalTestUrl, browserName }) => {
14+
if (shouldSkipTracingTest() || browserName !== 'chromium') {
15+
// Profiling only works when tracing is enabled
16+
sentryTest.skip();
17+
}
18+
19+
const url = await getLocalTestUrl({ testDir: __dirname });
20+
21+
// Assert that no profile_chunk envelope is sent without policy header
22+
const chunkCount = await countEnvelopes(page, { url, envelopeType: 'profile_chunk', timeout: 1500 });
23+
expect(chunkCount).toBe(0);
24+
},
25+
);
26+
27+
sentryTest(
28+
'sends profile_chunk envelopes in trace mode (multiple chunks)',
29+
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+
37+
// Expect at least 2 chunks because subject creates two separate root spans,
38+
// causing the profiler to stop and emit a chunk after each root span ends.
39+
const profileChunkEnvelopes = await getMultipleSentryEnvelopeRequests<ProfileChunkEnvelope>(
40+
page,
41+
2,
42+
{ url, envelopeType: 'profile_chunk', timeout: 5000 },
43+
properFullEnvelopeRequestParser,
44+
);
45+
46+
expect(profileChunkEnvelopes.length).toBeGreaterThanOrEqual(2);
47+
48+
// Validate the first chunk thoroughly
49+
const profileChunkEnvelopeItem = profileChunkEnvelopes[0][1][0];
50+
const envelopeItemHeader = profileChunkEnvelopeItem[0];
51+
const envelopeItemPayload1 = profileChunkEnvelopeItem[1];
52+
53+
expect(envelopeItemHeader).toHaveProperty('type', 'profile_chunk');
54+
55+
expect(envelopeItemPayload1.profile).toBeDefined();
56+
expect(envelopeItemPayload1.version).toBe('2');
57+
expect(envelopeItemPayload1.platform).toBe('javascript');
58+
59+
// Required profile metadata (Sample Format V2)
60+
expect(typeof envelopeItemPayload1.profiler_id).toBe('string');
61+
expect(envelopeItemPayload1.profiler_id).toMatch(/^[a-f0-9]{32}$/);
62+
expect(typeof envelopeItemPayload1.chunk_id).toBe('string');
63+
expect(envelopeItemPayload1.chunk_id).toMatch(/^[a-f0-9]{32}$/);
64+
expect(envelopeItemPayload1.client_sdk).toBeDefined();
65+
expect(typeof envelopeItemPayload1.client_sdk.name).toBe('string');
66+
expect(typeof envelopeItemPayload1.client_sdk.version).toBe('string');
67+
expect(typeof envelopeItemPayload1.release).toBe('string');
68+
expect(envelopeItemPayload1.debug_meta).toBeDefined();
69+
expect(Array.isArray(envelopeItemPayload1?.debug_meta?.images)).toBe(true);
70+
71+
const profile1 = envelopeItemPayload1.profile;
72+
73+
expect(profile1.samples).toBeDefined();
74+
expect(profile1.stacks).toBeDefined();
75+
expect(profile1.frames).toBeDefined();
76+
expect(profile1.thread_metadata).toBeDefined();
77+
78+
// Samples
79+
expect(profile1.samples.length).toBeGreaterThanOrEqual(2);
80+
let previousTimestamp = Number.NEGATIVE_INFINITY;
81+
for (const sample of profile1.samples) {
82+
expect(typeof sample.stack_id).toBe('number');
83+
expect(sample.stack_id).toBeGreaterThanOrEqual(0);
84+
expect(sample.stack_id).toBeLessThan(profile1.stacks.length);
85+
86+
// In trace lifecycle mode, samples carry a numeric timestamp (ms since epoch or similar clock)
87+
expect(typeof (sample as any).timestamp).toBe('number');
88+
const ts = (sample as any).timestamp as number;
89+
expect(Number.isFinite(ts)).toBe(true);
90+
expect(ts).toBeGreaterThan(0);
91+
// Monotonic non-decreasing timestamps
92+
expect(ts).toBeGreaterThanOrEqual(previousTimestamp);
93+
previousTimestamp = ts;
94+
95+
expect(sample.thread_id).toBe('0'); // Should be main thread
96+
}
97+
98+
// Stacks
99+
expect(profile1.stacks.length).toBeGreaterThan(0);
100+
for (const stack of profile1.stacks) {
101+
expect(Array.isArray(stack)).toBe(true);
102+
for (const frameIndex of stack) {
103+
expect(typeof frameIndex).toBe('number');
104+
expect(frameIndex).toBeGreaterThanOrEqual(0);
105+
expect(frameIndex).toBeLessThan(profile1.frames.length);
106+
}
107+
}
108+
109+
// Frames
110+
expect(profile1.frames.length).toBeGreaterThan(0);
111+
for (const frame of profile1.frames) {
112+
expect(frame).toHaveProperty('function');
113+
expect(typeof frame.function).toBe('string');
114+
115+
if (frame.function !== 'fetch' && frame.function !== 'setTimeout') {
116+
expect(frame).toHaveProperty('abs_path');
117+
expect(frame).toHaveProperty('lineno');
118+
expect(frame).toHaveProperty('colno');
119+
expect(typeof frame.abs_path).toBe('string');
120+
expect(typeof frame.lineno).toBe('number');
121+
expect(typeof frame.colno).toBe('number');
122+
}
123+
}
124+
125+
const functionNames = profile1.frames.map(frame => frame.function).filter(name => name !== '');
126+
127+
if ((process.env.PW_BUNDLE || '').endsWith('min')) {
128+
// In bundled mode, function names are minified
129+
expect(functionNames.length).toBeGreaterThan(0);
130+
expect((functionNames as string[]).every(name => name?.length > 0)).toBe(true); // Just make sure they're not empty strings
131+
} else {
132+
expect(functionNames).toEqual(
133+
expect.arrayContaining([
134+
'_startRootSpan',
135+
'withScope',
136+
'createChildOrRootSpan',
137+
'startSpanManual',
138+
'startJSSelfProfile',
139+
140+
// first function is captured (other one is in other chunk)
141+
'fibonacci',
142+
]),
143+
);
144+
}
145+
146+
expect(profile1.thread_metadata).toHaveProperty('0');
147+
expect(profile1.thread_metadata['0']).toHaveProperty('name');
148+
expect(profile1.thread_metadata['0'].name).toBe('main');
149+
150+
// Test that profile duration makes sense (should be > 20ms based on test setup)
151+
const startTimeSec = (profile1.samples[0] as any).timestamp as number;
152+
const endTimeSec = (profile1.samples[profile1.samples.length - 1] as any).timestamp as number;
153+
const durationSec = endTimeSec - startTimeSec;
154+
155+
// Should be at least 20ms based on our setTimeout(21) in the test
156+
expect(durationSec).toBeGreaterThan(0.2);
157+
158+
// === PROFILE CHUNK 2 ===
159+
160+
const profileChunkEnvelopeItem2 = profileChunkEnvelopes[1][1][0];
161+
const envelopeItemHeader2 = profileChunkEnvelopeItem2[0];
162+
const envelopeItemPayload2 = profileChunkEnvelopeItem2[1];
163+
164+
// Basic sanity on the second chunk: has correct envelope type and structure
165+
expect(envelopeItemHeader2).toHaveProperty('type', 'profile_chunk');
166+
expect(envelopeItemPayload2.profile).toBeDefined();
167+
expect(envelopeItemPayload2.version).toBe('2');
168+
expect(envelopeItemPayload2.platform).toBe('javascript');
169+
170+
// Required profile metadata (Sample Format V2)
171+
// https://develop.sentry.dev/sdk/telemetry/profiles/sample-format-v2/
172+
expect(typeof envelopeItemPayload2.profiler_id).toBe('string');
173+
expect(envelopeItemPayload2.profiler_id).toMatch(/^[a-f0-9]{32}$/);
174+
expect(typeof envelopeItemPayload2.chunk_id).toBe('string');
175+
expect(envelopeItemPayload2.chunk_id).toMatch(/^[a-f0-9]{32}$/);
176+
expect(envelopeItemPayload2.client_sdk).toBeDefined();
177+
expect(typeof envelopeItemPayload2.client_sdk.name).toBe('string');
178+
expect(typeof envelopeItemPayload2.client_sdk.version).toBe('string');
179+
expect(typeof envelopeItemPayload2.release).toBe('string');
180+
expect(envelopeItemPayload2.debug_meta).toBeDefined();
181+
expect(Array.isArray(envelopeItemPayload2?.debug_meta?.images)).toBe(true);
182+
183+
const profile2 = envelopeItemPayload2.profile;
184+
185+
const functionNames2 = profile2.frames.map(frame => frame.function).filter(name => name !== '');
186+
187+
if ((process.env.PW_BUNDLE || '').endsWith('min')) {
188+
// In bundled mode, function names are minified
189+
expect(functionNames2.length).toBeGreaterThan(0);
190+
expect((functionNames2 as string[]).every(name => name?.length > 0)).toBe(true); // Just make sure they're not empty strings
191+
} else {
192+
expect(functionNames2).toEqual(
193+
expect.arrayContaining([
194+
'_startRootSpan',
195+
'withScope',
196+
'createChildOrRootSpan',
197+
'startSpanManual',
198+
'startJSSelfProfile',
199+
200+
// second function is captured (other one is in other chunk)
201+
'largeSum',
202+
]),
203+
);
204+
}
205+
},
206+
);
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
profileLifecycle: 'trace',
12+
});
13+
14+
function largeSum(amount = 1000000) {
15+
let sum = 0;
16+
for (let i = 0; i < amount; i++) {
17+
sum += Math.sqrt(i) * Math.sin(i);
18+
}
19+
}
20+
21+
function fibonacci(n) {
22+
if (n <= 1) {
23+
return n;
24+
}
25+
return fibonacci(n - 1) + fibonacci(n - 2);
26+
}
27+
28+
let firstSpan;
29+
30+
Sentry.startSpanManual({ name: 'root-largeSum-1', parentSpan: null, forceTransaction: true }, span => {
31+
largeSum();
32+
firstSpan = span;
33+
});
34+
35+
await Sentry.startSpanManual({ name: 'root-fibonacci-2', parentSpan: null, forceTransaction: true }, async span => {
36+
fibonacci(40);
37+
38+
Sentry.startSpan({ name: 'child-fibonacci', parentSpan: span }, childSpan => {
39+
console.log('child span');
40+
});
41+
42+
// Timeout to prevent flaky tests. Integration samples every 20ms, if function is too fast it might not get sampled
43+
await new Promise(resolve => setTimeout(resolve, 21));
44+
span.end();
45+
});
46+
47+
await new Promise(r => setTimeout(r, 21));
48+
49+
firstSpan.end();
50+
51+
const client = Sentry.getClient();
52+
await client?.flush(5000);

0 commit comments

Comments
 (0)