Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,5 @@ module.exports = {
jasmine: 'readonly',
},
settings: {},
ignorePatterns: ['**/mocks/**/*'],
ignorePatterns: ['**/mocks/**/*', '**/test-app/**/*'],
}
23 changes: 17 additions & 6 deletions integration-tests/playwright-test/tests/rum.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,39 @@ const COUNTRY = "US"; //country (c)
const APP_VERSION = "v1.0.0"; //app version(v)
const HT = true; //CacheHit (ht)



test.describe("RUM - Request data", () => {
let page: Page;
let RumRequest: any;
let RumRequestBody: any;


//catch Rum request and save it
test.beforeAll(async ({ baseURL }) => {
const browser = await chromium.launch();
page = await browser.newPage();

await page.setViewportSize({width: WIDTH, height: HEIGHT});

const promiseRumRequest = new Promise((resolve, reject) => {
page.goto(baseURL || "");


const promiseRumRequest = new Promise((resolve, reject) => {
//check if RUM request was sent
page.on('request', request => {
if (request.url().startsWith("https://rum.ingress.edgio.net/v1/")) {
resolve(request);
}
});

// when page is loaded, set visibilityState to hidden, so we can trigger
// RUM to send request
page.goto(baseURL || "").then(() => {
page.evaluate(() => {
Object.defineProperty(document, 'visibilityState', { value: 'hidden', writable: true })
Object.defineProperty(document, 'hidden', { value: true, writable: true })
window.dispatchEvent(new Event('visibilitychange'))
})
})
});

await page.setViewportSize({width: WIDTH, height: HEIGHT});

// console.debug("Waiting for Rum request")
RumRequest = await promiseRumRequest
Expand Down
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'

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
Loading