-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
test(profiling): Add tests for current state of profiling #17470
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
38f2efb
8e121c4
2a52c24
b078eea
e905ec4
f1ffb35
ffccdc1
d7df494
7a952bd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import * as Sentry from '@sentry/browser'; | ||
|
||
window.Sentry = Sentry; | ||
|
||
Sentry.init({ | ||
dsn: 'https://[email protected]/1337', | ||
integrations: [Sentry.browserProfilingIntegration()], | ||
tracesSampleRate: 1, | ||
profilesSampleRate: 1, | ||
}); | ||
|
||
function fibonacci(n) { | ||
if (n <= 1) { | ||
return n; | ||
} | ||
return fibonacci(n - 1) + fibonacci(n - 2); | ||
} | ||
|
||
await Sentry.startSpanManual({ name: 'root-fibonacci-2', parentSpan: null, forceTransaction: true }, async span => { | ||
fibonacci(30); | ||
|
||
// Timeout to prevent flaky tests. Integration samples every 20ms, if function is too fast it might not get sampled | ||
await new Promise(resolve => setTimeout(resolve, 21)); | ||
span.end(); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { expect } from '@playwright/test'; | ||
import type { Event, Profile } from '@sentry/core'; | ||
import { sentryTest } from '../../../utils/fixtures'; | ||
import { | ||
properEnvelopeRequestParser, | ||
shouldSkipTracingTest, | ||
waitForTransactionRequestOnUrl, | ||
} from '../../../utils/helpers'; | ||
|
||
sentryTest('does not send profile envelope when document-policy is not set', async ({ page, getLocalTestUrl }) => { | ||
if (shouldSkipTracingTest()) { | ||
// Profiling only works when tracing is enabled | ||
sentryTest.skip(); | ||
} | ||
|
||
const url = await getLocalTestUrl({ testDir: __dirname }); | ||
|
||
const req = await waitForTransactionRequestOnUrl(page, url); | ||
const transactionEvent = properEnvelopeRequestParser<Event>(req, 0); | ||
const profileEvent = properEnvelopeRequestParser<Profile>(req, 1); | ||
|
||
expect(transactionEvent).toBeDefined(); | ||
|
||
expect(profileEvent).toBeUndefined(); | ||
}); | ||
|
||
sentryTest('sends profile envelope in legacy mode', async ({ page, getLocalTestUrl }) => { | ||
if (shouldSkipTracingTest()) { | ||
// Profiling only works when tracing is enabled | ||
sentryTest.skip(); | ||
} | ||
|
||
const url = await getLocalTestUrl({ testDir: __dirname, responseHeaders: { 'Document-Policy': 'js-profiling' } }); | ||
|
||
const req = await waitForTransactionRequestOnUrl(page, url); | ||
const profileEvent = properEnvelopeRequestParser<Profile>(req, 1); | ||
expect(profileEvent).toBeDefined(); | ||
|
||
const profile = profileEvent.profile; | ||
expect(profileEvent.profile).toBeDefined(); | ||
|
||
expect(profile.samples).toBeDefined(); | ||
expect(profile.stacks).toBeDefined(); | ||
expect(profile.frames).toBeDefined(); | ||
expect(profile.thread_metadata).toBeDefined(); | ||
|
||
// Samples | ||
expect(profile.samples.length).toBeGreaterThanOrEqual(2); | ||
for (const sample of profile.samples) { | ||
expect(typeof sample.elapsed_since_start_ns).toBe('string'); | ||
expect(sample.elapsed_since_start_ns).toMatch(/^\d+$/); // Numeric string | ||
expect(parseInt(sample.elapsed_since_start_ns, 10)).toBeGreaterThanOrEqual(0); | ||
|
||
expect(typeof sample.stack_id).toBe('number'); | ||
expect(sample.stack_id).toBeGreaterThanOrEqual(0); | ||
expect(sample.thread_id).toBe('0'); // Should be main thread | ||
} | ||
|
||
// Stacks | ||
expect(profile.stacks.length).toBeGreaterThan(0); | ||
for (const stack of profile.stacks) { | ||
expect(Array.isArray(stack)).toBe(true); | ||
for (const frameIndex of stack) { | ||
expect(typeof frameIndex).toBe('number'); | ||
expect(frameIndex).toBeGreaterThanOrEqual(0); | ||
expect(frameIndex).toBeLessThan(profile.frames.length); | ||
} | ||
} | ||
|
||
// Frames | ||
expect(profile.frames.length).toBeGreaterThan(0); | ||
for (const frame of profile.frames) { | ||
expect(frame).toHaveProperty('function'); | ||
expect(frame).toHaveProperty('abs_path'); | ||
expect(frame).toHaveProperty('lineno'); | ||
expect(frame).toHaveProperty('colno'); | ||
|
||
expect(typeof frame.function).toBe('string'); | ||
expect(typeof frame.abs_path).toBe('string'); | ||
expect(typeof frame.lineno).toBe('number'); | ||
expect(typeof frame.colno).toBe('number'); | ||
} | ||
|
||
const functionNames = profile.frames.map(frame => frame.function).filter(name => name !== ''); | ||
|
||
expect(functionNames).toEqual( | ||
expect.arrayContaining([ | ||
'_startRootSpan', | ||
'withScope', | ||
'createChildOrRootSpan', | ||
'startSpanManual', | ||
'startProfileForSpan', | ||
'startJSSelfProfile', | ||
]), | ||
); | ||
|
||
expect(profile.thread_metadata).toHaveProperty('0'); | ||
expect(profile.thread_metadata['0']).toHaveProperty('name'); | ||
expect(profile.thread_metadata['0'].name).toBe('main'); | ||
|
||
// Test that profile duration makes sense (should be > 20ms based on test setup) | ||
const startTime = parseInt(profile.samples[0].elapsed_since_start_ns, 10); | ||
const endTime = parseInt(profile.samples[profile.samples.length - 1].elapsed_since_start_ns, 10); | ||
const durationNs = endTime - startTime; | ||
const durationMs = durationNs / 1_000_000; // Convert ns to ms | ||
|
||
// Should be at least 20ms based on our setTimeout(21) in the test | ||
expect(durationMs).toBeGreaterThan(20); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,7 @@ export const envelopeParser = (request: Request | null): unknown[] => { | |
}); | ||
}; | ||
|
||
// Rather use the `properEnvelopeRequestParser`, as the `envelopeParser` does not follow the envelope spec. | ||
export const envelopeRequestParser = <T = SentryEvent>(request: Request | null, envelopeIndex = 2): T => { | ||
return envelopeParser(request)[envelopeIndex] as T; | ||
}; | ||
|
@@ -79,8 +80,12 @@ function getEventAndTraceHeader(envelope: EventEnvelope): EventAndTraceHeader { | |
return [event, trace]; | ||
} | ||
|
||
export const properEnvelopeRequestParser = <T = SentryEvent>(request: Request | null, envelopeIndex = 1): T => { | ||
return properEnvelopeParser(request)[0]?.[envelopeIndex] as T; | ||
export const properEnvelopeRequestParser = <T = SentryEvent>( | ||
request: Request | null, | ||
envelopeItemIndex: number, | ||
envelopeIndex = 1, // 1 is usually the payload of the envelope (0 is the header) | ||
): T => { | ||
return properEnvelopeParser(request)[envelopeItemIndex]?.[envelopeIndex] as T; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: Function Signature Change Causes Argument MismatchThe |
||
}; | ||
|
||
export const properFullEnvelopeRequestParser = <T extends Envelope>(request: Request | null): T => { | ||
|
Uh oh!
There was an error while loading. Please reload this page.