Skip to content

Commit dfdc3b0

Browse files
authored
test(profiling): Add tests for current state of profiling (#17470)
While working on #17279, I added some tests and this PR is the extracted test for the current behavior.
1 parent 895b385 commit dfdc3b0

File tree

5 files changed

+162
-4
lines changed

5 files changed

+162
-4
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
profilesSampleRate: 1,
11+
});
12+
13+
function fibonacci(n) {
14+
if (n <= 1) {
15+
return n;
16+
}
17+
return fibonacci(n - 1) + fibonacci(n - 2);
18+
}
19+
20+
await Sentry.startSpanManual({ name: 'root-fibonacci-2', parentSpan: null, forceTransaction: true }, async span => {
21+
fibonacci(30);
22+
23+
// Timeout to prevent flaky tests. Integration samples every 20ms, if function is too fast it might not get sampled
24+
await new Promise(resolve => setTimeout(resolve, 21));
25+
span.end();
26+
});
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event, Profile } from '@sentry/core';
3+
import { sentryTest } from '../../../utils/fixtures';
4+
import {
5+
properEnvelopeRequestParser,
6+
shouldSkipTracingTest,
7+
waitForTransactionRequestOnUrl,
8+
} from '../../../utils/helpers';
9+
10+
sentryTest(
11+
'does not send profile envelope when document-policy is not set',
12+
async ({ page, getLocalTestUrl, browserName }) => {
13+
if (shouldSkipTracingTest() || browserName !== 'chromium') {
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+
expect(profileEvent).toBeUndefined();
26+
},
27+
);
28+
29+
sentryTest('sends profile envelope in legacy 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+
37+
const req = await waitForTransactionRequestOnUrl(page, url);
38+
const profileEvent = properEnvelopeRequestParser<Profile>(req, 1);
39+
expect(profileEvent).toBeDefined();
40+
41+
const profile = profileEvent.profile;
42+
expect(profileEvent.profile).toBeDefined();
43+
44+
expect(profile.samples).toBeDefined();
45+
expect(profile.stacks).toBeDefined();
46+
expect(profile.frames).toBeDefined();
47+
expect(profile.thread_metadata).toBeDefined();
48+
49+
// Samples
50+
expect(profile.samples.length).toBeGreaterThanOrEqual(2);
51+
for (const sample of profile.samples) {
52+
expect(typeof sample.elapsed_since_start_ns).toBe('string');
53+
expect(sample.elapsed_since_start_ns).toMatch(/^\d+$/); // Numeric string
54+
expect(parseInt(sample.elapsed_since_start_ns, 10)).toBeGreaterThanOrEqual(0);
55+
56+
expect(typeof sample.stack_id).toBe('number');
57+
expect(sample.stack_id).toBeGreaterThanOrEqual(0);
58+
expect(sample.thread_id).toBe('0'); // Should be main thread
59+
}
60+
61+
// Stacks
62+
expect(profile.stacks.length).toBeGreaterThan(0);
63+
for (const stack of profile.stacks) {
64+
expect(Array.isArray(stack)).toBe(true);
65+
for (const frameIndex of stack) {
66+
expect(typeof frameIndex).toBe('number');
67+
expect(frameIndex).toBeGreaterThanOrEqual(0);
68+
expect(frameIndex).toBeLessThan(profile.frames.length);
69+
}
70+
}
71+
72+
// Frames
73+
expect(profile.frames.length).toBeGreaterThan(0);
74+
for (const frame of profile.frames) {
75+
expect(frame).toHaveProperty('function');
76+
expect(frame).toHaveProperty('abs_path');
77+
expect(frame).toHaveProperty('lineno');
78+
expect(frame).toHaveProperty('colno');
79+
80+
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');
84+
}
85+
86+
const functionNames = profile.frames.map(frame => frame.function).filter(name => name !== '');
87+
88+
if ((process.env.PW_BUNDLE || '').endsWith('min')) {
89+
// Function names are minified in minified bundles
90+
expect(functionNames.length).toBeGreaterThan(0);
91+
expect((functionNames as string[]).every(name => name?.length > 0)).toBe(true); // Just make sure they're not empty strings
92+
} else {
93+
expect(functionNames).toEqual(
94+
expect.arrayContaining([
95+
'_startRootSpan',
96+
'withScope',
97+
'createChildOrRootSpan',
98+
'startSpanManual',
99+
'startProfileForSpan',
100+
'startJSSelfProfile',
101+
]),
102+
);
103+
}
104+
105+
expect(profile.thread_metadata).toHaveProperty('0');
106+
expect(profile.thread_metadata['0']).toHaveProperty('name');
107+
expect(profile.thread_metadata['0'].name).toBe('main');
108+
109+
// Test that profile duration makes sense (should be > 20ms based on test setup)
110+
const startTime = parseInt(profile.samples[0].elapsed_since_start_ns, 10);
111+
const endTime = parseInt(profile.samples[profile.samples.length - 1].elapsed_since_start_ns, 10);
112+
const durationNs = endTime - startTime;
113+
const durationMs = durationNs / 1_000_000; // Convert ns to ms
114+
115+
// Should be at least 20ms based on our setTimeout(21) in the test
116+
expect(durationMs).toBeGreaterThan(20);
117+
});

dev-packages/browser-integration-tests/utils/fixtures.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export type TestFixtures = {
3535
skipRouteHandler?: boolean;
3636
skipDsnRouteHandler?: boolean;
3737
handleLazyLoadedFeedback?: boolean;
38+
responseHeaders?: Record<string, string>;
3839
}) => Promise<string>;
3940
forceFlushReplay: () => Promise<string>;
4041
enableConsole: () => void;
@@ -59,7 +60,13 @@ const sentryTest = base.extend<TestFixtures>({
5960

6061
getLocalTestUrl: ({ page }, use) => {
6162
return use(
62-
async ({ testDir, skipRouteHandler = false, skipDsnRouteHandler = false, handleLazyLoadedFeedback = false }) => {
63+
async ({
64+
testDir,
65+
skipRouteHandler = false,
66+
skipDsnRouteHandler = false,
67+
handleLazyLoadedFeedback = false,
68+
responseHeaders = {},
69+
}) => {
6370
const pagePath = `${TEST_HOST}/index.html`;
6471

6572
const tmpDir = path.join(testDir, 'dist', crypto.randomUUID());
@@ -86,7 +93,9 @@ const sentryTest = base.extend<TestFixtures>({
8693
const file = route.request().url().split('/').pop();
8794
const filePath = path.resolve(tmpDir, `./${file}`);
8895

89-
return fs.existsSync(filePath) ? route.fulfill({ path: filePath }) : route.continue();
96+
return fs.existsSync(filePath)
97+
? route.fulfill({ path: filePath, headers: responseHeaders })
98+
: route.continue();
9099
});
91100

92101
if (handleLazyLoadedFeedback) {

dev-packages/browser-integration-tests/utils/generatePlugin.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const IMPORTED_INTEGRATION_CDN_BUNDLE_PATHS: Record<string, string> = {
3636
feedbackIntegration: 'feedback',
3737
moduleMetadataIntegration: 'modulemetadata',
3838
graphqlClientIntegration: 'graphqlclient',
39+
browserProfilingIntegration: 'browserprofiling',
3940
// technically, this is not an integration, but let's add it anyway for simplicity
4041
makeMultiplexedTransport: 'multiplexedtransport',
4142
};

dev-packages/browser-integration-tests/utils/helpers.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const envelopeParser = (request: Request | null): unknown[] => {
2929
});
3030
};
3131

32+
// Rather use the `properEnvelopeRequestParser`, as the `envelopeParser` does not follow the envelope spec.
3233
export const envelopeRequestParser = <T = SentryEvent>(request: Request | null, envelopeIndex = 2): T => {
3334
return envelopeParser(request)[envelopeIndex] as T;
3435
};
@@ -79,8 +80,12 @@ function getEventAndTraceHeader(envelope: EventEnvelope): EventAndTraceHeader {
7980
return [event, trace];
8081
}
8182

82-
export const properEnvelopeRequestParser = <T = SentryEvent>(request: Request | null, envelopeIndex = 1): T => {
83-
return properEnvelopeParser(request)[0]?.[envelopeIndex] as T;
83+
export const properEnvelopeRequestParser = <T = SentryEvent>(
84+
request: Request | null,
85+
envelopeItemIndex: number,
86+
envelopeIndex = 1, // 1 is usually the payload of the envelope (0 is the header)
87+
): T => {
88+
return properEnvelopeParser(request)[envelopeItemIndex]?.[envelopeIndex] as T;
8489
};
8590

8691
export const properFullEnvelopeRequestParser = <T extends Envelope>(request: Request | null): T => {

0 commit comments

Comments
 (0)