Skip to content

Commit

Permalink
feat: collect attributions for LCP, CLS, and FID
Browse files Browse the repository at this point in the history
  • Loading branch information
williazz committed Aug 23, 2023
1 parent 9214c0a commit 40cf75b
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 34 deletions.
83 changes: 68 additions & 15 deletions src/plugins/event-plugins/WebVitalsPlugin.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { InternalPlugin } from '../InternalPlugin';
import { LargestContentfulPaintEvent } from '../../events/largest-contentful-paint-event';
import { FirstInputDelayEvent } from '../../events/first-input-delay-event';
import { CumulativeLayoutShiftEvent } from '../../events/cumulative-layout-shift-event';
import { Metric, onCLS, onFID, onLCP } from 'web-vitals';
import { FirstInputDelayEvent } from '../../events/first-input-delay-event';
import {
LCP_EVENT_TYPE,
CLSMetricWithAttribution,
FIDMetricWithAttribution,
LCPMetricWithAttribution,
Metric,
onCLS,
onFID,
onLCP
} from 'web-vitals/attribution';
import {
CLS_EVENT_TYPE,
FID_EVENT_TYPE,
CLS_EVENT_TYPE
LCP_EVENT_TYPE
} from '../utils/constant';

export const WEB_VITAL_EVENT_PLUGIN_ID = 'web-vitals';
Expand All @@ -25,20 +33,65 @@ export class WebVitalsPlugin extends InternalPlugin {
// eslint-disable-next-line @typescript-eslint/no-empty-function
configure(config: any): void {}

getWebVitalData(webVitalData: Metric, eventType: string): void {
const webVitalEvent:
| LargestContentfulPaintEvent
| FirstInputDelayEvent
| CumulativeLayoutShiftEvent = {
protected onload(): void {
onLCP((metric) => this.handleLCP(metric));
onFID((metric) => this.handleFID(metric));
onCLS((metric) => this.handleCLS(metric));
}

handleLCP(metric: LCPMetricWithAttribution | Metric) {
const lcpEvent: LargestContentfulPaintEvent = {
version: '1.0.0',
value: webVitalData.value
value: metric.value,
attribution: undefined
};
this.context?.record(eventType, webVitalEvent);
if ('attribution' in metric) {
const a = (metric as LCPMetricWithAttribution).attribution;
lcpEvent.attribution = {
element: a.element,
url: a.url,
timeToFirstByte: a.timeToFirstByte,
resourceLoadDelay: a.resourceLoadDelay,
resourceLoadTime: a.resourceLoadTime,
elementRenderDelay: a.elementRenderDelay
};
}
this.context?.record(LCP_EVENT_TYPE, lcpEvent);
}

protected onload(): void {
onLCP((data) => this.getWebVitalData(data, LCP_EVENT_TYPE));
onFID((data) => this.getWebVitalData(data, FID_EVENT_TYPE));
onCLS((data) => this.getWebVitalData(data, CLS_EVENT_TYPE));
handleCLS(metric: CLSMetricWithAttribution | Metric) {
const clsEvent: CumulativeLayoutShiftEvent = {
version: '1.0.0',
value: metric.value,
attribution: undefined
};
if ('attribution' in metric) {
const a = (metric as CLSMetricWithAttribution).attribution;
clsEvent.attribution = {
largestShiftTarget: a.largestShiftTarget,
largestShiftValue: a.largestShiftValue,
largestShiftTime: a.largestShiftTime,
loadState: a.loadState
};
}
this.context?.record(CLS_EVENT_TYPE, clsEvent);
}

handleFID(metric: FIDMetricWithAttribution | Metric) {
const fidEvent: FirstInputDelayEvent = {
version: '1.0.0',
value: metric.value,
attribution: undefined
};
if ('attribution' in metric) {
const a = (metric as FIDMetricWithAttribution).attribution;
fidEvent.attribution = {
eventTarget: a.eventTarget,
eventType: a.eventType,
eventTime: a.eventTime,
loadState: a.loadState
};
}
this.context?.record(FID_EVENT_TYPE, fidEvent);
}
}
98 changes: 79 additions & 19 deletions src/plugins/event-plugins/__tests__/WebVitalsPlugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,40 +10,52 @@ const mockLCPData = {
delta: 239.51,
id: 'v1-1621403597701-7933189041053',
name: 'LCP',
value: 239.51
value: 239.51,
attribution: {
element: '#root>div>div>div>img',
url: 'example.com/source.png',
timeToFirstByte: 1000,
resourceLoadDelay: 250,
resourceLoadTime: 1000,
elementRenderDelay: 250
}
};

const mockFIDData = {
delta: 1.2799999676644802,
id: 'v1-1621403597702-6132885858466',
name: 'FID',
value: 1.2799999676644802
value: 1.2799999676644802,
attribution: {
eventTime: 300,
eventTarget: '#root>div>div>div>img',
eventType: 'keydown',
loadState: 'dom-interactive'
}
};

const mockCLSData = {
delta: 0,
id: 'v1-1621403597702-8740659462223',
name: 'CLS',
value: 0.037451866876684094
value: 0.037451866876684094,
attribution: {
largestShiftTarget: '#root>div>div>div>img',
largestShiftValue: 0.03076529149893375,
largestShiftTime: 3447485.600000024,
loadState: 'dom-interactive'
}
};

jest.mock('web-vitals', () => {
jest.mock('web-vitals/attribution', () => {
return {
onLCP: jest
.fn()
.mockImplementation((callback) =>
callback(mockLCPData, LCP_EVENT_TYPE)
),
.mockImplementation((callback) => callback(mockLCPData)),
onFID: jest
.fn()
.mockImplementation((callback) =>
callback(mockFIDData, FID_EVENT_TYPE)
),
onCLS: jest
.fn()
.mockImplementation((callback) =>
callback(mockCLSData, CLS_EVENT_TYPE)
)
.mockImplementation((callback) => callback(mockFIDData)),
onCLS: jest.fn().mockImplementation((callback) => callback(mockCLSData))
};
});

Expand All @@ -52,7 +64,7 @@ describe('WebVitalsPlugin tests', () => {
record.mockClear();
});

test('When web vitals are present then events are recorded', async () => {
test('When web vitals are present then LCP is recorded with attributions', async () => {
// Setup
const plugin: WebVitalsPlugin = new WebVitalsPlugin();

Expand All @@ -67,23 +79,71 @@ describe('WebVitalsPlugin tests', () => {
expect(record.mock.calls[0][1]).toEqual(
expect.objectContaining({
version: '1.0.0',
value: mockLCPData.value
value: mockLCPData.value,
attribution: {
element: mockLCPData.attribution.element,
url: mockLCPData.attribution.url,
timeToFirstByte: mockLCPData.attribution.timeToFirstByte,
resourceLoadDelay:
mockLCPData.attribution.resourceLoadDelay,
resourceLoadTime: mockLCPData.attribution.resourceLoadTime,
elementRenderDelay:
mockLCPData.attribution.elementRenderDelay
}
})
);
});

test('When web vitals are present then FID is recorded with attribution', async () => {
// Setup
const plugin: WebVitalsPlugin = new WebVitalsPlugin();

// Run
plugin.load(context);
window.dispatchEvent(new Event('load'));

// Assert
expect(record).toHaveBeenCalledTimes(3);

expect(record.mock.calls[1][0]).toEqual(FID_EVENT_TYPE);
expect(record.mock.calls[1][1]).toEqual(
expect.objectContaining({
version: '1.0.0',
value: mockFIDData.value
value: mockFIDData.value,
attribution: {
eventTarget: mockFIDData.attribution.eventTarget,
eventType: mockFIDData.attribution.eventType,
eventTime: mockFIDData.attribution.eventTime,
loadState: mockFIDData.attribution.loadState
}
})
);
});

test('When web vitals are present then CLS is recorded with attribution', async () => {
// Setup
const plugin: WebVitalsPlugin = new WebVitalsPlugin();

// Run
plugin.load(context);
window.dispatchEvent(new Event('load'));

// Assert
expect(record).toHaveBeenCalledTimes(3);

expect(record.mock.calls[2][0]).toEqual(CLS_EVENT_TYPE);
expect(record.mock.calls[2][1]).toEqual(
expect.objectContaining({
version: '1.0.0',
value: mockCLSData.value
value: mockCLSData.value,
attribution: {
largestShiftTarget:
mockCLSData.attribution.largestShiftTarget,
largestShiftValue:
mockCLSData.attribution.largestShiftValue,
largestShiftTime: mockCLSData.attribution.largestShiftTime,
loadState: mockCLSData.attribution.loadState
}
})
);
});
Expand Down

0 comments on commit 40cf75b

Please sign in to comment.