1- import type { Page , Request , Route } from '@playwright/test' ;
1+ import type { Page , Route } from '@playwright/test' ;
22import { expect } from '@playwright/test' ;
3- import type { Envelope } from '@sentry/core' ;
3+ import type { SerializedMetric } from '@sentry/core' ;
44import { 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
8415sentryTest (
@@ -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' ) ;
0 commit comments