Skip to content

Commit 51231a4

Browse files
committed
add profile validation
1 parent 5847efa commit 51231a4

File tree

3 files changed

+72
-3
lines changed

3 files changed

+72
-3
lines changed

dev-packages/browser-integration-tests/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"postinstall": "yarn install-browsers",
1717
"pretest": "yarn clean && yarn type-check",
1818
"test": "yarn test:all --project='chromium'",
19-
"test:all": "npx playwright test -c playwright.browser.config.ts",
19+
"test:all": "npx playwright test -c playwright.browser.config.ts -g 'trace mode'",
2020
"test:bundle": "PW_BUNDLE=bundle yarn test",
2121
"test:bundle:min": "PW_BUNDLE=bundle_min yarn test",
2222
"test:bundle:replay": "PW_BUNDLE=bundle_replay yarn test",

packages/browser/src/profiling/lifecycleMode/traceLifecycleProfiler.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from '@sentry/core';
1313
import { DEBUG_BUILD } from '../../debug-build';
1414
import type { JSSelfProfiler } from '../jsSelfProfiling';
15-
import { createProfileChunkPayload, startJSSelfProfile } from '../utils';
15+
import { createProfileChunkPayload, startJSSelfProfile, validateProfileChunk } from '../utils';
1616

1717
const CHUNK_INTERVAL_MS = 60_000;
1818

@@ -269,10 +269,18 @@ export class BrowserTraceLifecycleProfiler {
269269
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
270270
const chunk = createProfileChunkPayload(profile, this._client!, this._profilerId);
271271

272+
// Validate chunk before sending
273+
const { valid, reason } = validateProfileChunk(chunk);
274+
if (!valid) {
275+
DEBUG_BUILD &&
276+
debug.log('[Profiling] Discarding invalid profile chunk (this is probably a bug in the SDK):', reason);
277+
return;
278+
}
279+
272280
this._sendProfileChunk(chunk);
273281
DEBUG_BUILD && debug.log('[Profiling] Collected browser profile chunk.');
274282
} catch (e) {
275-
DEBUG_BUILD && debug.log('[Profiling] Error while stopping JS self profiler for chunk:', e);
283+
DEBUG_BUILD && debug.log('[Profiling] Error while stopping JS Profiler for chunk:', e);
276284
}
277285
}
278286

packages/browser/src/profiling/utils.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,67 @@ export function createProfileChunkPayload(
248248
};
249249
}
250250

251+
/**
252+
* Validate a profile chunk against the Sample Format V2 requirements.
253+
* https://develop.sentry.dev/sdk/telemetry/profiles/sample-format-v2/
254+
* - Presence of samples, stacks, frames
255+
* - Required metadata fields
256+
*/
257+
export function validateProfileChunk(chunk: ProfileChunk): { valid: boolean; reason?: string } {
258+
try {
259+
// Required metadata
260+
if (!chunk || typeof chunk !== 'object') {
261+
return { valid: false, reason: 'chunk is not an object' };
262+
}
263+
264+
// profiler_id and chunk_id must be 32 lowercase hex chars
265+
const isHex32 = (val: unknown): boolean => typeof val === 'string' && /^[a-f0-9]{32}$/.test(val);
266+
if (!isHex32(chunk.profiler_id)) {
267+
return { valid: false, reason: 'missing or invalid profiler_id' };
268+
}
269+
if (!isHex32(chunk.chunk_id)) {
270+
return { valid: false, reason: 'missing or invalid chunk_id' };
271+
}
272+
273+
// client_sdk name/version are required
274+
if (
275+
!chunk.client_sdk ||
276+
typeof chunk.client_sdk.name !== 'string' ||
277+
typeof chunk.client_sdk.version !== 'string'
278+
) {
279+
return { valid: false, reason: 'missing client_sdk metadata' };
280+
}
281+
282+
if (typeof chunk.platform !== 'string') {
283+
return { valid: false, reason: 'missing platform' };
284+
}
285+
286+
if (typeof chunk.release !== 'string') {
287+
return { valid: false, reason: 'missing release' };
288+
}
289+
290+
// Profile data must have frames, stacks, samples
291+
const profile = chunk.profile as { frames?: unknown[]; stacks?: unknown[]; samples?: unknown[] } | undefined;
292+
if (!profile) {
293+
return { valid: false, reason: 'missing profile data' };
294+
}
295+
296+
if (!Array.isArray(profile.frames) || profile.frames.length === 0) {
297+
return { valid: false, reason: 'profile has no frames' };
298+
}
299+
if (!Array.isArray(profile.stacks) || profile.stacks.length === 0) {
300+
return { valid: false, reason: 'profile has no stacks' };
301+
}
302+
if (!Array.isArray(profile.samples) || profile.samples.length === 0) {
303+
return { valid: false, reason: 'profile has no samples' };
304+
}
305+
306+
return { valid: true };
307+
} catch (e) {
308+
return { valid: false, reason: `unknown validation error: ${e}` };
309+
}
310+
}
311+
251312
/**
252313
* Convert from JSSelfProfile format to ContinuousThreadCpuProfile format.
253314
*/

0 commit comments

Comments
 (0)