rsnext/packages/next/client/performance-relayer.ts
Keen Yee Liau c7ab8314d7
add attribution to web vitals (#39368)
This commit implements the main proposal presented in
https://github.com/vercel/next.js/issues/39241
to add attribution to web vitals.

Attribution adds more specific debugging info to web vitals,
for example in the case of Cumulative Layout Shift (CLS),
we might want to know
> What's the first element that shifted when the single largest layout shift occurred?

on in the case of Largest Contentful Paint (LCP),
> What's the element corresponding to the LCP for the page?
> If it is an image, what's the URL of the image resource?

Attribution is *disabled* by default because it could potentially
generate a lot data and overwhelm the RUM backend.
It is enabled *per metric* (LCP, FCP, CLS, etc)

As part of this change, `web-vitals` has been upgraded to v3.0.0
This version contains minor bug fixes, please see changelog at
9fe3cc02c8

Fixes #39241 



## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

## Feature

- [x] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [x] Related issues linked using `fixes #number`
- [x] Integration tests added
- [x] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `pnpm lint`
- [x] The examples guidelines are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing.md#adding-examples)


Co-authored-by: JJ Kasper <22380829+ijjk@users.noreply.github.com>
2022-10-04 00:17:30 +00:00

101 lines
3.2 KiB
TypeScript

/* global location */
import type { Metric, ReportCallback } from 'next/dist/compiled/web-vitals'
// copied to prevent pulling in un-necessary utils
const WEB_VITALS = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB']
const initialHref = location.href
let isRegistered = false
let userReportHandler: ReportCallback | undefined
type Attribution = typeof WEB_VITALS[number]
function onReport(metric: Metric): void {
if (userReportHandler) {
userReportHandler(metric)
}
// This code is not shipped, executed, or present in the client-side
// JavaScript bundle unless explicitly enabled in your application.
//
// When this feature is enabled, we'll make it very clear by printing a
// message during the build (`next build`).
if (
process.env.NODE_ENV === 'production' &&
// This field is empty unless you explicitly configure it:
process.env.__NEXT_ANALYTICS_ID
) {
const body: Record<string, string> = {
dsn: process.env.__NEXT_ANALYTICS_ID,
id: metric.id,
page: window.__NEXT_DATA__?.page,
href: initialHref,
event_name: metric.name,
value: metric.value.toString(),
speed:
'connection' in navigator &&
(navigator as any)['connection'] &&
'effectiveType' in (navigator as any)['connection']
? ((navigator as any)['connection']['effectiveType'] as string)
: '',
}
const blob = new Blob([new URLSearchParams(body).toString()], {
// This content type is necessary for `sendBeacon`:
type: 'application/x-www-form-urlencoded',
})
const vitalsUrl = 'https://vitals.vercel-insights.com/v1/vitals'
// Navigator has to be bound to ensure it does not error in some browsers
// https://xgwang.me/posts/you-may-not-know-beacon/#it-may-throw-error%2C-be-sure-to-catch
const send = navigator.sendBeacon && navigator.sendBeacon.bind(navigator)
function fallbackSend() {
fetch(vitalsUrl, {
body: blob,
method: 'POST',
credentials: 'omit',
keepalive: true,
// console.error is used here as when the fetch fails it does not affect functioning of the app
}).catch(console.error)
}
try {
// If send is undefined it'll throw as well. This reduces output code size.
send!(vitalsUrl, blob) || fallbackSend()
} catch (err) {
fallbackSend()
}
}
}
export default (onPerfEntry?: ReportCallback): void => {
// Update function if it changes:
userReportHandler = onPerfEntry
// Only register listeners once:
if (isRegistered) {
return
}
isRegistered = true
const attributions: Attribution[] | undefined = process.env
.__NEXT_WEB_VITALS_ATTRIBUTION as any
for (const webVital of WEB_VITALS) {
try {
let mod: any
if (process.env.__NEXT_HAS_WEB_VITALS_ATTRIBUTION) {
if (attributions?.includes(webVital)) {
mod = require('next/dist/compiled/web-vitals-attribution')
}
}
if (!mod) {
mod = require('next/dist/compiled/web-vitals')
}
mod[`on${webVital}`](onReport)
} catch (err) {
// Do nothing if the module fails to load
console.warn(`Failed to track ${webVital} web-vital`, err)
}
}
}