Skip to content

Commit

Permalink
Add web vitals with attribution instrumentation (#595)
Browse files Browse the repository at this point in the history
  • Loading branch information
vitebo committed Jun 11, 2024
1 parent eedf08e commit ae037b3
Show file tree
Hide file tree
Showing 16 changed files with 572 additions and 31 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Enhancement (`@grafana/faro-web-sdk`): Auto extend a session if the Faro receiver indicates that a
session is invalid (#591).
- Feature (`@grafana/faro-web-sdk`): track `web vitals` attribution (#595).

## 1.7.3

Expand Down
1 change: 1 addition & 0 deletions demo/src/client/faro/initialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export function initializeFaro(): Faro {
const faro = coreInit({
url: `http://localhost:${env.faro.portAppReceiver}/collect`,
apiKey: env.faro.apiKey,
trackWebVitalsAttribution: true,
instrumentations: [
...getWebInstrumentations({
captureConsole: true,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export interface Config<P = APIEvent> {
eventDomain?: string;

trackResources?: boolean;
trackWebVitalsAttribution?: boolean;
}

export type Patterns = Array<string | RegExp>;
2 changes: 1 addition & 1 deletion packages/web-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
"dependencies": {
"@grafana/faro-core": "^1.7.3",
"ua-parser-js": "^1.0.32",
"web-vitals": "^3.1.1"
"web-vitals": "^4.0.1"
},
"devDependencies": {
"@types/ua-parser-js": "^0.7.36",
Expand Down
1 change: 1 addition & 0 deletions packages/web-sdk/src/config/makeCoreConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ export function makeCoreConfig(browserConfig: BrowserConfig): Config | undefined
user: browserConfig.user,
view: browserConfig.view ?? defaultViewMeta,
trackResources: browserConfig.trackResources,
trackWebVitalsAttribution: browserConfig.trackWebVitalsAttribution,
};

return config;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const NAVIGATION_ID_STORAGE_KEY = 'com.grafana.faro.lastNavigationId';
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import * as faroCoreModule from '@grafana/faro-core';

import * as webStorageModule from '../../utils/webStorage';
import { webStorageType } from '../../utils/webStorage';
import { NAVIGATION_ID_STORAGE_KEY } from '../instrumentationConstants';

import { getNavigationTimings } from './navigation';
import { NAVIGATION_ID_STORAGE_KEY } from './performanceConstants';
import * as performanceUtilsModule from './performanceUtils';
import { createFaroNavigationTiming, createFaroResourceTiming } from './performanceUtils';
import { performanceNavigationEntry, performanceResourceEntry } from './performanceUtilsTestData';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { genShortID } from '@grafana/faro-core';
import type { EventsAPI } from '@grafana/faro-core';

import { getItem, setItem, webStorageType } from '../../utils';
import { NAVIGATION_ID_STORAGE_KEY } from '../instrumentationConstants';

import { NAVIGATION_ENTRY, NAVIGATION_ID_STORAGE_KEY } from './performanceConstants';
import { NAVIGATION_ENTRY } from './performanceConstants';
import { createFaroNavigationTiming, entryUrlIsIgnored } from './performanceUtils';
import type { FaroNavigationItem } from './types';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
export const NAVIGATION_ID_STORAGE_KEY = 'com.grafana.faro.lastNavigationId';

export const NAVIGATION_ENTRY = 'navigation';
export const RESOURCE_ENTRY = 'resource';
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { initializeFaro } from '@grafana/faro-core';
import { mockConfig, MockTransport } from '@grafana/faro-core/src/testUtils';

import { WebVitalsInstrumentation } from './instrumentation';
import { WebVitalsBasic } from './webVitalsBasic';
import { WebVitalsWithAttribution } from './webVitalsWithAttribution';

jest.mock('./webVitalsWithAttribution');
jest.mock('./webVitalsBasic');

describe('WebVitals Instrumentation', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('load WebVitalsBasic by default', () => {
const transport = new MockTransport();

initializeFaro(
mockConfig({
transports: [transport],
instrumentations: [new WebVitalsInstrumentation()],
})
);

expect(WebVitalsBasic).toHaveBeenCalledTimes(1);
expect(WebVitalsWithAttribution).toHaveBeenCalledTimes(0);
});

it('load WebVitalsWithAttribution when trackWebVitalAttribution is true', () => {
const transport = new MockTransport();

initializeFaro(
mockConfig({
trackWebVitalsAttribution: true,
transports: [transport],
instrumentations: [new WebVitalsInstrumentation()],
})
);

expect(WebVitalsBasic).toHaveBeenCalledTimes(0);
expect(WebVitalsWithAttribution).toHaveBeenCalledTimes(1);
});
});
33 changes: 11 additions & 22 deletions packages/web-sdk/src/instrumentations/webVitals/instrumentation.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,22 @@
import { onCLS, onFCP, onFID, onINP, onLCP, onTTFB } from 'web-vitals';

import { BaseInstrumentation, VERSION } from '@grafana/faro-core';

import { WebVitalsBasic } from './webVitalsBasic';
import { WebVitalsWithAttribution } from './webVitalsWithAttribution';

export class WebVitalsInstrumentation extends BaseInstrumentation {
readonly name = '@grafana/faro-web-sdk:instrumentation-web-vitals';
readonly version = VERSION;

static mapping = {
cls: onCLS,
fcp: onFCP,
fid: onFID,
inp: onINP,
lcp: onLCP,
ttfb: onTTFB,
};

initialize(): void {
this.logDebug('Initializing');
const webVitals = this.intializeWebVitalsInstrumentation();
webVitals.initialize();
}

Object.entries(WebVitalsInstrumentation.mapping).forEach(([indicator, executor]) => {
executor((metric) => {
this.api.pushMeasurement({
type: 'web-vitals',

values: {
[indicator]: metric.value,
},
});
});
});
private intializeWebVitalsInstrumentation() {
if (this.config.trackWebVitalsAttribution) {
return new WebVitalsWithAttribution(this.api.pushMeasurement);
}
return new WebVitalsBasic(this.api.pushMeasurement);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { Metric } from 'web-vitals';

import { WebVitalsBasic } from './webVitalsBasic';

jest.mock('web-vitals', () => {
type MetricName = Metric['name'];

function createMetric(name: MetricName): Metric {
return {
name,
value: 0.1,
rating: 'good',
delta: 0.1,
id: 'id',
entries: [],
navigationType: 'navigate',
};
}

return {
onCLS: (cb: (metric: Metric) => void) => cb(createMetric('CLS')),
onFCP: (cb: (metric: Metric) => void) => cb(createMetric('FCP')),
onFID: (cb: (metric: Metric) => void) => cb(createMetric('FID')),
onLCP: (cb: (metric: Metric) => void) => cb(createMetric('LCP')),
onTTFB: (cb: (metric: Metric) => void) => cb(createMetric('TTFB')),
onINP: (cb: (metric: Metric) => void) => cb(createMetric('INP')),
};
});

describe('WebVitalsBasicInstrumentation', () => {
it.each(['cls', 'fcp', 'fid', 'inp', 'lcp', 'ttfb'])('send %p metrics correctly', (metric) => {
const pushMeasurement = jest.fn();
new WebVitalsBasic(pushMeasurement).initialize();

expect(pushMeasurement).toHaveBeenCalledWith({
type: 'web-vitals',
values: {
[metric]: 0.1,
},
});
});
});
30 changes: 30 additions & 0 deletions packages/web-sdk/src/instrumentations/webVitals/webVitalsBasic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { onCLS, onFCP, onFID, onINP, onLCP, onTTFB } from 'web-vitals';

import type { MeasurementsAPI } from '@grafana/faro-core';

export class WebVitalsBasic {
static mapping = {
cls: onCLS,
fcp: onFCP,
fid: onFID,
inp: onINP,
lcp: onLCP,
ttfb: onTTFB,
};

constructor(private pushMeasurement: MeasurementsAPI['pushMeasurement']) {}

initialize(): void {
Object.entries(WebVitalsBasic.mapping).forEach(([indicator, executor]) => {
executor((metric) => {
this.pushMeasurement({
type: 'web-vitals',

values: {
[indicator]: metric.value,
},
});
});
});
}
}
Loading

0 comments on commit ae037b3

Please sign in to comment.