Skip to content

Commit 5e34504

Browse files
logaretmclaude
andcommitted
ref(browser-tests): Add waitForMetricRequest helper and refactor element timing tests
Replace custom createMetricCollector/waitForIdentifiers polling pattern with a shared waitForMetricRequest helper that matches existing waitFor patterns (waitForErrorRequest, waitForTransactionRequest, etc.). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
1 parent c0d52df commit 5e34504

File tree

2 files changed

+84
-97
lines changed
  • dev-packages/browser-integration-tests

2 files changed

+84
-97
lines changed

dev-packages/browser-integration-tests/suites/tracing/metrics/element-timing/test.ts

Lines changed: 32 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,15 @@
1-
import type { Page, Request, Route } from '@playwright/test';
1+
import type { Page, Route } from '@playwright/test';
22
import { expect } from '@playwright/test';
3-
import type { Envelope } from '@sentry/core';
3+
import type { SerializedMetric } from '@sentry/core';
44
import { sentryTest } from '../../../../utils/fixtures';
5-
import {
6-
properFullEnvelopeRequestParser,
7-
shouldSkipMetricsTest,
8-
shouldSkipTracingTest,
9-
} from '../../../../utils/helpers';
10-
11-
type MetricItem = Record<string, unknown> & {
12-
name: string;
13-
type: string;
14-
value: number;
15-
unit?: string;
16-
attributes: Record<string, { value: string | number; type: string }>;
17-
};
18-
19-
function extractMetricsFromRequest(req: Request): MetricItem[] {
20-
try {
21-
const envelope = properFullEnvelopeRequestParser<Envelope>(req);
22-
const items = envelope[1];
23-
const metrics: MetricItem[] = [];
24-
for (const item of items) {
25-
const [header] = item;
26-
if (header.type === 'trace_metric') {
27-
const payload = item[1] as { items?: MetricItem[] };
28-
if (payload.items) {
29-
metrics.push(...payload.items);
30-
}
31-
}
32-
}
33-
return metrics;
34-
} catch {
35-
return [];
36-
}
37-
}
38-
39-
/**
40-
* Collects element timing metrics from envelope requests on the page.
41-
* Returns a function to get all collected metrics so far and a function
42-
* that waits until all expected identifiers have been seen in render_time metrics.
43-
*/
44-
function createMetricCollector(page: Page) {
45-
const collectedRequests: Request[] = [];
46-
47-
page.on('request', req => {
48-
if (!req.url().includes('/api/1337/envelope/')) return;
49-
const metrics = extractMetricsFromRequest(req);
50-
if (metrics.some(m => m.name.startsWith('ui.element.'))) {
51-
collectedRequests.push(req);
52-
}
53-
});
54-
55-
function getAll(): MetricItem[] {
56-
return collectedRequests.flatMap(req => extractMetricsFromRequest(req));
57-
}
5+
import { shouldSkipMetricsTest, shouldSkipTracingTest, waitForMetricRequest } from '../../../../utils/helpers';
586

59-
async function waitForIdentifiers(identifiers: string[], timeout = 30_000): Promise<void> {
60-
const deadline = Date.now() + timeout;
61-
while (Date.now() < deadline) {
62-
const all = getAll().filter(m => m.name === 'ui.element.render_time');
63-
const seen = new Set(all.map(m => m.attributes['ui.element.identifier']?.value));
64-
if (identifiers.every(id => seen.has(id))) {
65-
return;
66-
}
67-
await page.waitForTimeout(500);
68-
}
69-
// Final check with assertion for clear error message
70-
const all = getAll().filter(m => m.name === 'ui.element.render_time');
71-
const seen = all.map(m => m.attributes['ui.element.identifier']?.value);
72-
for (const id of identifiers) {
73-
expect(seen).toContain(id);
74-
}
75-
}
76-
77-
function reset(): void {
78-
collectedRequests.length = 0;
79-
}
7+
function getIdentifier(m: SerializedMetric): unknown {
8+
return m.attributes?.['ui.element.identifier']?.value;
9+
}
8010

81-
return { getAll, waitForIdentifiers, reset };
11+
function getPaintType(m: SerializedMetric): unknown {
12+
return m.attributes?.['ui.element.paint_type']?.value;
8213
}
8314

8415
sentryTest(
@@ -91,19 +22,23 @@ sentryTest(
9122
serveAssets(page);
9223

9324
const url = await getLocalTestUrl({ testDir: __dirname });
94-
const collector = createMetricCollector(page);
9525

96-
await page.goto(url);
26+
const expectedIdentifiers = ['image-fast', 'text1', 'button1', 'image-slow', 'lazy-image', 'lazy-text'];
9727

98-
// Wait until all expected element identifiers have been flushed as metrics
99-
await collector.waitForIdentifiers(['image-fast', 'text1', 'button1', 'image-slow', 'lazy-image', 'lazy-text']);
28+
// Wait for all expected element identifiers to arrive as metrics
29+
const [allMetrics] = await Promise.all([
30+
waitForMetricRequest(page, metrics => {
31+
const seen = new Set(metrics.filter(m => m.name === 'ui.element.render_time').map(getIdentifier));
32+
return expectedIdentifiers.every(id => seen.has(id));
33+
}),
34+
page.goto(url),
35+
]);
10036

101-
const allMetrics = collector.getAll().filter(m => m.name.startsWith('ui.element.'));
10237
const renderTimeMetrics = allMetrics.filter(m => m.name === 'ui.element.render_time');
10338
const loadTimeMetrics = allMetrics.filter(m => m.name === 'ui.element.load_time');
10439

105-
const renderIdentifiers = renderTimeMetrics.map(m => m.attributes['ui.element.identifier']?.value);
106-
const loadIdentifiers = loadTimeMetrics.map(m => m.attributes['ui.element.identifier']?.value);
40+
const renderIdentifiers = renderTimeMetrics.map(getIdentifier);
41+
const loadIdentifiers = loadTimeMetrics.map(getIdentifier);
10742

10843
// All text and image elements should have render_time
10944
expect(renderIdentifiers).toContain('image-fast');
@@ -124,18 +59,18 @@ sentryTest(
12459
expect(loadIdentifiers).not.toContain('lazy-text');
12560

12661
// Validate metric structure for image-fast
127-
const imageFastRender = renderTimeMetrics.find(m => m.attributes['ui.element.identifier']?.value === 'image-fast');
62+
const imageFastRender = renderTimeMetrics.find(m => getIdentifier(m) === 'image-fast');
12863
expect(imageFastRender).toMatchObject({
12964
name: 'ui.element.render_time',
13065
type: 'distribution',
13166
unit: 'millisecond',
13267
value: expect.any(Number),
13368
});
134-
expect(imageFastRender!.attributes['ui.element.paint_type']?.value).toBe('image-paint');
69+
expect(getPaintType(imageFastRender!)).toBe('image-paint');
13570

13671
// Validate text-paint metric
137-
const text1Render = renderTimeMetrics.find(m => m.attributes['ui.element.identifier']?.value === 'text1');
138-
expect(text1Render!.attributes['ui.element.paint_type']?.value).toBe('text-paint');
72+
const text1Render = renderTimeMetrics.find(m => getIdentifier(m) === 'text1');
73+
expect(getPaintType(text1Render!)).toBe('text-paint');
13974
},
14075
);
14176

@@ -147,25 +82,25 @@ sentryTest('emits element timing metrics after navigation', async ({ getLocalTes
14782
serveAssets(page);
14883

14984
const url = await getLocalTestUrl({ testDir: __dirname });
150-
const collector = createMetricCollector(page);
15185

15286
await page.goto(url);
15387

15488
// Wait for pageload element timing metrics to arrive before navigating
155-
await collector.waitForIdentifiers(['image-fast', 'text1']);
156-
157-
// Reset so we only capture post-navigation metrics
158-
collector.reset();
89+
await waitForMetricRequest(page, metrics =>
90+
metrics.some(m => m.name === 'ui.element.render_time' && getIdentifier(m) === 'image-fast'),
91+
);
15992

16093
// Trigger navigation
16194
await page.locator('#button1').click();
16295

16396
// Wait for navigation element timing metrics
164-
await collector.waitForIdentifiers(['navigation-image', 'navigation-text']);
97+
const navigationMetrics = await waitForMetricRequest(page, metrics => {
98+
const seen = new Set(metrics.filter(m => m.name === 'ui.element.render_time').map(getIdentifier));
99+
return seen.has('navigation-image') && seen.has('navigation-text');
100+
});
165101

166-
const allMetrics = collector.getAll();
167-
const renderTimeMetrics = allMetrics.filter(m => m.name === 'ui.element.render_time');
168-
const renderIdentifiers = renderTimeMetrics.map(m => m.attributes['ui.element.identifier']?.value);
102+
const renderTimeMetrics = navigationMetrics.filter(m => m.name === 'ui.element.render_time');
103+
const renderIdentifiers = renderTimeMetrics.map(getIdentifier);
169104

170105
expect(renderIdentifiers).toContain('navigation-image');
171106
expect(renderIdentifiers).toContain('navigation-text');

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

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import type {
88
Event as SentryEvent,
99
EventEnvelope,
1010
EventEnvelopeHeaders,
11+
SerializedMetric,
12+
SerializedMetricContainer,
1113
SerializedSession,
1214
TransactionEvent,
1315
} from '@sentry/core';
@@ -283,6 +285,56 @@ export function waitForClientReportRequest(page: Page, callback?: (report: Clien
283285
});
284286
}
285287

288+
/**
289+
* Wait for metric requests. Accumulates metrics across all matching requests
290+
* and resolves when the callback returns true for the full set of collected metrics.
291+
* If no callback is provided, resolves on the first request containing metrics.
292+
*/
293+
export function waitForMetricRequest(
294+
page: Page,
295+
callback?: (metrics: SerializedMetric[]) => boolean,
296+
): Promise<SerializedMetric[]> {
297+
const collected: SerializedMetric[] = [];
298+
299+
return page
300+
.waitForRequest(req => {
301+
const postData = req.postData();
302+
if (!postData) {
303+
return false;
304+
}
305+
306+
try {
307+
const envelope = properFullEnvelopeRequestParser<Envelope>(req);
308+
const items = envelope[1];
309+
const metrics: SerializedMetric[] = [];
310+
for (const item of items) {
311+
const [header] = item;
312+
if (header.type === 'trace_metric') {
313+
const payload = item[1] as SerializedMetricContainer;
314+
if (payload.items) {
315+
metrics.push(...payload.items);
316+
}
317+
}
318+
}
319+
320+
if (metrics.length === 0) {
321+
return false;
322+
}
323+
324+
collected.push(...metrics);
325+
326+
if (callback) {
327+
return callback(collected);
328+
}
329+
330+
return true;
331+
} catch {
332+
return false;
333+
}
334+
})
335+
.then(() => collected);
336+
}
337+
286338
export async function waitForSession(
287339
page: Page,
288340
callback?: (session: SerializedSession) => boolean,

0 commit comments

Comments
 (0)