Skip to content

Commit 1525603

Browse files
authored
feat(browserprofiling): Add manual mode and deprecate old profiling (#18189)
Adds the `manual` mode for profiling and browser integration tests. - adds deprecation note for old option - adds some JSDoc comments to public-facing API to make the difference between Node and UI profiling better visible. Closes #17279
1 parent 3d48cc6 commit 1525603

File tree

13 files changed

+681
-139
lines changed

13 files changed

+681
-139
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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://public@dsn.ingest.sentry.io/1337',
8+
integrations: [browserProfilingIntegration()],
9+
tracesSampleRate: 1,
10+
profileSessionSampleRate: 1,
11+
profileLifecycle: 'manual',
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+
function fibonacci1(n) {
29+
if (n <= 1) {
30+
return n;
31+
}
32+
return fibonacci1(n - 1) + fibonacci1(n - 2);
33+
}
34+
35+
function fibonacci2(n) {
36+
if (n <= 1) {
37+
return n;
38+
}
39+
return fibonacci1(n - 1) + fibonacci1(n - 2);
40+
}
41+
42+
function notProfiledFib(n) {
43+
if (n <= 1) {
44+
return n;
45+
}
46+
return fibonacci1(n - 1) + fibonacci1(n - 2);
47+
}
48+
49+
// Adding setTimeout to ensure we cross the sampling interval to avoid flakes
50+
51+
Sentry.uiProfiler.startProfiler();
52+
53+
fibonacci(40);
54+
await new Promise(resolve => setTimeout(resolve, 25));
55+
56+
largeSum();
57+
await new Promise(resolve => setTimeout(resolve, 25));
58+
59+
Sentry.uiProfiler.stopProfiler();
60+
61+
// ---
62+
63+
notProfiledFib(40);
64+
await new Promise(resolve => setTimeout(resolve, 25));
65+
66+
// ---
67+
68+
Sentry.uiProfiler.startProfiler();
69+
70+
fibonacci2(40);
71+
await new Promise(resolve => setTimeout(resolve, 25));
72+
73+
Sentry.uiProfiler.stopProfiler();
74+
75+
const client = Sentry.getClient();
76+
await client?.flush(8000);
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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+
import { validateProfile, validateProfilePayloadMetadata } from '../test-utils';
11+
12+
sentryTest(
13+
'does not send profile envelope when document-policy is not set',
14+
async ({ page, getLocalTestUrl, browserName }) => {
15+
if (shouldSkipTracingTest() || browserName !== 'chromium') {
16+
// Profiling only works when tracing is enabled
17+
sentryTest.skip();
18+
}
19+
20+
const url = await getLocalTestUrl({ testDir: __dirname });
21+
22+
// Assert that no profile_chunk envelope is sent without policy header
23+
const chunkCount = await countEnvelopes(page, { url, envelopeType: 'profile_chunk', timeout: 1500 });
24+
expect(chunkCount).toBe(0);
25+
},
26+
);
27+
28+
sentryTest('sends profile_chunk envelopes in manual mode', async ({ page, getLocalTestUrl, browserName }) => {
29+
if (shouldSkipTracingTest() || browserName !== 'chromium') {
30+
// Profiling only works when tracing is enabled
31+
sentryTest.skip();
32+
}
33+
34+
const url = await getLocalTestUrl({ testDir: __dirname, responseHeaders: { 'Document-Policy': 'js-profiling' } });
35+
36+
// In manual mode we start and stop once -> expect exactly one chunk
37+
const profileChunkEnvelopes = await getMultipleSentryEnvelopeRequests<ProfileChunkEnvelope>(
38+
page,
39+
2,
40+
{ url, envelopeType: 'profile_chunk', timeout: 8000 },
41+
properFullEnvelopeRequestParser,
42+
);
43+
44+
expect(profileChunkEnvelopes.length).toBe(2);
45+
46+
// Validate the first chunk thoroughly
47+
const profileChunkEnvelopeItem = profileChunkEnvelopes[0][1][0];
48+
const envelopeItemHeader = profileChunkEnvelopeItem[0];
49+
const envelopeItemPayload1 = profileChunkEnvelopeItem[1];
50+
51+
expect(envelopeItemHeader).toHaveProperty('type', 'profile_chunk');
52+
expect(envelopeItemPayload1.profile).toBeDefined();
53+
54+
const profilerId1 = envelopeItemPayload1.profiler_id;
55+
56+
validateProfilePayloadMetadata(envelopeItemPayload1);
57+
58+
validateProfile(envelopeItemPayload1.profile, {
59+
expectedFunctionNames: ['startJSSelfProfile', 'fibonacci', 'largeSum'],
60+
minSampleDurationMs: 20,
61+
isChunkFormat: true,
62+
});
63+
64+
// only contains fibonacci
65+
const functionNames1 = envelopeItemPayload1.profile.frames.map(frame => frame.function).filter(name => name !== '');
66+
expect(functionNames1).toEqual(expect.not.arrayContaining(['fibonacci1', 'fibonacci2', 'fibonacci3']));
67+
68+
// === PROFILE CHUNK 2 ===
69+
70+
const profileChunkEnvelopeItem2 = profileChunkEnvelopes[1][1][0];
71+
const envelopeItemHeader2 = profileChunkEnvelopeItem2[0];
72+
const envelopeItemPayload2 = profileChunkEnvelopeItem2[1];
73+
74+
expect(envelopeItemHeader2).toHaveProperty('type', 'profile_chunk');
75+
expect(envelopeItemPayload2.profile).toBeDefined();
76+
77+
expect(envelopeItemPayload2.profiler_id).toBe(profilerId1); // same profiler id for the whole session
78+
79+
validateProfilePayloadMetadata(envelopeItemPayload2);
80+
81+
validateProfile(envelopeItemPayload2.profile, {
82+
expectedFunctionNames: [
83+
'startJSSelfProfile',
84+
'fibonacci1', // called by fibonacci2
85+
'fibonacci2',
86+
],
87+
isChunkFormat: true,
88+
});
89+
90+
// does not contain notProfiledFib (called during unprofiled part)
91+
const functionNames2 = envelopeItemPayload2.profile.frames.map(frame => frame.function).filter(name => name !== '');
92+
expect(functionNames2).toEqual(expect.not.arrayContaining(['notProfiledFib']));
93+
});

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export function validateProfile(
9090
}
9191
}
9292

93-
// Frames
93+
// FRAMES
9494
expect(profile.frames.length).toBeGreaterThan(0);
9595
for (const frame of profile.frames) {
9696
expect(frame).toHaveProperty('function');

packages/browser/src/client.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,6 @@ export class BrowserClient extends Client<BrowserClientOptions> {
130130

131131
// Flush logs and metrics when page becomes hidden (e.g., tab switch, navigation)
132132
// todo(v11): Remove the experimental flag
133-
// eslint-disable-next-line deprecation/deprecation
134133
if (WINDOW.document && (sendClientReports || enableLogs || enableMetrics)) {
135134
WINDOW.document.addEventListener('visibilitychange', () => {
136135
if (WINDOW.document.visibilityState === 'hidden') {

packages/browser/src/exports.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ export {
7878
export { WINDOW } from './helpers';
7979
export { BrowserClient } from './client';
8080
export { makeFetchTransport } from './transports/fetch';
81+
export { uiProfiler } from './profiling';
8182
export {
8283
defaultStackParser,
8384
defaultStackLineParsers,

0 commit comments

Comments
 (0)