diff --git a/src/format-event-data.ts b/src/format-event-data.ts index 89a6c8b..d8dffa2 100644 --- a/src/format-event-data.ts +++ b/src/format-event-data.ts @@ -1,14 +1,16 @@ import type { - LCPAttribution, CLSAttribution, + FCPAttribution, INPAttribution, + LCPAttribution, } from 'web-vitals'; -export type WebVitalsName = 'LCP' | 'INP' | 'CLS'; +export type WebVitalsName = 'CLS' | 'FCP' | 'INP' | 'LCP'; export type WebVitalsAttribution = - | LCPAttribution + | CLSAttribution + | FCPAttribution | INPAttribution - | CLSAttribution; + | LCPAttribution; export const formatEventData = ( name: WebVitalsName, @@ -17,18 +19,21 @@ export const formatEventData = ( // In some cases there won't be any entries (e.g. if CLS is 0, // or for LCP after a bfcache restore), so we have to check first. if (attribution) { - if (name === 'LCP') { + if (name === 'CLS') { return { - debug_url: (attribution as LCPAttribution).url, - debug_time_to_first_byte: (attribution as LCPAttribution) + debug_time: (attribution as CLSAttribution).largestShiftTime, + debug_load_state: (attribution as CLSAttribution).loadState, + debug_target: + (attribution as CLSAttribution).largestShiftTarget || '(not set)', + }; + } + if (name === 'FCP') { + return { + debug_time_to_first_byte: (attribution as FCPAttribution) .timeToFirstByte, - debug_resource_load_delay: (attribution as LCPAttribution) - .resourceLoadDelay, - debug_resource_load_duration: (attribution as LCPAttribution) - .resourceLoadDuration, - debug_element_render_delay: (attribution as LCPAttribution) - .elementRenderDelay, - debug_target: (attribution as LCPAttribution).element || '(not set)', + debug_first_byte_to_fcp: (attribution as FCPAttribution).firstByteToFCP, + debug_load_state: (attribution as FCPAttribution).loadState, + debug_target: (attribution as FCPAttribution).loadState || '(not set)', }; } if (name === 'INP') { @@ -47,14 +52,21 @@ export const formatEventData = ( debug_presentation_delay: Math.round( (attribution as INPAttribution).presentationDelay, ), + // TODO: add LoAf attribution here }; } - if (name === 'CLS') { + if (name === 'LCP') { return { - debug_time: (attribution as CLSAttribution).largestShiftTime, - debug_load_state: (attribution as CLSAttribution).loadState, - debug_target: - (attribution as CLSAttribution).largestShiftTarget || '(not set)', + debug_url: (attribution as LCPAttribution).url, + debug_time_to_first_byte: (attribution as LCPAttribution) + .timeToFirstByte, + debug_resource_load_delay: (attribution as LCPAttribution) + .resourceLoadDelay, + debug_resource_load_duration: (attribution as LCPAttribution) + .resourceLoadDuration, + debug_element_render_delay: (attribution as LCPAttribution) + .elementRenderDelay, + debug_target: (attribution as LCPAttribution).element || '(not set)', }; } } diff --git a/src/index.ts b/src/index.ts index 8b76c89..48e86e4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,16 +1,19 @@ -import { onCLS, onINP, onLCP } from './web-vitals.js'; +import { onCLS, onINP, onFCP, onLCP } from './web-vitals.js'; import { sendToAnalytics } from './send-to-analytics.js'; -if (typeof window !== 'undefined' && 'gas4' in window) { - performance.mark('dap-loaded'); +const initWebVitalsEvents = () => { onCLS(sendToAnalytics); + onFCP(sendToAnalytics); onINP(sendToAnalytics); onLCP(sendToAnalytics); +}; + +if (typeof window !== 'undefined' && 'gas4' in window) { + initWebVitalsEvents(); } else { window.addEventListener('dap-universal-federated-analytics-load', () => { - performance.mark('dap-loaded'); - onCLS(sendToAnalytics); - onINP(sendToAnalytics); - onLCP(sendToAnalytics); + initWebVitalsEvents(); }); } + +performance.mark('dap-performance-addon-loaded'); diff --git a/src/send-to-analytics.ts b/src/send-to-analytics.ts index ab0a4c0..d7f3601 100644 --- a/src/send-to-analytics.ts +++ b/src/send-to-analytics.ts @@ -1,8 +1,17 @@ -import { - formatEventData, - type WebVitalsAttribution, - type WebVitalsName, -} from './format-event-data.js'; +import type { + CLSMetricWithAttribution, + FCPMetricWithAttribution, + INPMetricWithAttribution, + LCPMetricWithAttribution, +} from 'web-vitals'; + +import { formatEventData, type WebVitalsName } from './format-event-data.js'; + +export type WebVitalsWithAttribution = + | CLSMetricWithAttribution + | FCPMetricWithAttribution + | INPMetricWithAttribution + | LCPMetricWithAttribution; declare const gas4: any; @@ -11,14 +20,9 @@ export const sendToAnalytics = ({ delta, value, id, + navigationType, attribution, -}: { - name: string; - delta: number; - value: number; - id: string; - attribution: WebVitalsAttribution; -}) => { +}: WebVitalsWithAttribution) => { if (typeof gas4 === 'function') { gas4(name, { // Built-in params: @@ -27,6 +31,7 @@ export const sendToAnalytics = ({ metric_id: id, // Needed to aggregate events. metric_value: value, // Value for querying in BQ metric_delta: delta, // Delta for querying in BQ + metric_navigation_type: navigationType, // Send the returned values from getDebugInfo() as custom parameters ...formatEventData(name as WebVitalsName, attribution), }); diff --git a/test/unit/format-event-data.test.ts b/test/unit/format-event-data.test.ts index f36a5dd..bf56b96 100644 --- a/test/unit/format-event-data.test.ts +++ b/test/unit/format-event-data.test.ts @@ -2,8 +2,43 @@ import { it, describe, expect } from 'vitest'; import { formatEventData } from '../../src/format-event-data.js'; describe('formatEventData', () => { + it('should format CLS data correctly', () => { + const attribution = { + largestShiftTime: 1000, + loadState: 'complete', + largestShiftTarget: 'body>div#main', + }; + + // @ts-ignore + const result = formatEventData('CLS', attribution); + + expect(result).toEqual({ + debug_time: attribution.largestShiftTime, + debug_load_state: attribution.loadState, + debug_target: attribution.largestShiftTarget, + }); + }); + + it('should format FCP data correctly', () => { + const attribution = { + timeToFirstByte: 7.199999999254942, + firstByteToFCP: 456.9000000022352, + loadState: 'dom-interactive', + }; + + // @ts-ignore + const result = formatEventData('FCP', attribution); + + expect(result).toEqual({ + debug_time_to_first_byte: attribution.timeToFirstByte, + debug_first_byte_to_fcp: attribution.firstByteToFCP, + debug_load_state: attribution.loadState, + debug_target: attribution.loadState, + }); + }); + it('should format LCP data correctly', () => { - const lcpAttribution = { + const attribution = { url: 'https://localhost/', timeToFirstByte: 100, resourceLoadDelay: 50, @@ -12,20 +47,20 @@ describe('formatEventData', () => { element: 'body>div#main', }; - const result = formatEventData('LCP', lcpAttribution); + const result = formatEventData('LCP', attribution); expect(result).toEqual({ - debug_url: 'https://localhost/', - debug_time_to_first_byte: 100, - debug_resource_load_delay: 50, - debug_resource_load_duration: 120, - debug_element_render_delay: 10, - debug_target: 'body>div#main', + debug_url: attribution.url, + debug_time_to_first_byte: attribution.timeToFirstByte, + debug_resource_load_delay: attribution.resourceLoadDelay, + debug_resource_load_duration: attribution.resourceLoadDuration, + debug_element_render_delay: attribution.elementRenderDelay, + debug_target: attribution.element, }); }); it('should format INP data correctly', () => { - const inpAttribution = { + const attribution = { interactionType: 'mousedown', interactionTime: 1000, loadState: 'interactive', @@ -36,33 +71,16 @@ describe('formatEventData', () => { }; // @ts-ignore - const result = formatEventData('INP', inpAttribution); - - expect(result).toEqual({ - debug_event: 'mousedown', - debug_time: 1000, - debug_load_state: 'interactive', - debug_target: 'body>button#submit', - debug_interaction_delay: 50, - debug_processing_duration: 20, - debug_presentation_delay: 5, - }); - }); - - it('should format CLS data correctly', () => { - const clsAttribution = { - largestShiftTime: 1000, - loadState: 'complete', - largestShiftTarget: 'body>div#main', - }; - - // @ts-ignore - const result = formatEventData('CLS', clsAttribution); + const result = formatEventData('INP', attribution); expect(result).toEqual({ - debug_time: 1000, - debug_load_state: 'complete', - debug_target: 'body>div#main', + debug_event: attribution.interactionType, + debug_time: attribution.interactionTime, + debug_load_state: attribution.loadState, + debug_target: attribution.interactionTarget, + debug_interaction_delay: attribution.inputDelay, + debug_processing_duration: attribution.processingDuration, + debug_presentation_delay: attribution.presentationDelay, }); });