From 96b433b0d1a3edf845fcbb1d86b7daa5e464231a Mon Sep 17 00:00:00 2001 From: Paul D'Ambra Date: Tue, 15 Oct 2024 21:34:45 +0100 Subject: [PATCH] fix: web vitals delayed capture --- src/__tests__/extensions/web-vitals.test.ts | 32 +++++++++++++++++---- src/extensions/web-vitals/index.ts | 11 +++++-- src/types.ts | 6 ++++ 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/__tests__/extensions/web-vitals.test.ts b/src/__tests__/extensions/web-vitals.test.ts index 80c8738d6..e83ae0494 100644 --- a/src/__tests__/extensions/web-vitals.test.ts +++ b/src/__tests__/extensions/web-vitals.test.ts @@ -1,9 +1,9 @@ import { createPosthogInstance } from '../helpers/posthog-instance' import { uuidv7 } from '../../uuidv7' import { PostHog } from '../../posthog-core' -import { DecideResponse, SupportedWebVitalsMetrics } from '../../types' +import { DecideResponse, PerformanceCaptureConfig, SupportedWebVitalsMetrics } from '../../types' import { assignableWindow } from '../../utils/globals' -import { FLUSH_TO_CAPTURE_TIMEOUT_MILLISECONDS, FIFTEEN_MINUTES_IN_MILLIS } from '../../extensions/web-vitals' +import { DEFAULT_FLUSH_TO_CAPTURE_TIMEOUT_MILLISECONDS, FIFTEEN_MINUTES_IN_MILLIS } from '../../extensions/web-vitals' jest.mock('../../utils/logger') jest.useFakeTimers() @@ -134,12 +134,12 @@ describe('web vitals', () => { ]) }) - it('should emit after 8 seconds even when only 1 to 3 metrics captured', async () => { + it('should emit after 5 seconds even when only 1 to 3 metrics captured', async () => { onCLSCallback?.({ name: 'CLS', value: 123.45, extra: 'property' }) expect(onCapture).toBeCalledTimes(0) - jest.advanceTimersByTime(FLUSH_TO_CAPTURE_TIMEOUT_MILLISECONDS + 1) + jest.advanceTimersByTime(DEFAULT_FLUSH_TO_CAPTURE_TIMEOUT_MILLISECONDS + 1) // for some reason advancing the timer emits a $pageview event as well 🤷 // expect(onCapture).toBeCalledTimes(2) @@ -155,12 +155,32 @@ describe('web vitals', () => { ]) }) + it('should emit after configured timeout even when only 1 to 3 metrics captured', async () => { + ;(posthog.config.capture_performance as PerformanceCaptureConfig).web_vitals_delayed_flush_ms = 1000 + onCLSCallback?.({ name: 'CLS', value: 123.45, extra: 'property' }) + + expect(onCapture).toBeCalledTimes(0) + + jest.advanceTimersByTime(1000 + 1) + + expect(onCapture.mock.lastCall).toMatchObject([ + '$web_vitals', + { + event: '$web_vitals', + properties: { + $web_vitals_CLS_event: expectedEmittedWebVitals('CLS'), + $web_vitals_CLS_value: 123.45, + }, + }, + ]) + }) + it('should ignore a ridiculous value', async () => { onCLSCallback?.({ name: 'CLS', value: FIFTEEN_MINUTES_IN_MILLIS, extra: 'property' }) expect(onCapture).toBeCalledTimes(0) - jest.advanceTimersByTime(FLUSH_TO_CAPTURE_TIMEOUT_MILLISECONDS + 1) + jest.advanceTimersByTime(DEFAULT_FLUSH_TO_CAPTURE_TIMEOUT_MILLISECONDS + 1) expect(onCapture.mock.calls).toEqual([]) }) @@ -171,7 +191,7 @@ describe('web vitals', () => { expect(onCapture).toBeCalledTimes(0) - jest.advanceTimersByTime(FLUSH_TO_CAPTURE_TIMEOUT_MILLISECONDS + 1) + jest.advanceTimersByTime(DEFAULT_FLUSH_TO_CAPTURE_TIMEOUT_MILLISECONDS + 1) expect(onCapture).toBeCalledTimes(1) }) diff --git a/src/extensions/web-vitals/index.ts b/src/extensions/web-vitals/index.ts index 1d966c08d..706806caf 100644 --- a/src/extensions/web-vitals/index.ts +++ b/src/extensions/web-vitals/index.ts @@ -7,7 +7,7 @@ import { assignableWindow, window } from '../../utils/globals' type WebVitalsMetricCallback = (metric: any) => void -export const FLUSH_TO_CAPTURE_TIMEOUT_MILLISECONDS = 8000 +export const DEFAULT_FLUSH_TO_CAPTURE_TIMEOUT_MILLISECONDS = 5000 const ONE_MINUTE_IN_MILLIS = 60 * 1000 export const FIFTEEN_MINUTES_IN_MILLIS = 15 * ONE_MINUTE_IN_MILLIS @@ -38,6 +38,13 @@ export class WebVitalsAutocapture { : this.instance.persistence?.props[WEB_VITALS_ALLOWED_METRICS] || ['CLS', 'FCP', 'INP', 'LCP'] } + public get flushToCaptureTimeoutMs(): number { + const clientConfig: number | undefined = isObject(this.instance.config.capture_performance) + ? this.instance.config.capture_performance.web_vitals_delayed_flush_ms + : undefined + return clientConfig || DEFAULT_FLUSH_TO_CAPTURE_TIMEOUT_MILLISECONDS + } + public get _maxAllowedValue(): number { const configured = isObject(this.instance.config.capture_performance) && @@ -163,7 +170,7 @@ export class WebVitalsAutocapture { // poor performance is >4s, we wait twice that time to send // this is in case we haven't received all metrics // we'll at least gather some - this._delayedFlushTimer = setTimeout(this._flushToCapture, FLUSH_TO_CAPTURE_TIMEOUT_MILLISECONDS) + this._delayedFlushTimer = setTimeout(this._flushToCapture, this.flushToCaptureTimeoutMs) } if (isUndefined(this.buffer.url)) { diff --git a/src/types.ts b/src/types.ts index e18ced49f..21f49efa3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -114,6 +114,12 @@ export interface PerformanceCaptureConfig { * NB setting this does not override whether the capture is enabled */ web_vitals_allowed_metrics?: SupportedWebVitalsMetrics[] + /** + * we delay flushing web vitals metrics to reduce the number of events we send + * this is the maximum time we will wait before sending the metrics + * if not set it defaults to 5 seconds + */ + web_vitals_delayed_flush_ms?: number } export interface HeatmapConfig {