11import {
22 buildFromError ,
33 buildFromMessage ,
4- buildFromPerformance ,
4+ buildFromVitals ,
55} from "./payload.js" ;
66import { Transport } from "./transport.js" ;
77
@@ -280,26 +280,31 @@ class DevPulseClient {
280280 _trackWebVitals ( ) {
281281 if ( ! ( "PerformanceObserver" in window ) ) return ;
282282
283- // LCP — only report the final value
283+ // Accumulate all metrics; send ONE combined event per page load.
284+ // Keeping the message constant ("Performance vitals") means all page loads
285+ // group into a single issue instead of flooding the list.
286+ const vitals = { } ;
287+ let sent = false ;
288+
289+ const sendVitals = ( ) => {
290+ if ( sent || Object . keys ( vitals ) . length === 0 ) return ;
291+ sent = true ;
292+ const payload = {
293+ ...buildFromVitals ( vitals ) ,
294+ environment : this . config . environment ,
295+ release : this . config . release ,
296+ session_id : this . _sessionId ,
297+ } ;
298+ this . transport . send ( payload ) ;
299+ } ;
300+
301+ // LCP — only the final value before page hides
284302 let latestLcp = null ;
285303 this . _observe ( "largest-contentful-paint" , ( entries ) => {
286304 latestLcp = entries [ entries . length - 1 ] ;
287305 } ) ;
288306
289- const sendLcp = ( ) => {
290- if ( latestLcp ) {
291- this . transport . send ( buildFromPerformance ( "LCP" , latestLcp . startTime ) ) ;
292- latestLcp = null ;
293- }
294- } ;
295- window . addEventListener ( "pagehide" , sendLcp , { once : true } ) ;
296- document . addEventListener (
297- "visibilitychange" ,
298- ( ) => { if ( document . visibilityState === "hidden" ) sendLcp ( ) ; } ,
299- { once : true } ,
300- ) ;
301-
302- // INP — Interaction to Next Paint
307+ // INP — worst interaction on the page
303308 let inpValue = 0 ;
304309 this . _observe (
305310 "event" ,
@@ -310,29 +315,38 @@ class DevPulseClient {
310315 } ,
311316 { durationThreshold : 40 } ,
312317 ) ;
313- window . addEventListener ( "pagehide" , ( ) => {
314- if ( inpValue > 0 ) this . transport . send ( buildFromPerformance ( "INP" , inpValue ) ) ;
315- } ) ;
316318
317- // CLS — Cumulative Layout Shift (unitless)
319+ // CLS — cumulative score (unitless 0–1 )
318320 let clsValue = 0 ;
319321 this . _observe ( "layout-shift" , ( entries ) => {
320322 for ( const entry of entries ) {
321323 if ( ! entry . hadRecentInput ) clsValue += entry . value ;
322324 }
323325 } ) ;
324- window . addEventListener ( "pagehide" , ( ) => {
325- if ( clsValue > 0 ) this . transport . send ( buildFromPerformance ( "CLS" , clsValue , { unit : "" } ) ) ;
326- } ) ;
327326
328- // TTFB + PageLoad
327+ // TTFB + PageLoad — available after load event
329328 window . addEventListener ( "load" , ( ) => {
330329 const nav = performance . getEntriesByType ( "navigation" ) [ 0 ] ;
331330 if ( nav ) {
332- this . transport . send ( buildFromPerformance ( "TTFB" , nav . responseStart ) ) ;
333- this . transport . send ( buildFromPerformance ( "PageLoad" , nav . loadEventEnd ) ) ;
331+ vitals . ttfb = Math . round ( nav . responseStart ) ;
332+ vitals . page_load = Math . round ( nav . loadEventEnd ) ;
334333 }
335334 } ) ;
335+
336+ // Flush all accumulated vitals on page hide / tab switch
337+ const onHide = ( ) => {
338+ if ( latestLcp ) vitals . lcp = Math . round ( latestLcp . startTime ) ;
339+ if ( inpValue > 0 ) vitals . inp = Math . round ( inpValue ) ;
340+ if ( clsValue > 0 ) vitals . cls = + Number ( clsValue ) . toFixed ( 4 ) ;
341+ sendVitals ( ) ;
342+ } ;
343+
344+ window . addEventListener ( "pagehide" , onHide , { once : true } ) ;
345+ document . addEventListener (
346+ "visibilitychange" ,
347+ ( ) => { if ( document . visibilityState === "hidden" ) onHide ( ) ; } ,
348+ { once : true } ,
349+ ) ;
336350 }
337351
338352 _observe ( type , callback , observeOptions = { } ) {
0 commit comments