Skip to content

Commit 9149bc4

Browse files
authored
Replace CWV metrics with GoogleChrome/web-vitals library NEWRELIC-6214 (#435)
* Swap LCP NEWRELIC-7102 * Remove nav-cookie NR-57391 & update agent start times and offset * Swap FID NEWRELIC-7103 * Swap FP & FCP NEWRELIC-7105 * Cap LCP, FCP, and FID to once after swap * Swap TTFB and PVE instrument timings NEWRELIC-7106 * Change fe & dc to event end instead of event start * Exclude onTTFB from non-main ctx agents & remove old test * Update failing addeventlistener test * Recalibrate vitals matchers and fix PVE tests * Swap CLS NEWRELIC-7104 * Fix a cls browser test * Undo onCLS callback capping * Defer PVE & PVT loader code to post-load time * Exclude current ios from FCP test * Update LCP browser tests after pvt agg change * Revert ios FCP test exclusion & introduce fallback * Combine if-cases in PVE
1 parent 435a6ca commit 9149bc4

27 files changed

+307
-782
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const isiOS = /(iPad|iPhone|iPod)/g.test(navigator.userAgent)
2+
3+
/* Feature detection to get our version(s). */
4+
5+
// Shared Web Workers introduced in iOS 16.0+ and n/a in 15.6-
6+
export const iOS_below16 = isiOS && Boolean(typeof SharedWorker === 'undefined')
7+
/* ^ It was discovered in Safari 14 (https://bugs.webkit.org/show_bug.cgi?id=225305) that the buffered flag in PerformanceObserver
8+
did not work. This affects our onFCP metric in particular since web-vitals uses that flag to retrieve paint timing entries.
9+
This was fixed in v16+.
10+
*/

src/common/cookie/nav-cookie.js

Lines changed: 0 additions & 23 deletions
This file was deleted.

src/common/metrics/paint-metrics.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,4 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
export var paintMetrics = {}
7-
8-
export function addMetric (name, value) {
9-
paintMetrics[name] = value
10-
}
6+
export const paintMetrics = {}

src/common/timing/now.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,7 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
/**
7-
* This is TO BE REMOVED AND REPLACED by web-vitals TTFB
8-
* @type {number} - An integer time-stamp representing the time the agent side-effects first ran
9-
*/
10-
export const importTimestamp = new Date().getTime()
11-
6+
// This is our own layer around performance.now. It's not strictly necessary, but we keep it in case of future mod-ing of the value for refactor purpose.
127
export function now () {
138
return Math.round(performance.now())
149
}

src/common/timing/stopwatch.js

Lines changed: 0 additions & 25 deletions
This file was deleted.

src/common/util/user-agent.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent#Browser_Name
1111
var agentName = null
1212
var agentVersion = null
13-
var safari = /Version\/(\S+)\s+Safari/
13+
const safari = /Version\/(\S+)\s+Safari/
1414

1515
if (navigator.userAgent) {
1616
var userAgent = navigator.userAgent

src/common/window/page-visibility.js

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,3 @@ export function subscribeToVisibilityChange (cb, toHiddenOnly = false) {
2222
cb(document.visibilityState)
2323
}
2424
}
25-
26-
export function initializeHiddenTime () {
27-
return document.visibilityState === 'hidden' ? -1 : Infinity
28-
}
Lines changed: 55 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,52 @@
1-
import { measure } from '../../../common/timing/stopwatch'
1+
import { handle } from '../../../common/event-emitter/handle'
2+
import { FEATURE_NAMES } from '../../../loaders/features/features'
3+
import { isiOS } from '../../../common/browser-version/ios-version'
4+
import { onTTFB } from 'web-vitals'
25
import { mapOwn } from '../../../common/util/map-own'
36
import { param, fromArray } from '../../../common/url/encode'
47
import { addPT, addPN } from '../../../common/timing/nav-timing'
58
import { stringify } from '../../../common/util/stringify'
6-
import { addMetric as addPaintMetric } from '../../../common/metrics/paint-metrics'
9+
import { paintMetrics } from '../../../common/metrics/paint-metrics'
710
import { submitData } from '../../../common/util/submit-data'
811
import { getConfigurationValue, getInfo, getRuntime } from '../../../common/config/config'
912
import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler'
1013
import { AggregateBase } from '../../utils/aggregate-base'
11-
import { FEATURE_NAME } from '../constants'
14+
import * as CONSTANTS from '../constants'
1215
import { getActivatedFeaturesFlags } from './initialized-features'
13-
import { globalScope } from '../../../common/util/global-scope'
16+
import { globalScope, isBrowserScope } from '../../../common/util/global-scope'
1417
import { drain } from '../../../common/drain/drain'
1518

1619
const jsonp = 'NREUM.setToken'
1720

1821
export class Aggregate extends AggregateBase {
19-
static featureName = FEATURE_NAME
22+
static featureName = CONSTANTS.FEATURE_NAME
2023
constructor (agentIdentifier, aggregator) {
21-
super(agentIdentifier, aggregator, FEATURE_NAME)
22-
this.sendRum()
24+
super(agentIdentifier, aggregator, CONSTANTS.FEATURE_NAME)
25+
26+
if (typeof PerformanceNavigationTiming !== 'undefined' && !isiOS) {
27+
this.alreadySent = false // we don't support timings on BFCache restores
28+
const agentRuntime = getRuntime(agentIdentifier) // we'll store timing values on the runtime obj to be read by the aggregate module
29+
30+
/* Time To First Byte
31+
This listener must record these values *before* PVE's aggregate sends RUM. */
32+
onTTFB(({ value, entries }) => {
33+
if (this.alreadySent) return
34+
this.alreadySent = true
35+
36+
agentRuntime[CONSTANTS.TTFB] = Math.round(value) // this is our "backend" duration; web-vitals will ensure it's lower bounded at 0
37+
38+
// Similar to what vitals does for ttfb, we have to factor in activation-start when calculating relative timings:
39+
const navEntry = entries[0]
40+
const respOrActivStart = Math.max(navEntry.responseStart, navEntry.activationStart || 0)
41+
agentRuntime[CONSTANTS.FBTWL] = Math.max(Math.round(navEntry.loadEventEnd - respOrActivStart), 0) // our "frontend" duration
42+
handle('timing', ['load', Math.round(navEntry.loadEventEnd)], undefined, FEATURE_NAMES.pageViewTiming, this.ee)
43+
agentRuntime[CONSTANTS.FBTDC] = Math.max(Math.round(navEntry.domContentLoadedEventEnd - respOrActivStart), 0) // our "dom processing" duration
44+
45+
this.sendRum()
46+
})
47+
} else {
48+
this.sendRum() // timings either already in runtime from instrument or is meant to get 0'd.
49+
}
2350
}
2451

2552
getScheme () {
@@ -31,15 +58,15 @@ export class Aggregate extends AggregateBase {
3158
if (!info.beacon) return
3259
if (info.queueTime) this.aggregator.store('measures', 'qt', { value: info.queueTime })
3360
if (info.applicationTime) this.aggregator.store('measures', 'ap', { value: info.applicationTime })
61+
const agentRuntime = getRuntime(this.agentIdentifier)
3462

35-
// some time in the past some code will have called stopwatch.mark('starttime', Date.now())
36-
// calling measure like this will create a metric that measures the time differential between
37-
// the two marks.
38-
measure(this.aggregator, 'be', 'starttime', 'firstbyte')
39-
measure(this.aggregator, 'fe', 'firstbyte', 'onload')
40-
measure(this.aggregator, 'dc', 'firstbyte', 'domContent')
63+
// These 3 values should've been recorded after load and before this func runs. They are part of the minimum required for PageView events to be created.
64+
// Following PR #428, which demands that all agents send RUM call, these need to be sent even outside of the main window context where PerformanceTiming
65+
// or PerformanceNavigationTiming do not exists. Hence, they'll be filled in by 0s instead in, for example, worker threads that still init the PVE module.
66+
this.aggregator.store('measures', 'be', { value: isBrowserScope ? agentRuntime[CONSTANTS.TTFB] : 0 })
67+
this.aggregator.store('measures', 'fe', { value: isBrowserScope ? agentRuntime[CONSTANTS.FBTWL] : 0 })
68+
this.aggregator.store('measures', 'dc', { value: isBrowserScope ? agentRuntime[CONSTANTS.FBTDC] : 0 })
4169

42-
const agentRuntime = getRuntime(this.agentIdentifier)
4370
var measuresMetrics = this.aggregator.get('measures')
4471

4572
var measuresQueryString = mapOwn(measuresMetrics, function (metricName, measure) {
@@ -64,29 +91,25 @@ export class Aggregate extends AggregateBase {
6491

6592
if (globalScope.performance && typeof (globalScope.performance.timing) !== 'undefined') {
6693
var navTimingApiData = ({
67-
timing: addPT(getRuntime(this.agentIdentifier).offset, globalScope.performance.timing, {}),
94+
timing: addPT(agentRuntime.offset, globalScope.performance.timing, {}),
6895
navigation: addPN(globalScope.performance.navigation, {})
6996
})
7097
chunksForQueryString.push(param('perf', stringify(navTimingApiData)))
7198
}
7299

73-
if (globalScope.performance && globalScope.performance.getEntriesByType) {
100+
try { // PVTiming sends these too, albeit using web-vitals and slightly different; it's unknown why they're duplicated, but PVT should be the truth
74101
var entries = globalScope.performance.getEntriesByType('paint')
75-
if (entries && entries.length > 0) {
76-
entries.forEach(function (entry) {
77-
if (!entry.startTime || entry.startTime <= 0) return
78-
79-
if (entry.name === 'first-paint') {
80-
chunksForQueryString.push(param('fp',
81-
String(Math.floor(entry.startTime))))
82-
} else if (entry.name === 'first-contentful-paint') {
83-
chunksForQueryString.push(param('fcp',
84-
String(Math.floor(entry.startTime))))
85-
}
86-
addPaintMetric(entry.name, Math.floor(entry.startTime))
87-
})
88-
}
89-
}
102+
entries.forEach(function (entry) {
103+
if (!entry.startTime || entry.startTime <= 0) return
104+
105+
if (entry.name === 'first-paint') {
106+
chunksForQueryString.push(param('fp', String(Math.floor(entry.startTime))))
107+
} else if (entry.name === 'first-contentful-paint') {
108+
chunksForQueryString.push(param('fcp', String(Math.floor(entry.startTime))))
109+
}
110+
paintMetrics[entry.name] = Math.floor(entry.startTime) // this is consumed by Spa module
111+
})
112+
} catch (e) {}
90113

91114
chunksForQueryString.push(param('xx', info.extra))
92115
chunksForQueryString.push(param('ua', info.userAttributes))
@@ -102,6 +125,6 @@ export class Aggregate extends AggregateBase {
102125
)
103126
// Usually `drain` is invoked automatically after processing feature flags contained in the JSONP callback from
104127
// ingest (see `activateFeatures`), so when JSONP cannot execute (as with module workers), we drain manually.
105-
if (!isValidJsonp) drain(this.agentIdentifier, this.featureName)
128+
if (!isValidJsonp) drain(this.agentIdentifier, CONSTANTS.FEATURE_NAME)
106129
}
107130
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
import { FEATURE_NAMES } from '../../loaders/features/features'
22

33
export const FEATURE_NAME = FEATURE_NAMES.pageViewEvent
4+
export const TTFB = 'firstbyte'
5+
export const FBTDC = 'domcontent'
6+
export const FBTWL = 'windowload'
Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,33 @@
11

22
import { handle } from '../../../common/event-emitter/handle'
3-
import { now, importTimestamp } from '../../../common/timing/now'
4-
import { mark } from '../../../common/timing/stopwatch'
3+
import { isiOS } from '../../../common/browser-version/ios-version'
54
import { InstrumentBase } from '../../utils/instrument-base'
6-
import { onDOMContentLoaded, onWindowLoad } from '../../../common/window/load'
7-
import { FEATURE_NAME } from '../constants'
5+
import * as CONSTANTS from '../constants'
86
import { FEATURE_NAMES } from '../../../loaders/features/features'
97
import { getRuntime } from '../../../common/config/config'
8+
import { onDOMContentLoaded, onWindowLoad } from '../../../common/window/load'
9+
import { now } from '../../../common/timing/now'
1010

1111
export class Instrument extends InstrumentBase {
12-
static featureName = FEATURE_NAME
12+
static featureName = CONSTANTS.FEATURE_NAME
1313
constructor (agentIdentifier, aggregator, auto = true) {
14-
super(agentIdentifier, aggregator, FEATURE_NAME, auto)
14+
super(agentIdentifier, aggregator, CONSTANTS.FEATURE_NAME, auto)
1515

16-
mark(agentIdentifier, 'starttime', getRuntime(agentIdentifier).offset)
17-
mark(agentIdentifier, 'firstbyte', importTimestamp)
16+
if ((typeof PerformanceNavigationTiming === 'undefined' || isiOS) && typeof PerformanceTiming !== 'undefined') {
17+
// For majority browser versions in which PNT exists, we can get load timings later from the nav entry (in the aggregate portion). At minimum, PT should exist for main window.
18+
// *cli Mar'23 - iOS 15.2 & 15.4 testing in Sauce still fails with onTTFB. Hence, all iOS will fallback to this for now. Unknown if this is similar in nature to iOS_below16 bug.
19+
const agentRuntime = getRuntime(agentIdentifier)
1820

19-
onDOMContentLoaded(() => this.measureDomContentLoaded())
20-
onWindowLoad(() => this.measureWindowLoaded(), true) // we put this in the front of load listeners (useCapture=true) for better precision on measuring when it fires!
21-
this.importAggregator() // the measureWindowLoaded cb should run *before* the page_view_event agg runs
22-
}
23-
24-
// should be called on window.load or window.onload, will not be called if agent is loaded after window load
25-
measureWindowLoaded () {
26-
var ts = now()
27-
mark(this.agentIdentifier, 'onload', ts + getRuntime(this.agentIdentifier).offset)
28-
handle('timing', ['load', ts], undefined, FEATURE_NAMES.pageViewTiming, this.ee)
29-
}
21+
agentRuntime[CONSTANTS.TTFB] = Math.max(Date.now() - agentRuntime.offset, 0)
22+
onDOMContentLoaded(() => agentRuntime[CONSTANTS.FBTDC] = Math.max(now() - agentRuntime[CONSTANTS.TTFB], 0))
23+
onWindowLoad(() => {
24+
const timeNow = now()
25+
agentRuntime[CONSTANTS.FBTWL] = Math.max(timeNow - agentRuntime[CONSTANTS.TTFB], 0)
26+
handle('timing', ['load', timeNow], undefined, FEATURE_NAMES.pageViewTiming, this.ee)
27+
})
28+
}
29+
// Else, inference: inside worker or some other env where these events are irrelevant. They'll get filled in with 0s in RUM call.
3030

31-
// should be called on DOMContentLoaded, will not be called if agent is loaded after DOMContentLoaded
32-
measureDomContentLoaded () {
33-
mark(this.agentIdentifier, 'domContent', now() + getRuntime(this.agentIdentifier).offset)
31+
this.importAggregator()
3432
}
3533
}

0 commit comments

Comments
 (0)