Skip to content

Commit

Permalink
EDGSITES-592 - LCP not reported when the user does not interact with …
Browse files Browse the repository at this point in the history
…the page before navigation
  • Loading branch information
Mark Brocato authored and negyxo committed Nov 22, 2023
1 parent 5768be8 commit 6bb4e88
Show file tree
Hide file tree
Showing 9 changed files with 389 additions and 1,985 deletions.
54 changes: 54 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

126 changes: 68 additions & 58 deletions src/Metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@ import {
CLSAttribution,
INPAttribution,
} from 'web-vitals/attribution'
import { ReportOpts } from 'web-vitals/src/types'
import { CACHE_MANIFEST_TTL, DEST_URL, SEND_DELAY } from './constants'
import { CACHE_MANIFEST_TTL, DEST_URL } from './constants'
import getCookieValue from './getCookieValue'
import { ServerTiming } from './getServerTiming'
import Router from './Router'
import uuid from './uuid'
import debounce from 'lodash.debounce'
import CacheManifest from './CacheManifest'
import { isV7orGreater, getServerTiming, isServerTimingSupported } from './utils'
import { CookiesInfo } from './CookiesInfo'
Expand Down Expand Up @@ -190,7 +188,8 @@ export interface MetricsOptions {
*/
sendTo?: string
/**
* Set to true to output all measurements to the console
* Set to true to output all measurements to the console. You can also enable debug output
* by setting the `edgio_rum_debug` cookie to `true` in your browser.
*/
debug?: boolean
/**
Expand All @@ -207,7 +206,7 @@ interface Metrics {
/**
* Collects all metrics and reports them to Edgio RUM.
*/
collect(): Promise<void>
collect(): void
}

// eslint-disable-next-line @typescript-eslint/no-redeclare
Expand Down Expand Up @@ -238,6 +237,8 @@ class BrowserMetrics implements Metrics {
private connectionType?: string
private manifest?: CacheManifest
private cookiesInfo: CookiesInfo
private queue: Set<MetricWithAttribution>
private debug: boolean = false

constructor(options: MetricsOptions = {}) {
this.originalURL = location.href
Expand All @@ -248,12 +249,17 @@ class BrowserMetrics implements Metrics {
this.pageID = uuid()
this.metrics = this.flushMetrics()
this.cookiesInfo = new CookiesInfo()
this.queue = new Set()

this.debug =
this.options.debug ??
this.cookiesInfo.cookies.find(c => c.key === 'edgio_rum_debug')?.value === 'true'

try {
// @ts-ignore
this.connectionType = navigator.connection.effectiveType
} catch (e) {
if (this.options.debug) {
if (this.debug) {
console.debug('[RUM] could not obtain navigator.connection metrics')
}
}
Expand All @@ -273,45 +279,40 @@ class BrowserMetrics implements Metrics {
// how we handle MISS/HIT ration in the RUM Edgio BE
if (!isServerTimingSupported()) return Promise.resolve()

return Promise.all([
this.toPromise(onTTFB),
this.toPromise(onFCP),
this.toPromise(onLCP),
this.toPromise(onINP),
this.toPromise(onFID),
this.toPromise(onCLS),
]).then(() => {})
}
// See https://github.com/GoogleChrome/web-vitals#batch-multiple-reports-together
// Report all available metrics whenever the page is backgrounded or unloaded.
addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.flushQueue('visibilitychange')
}
})

/**
* Sends a beacon to Edgio Porkfish, which helps us improve anycast routing performance.
*/
private sendPorkfishBeacon() {
try {
const uuid = crypto.randomUUID()
navigator.sendBeacon(`https://${uuid}.ac.bcon.ecdns.net/udp/${this.token}`)
} catch (e) {
console.warn('could not send beacon', e)
}
// NOTE: Safari does not reliably fire the `visibilitychange` event when the
// page is being unloaded. If Safari support is needed, you should also flush
// the queue in the `pagehide` event.
addEventListener('pagehide', () => this.flushQueue('pagehide'))

onFCP(this.addToQueue)
onTTFB(this.addToQueue)
onLCP(this.addToQueue)
onINP(this.addToQueue)
onFID(this.addToQueue)
onCLS(this.addToQueue)
}

private flushMetrics() {
return { clsel: [] }
addToQueue = (metric: any) => {
this.queue.add(metric)

if (this.debug) {
console.log('[RUM]', metric.name, metric.value, `(pageID: ${this.pageID})`)
}
}

/**
* Returns a promise that resolves once the specified metric has been collected.
* @param getMetric
* @param params
*/
private toPromise(getMetric: Function, params?: ReportOpts) {
return new Promise<void>(resolve => {
getMetric((metric: MetricWithAttribution) => {
if (metric.delta === 0) {
// metrics like LCP will get reported as a final value on first input. If there is no change from the previous measurement, don't bother reporting
return resolve()
}
flushQueue = (event: string) => {
const { queue } = this

if (queue.size > 0) {
Array.from(this.queue).forEach(metric => {
this.metrics[metric.name.toLowerCase()] = metric.value

if (!this.clientNavigationHasOccurred) {
Expand All @@ -332,26 +333,32 @@ class BrowserMetrics implements Metrics {
}

// record the element that shifted
// @ts-ignore this.metrics.clsel is always initialized to an empty array
this.metrics.clsel.push(attribution.largestShiftTarget)

if (this.options.debug) {
console.log(
`[RUM] largest layout shift target: ${attribution.largestShiftTarget}`,
`(pageID: ${this.pageID})`
)
if (attribution.largestShiftTarget) {
// @ts-ignore this.metrics.clsel is always initialized to an empty array
this.metrics.clsel.push(attribution.largestShiftTarget)
}
}
})

if (this.options.debug) {
console.log('[RUM]', metric.name, metric.value, `(pageID: ${this.pageID})`)
}
queue.clear()
this.send()
}
}

this.send()
/**
* Sends a beacon to Edgio Porkfish, which helps us improve anycast routing performance.
*/
private sendPorkfishBeacon() {
try {
const uuid = crypto.randomUUID()
navigator.sendBeacon(`https://${uuid}.ac.bcon.ecdns.net/udp/${this.token}`)
} catch (e) {
console.warn('could not send beacon', e)
}
}

resolve()
}, params)
})
private flushMetrics() {
return { clsel: [] }
}

/**
Expand Down Expand Up @@ -380,7 +387,7 @@ class BrowserMetrics implements Metrics {
// @ts-ignore
this.connectionType = navigator.connection.effectiveType
} catch (e) {
if (this.options.debug) {
if (this.debug) {
console.debug('[RUM] could not obtain navigator.connection metrics')
}
}
Expand Down Expand Up @@ -503,7 +510,7 @@ class BrowserMetrics implements Metrics {
/**
* Sends all collected metrics to Edgio RUM.
*/
send = debounce(() => {
send = () => {
const body = this.createPayload()

if (!this.token) {
Expand All @@ -520,16 +527,19 @@ class BrowserMetrics implements Metrics {
return
}

if (this.debug) {
console.log('[RUM] sending', JSON.parse(body))
}

if (navigator.sendBeacon) {
// Why we use sendBea
// Use `navigator.sendBeacon()` if available, falling back to `fetch()`.
navigator.sendBeacon(this.sendTo, body)
} else {
fetch(this.sendTo, { body, method: 'POST', keepalive: true })
}

this.index++
}, SEND_DELAY)
}
}

const getEnvironmentCookieValue = () => {
Expand Down
6 changes: 3 additions & 3 deletions test-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
},
"dependencies": {
"@edgio/rum": "file:.yalc/@edgio/rum",
"next": "10.2.0",
"react": "17.0.2",
"react-dom": "17.0.2"
"next": "^14.0.3",
"react": "^18.2.0",
"react-dom": "^18.2.0"
}
}
15 changes: 15 additions & 0 deletions test-app/pages/help.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import Link from 'next/link'

Check failure on line 1 in test-app/pages/help.js

View workflow job for this annotation

GitHub Actions / test

'Link' is defined but never used

export default function Help() {
return (
<div>
<h1>Help</h1>
<p>
<a href="/">Home (reload)</a>
</p>
<p>
<Link href="/">Home</Link>
</p>
</div>
)
}
4 changes: 4 additions & 0 deletions test-app/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import Head from 'next/head'
import { useCallback, useState } from 'react'
import styles from '../styles/Home.module.css'
import Link from 'next/link'

export default function Home() {
const [elements, setElements] = useState([])
Expand All @@ -23,6 +24,9 @@ export default function Home() {
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>

<Link href="/help">Help</Link>

<button onClick={createLayoutShift}>Create Layout Shift</button>
{elements}
</main>
Expand Down

0 comments on commit 6bb4e88

Please sign in to comment.