Skip to content
Draft
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
3 changes: 2 additions & 1 deletion packages/analytics-browser/src/browser-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
isPageViewTrackingEnabled,
isNetworkTrackingEnabled,
isWebVitalsEnabled,
getWebVitalsConfig,
isFrustrationInteractionsEnabled,
getFrustrationInteractionsConfig,
isPerformanceTrackingEnabled,
Expand Down Expand Up @@ -374,7 +375,7 @@ export class AmplitudeBrowser extends AmplitudeCore implements BrowserClient, An

if (isWebVitalsEnabled(this.config.autocapture)) {
this.config.loggerProvider.debug('Adding web vitals plugin');
await this.add(webVitalsPlugin()).promise;
await this.add(webVitalsPlugin(getWebVitalsConfig(this.config))).promise;
}

if (isPerformanceTrackingEnabled(this.config.autocapture)) {
Expand Down
26 changes: 24 additions & 2 deletions packages/analytics-browser/src/default-tracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
FrustrationInteractionsOptions,
CustomEnrichmentOptions,
PerformanceTrackingOptions,
WebVitalsOptions,
isChromeExtension,
normalizeNetworkCaptureRules,
} from '@amplitude/analytics-core';
Expand Down Expand Up @@ -111,21 +112,42 @@ export const isElementInteractionsEnabled = (autocapture: AutocaptureOptions | b
/**
* Returns true if
* 1. autocapture === true
* 2. if autocapture.webVitals === true
* 2. if autocapture.webVitals === true or is an options object
* otherwise returns false
*/
export const isWebVitalsEnabled = (autocapture: AutocaptureOptions | boolean | undefined): boolean => {
if (typeof autocapture === 'boolean') {
return autocapture;
}

if (typeof autocapture === 'object' && autocapture.webVitals === true) {
if (
typeof autocapture === 'object' &&
(autocapture.webVitals === true || (typeof autocapture.webVitals === 'object' && autocapture.webVitals !== null))
) {
return true;
}

return false;
};

/**
* Returns the web vitals options when autocapture.webVitals is configured with an options object,
* otherwise returns undefined so the plugin falls back to its defaults.
*/
export const getWebVitalsConfig = (config: BrowserOptions): WebVitalsOptions | undefined => {
if (typeof config.autocapture !== 'object') {
return undefined;
}

const webVitals = config.autocapture.webVitals;

if (typeof webVitals === 'object' && webVitals !== null) {
return webVitals;
}

return undefined;
};

export const isFrustrationInteractionsEnabled = (autocapture: AutocaptureOptions | boolean | undefined): boolean => {
if (typeof autocapture === 'boolean') {
return autocapture;
Expand Down
12 changes: 12 additions & 0 deletions packages/analytics-browser/test/browser-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,18 @@ describe('browser-client', () => {
},
}).promise;
expect(webVitalsPlugin).toHaveBeenCalledTimes(1);
expect(webVitalsPlugin).toHaveBeenCalledWith(undefined);
});

test('should pass web vitals options to the plugin when autocapture.webVitals is an object', async () => {
const webVitalsPlugin = jest.spyOn(webVitals, 'webVitalsPlugin');
await client.init(apiKey, userId, {
autocapture: {
webVitals: { reportSoftNav: true },
},
}).promise;
expect(webVitalsPlugin).toHaveBeenCalledTimes(1);
expect(webVitalsPlugin).toHaveBeenCalledWith({ reportSoftNav: true });
});

test('should listen for network change to online', async () => {
Expand Down
32 changes: 32 additions & 0 deletions packages/analytics-browser/test/default-tracking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getNetworkTrackingConfig,
getPageViewTrackingConfig,
getPerformanceTrackingConfig,
getWebVitalsConfig,
isAttributionTrackingEnabled,
isCustomEnrichmentEnabled,
isElementInteractionsEnabled,
Expand Down Expand Up @@ -60,6 +61,11 @@ describe('isWebVitalsEnabled', () => {
test('autocapture.webVitals=true', () => {
expect(isWebVitalsEnabled({ webVitals: true })).toBe(true);
});

test('autocapture.webVitals is an options object', () => {
expect(isWebVitalsEnabled({ webVitals: { reportSoftNav: true } })).toBe(true);
expect(isWebVitalsEnabled({ webVitals: {} })).toBe(true);
});
});

describe('is false when', () => {
Expand All @@ -74,6 +80,32 @@ describe('isWebVitalsEnabled', () => {
test('autocapture.webVitals is undefined', () => {
expect(isWebVitalsEnabled({ networkTracking: true })).toBe(false);
});

test('autocapture.webVitals is null', () => {
expect(isWebVitalsEnabled({ webVitals: null as unknown as undefined })).toBe(false);
});
});
});

describe('getWebVitalsConfig', () => {
test('should return the options when autocapture.webVitals is an options object', () => {
expect(getWebVitalsConfig({ autocapture: { webVitals: { reportSoftNav: true } } })).toEqual({
reportSoftNav: true,
});
});

test('should return undefined when autocapture.webVitals is a boolean', () => {
expect(getWebVitalsConfig({ autocapture: { webVitals: true } })).toBeUndefined();
expect(getWebVitalsConfig({ autocapture: { webVitals: false } })).toBeUndefined();
});

test('should return undefined when autocapture.webVitals is null', () => {
expect(getWebVitalsConfig({ autocapture: { webVitals: null as unknown as undefined } })).toBeUndefined();
});

test('should return undefined when autocapture is not an object', () => {
expect(getWebVitalsConfig({ autocapture: true })).toBeUndefined();
expect(getWebVitalsConfig({})).toBeUndefined();
});
});

Expand Down
1 change: 1 addition & 0 deletions packages/analytics-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export { SAFE_HEADERS, FORBIDDEN_HEADERS } from './types/constants';
export { PageUrlEnrichmentOptions } from './types/page-url-enrichment';
export { CustomEnrichmentOptions } from './types/custom-enrichment';
export { PerformanceTrackingOptions, MainThreadBlockOptions } from './types/performance-tracking';
export { WebVitalsOptions } from './types/web-vitals';

// Campaign
export { Campaign, UTMParameters, ReferrerParameters, ClickIdParameters, ICampaignParser } from './types/campaign';
Expand Down
5 changes: 3 additions & 2 deletions packages/analytics-core/src/types/config/browser-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { PageTrackingOptions } from '../page-view-tracking';
import { NetworkTrackingOptions } from '../network-tracking';
import { FrustrationInteractionsOptions } from '../frustration-interactions';
import { PerformanceTrackingOptions } from '../performance-tracking';
import { WebVitalsOptions } from '../web-vitals';
import { IDiagnosticsClient } from '../../diagnostics/diagnostics-client';
import { IRemoteConfigClient } from '../../remote-config/remote-config';
import { CustomEnrichmentOptions } from '../custom-enrichment';
Expand Down Expand Up @@ -198,10 +199,10 @@ export interface AutocaptureOptions {
*/
networkTracking?: boolean | NetworkTrackingOptions;
/**
* Enables/disables web vitals tracking.
* Enables/disables web vitals tracking or config with detailed web vitals options.
* @defaultValue `false`
*/
webVitals?: boolean;
webVitals?: boolean | WebVitalsOptions;
/**
* Enables/disables performance tracking.
* @defaultValue `false`
Expand Down
21 changes: 21 additions & 0 deletions packages/analytics-core/src/types/web-vitals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Configuration options for web vitals tracking.
*/
export interface WebVitalsOptions {
/**
* Enables/disables reporting web vitals for soft navigations, in addition to the initial page load.
*
* Single page applications update the URL and history without a full page navigation, so by default
* Core Web Vitals are only measured once, for the initial page load. When this is enabled, LCP, FCP,
* INP, CLS and TTFB are also measured per soft navigation, and one `[Amplitude] Web Vitals` event is
* sent per navigation with the page properties of the URL the metrics belong to.
*
* Requires browser support for the Soft Navigations API (Chromium 151+). In browsers without it,
* reporting is unchanged from the default behavior.
*
* See {@link https://github.com/WICG/soft-navigations}.
*
* @defaultValue `false`
*/
reportSoftNav?: boolean;
}
36 changes: 36 additions & 0 deletions packages/plugin-web-vitals-browser/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,42 @@ import { webVitalsPlugin } from '@amplitude/plugin-web-vitals-browser';
const plugin = webVitalsPlugin();
```

#### Options

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `reportSoftNav` | `boolean` | `false` | Also report web vitals for soft navigations, not only for the initial page load. |

##### `reportSoftNav`

Single page applications update the URL and history without a full page navigation, so by default
Core Web Vitals are only measured once, for the initial page load. With `reportSoftNav` enabled,
LCP, FCP, INP, CLS and TTFB are also measured per
[soft navigation](https://github.com/WICG/soft-navigations), and one `[Amplitude] Web Vitals` event
is sent per navigation, with the page properties of the URL the metrics belong to. Metrics measured
for a soft navigation have a `navigationType` of `soft-navigation`.

```typescript
const plugin = webVitalsPlugin({ reportSoftNav: true });
```

This requires browser support for the Soft Navigations API (Chromium 151+). In browsers without it,
reporting is unchanged from the default behavior.

Note that enabling this also changes how the initial page load is measured: its metrics are
finalized once the first soft navigation occurs, rather than when the page is hidden.

When using the Browser SDK's autocapture, the same option can be set through
`autocapture.webVitals`:

```typescript
amplitude.init('API_KEY', {
autocapture: {
webVitals: { reportSoftNav: true },
},
});
```

### 3. Install plugin to Amplitude SDK

```typescript
Expand Down
2 changes: 1 addition & 1 deletion packages/plugin-web-vitals-browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"dependencies": {
"@amplitude/analytics-core": "workspace:*",
"tslib": "^2.4.1",
"web-vitals": "5.1.0"
"web-vitals": "6.2.1"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^23.0.4",
Expand Down
7 changes: 7 additions & 0 deletions packages/plugin-web-vitals-browser/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
export const PLUGIN_NAME = 'web-vitals-browser';
export const WEB_VITALS_EVENT_NAME = '[Amplitude] Web Vitals';

/**
* How long to wait, after a newer navigation starts reporting metrics, before sending the event for
* a superseded navigation. Metrics for a navigation can be reported slightly after the next soft
* navigation begins, so the event is deferred to give those late metrics a chance to land.
*/
export const SOFT_NAV_FLUSH_DELAY_MS = 1000;
Loading
Loading