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

feat!: Load web-vitals asynchronously #322

Merged
merged 2 commits into from
Mar 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 70 additions & 40 deletions src/coreWebVitals/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,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 +97,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 +120,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 +174,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 +195,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 +205,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 +215,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 +229,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 +243,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 +256,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 +275,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 +287,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 +306,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 +338,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 +371,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
22 changes: 13 additions & 9 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 All @@ -116,22 +118,22 @@ type InitCoreWebVitalsOptions = {
/**
* Initialise sending Core Web Vitals metrics to a logging endpoint.
*
* @param init - the initialisation options
* @param {InitCoreWebVitalsOptions} init - the initialisation options
* @param init.isDev - used to determine whether to use CODE or PROD endpoints.
* @param init.browserId - identifies the browser. Usually available via `getCookie({ name: 'bwid' })`. Defaults to `null`
* @param init.pageViewId - identifies the page view. Usually available on `guardian.config.ophan.pageViewId`. Defaults to `null`
*
* @param init.sampling - sampling rate for sending data. Defaults to `0.01`.
*
* @param team - Optional team to trigger a log event once metrics are queued.
* @param init.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