From 71b3e25638b4a5649451de7e39d42657c3d20165 Mon Sep 17 00:00:00 2001 From: s1gr1d <32902192+s1gr1d@users.noreply.github.com> Date: Tue, 11 Nov 2025 15:16:00 +0100 Subject: [PATCH] test(profiling): Add test utils to validate Profile Chunk envelope --- .../suites/profiling/legacyMode/test.ts | 88 ++------- .../suites/profiling/test-utils.ts | 151 +++++++++++++++ .../test.ts | 172 ++++-------------- .../test.ts | 121 ++---------- 4 files changed, 214 insertions(+), 318 deletions(-) create mode 100644 dev-packages/browser-integration-tests/suites/profiling/test-utils.ts diff --git a/dev-packages/browser-integration-tests/suites/profiling/legacyMode/test.ts b/dev-packages/browser-integration-tests/suites/profiling/legacyMode/test.ts index d473236cdfda..4d8caa3a2be3 100644 --- a/dev-packages/browser-integration-tests/suites/profiling/legacyMode/test.ts +++ b/dev-packages/browser-integration-tests/suites/profiling/legacyMode/test.ts @@ -6,6 +6,7 @@ import { shouldSkipTracingTest, waitForTransactionRequestOnUrl, } from '../../../utils/helpers'; +import { validateProfile } from '../test-utils'; sentryTest( 'does not send profile envelope when document-policy is not set', @@ -41,79 +42,16 @@ sentryTest('sends profile envelope in legacy mode', async ({ page, getLocalTestU 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(typeof frame.function).toBe('string'); - - if (frame.function !== 'fetch' && frame.function !== 'setTimeout') { - expect(frame).toHaveProperty('abs_path'); - expect(frame).toHaveProperty('lineno'); - expect(frame).toHaveProperty('colno'); - 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 !== ''); - - if ((process.env.PW_BUNDLE || '').endsWith('min')) { - // Function names are minified in minified bundles - expect(functionNames.length).toBeGreaterThan(0); - expect((functionNames as string[]).every(name => name?.length > 0)).toBe(true); // Just make sure they're not empty strings - } else { - 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); + validateProfile(profile, { + expectedFunctionNames: [ + '_startRootSpan', + 'withScope', + 'createChildOrRootSpan', + 'startSpanManual', + 'startProfileForSpan', + 'startJSSelfProfile', + ], + minSampleDurationMs: 20, + isChunkFormat: false, + }); }); diff --git a/dev-packages/browser-integration-tests/suites/profiling/test-utils.ts b/dev-packages/browser-integration-tests/suites/profiling/test-utils.ts new file mode 100644 index 000000000000..e150be2d56bc --- /dev/null +++ b/dev-packages/browser-integration-tests/suites/profiling/test-utils.ts @@ -0,0 +1,151 @@ +import { expect } from '@playwright/test'; +import type { ContinuousThreadCpuProfile, ProfileChunk, ThreadCpuProfile } from '@sentry/core'; + +interface ValidateProfileOptions { + expectedFunctionNames?: string[]; + minSampleDurationMs?: number; + isChunkFormat?: boolean; +} + +/** + * Validates the metadata of a profile chunk envelope. + * https://develop.sentry.dev/sdk/telemetry/profiles/sample-format-v2/ + */ +export function validateProfilePayloadMetadata(profileChunk: ProfileChunk): void { + expect(profileChunk.version).toBe('2'); + expect(profileChunk.platform).toBe('javascript'); + + expect(typeof profileChunk.profiler_id).toBe('string'); + expect(profileChunk.profiler_id).toMatch(/^[a-f\d]{32}$/); + + expect(typeof profileChunk.chunk_id).toBe('string'); + expect(profileChunk.chunk_id).toMatch(/^[a-f\d]{32}$/); + + expect(profileChunk.client_sdk).toBeDefined(); + expect(typeof profileChunk.client_sdk.name).toBe('string'); + expect(typeof profileChunk.client_sdk.version).toBe('string'); + + expect(typeof profileChunk.release).toBe('string'); + + expect(profileChunk.debug_meta).toBeDefined(); + expect(Array.isArray(profileChunk?.debug_meta?.images)).toBe(true); +} + +/** + * Validates the basic structure and content of a Sentry profile. + */ +export function validateProfile( + profile: ThreadCpuProfile | ContinuousThreadCpuProfile, + options: ValidateProfileOptions = {}, +): void { + const { expectedFunctionNames, minSampleDurationMs, isChunkFormat = false } = options; + + // Basic profile structure + expect(profile.samples).toBeDefined(); + expect(profile.stacks).toBeDefined(); + expect(profile.frames).toBeDefined(); + expect(profile.thread_metadata).toBeDefined(); + + // SAMPLES + expect(profile.samples.length).toBeGreaterThanOrEqual(2); + let previousTimestamp: number = Number.NEGATIVE_INFINITY; + + for (const sample of profile.samples) { + expect(typeof sample.stack_id).toBe('number'); + expect(sample.stack_id).toBeGreaterThanOrEqual(0); + expect(sample.stack_id).toBeLessThan(profile.stacks.length); + + expect(sample.thread_id).toBe('0'); // Should be main thread + + // Timestamp validation - differs between chunk format (v2) and legacy format + if (isChunkFormat) { + const chunkProfileSample = sample as ContinuousThreadCpuProfile['samples'][number]; + + // Chunk format uses numeric timestamps (UNIX timestamp in seconds with microseconds precision) + expect(typeof chunkProfileSample.timestamp).toBe('number'); + const ts = chunkProfileSample.timestamp; + expect(Number.isFinite(ts)).toBe(true); + expect(ts).toBeGreaterThan(0); + // Monotonic non-decreasing timestamps + expect(ts).toBeGreaterThanOrEqual(previousTimestamp); + previousTimestamp = ts; + } else { + // Legacy format uses elapsed_since_start_ns as a string + const legacyProfileSample = sample as ThreadCpuProfile['samples'][number]; + + expect(typeof legacyProfileSample.elapsed_since_start_ns).toBe('string'); + expect(legacyProfileSample.elapsed_since_start_ns).toMatch(/^\d+$/); // Numeric string + expect(parseInt(legacyProfileSample.elapsed_since_start_ns, 10)).toBeGreaterThanOrEqual(0); + } + } + + // 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(typeof frame.function).toBe('string'); + + // Some browser functions (fetch, setTimeout) may not have file locations + if (frame.function !== 'fetch' && frame.function !== 'setTimeout') { + expect(frame).toHaveProperty('abs_path'); + expect(frame).toHaveProperty('lineno'); + expect(frame).toHaveProperty('colno'); + expect(typeof frame.abs_path).toBe('string'); + expect(typeof frame.lineno).toBe('number'); + expect(typeof frame.colno).toBe('number'); + } + } + + // Function names validation (only when not minified and expected names provided) + if (expectedFunctionNames && expectedFunctionNames.length > 0) { + const functionNames = profile.frames.map(frame => frame.function).filter(name => name !== ''); + + if ((process.env.PW_BUNDLE || '').endsWith('min')) { + // In minified bundles, just check that we have some non-empty function names + expect(functionNames.length).toBeGreaterThan(0); + expect((functionNames as string[]).every(name => name?.length > 0)).toBe(true); + } else { + // In non-minified bundles, check for expected function names + expect(functionNames).toEqual(expect.arrayContaining(expectedFunctionNames)); + } + } + + // THREAD METADATA + expect(profile.thread_metadata).toHaveProperty('0'); + expect(profile.thread_metadata['0']).toHaveProperty('name'); + expect(profile.thread_metadata['0'].name).toBe('main'); + + // DURATION + if (minSampleDurationMs !== undefined) { + let durationMs: number; + + if (isChunkFormat) { + // Chunk format: timestamps are in seconds + const chunkProfile = profile as ContinuousThreadCpuProfile; + + const startTimeSec = chunkProfile.samples[0].timestamp; + const endTimeSec = chunkProfile.samples[chunkProfile.samples.length - 1].timestamp; + durationMs = (endTimeSec - startTimeSec) * 1000; // Convert to ms + } else { + // Legacy format: elapsed_since_start_ns is in nanoseconds + const legacyProfile = profile as ThreadCpuProfile; + + const startTimeNs = parseInt(legacyProfile.samples[0].elapsed_since_start_ns, 10); + const endTimeNs = parseInt(legacyProfile.samples[legacyProfile.samples.length - 1].elapsed_since_start_ns, 10); + durationMs = (endTimeNs - startTimeNs) / 1_000_000; // Convert ns to ms + } + + expect(durationMs).toBeGreaterThan(minSampleDurationMs); + } +} diff --git a/dev-packages/browser-integration-tests/suites/profiling/traceLifecycleMode_multiple-chunks/test.ts b/dev-packages/browser-integration-tests/suites/profiling/traceLifecycleMode_multiple-chunks/test.ts index 421cdfc1e645..5afc23a3a75f 100644 --- a/dev-packages/browser-integration-tests/suites/profiling/traceLifecycleMode_multiple-chunks/test.ts +++ b/dev-packages/browser-integration-tests/suites/profiling/traceLifecycleMode_multiple-chunks/test.ts @@ -7,6 +7,7 @@ import { properFullEnvelopeRequestParser, shouldSkipTracingTest, } from '../../../utils/helpers'; +import { validateProfile, validateProfilePayloadMetadata } from '../test-utils'; sentryTest( 'does not send profile envelope when document-policy is not set', @@ -51,109 +52,24 @@ sentryTest( const envelopeItemPayload1 = profileChunkEnvelopeItem[1]; expect(envelopeItemHeader).toHaveProperty('type', 'profile_chunk'); - expect(envelopeItemPayload1.profile).toBeDefined(); - expect(envelopeItemPayload1.version).toBe('2'); - expect(envelopeItemPayload1.platform).toBe('javascript'); - - // Required profile metadata (Sample Format V2) - expect(typeof envelopeItemPayload1.profiler_id).toBe('string'); - expect(envelopeItemPayload1.profiler_id).toMatch(/^[a-f\d]{32}$/); - expect(typeof envelopeItemPayload1.chunk_id).toBe('string'); - expect(envelopeItemPayload1.chunk_id).toMatch(/^[a-f\d]{32}$/); - expect(envelopeItemPayload1.client_sdk).toBeDefined(); - expect(typeof envelopeItemPayload1.client_sdk.name).toBe('string'); - expect(typeof envelopeItemPayload1.client_sdk.version).toBe('string'); - expect(typeof envelopeItemPayload1.release).toBe('string'); - expect(envelopeItemPayload1.debug_meta).toBeDefined(); - expect(Array.isArray(envelopeItemPayload1?.debug_meta?.images)).toBe(true); - - const profile1 = envelopeItemPayload1.profile; - - expect(profile1.samples).toBeDefined(); - expect(profile1.stacks).toBeDefined(); - expect(profile1.frames).toBeDefined(); - expect(profile1.thread_metadata).toBeDefined(); - - // Samples - expect(profile1.samples.length).toBeGreaterThanOrEqual(2); - let previousTimestamp = Number.NEGATIVE_INFINITY; - for (const sample of profile1.samples) { - expect(typeof sample.stack_id).toBe('number'); - expect(sample.stack_id).toBeGreaterThanOrEqual(0); - expect(sample.stack_id).toBeLessThan(profile1.stacks.length); - - // In trace lifecycle mode, samples carry a numeric timestamp (ms since epoch or similar clock) - expect(typeof (sample as any).timestamp).toBe('number'); - const ts = (sample as any).timestamp as number; - expect(Number.isFinite(ts)).toBe(true); - expect(ts).toBeGreaterThan(0); - // Monotonic non-decreasing timestamps - expect(ts).toBeGreaterThanOrEqual(previousTimestamp); - previousTimestamp = ts; - - expect(sample.thread_id).toBe('0'); // Should be main thread - } - - // Stacks - expect(profile1.stacks.length).toBeGreaterThan(0); - for (const stack of profile1.stacks) { - expect(Array.isArray(stack)).toBe(true); - for (const frameIndex of stack) { - expect(typeof frameIndex).toBe('number'); - expect(frameIndex).toBeGreaterThanOrEqual(0); - expect(frameIndex).toBeLessThan(profile1.frames.length); - } - } - - // Frames - expect(profile1.frames.length).toBeGreaterThan(0); - for (const frame of profile1.frames) { - expect(frame).toHaveProperty('function'); - expect(typeof frame.function).toBe('string'); - - if (frame.function !== 'fetch' && frame.function !== 'setTimeout') { - expect(frame).toHaveProperty('abs_path'); - expect(frame).toHaveProperty('lineno'); - expect(frame).toHaveProperty('colno'); - expect(typeof frame.abs_path).toBe('string'); - expect(typeof frame.lineno).toBe('number'); - expect(typeof frame.colno).toBe('number'); - } - } - const functionNames = profile1.frames.map(frame => frame.function).filter(name => name !== ''); - - if ((process.env.PW_BUNDLE || '').endsWith('min')) { - // In bundled mode, function names are minified - expect(functionNames.length).toBeGreaterThan(0); - expect((functionNames as string[]).every(name => name?.length > 0)).toBe(true); // Just make sure they're not empty strings - } else { - expect(functionNames).toEqual( - expect.arrayContaining([ - '_startRootSpan', - 'withScope', - 'createChildOrRootSpan', - 'startSpanManual', - 'startJSSelfProfile', - - // first function is captured (other one is in other chunk) - 'fibonacci', - ]), - ); - } - - expect(profile1.thread_metadata).toHaveProperty('0'); - expect(profile1.thread_metadata['0']).toHaveProperty('name'); - expect(profile1.thread_metadata['0'].name).toBe('main'); - - // Test that profile duration makes sense (should be > 20ms based on test setup) - const startTimeSec = (profile1.samples[0] as any).timestamp as number; - const endTimeSec = (profile1.samples[profile1.samples.length - 1] as any).timestamp as number; - const durationSec = endTimeSec - startTimeSec; - - // Should be at least 20ms based on our setTimeout(21) in the test - expect(durationSec).toBeGreaterThan(0.2); + validateProfilePayloadMetadata(envelopeItemPayload1); + + validateProfile(envelopeItemPayload1.profile, { + expectedFunctionNames: [ + '_startRootSpan', + 'withScope', + 'createChildOrRootSpan', + 'startSpanManual', + 'startJSSelfProfile', + // first function is captured (other one is in other chunk) + 'fibonacci', + ], + // Should be at least 20ms based on our setTimeout(21) in the test + minSampleDurationMs: 20, + isChunkFormat: true, + }); // === PROFILE CHUNK 2 === @@ -161,46 +77,22 @@ sentryTest( const envelopeItemHeader2 = profileChunkEnvelopeItem2[0]; const envelopeItemPayload2 = profileChunkEnvelopeItem2[1]; - // Basic sanity on the second chunk: has correct envelope type and structure expect(envelopeItemHeader2).toHaveProperty('type', 'profile_chunk'); expect(envelopeItemPayload2.profile).toBeDefined(); - expect(envelopeItemPayload2.version).toBe('2'); - expect(envelopeItemPayload2.platform).toBe('javascript'); - - // Required profile metadata (Sample Format V2) - // https://develop.sentry.dev/sdk/telemetry/profiles/sample-format-v2/ - expect(typeof envelopeItemPayload2.profiler_id).toBe('string'); - expect(envelopeItemPayload2.profiler_id).toMatch(/^[a-f\d]{32}$/); - expect(typeof envelopeItemPayload2.chunk_id).toBe('string'); - expect(envelopeItemPayload2.chunk_id).toMatch(/^[a-f\d]{32}$/); - expect(envelopeItemPayload2.client_sdk).toBeDefined(); - expect(typeof envelopeItemPayload2.client_sdk.name).toBe('string'); - expect(typeof envelopeItemPayload2.client_sdk.version).toBe('string'); - expect(typeof envelopeItemPayload2.release).toBe('string'); - expect(envelopeItemPayload2.debug_meta).toBeDefined(); - expect(Array.isArray(envelopeItemPayload2?.debug_meta?.images)).toBe(true); - - const profile2 = envelopeItemPayload2.profile; - - const functionNames2 = profile2.frames.map(frame => frame.function).filter(name => name !== ''); - - if ((process.env.PW_BUNDLE || '').endsWith('min')) { - // In bundled mode, function names are minified - expect(functionNames2.length).toBeGreaterThan(0); - expect((functionNames2 as string[]).every(name => name?.length > 0)).toBe(true); // Just make sure they're not empty strings - } else { - expect(functionNames2).toEqual( - expect.arrayContaining([ - '_startRootSpan', - 'withScope', - 'createChildOrRootSpan', - 'startSpanManual', - 'startJSSelfProfile', - - // second function is captured (other one is in other chunk) - 'largeSum', - ]), - ); - } + + validateProfilePayloadMetadata(envelopeItemPayload2); + + validateProfile(envelopeItemPayload2.profile, { + expectedFunctionNames: [ + '_startRootSpan', + 'withScope', + 'createChildOrRootSpan', + 'startSpanManual', + 'startJSSelfProfile', + // second function is captured (other one is in other chunk) + 'largeSum', + ], + isChunkFormat: true, + }); }, ); diff --git a/dev-packages/browser-integration-tests/suites/profiling/traceLifecycleMode_overlapping-spans/test.ts b/dev-packages/browser-integration-tests/suites/profiling/traceLifecycleMode_overlapping-spans/test.ts index 161f74d64e83..fa66a225b49b 100644 --- a/dev-packages/browser-integration-tests/suites/profiling/traceLifecycleMode_overlapping-spans/test.ts +++ b/dev-packages/browser-integration-tests/suites/profiling/traceLifecycleMode_overlapping-spans/test.ts @@ -8,6 +8,7 @@ import { shouldSkipTracingTest, waitForTransactionRequestOnUrl, } from '../../../utils/helpers'; +import { validateProfile, validateProfilePayloadMetadata } from '../test-utils'; sentryTest( 'does not send profile envelope when document-policy is not set', @@ -52,111 +53,25 @@ sentryTest( const envelopeItemPayload = profileChunkEnvelopeItem[1]; expect(envelopeItemHeader).toHaveProperty('type', 'profile_chunk'); - expect(envelopeItemPayload.profile).toBeDefined(); - expect(envelopeItemPayload.version).toBe('2'); - expect(envelopeItemPayload.platform).toBe('javascript'); - - // Required profile metadata (Sample Format V2) - // https://develop.sentry.dev/sdk/telemetry/profiles/sample-format-v2/ - expect(typeof envelopeItemPayload.profiler_id).toBe('string'); - expect(envelopeItemPayload.profiler_id).toMatch(/^[a-f\d]{32}$/); - expect(typeof envelopeItemPayload.chunk_id).toBe('string'); - expect(envelopeItemPayload.chunk_id).toMatch(/^[a-f\d]{32}$/); - expect(envelopeItemPayload.client_sdk).toBeDefined(); - expect(typeof envelopeItemPayload.client_sdk.name).toBe('string'); - expect(typeof envelopeItemPayload.client_sdk.version).toBe('string'); - expect(typeof envelopeItemPayload.release).toBe('string'); - expect(envelopeItemPayload.debug_meta).toBeDefined(); - expect(Array.isArray(envelopeItemPayload?.debug_meta?.images)).toBe(true); - - const profile = envelopeItemPayload.profile; - - expect(profile.samples).toBeDefined(); - expect(profile.stacks).toBeDefined(); - expect(profile.frames).toBeDefined(); - expect(profile.thread_metadata).toBeDefined(); - - // Samples - expect(profile.samples.length).toBeGreaterThanOrEqual(2); - let previousTimestamp = Number.NEGATIVE_INFINITY; - for (const sample of profile.samples) { - expect(typeof sample.stack_id).toBe('number'); - expect(sample.stack_id).toBeGreaterThanOrEqual(0); - expect(sample.stack_id).toBeLessThan(profile.stacks.length); - - // In trace lifecycle mode, samples carry a numeric timestamp (ms since epoch or similar clock) - expect(typeof sample.timestamp).toBe('number'); - const ts = sample.timestamp; - expect(Number.isFinite(ts)).toBe(true); - expect(ts).toBeGreaterThan(0); - // Monotonic non-decreasing timestamps - expect(ts).toBeGreaterThanOrEqual(previousTimestamp); - previousTimestamp = ts; - - 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(typeof frame.function).toBe('string'); - - if (frame.function !== 'fetch' && frame.function !== 'setTimeout') { - expect(frame).toHaveProperty('abs_path'); - expect(frame).toHaveProperty('lineno'); - expect(frame).toHaveProperty('colno'); - 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 !== ''); - - if ((process.env.PW_BUNDLE || '').endsWith('min')) { - // In bundled mode, function names are minified - expect(functionNames.length).toBeGreaterThan(0); - expect((functionNames as string[]).every(name => name?.length > 0)).toBe(true); // Just make sure they're not empty strings - } else { - expect(functionNames).toEqual( - expect.arrayContaining([ - '_startRootSpan', - 'withScope', - 'createChildOrRootSpan', - 'startSpanManual', - 'startJSSelfProfile', - - // both functions are captured - 'fibonacci', - 'largeSum', - ]), - ); - } - - 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 startTimeSec = (profile.samples[0] as any).timestamp as number; - const endTimeSec = (profile.samples[profile.samples.length - 1] as any).timestamp as number; - const durationSec = endTimeSec - startTimeSec; - // Should be at least 20ms based on our setTimeout(21) in the test - expect(durationSec).toBeGreaterThan(0.2); + validateProfilePayloadMetadata(envelopeItemPayload); + + validateProfile(envelopeItemPayload.profile, { + expectedFunctionNames: [ + '_startRootSpan', + 'withScope', + 'createChildOrRootSpan', + 'startSpanManual', + 'startJSSelfProfile', + // both functions are captured + 'fibonacci', + 'largeSum', + ], + // Test that profile duration makes sense (should be > 20ms based on test setup + minSampleDurationMs: 20, + isChunkFormat: true, + }); }, );