Skip to content
This repository has been archived by the owner on Sep 1, 2022. It is now read-only.

Commit

Permalink
feat!: load web-vitals asynchronously
Browse files Browse the repository at this point in the history
BREAKING CHANGE: async initCoreWebVitals & bypassCoreWebVitalsSampling

This prevents loading the web-vitals module when the user is not in
the sampling group and the sampling has not been bypassed.

Less JS sent to the vast majority (~99%) of our users.
  • Loading branch information
mxdvl committed Mar 22, 2022
1 parent add9281 commit f2122d2
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 48 deletions.
113 changes: 72 additions & 41 deletions src/coreWebVitals/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,11 @@ const mockConsoleWarn = jest

const spyLog = jest.spyOn(logger, 'log');

const setVisibilityState = (value: VisibilityState = 'visible') => {
const setVisibilityState = (value: DocumentVisibilityState = 'visible') => {
Object.defineProperty(document, 'visibilityState', {
writable: true,
configurable: true,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- missing type declaration
value,
});
};
Expand All @@ -77,11 +78,16 @@ describe('coreWebVitals', () => {
setVisibilityState();
});

it('sends a beacon when sampling is 100%', () => {
it('sends a beacon when sampling is 100%', async () => {
const mockAddEventListener = jest.spyOn(global, 'addEventListener');

const sampling = 100 / 100;
initCoreWebVitals({ browserId, pageViewId, isDev: true, sampling });
await initCoreWebVitals({
browserId,
pageViewId,
isDev: true,
sampling,
});

expect(mockAddEventListener).toHaveBeenCalledTimes(2);

Expand All @@ -92,9 +98,14 @@ describe('coreWebVitals', () => {
expect(mockBeacon).toHaveBeenCalledTimes(1);
});

it('does not run web-vitals if sampling is 0%', () => {
it('does not run web-vitals if sampling is 0%', async () => {
const sampling = 0 / 100;
initCoreWebVitals({ browserId, pageViewId, isDev: true, sampling });
await initCoreWebVitals({
browserId,
pageViewId,
isDev: true,
sampling,
});

setVisibilityState('hidden');
global.dispatchEvent(new Event('visibilitychange'));
Expand All @@ -110,32 +121,47 @@ describe('coreWebVitals', () => {
});
});

it('sends a beacon if sampling at 0% but bypassed via hash', () => {
it('sends a beacon if sampling at 0% but bypassed via hash', async () => {
window.location.hash = '#bypassCoreWebVitalsSampling';
const sampling = 0 / 100;
initCoreWebVitals({ browserId, pageViewId, isDev: true, sampling });
await initCoreWebVitals({
browserId,
pageViewId,
isDev: true,
sampling,
});
window.location.hash = '';

global.dispatchEvent(new Event('pagehide'));

expect(mockBeacon).toHaveBeenCalledTimes(1);
});

it('sends a beacon if sampling at 0% but bypassed asynchronously', () => {
it('sends a beacon if sampling at 0% but bypassed asynchronously', async () => {
const sampling = 0 / 100;
initCoreWebVitals({ browserId, pageViewId, isDev: true, sampling });
await initCoreWebVitals({
browserId,
pageViewId,
isDev: true,
sampling,
});

expect(mockBeacon).not.toHaveBeenCalled();

bypassCoreWebVitalsSampling();
await bypassCoreWebVitalsSampling();

global.dispatchEvent(new Event('pagehide'));

expect(mockBeacon).toHaveBeenCalledTimes(1);
});

it('only registers pagehide if document is visible', () => {
initCoreWebVitals({ browserId, pageViewId, isDev: true, sampling: 1 });
it('only registers pagehide if document is visible', async () => {
await initCoreWebVitals({
browserId,
pageViewId,
isDev: true,
sampling: 1,
});

setVisibilityState('visible');
global.dispatchEvent(new Event('visibilitychange'));
Expand All @@ -149,18 +175,18 @@ describe('Warnings', () => {
reset();
});

it('should warn if already initialised', () => {
initCoreWebVitals({ pageViewId, browserId, isDev: true });
initCoreWebVitals({ pageViewId, browserId, isDev: true });
it('should warn if already initialised', async () => {
await initCoreWebVitals({ pageViewId, browserId, isDev: true });
await initCoreWebVitals({ pageViewId, browserId, isDev: true });

expect(mockConsoleWarn).toHaveBeenCalledWith(
'initCoreWebVitals already initialised',
expect.any(String),
);
});

it('expect to be initialised before calling bypassCoreWebVitalsSampling', () => {
bypassCoreWebVitalsSampling();
it('expect to be initialised before calling bypassCoreWebVitalsSampling', async () => {
await bypassCoreWebVitalsSampling();

expect(mockConsoleWarn).toHaveBeenCalledWith(
'initCoreWebVitals not yet initialised',
Expand All @@ -170,8 +196,8 @@ describe('Warnings', () => {
expect(mockBeacon).not.toHaveBeenCalled();
});

it('should warn if browserId is missing', () => {
initCoreWebVitals({ pageViewId, isDev: true });
it('should warn if browserId is missing', async () => {
await initCoreWebVitals({ pageViewId, isDev: true });

expect(mockConsoleWarn).toHaveBeenCalledWith(
'browserId or pageViewId missing from Core Web Vitals.',
Expand All @@ -180,8 +206,8 @@ describe('Warnings', () => {
);
});

it('should warn if pageViewId is missing', () => {
initCoreWebVitals({ browserId, isDev: true });
it('should warn if pageViewId is missing', async () => {
await initCoreWebVitals({ browserId, isDev: true });

expect(mockConsoleWarn).toHaveBeenCalledWith(
'browserId or pageViewId missing from Core Web Vitals.',
Expand All @@ -190,8 +216,8 @@ describe('Warnings', () => {
);
});

it('should warn if sampling is below 0', () => {
initCoreWebVitals({
it('should warn if sampling is below 0', async () => {
await initCoreWebVitals({
browserId,
pageViewId,
isDev: true,
Expand All @@ -204,8 +230,8 @@ describe('Warnings', () => {
);
});

it('should warn if sampling is above 1', () => {
initCoreWebVitals({
it('should warn if sampling is above 1', async () => {
await initCoreWebVitals({
browserId,
pageViewId,
isDev: true,
Expand All @@ -218,8 +244,8 @@ describe('Warnings', () => {
);
});

it('should warn if sampling is above at 0%', () => {
initCoreWebVitals({
it('should warn if sampling is above at 0%', async () => {
await initCoreWebVitals({
browserId,
pageViewId,
isDev: true,
Expand All @@ -231,8 +257,8 @@ describe('Warnings', () => {
);
});

it('should warn if sampling is above at 100%', () => {
initCoreWebVitals({
it('should warn if sampling is above at 100%', async () => {
await initCoreWebVitals({
browserId,
pageViewId,
isDev: true,
Expand All @@ -250,9 +276,9 @@ describe('Endpoints', () => {
reset();
});

it('should use CODE URL if isDev', () => {
it('should use CODE URL if isDev', async () => {
const isDev = true;
initCoreWebVitals({ browserId, pageViewId, isDev, sampling: 1 });
await initCoreWebVitals({ browserId, pageViewId, isDev, sampling: 1 });

global.dispatchEvent(new Event('pagehide'));

Expand All @@ -262,9 +288,9 @@ describe('Endpoints', () => {
);
});

it('should use PROD URL if isDev is false', () => {
it('should use PROD URL if isDev is false', async () => {
const isDev = false;
initCoreWebVitals({ browserId, pageViewId, isDev, sampling: 1 });
await initCoreWebVitals({ browserId, pageViewId, isDev, sampling: 1 });

global.dispatchEvent(new Event('pagehide'));

Expand All @@ -281,11 +307,16 @@ describe('Logging', () => {
setVisibilityState();
});

it('should log for every team that registered', () => {
it('should log for every team that registered', async () => {
const isDev = true;
initCoreWebVitals({ browserId, pageViewId, isDev, team: 'dotcom' });
bypassCoreWebVitalsSampling('design');
bypassCoreWebVitalsSampling('commercial');
await initCoreWebVitals({
browserId,
pageViewId,
isDev,
team: 'dotcom',
});
await bypassCoreWebVitalsSampling('design');
await bypassCoreWebVitalsSampling('commercial');

setVisibilityState('hidden');
global.dispatchEvent(new Event('visibilitychange'));
Expand All @@ -308,11 +339,11 @@ describe('Logging', () => {
);
});

it('should log a failure if it happens', () => {
it('should log a failure if it happens', async () => {
const mockAddEventListener = jest.spyOn(global, 'addEventListener');
const isDev = true;
const sampling = 100 / 100;
initCoreWebVitals({
await initCoreWebVitals({
browserId,
pageViewId,
isDev,
Expand Down Expand Up @@ -341,9 +372,9 @@ describe('web-vitals', () => {
setVisibilityState();
});

it('should not send data if FCP is null', () => {
it('should not send data if FCP is null', async () => {
const isDev = true;
initCoreWebVitals({ browserId, pageViewId, isDev, sampling: 1 });
await initCoreWebVitals({ browserId, pageViewId, isDev, sampling: 1 });

_.coreWebVitalsPayload.fcp = null; // simulate a failing FCP

Expand Down
18 changes: 11 additions & 7 deletions src/coreWebVitals/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { ReportHandler } from 'web-vitals';
import { getCLS, getFCP, getFID, getLCP, getTTFB } from 'web-vitals';
import type { TeamName } from '../logger/@types/logger';
import { log } from '../logger/log';
import type { CoreWebVitalsPayload } from './@types/CoreWebVitalsPayload';
Expand Down Expand Up @@ -89,7 +88,10 @@ const listener = (e: Event): void => {
}
};

const getCoreWebVitals = (): void => {
const getCoreWebVitals = async (): Promise<void> => {
const webVitals = await import('web-vitals');
const { getCLS, getFCP, getFID, getLCP, getTTFB } = webVitals;

getCLS(onReport, false);
getFID(onReport);
getLCP(onReport);
Expand Down Expand Up @@ -125,13 +127,13 @@ type InitCoreWebVitalsOptions = {
*
* @param team - Optional team to trigger a log event once metrics are queued.
*/
export const initCoreWebVitals = ({
export const initCoreWebVitals = async ({
browserId = null,
pageViewId = null,
sampling = 1 / 100, // 1% of page view by default
isDev,
team,
}: InitCoreWebVitalsOptions): void => {
}: InitCoreWebVitalsOptions): Promise<void> => {
if (initialised) {
console.warn(
'initCoreWebVitals already initialised',
Expand Down Expand Up @@ -170,20 +172,22 @@ export const initCoreWebVitals = ({
const bypassWithHash =
window.location.hash === '#bypassCoreWebVitalsSampling';

if (pageViewInSample || bypassWithHash) getCoreWebVitals();
if (pageViewInSample || bypassWithHash) return getCoreWebVitals();
};

/**
* A method to asynchronously send web vitals after initialization.
* @param team - Optional team to trigger a log event once metrics are queued.
*/
export const bypassCoreWebVitalsSampling = (team?: TeamName): void => {
export const bypassCoreWebVitalsSampling = async (
team?: TeamName,
): Promise<void> => {
if (!initialised) {
console.warn('initCoreWebVitals not yet initialised');
return;
}
if (team) teamsForLogging.add(team);
getCoreWebVitals();
return getCoreWebVitals();
};

export const _ = {
Expand Down

0 comments on commit f2122d2

Please sign in to comment.