-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathtest-utils.ts
More file actions
154 lines (127 loc) · 6.17 KB
/
test-utils.ts
File metadata and controls
154 lines (127 loc) · 6.17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import { expect } from '@playwright/test';
import type { ContinuousThreadCpuProfile, ProfileChunk, ThreadCpuProfile } from '@sentry/core';
interface ValidateProfileOptions {
expectedFunctionNames?: string[];
minSampleDurationMs?: number;
isChunkFormat?: boolean;
}
/** Seconds — consecutive chunk timestamps can jitter slightly below float precision (see profiling flakes). */
const CHUNK_SAMPLE_TIMESTAMP_EPSILON_SEC = 1e-5;
/**
* 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 (epsilon: jitter / IEEE754 around ~1e9 epoch seconds)
expect(ts).toBeGreaterThanOrEqual(previousTimestamp - CHUNK_SAMPLE_TIMESTAMP_EPSILON_SEC);
previousTimestamp = Math.max(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);
}
}