@newrelic/browser-agent 1.324.1-rc.1 → 1.324.1-rc.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/vitals/vital-metric.js +16 -1
- package/dist/cjs/features/page_view_timing/aggregate/index.js +28 -9
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/vitals/vital-metric.js +15 -1
- package/dist/esm/features/page_view_timing/aggregate/index.js +28 -9
- package/dist/types/common/vitals/vital-metric.d.ts +9 -6
- package/dist/types/common/vitals/vital-metric.d.ts.map +1 -1
- package/dist/types/features/page_view_timing/aggregate/index.d.ts +17 -4
- package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/common/vitals/vital-metric.js +12 -1
- package/src/features/page_view_timing/aggregate/index.js +26 -9
|
@@ -17,7 +17,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.RRWEB_PACKAGE_NAME = exports.D
|
|
|
17
17
|
/**
|
|
18
18
|
* Exposes the version of the agent
|
|
19
19
|
*/
|
|
20
|
-
const VERSION = exports.VERSION = "1.324.1-rc.
|
|
20
|
+
const VERSION = exports.VERSION = "1.324.1-rc.3";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Exposes the build type of the agent
|
|
@@ -17,7 +17,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.RRWEB_PACKAGE_NAME = exports.D
|
|
|
17
17
|
/**
|
|
18
18
|
* Exposes the version of the agent
|
|
19
19
|
*/
|
|
20
|
-
const VERSION = exports.VERSION = "1.324.1-rc.
|
|
20
|
+
const VERSION = exports.VERSION = "1.324.1-rc.3";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Exposes the build type of the agent
|
|
@@ -4,10 +4,12 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.VitalMetric = void 0;
|
|
7
|
+
var _now = require("../timing/now");
|
|
7
8
|
/**
|
|
8
9
|
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
9
10
|
* SPDX-License-Identifier: Apache-2.0
|
|
10
11
|
*/
|
|
12
|
+
|
|
11
13
|
class VitalMetric {
|
|
12
14
|
#subscribers = new Set();
|
|
13
15
|
history = [];
|
|
@@ -16,16 +18,29 @@ class VitalMetric {
|
|
|
16
18
|
this.attrs = {};
|
|
17
19
|
this.roundingMethod = typeof roundingMethod === 'function' ? roundingMethod : Math.floor;
|
|
18
20
|
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Records a new value for this metric and notifies subscribers.
|
|
24
|
+
* @param {object} params
|
|
25
|
+
* @param {number} params.value The metric's value. For most metrics this is itself a ms offset from page origin
|
|
26
|
+
* (i.e. the metric's own real time of occurrence); for score-based metrics like CLS it isn't a time at all.
|
|
27
|
+
* @param {object} [params.attrs] Metric-specific attribution attrs.
|
|
28
|
+
*/
|
|
19
29
|
update({
|
|
20
30
|
value,
|
|
21
31
|
attrs = {}
|
|
22
32
|
}) {
|
|
23
33
|
if (value === undefined || value === null || value < 0) return;
|
|
34
|
+
const metricTimestamps = {
|
|
35
|
+
inp: attrs.interactionTime ?? (0, _now.now)(),
|
|
36
|
+
cls: attrs.largestShiftTime ?? (0, _now.now)()
|
|
37
|
+
};
|
|
24
38
|
const state = {
|
|
25
39
|
value: this.roundingMethod(value),
|
|
26
40
|
name: this.name,
|
|
27
41
|
// optional web-vitals attribution fields are omitted rather than being serialized as null attributes
|
|
28
|
-
attrs: Object.fromEntries(Object.entries(attrs).filter(([, attrValue]) => attrValue !== undefined))
|
|
42
|
+
attrs: Object.fromEntries(Object.entries(attrs).filter(([, attrValue]) => attrValue !== undefined)),
|
|
43
|
+
relativeTimestamp: metricTimestamps[this.name] ?? value
|
|
29
44
|
};
|
|
30
45
|
this.history.push(state);
|
|
31
46
|
this.#subscribers.forEach(cb => {
|
|
@@ -35,9 +35,10 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
35
35
|
#handleVitalMetric = ({
|
|
36
36
|
name,
|
|
37
37
|
value,
|
|
38
|
-
attrs
|
|
38
|
+
attrs,
|
|
39
|
+
relativeTimestamp
|
|
39
40
|
}) => {
|
|
40
|
-
this.addTiming(name, value, attrs);
|
|
41
|
+
this.addTiming(name, value, attrs, relativeTimestamp);
|
|
41
42
|
};
|
|
42
43
|
constructor(agentRef) {
|
|
43
44
|
super(agentRef, _constants.FEATURE_NAME);
|
|
@@ -49,7 +50,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
49
50
|
this.obfuscator = new _obfuscate.Obfuscator(agentRef, _events.EVENT_TYPES.PVT);
|
|
50
51
|
(0, _registerHandler.registerHandler)('docHidden', msTimestamp => this.endCurrentSession(msTimestamp), this.featureName, this.ee);
|
|
51
52
|
// Add the time of _window pagehide event_ firing to the next PVT harvest == NRDB windowUnload attr:
|
|
52
|
-
(0, _registerHandler.registerHandler)('winPagehide', msTimestamp => this.addTiming('unload', msTimestamp
|
|
53
|
+
(0, _registerHandler.registerHandler)('winPagehide', msTimestamp => this.addTiming('unload', msTimestamp), this.featureName, this.ee);
|
|
53
54
|
this.waitForFlags([]).then(() => {
|
|
54
55
|
_firstPaint.firstPaint.subscribe(this.#handleVitalMetric);
|
|
55
56
|
_firstContentfulPaint.firstContentfulPaint.subscribe(this.#handleVitalMetric);
|
|
@@ -68,10 +69,11 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
68
69
|
const {
|
|
69
70
|
name,
|
|
70
71
|
value,
|
|
71
|
-
attrs
|
|
72
|
+
attrs,
|
|
73
|
+
relativeTimestamp
|
|
72
74
|
} = _cumulativeLayoutShift.cumulativeLayoutShift.current;
|
|
73
75
|
if (value === undefined) return;
|
|
74
|
-
this.addTiming(name, value * 1000, attrs);
|
|
76
|
+
this.addTiming(name, value * 1000, attrs, relativeTimestamp);
|
|
75
77
|
}, true, true); // CLS node should only report on vis change rather than on every change.
|
|
76
78
|
/* NOTE: the capture=true is required -- empirically verified against a real browser (Chrome) for CLS
|
|
77
79
|
timing node to reliably be in the same final harvest as the rest, alongside Harvester#startTimer's
|
|
@@ -89,12 +91,25 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
89
91
|
endCurrentSession(timestamp) {
|
|
90
92
|
if (!this.curSessEndRecorded) {
|
|
91
93
|
// TO DO: stage 2 - we don't want to capture this timing twice on page navigating away, but it should run again if we return to page and away *again*
|
|
92
|
-
this.addTiming('pageHide', timestamp
|
|
94
|
+
this.addTiming('pageHide', timestamp);
|
|
93
95
|
this.curSessEndRecorded = true;
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Records a page view timing node, decorating it with system attributes (page URL, connection info,
|
|
101
|
+
* CLS-so-far, webdriver detection, and a server-corrected absolute timestamp) before buffering it for harvest.
|
|
102
|
+
* @param {string} name The timing node's name (e.g. 'fp', 'fcp', 'lcp', 'cls', 'fi', 'pageHide', 'unload').
|
|
103
|
+
* @param {number} value The timing node's value; a ms offset from page origin for most nodes, a duration for
|
|
104
|
+
* 'inp', and a unitless score for 'cls'.
|
|
105
|
+
* @param {object} [attrs] Additional custom attributes to attach to the node.
|
|
106
|
+
* @param {number} [relativeTimestamp] The ms offset from page origin at which the underlying event occurred. It is
|
|
107
|
+
* server-corrected into the node's absolute `timestamp` attribute, which is omitted if the time keeper isn't ready
|
|
108
|
+
* or this isn't a finite number. Supplied by VitalMetric for nodes whose `value` isn't itself the event's time
|
|
109
|
+
* (e.g. 'inp' uses the interaction time, 'cls' uses the largest shift time). Defaults to `value`.
|
|
110
|
+
* @returns {{name: string, value: number, attrs: object}} The recorded timing node.
|
|
111
|
+
*/
|
|
112
|
+
addTiming(name, value, attrs = {}, relativeTimestamp = value) {
|
|
98
113
|
attrs.pageUrl = (0, _cleanUrl.cleanURL)((0, _runtime.getNavigationEntry)()?.name || _runtime.initialLocation);
|
|
99
114
|
addConnectionAttributes(attrs); // network conditions may differ from the actual for VitalMetrics when they were captured
|
|
100
115
|
|
|
@@ -110,6 +125,10 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
110
125
|
attrs.cls = _cumulativeLayoutShift.cumulativeLayoutShift.current.value;
|
|
111
126
|
}
|
|
112
127
|
attrs.webdriverDetected = _webdriverDetection.webdriverDetected;
|
|
128
|
+
const timeKeeper = this.agentRef.runtime.timeKeeper;
|
|
129
|
+
if (timeKeeper?.ready && Number.isFinite(relativeTimestamp)) {
|
|
130
|
+
attrs.timestamp = Math.floor(timeKeeper.correctRelativeTimestamp(relativeTimestamp));
|
|
131
|
+
}
|
|
113
132
|
const timing = {
|
|
114
133
|
name,
|
|
115
134
|
value,
|
|
@@ -140,7 +159,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
140
159
|
});
|
|
141
160
|
}
|
|
142
161
|
#getGlobalCustomAttributes() {
|
|
143
|
-
const reservedAttributes = ['size', 'eid', 'cls', 'type', 'fid', 'elTag', 'elUrl', 'net-type', 'net-etype', 'net-rtt', 'net-dlink', 'webdriverDetected'];
|
|
162
|
+
const reservedAttributes = ['size', 'eid', 'cls', 'type', 'fid', 'elTag', 'elUrl', 'net-type', 'net-etype', 'net-rtt', 'net-dlink', 'webdriverDetected', 'timestamp'];
|
|
144
163
|
return Object.fromEntries(Object.entries(this.agentRef.info.jsAttributes || {}).filter(([key]) => !reservedAttributes.includes(key)));
|
|
145
164
|
}
|
|
146
165
|
preHarvestChecks() {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
|
+
import { now } from '../timing/now';
|
|
5
6
|
export class VitalMetric {
|
|
6
7
|
#subscribers = new Set();
|
|
7
8
|
history = [];
|
|
@@ -10,16 +11,29 @@ export class VitalMetric {
|
|
|
10
11
|
this.attrs = {};
|
|
11
12
|
this.roundingMethod = typeof roundingMethod === 'function' ? roundingMethod : Math.floor;
|
|
12
13
|
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Records a new value for this metric and notifies subscribers.
|
|
17
|
+
* @param {object} params
|
|
18
|
+
* @param {number} params.value The metric's value. For most metrics this is itself a ms offset from page origin
|
|
19
|
+
* (i.e. the metric's own real time of occurrence); for score-based metrics like CLS it isn't a time at all.
|
|
20
|
+
* @param {object} [params.attrs] Metric-specific attribution attrs.
|
|
21
|
+
*/
|
|
13
22
|
update({
|
|
14
23
|
value,
|
|
15
24
|
attrs = {}
|
|
16
25
|
}) {
|
|
17
26
|
if (value === undefined || value === null || value < 0) return;
|
|
27
|
+
const metricTimestamps = {
|
|
28
|
+
inp: attrs.interactionTime ?? now(),
|
|
29
|
+
cls: attrs.largestShiftTime ?? now()
|
|
30
|
+
};
|
|
18
31
|
const state = {
|
|
19
32
|
value: this.roundingMethod(value),
|
|
20
33
|
name: this.name,
|
|
21
34
|
// optional web-vitals attribution fields are omitted rather than being serialized as null attributes
|
|
22
|
-
attrs: Object.fromEntries(Object.entries(attrs).filter(([, attrValue]) => attrValue !== undefined))
|
|
35
|
+
attrs: Object.fromEntries(Object.entries(attrs).filter(([, attrValue]) => attrValue !== undefined)),
|
|
36
|
+
relativeTimestamp: metricTimestamps[this.name] ?? value
|
|
23
37
|
};
|
|
24
38
|
this.history.push(state);
|
|
25
39
|
this.#subscribers.forEach(cb => {
|
|
@@ -29,9 +29,10 @@ export class Aggregate extends AggregateBase {
|
|
|
29
29
|
#handleVitalMetric = ({
|
|
30
30
|
name,
|
|
31
31
|
value,
|
|
32
|
-
attrs
|
|
32
|
+
attrs,
|
|
33
|
+
relativeTimestamp
|
|
33
34
|
}) => {
|
|
34
|
-
this.addTiming(name, value, attrs);
|
|
35
|
+
this.addTiming(name, value, attrs, relativeTimestamp);
|
|
35
36
|
};
|
|
36
37
|
constructor(agentRef) {
|
|
37
38
|
super(agentRef, FEATURE_NAME);
|
|
@@ -43,7 +44,7 @@ export class Aggregate extends AggregateBase {
|
|
|
43
44
|
this.obfuscator = new Obfuscator(agentRef, EVENT_TYPES.PVT);
|
|
44
45
|
registerHandler('docHidden', msTimestamp => this.endCurrentSession(msTimestamp), this.featureName, this.ee);
|
|
45
46
|
// Add the time of _window pagehide event_ firing to the next PVT harvest == NRDB windowUnload attr:
|
|
46
|
-
registerHandler('winPagehide', msTimestamp => this.addTiming('unload', msTimestamp
|
|
47
|
+
registerHandler('winPagehide', msTimestamp => this.addTiming('unload', msTimestamp), this.featureName, this.ee);
|
|
47
48
|
this.waitForFlags([]).then(() => {
|
|
48
49
|
firstPaint.subscribe(this.#handleVitalMetric);
|
|
49
50
|
firstContentfulPaint.subscribe(this.#handleVitalMetric);
|
|
@@ -62,10 +63,11 @@ export class Aggregate extends AggregateBase {
|
|
|
62
63
|
const {
|
|
63
64
|
name,
|
|
64
65
|
value,
|
|
65
|
-
attrs
|
|
66
|
+
attrs,
|
|
67
|
+
relativeTimestamp
|
|
66
68
|
} = cumulativeLayoutShift.current;
|
|
67
69
|
if (value === undefined) return;
|
|
68
|
-
this.addTiming(name, value * 1000, attrs);
|
|
70
|
+
this.addTiming(name, value * 1000, attrs, relativeTimestamp);
|
|
69
71
|
}, true, true); // CLS node should only report on vis change rather than on every change.
|
|
70
72
|
/* NOTE: the capture=true is required -- empirically verified against a real browser (Chrome) for CLS
|
|
71
73
|
timing node to reliably be in the same final harvest as the rest, alongside Harvester#startTimer's
|
|
@@ -83,12 +85,25 @@ export class Aggregate extends AggregateBase {
|
|
|
83
85
|
endCurrentSession(timestamp) {
|
|
84
86
|
if (!this.curSessEndRecorded) {
|
|
85
87
|
// TO DO: stage 2 - we don't want to capture this timing twice on page navigating away, but it should run again if we return to page and away *again*
|
|
86
|
-
this.addTiming('pageHide', timestamp
|
|
88
|
+
this.addTiming('pageHide', timestamp);
|
|
87
89
|
this.curSessEndRecorded = true;
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Records a page view timing node, decorating it with system attributes (page URL, connection info,
|
|
95
|
+
* CLS-so-far, webdriver detection, and a server-corrected absolute timestamp) before buffering it for harvest.
|
|
96
|
+
* @param {string} name The timing node's name (e.g. 'fp', 'fcp', 'lcp', 'cls', 'fi', 'pageHide', 'unload').
|
|
97
|
+
* @param {number} value The timing node's value; a ms offset from page origin for most nodes, a duration for
|
|
98
|
+
* 'inp', and a unitless score for 'cls'.
|
|
99
|
+
* @param {object} [attrs] Additional custom attributes to attach to the node.
|
|
100
|
+
* @param {number} [relativeTimestamp] The ms offset from page origin at which the underlying event occurred. It is
|
|
101
|
+
* server-corrected into the node's absolute `timestamp` attribute, which is omitted if the time keeper isn't ready
|
|
102
|
+
* or this isn't a finite number. Supplied by VitalMetric for nodes whose `value` isn't itself the event's time
|
|
103
|
+
* (e.g. 'inp' uses the interaction time, 'cls' uses the largest shift time). Defaults to `value`.
|
|
104
|
+
* @returns {{name: string, value: number, attrs: object}} The recorded timing node.
|
|
105
|
+
*/
|
|
106
|
+
addTiming(name, value, attrs = {}, relativeTimestamp = value) {
|
|
92
107
|
attrs.pageUrl = cleanURL(getNavigationEntry()?.name || initialLocation);
|
|
93
108
|
addConnectionAttributes(attrs); // network conditions may differ from the actual for VitalMetrics when they were captured
|
|
94
109
|
|
|
@@ -104,6 +119,10 @@ export class Aggregate extends AggregateBase {
|
|
|
104
119
|
attrs.cls = cumulativeLayoutShift.current.value;
|
|
105
120
|
}
|
|
106
121
|
attrs.webdriverDetected = webdriverDetected;
|
|
122
|
+
const timeKeeper = this.agentRef.runtime.timeKeeper;
|
|
123
|
+
if (timeKeeper?.ready && Number.isFinite(relativeTimestamp)) {
|
|
124
|
+
attrs.timestamp = Math.floor(timeKeeper.correctRelativeTimestamp(relativeTimestamp));
|
|
125
|
+
}
|
|
107
126
|
const timing = {
|
|
108
127
|
name,
|
|
109
128
|
value,
|
|
@@ -134,7 +153,7 @@ export class Aggregate extends AggregateBase {
|
|
|
134
153
|
});
|
|
135
154
|
}
|
|
136
155
|
#getGlobalCustomAttributes() {
|
|
137
|
-
const reservedAttributes = ['size', 'eid', 'cls', 'type', 'fid', 'elTag', 'elUrl', 'net-type', 'net-etype', 'net-rtt', 'net-dlink', 'webdriverDetected'];
|
|
156
|
+
const reservedAttributes = ['size', 'eid', 'cls', 'type', 'fid', 'elTag', 'elUrl', 'net-type', 'net-etype', 'net-rtt', 'net-dlink', 'webdriverDetected', 'timestamp'];
|
|
138
157
|
return Object.fromEntries(Object.entries(this.agentRef.info.jsAttributes || {}).filter(([key]) => !reservedAttributes.includes(key)));
|
|
139
158
|
}
|
|
140
159
|
preHarvestChecks() {
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
3
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
-
*/
|
|
5
1
|
export class VitalMetric {
|
|
6
2
|
constructor(name: any, roundingMethod: any);
|
|
7
3
|
history: any[];
|
|
8
4
|
name: any;
|
|
9
5
|
attrs: {};
|
|
10
6
|
roundingMethod: any;
|
|
7
|
+
/**
|
|
8
|
+
* Records a new value for this metric and notifies subscribers.
|
|
9
|
+
* @param {object} params
|
|
10
|
+
* @param {number} params.value The metric's value. For most metrics this is itself a ms offset from page origin
|
|
11
|
+
* (i.e. the metric's own real time of occurrence); for score-based metrics like CLS it isn't a time at all.
|
|
12
|
+
* @param {object} [params.attrs] Metric-specific attribution attrs.
|
|
13
|
+
*/
|
|
11
14
|
update({ value, attrs }: {
|
|
12
|
-
value:
|
|
13
|
-
attrs?:
|
|
15
|
+
value: number;
|
|
16
|
+
attrs?: object | undefined;
|
|
14
17
|
}): void;
|
|
15
18
|
get current(): any;
|
|
16
19
|
get isValid(): boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vital-metric.d.ts","sourceRoot":"","sources":["../../../../src/common/vitals/vital-metric.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"vital-metric.d.ts","sourceRoot":"","sources":["../../../../src/common/vitals/vital-metric.js"],"names":[],"mappings":"AAMA;IAIE,4CAIC;IAND,eAAY;IAGV,UAAgB;IAChB,UAAe;IACf,oBAAwF;IAG1F;;;;;;OAMG;IACH,yBAJG;QAAuB,KAAK,EAApB,MAAM;QAEU,KAAK;KAC/B,QAoBA;IAED,mBAMC;IAED,uBAEC;IAED,uEAMC;;CACF"}
|
|
@@ -9,10 +9,23 @@ export class Aggregate extends AggregateBase {
|
|
|
9
9
|
* @param {number} timestamp
|
|
10
10
|
*/
|
|
11
11
|
endCurrentSession(timestamp: number): void;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Records a page view timing node, decorating it with system attributes (page URL, connection info,
|
|
14
|
+
* CLS-so-far, webdriver detection, and a server-corrected absolute timestamp) before buffering it for harvest.
|
|
15
|
+
* @param {string} name The timing node's name (e.g. 'fp', 'fcp', 'lcp', 'cls', 'fi', 'pageHide', 'unload').
|
|
16
|
+
* @param {number} value The timing node's value; a ms offset from page origin for most nodes, a duration for
|
|
17
|
+
* 'inp', and a unitless score for 'cls'.
|
|
18
|
+
* @param {object} [attrs] Additional custom attributes to attach to the node.
|
|
19
|
+
* @param {number} [relativeTimestamp] The ms offset from page origin at which the underlying event occurred. It is
|
|
20
|
+
* server-corrected into the node's absolute `timestamp` attribute, which is omitted if the time keeper isn't ready
|
|
21
|
+
* or this isn't a finite number. Supplied by VitalMetric for nodes whose `value` isn't itself the event's time
|
|
22
|
+
* (e.g. 'inp' uses the interaction time, 'cls' uses the largest shift time). Defaults to `value`.
|
|
23
|
+
* @returns {{name: string, value: number, attrs: object}} The recorded timing node.
|
|
24
|
+
*/
|
|
25
|
+
addTiming(name: string, value: number, attrs?: object, relativeTimestamp?: number): {
|
|
26
|
+
name: string;
|
|
27
|
+
value: number;
|
|
28
|
+
attrs: object;
|
|
16
29
|
};
|
|
17
30
|
/**
|
|
18
31
|
* Checks the performance API to see if the agent can set a first interaction event value
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/page_view_timing/aggregate/index.js"],"names":[],"mappings":"AA2BA;IACE,2BAAiC;IAMjC,2BAmCC;IAjCC,4BAA+B;IAC/B,0BAA6B;IAK7B,uBAA2D;IA6B7D;;;OAGG;IACH,6BAFW,MAAM,QAOhB;IAED
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/page_view_timing/aggregate/index.js"],"names":[],"mappings":"AA2BA;IACE,2BAAiC;IAMjC,2BAmCC;IAjCC,4BAA+B;IAC/B,0BAA6B;IAK7B,uBAA2D;IA6B7D;;;OAGG;IACH,6BAFW,MAAM,QAOhB;IAED;;;;;;;;;;;;OAYG;IACH,gBAVW,MAAM,SACN,MAAM,UAEN,MAAM,sBACN,MAAM,GAIJ;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,CAuCxD;IAED;;;OAGG;IACH,4BAFa,IAAI,CAahB;IAWD,4BAGC;IAGD,qCA8BC;;CACF;8BA3L6B,4BAA4B;2BAC/B,gCAAgC"}
|
package/package.json
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
|
+
import { now } from '../timing/now'
|
|
6
|
+
|
|
5
7
|
export class VitalMetric {
|
|
6
8
|
#subscribers = new Set()
|
|
7
9
|
history = []
|
|
@@ -12,13 +14,22 @@ export class VitalMetric {
|
|
|
12
14
|
this.roundingMethod = typeof roundingMethod === 'function' ? roundingMethod : Math.floor
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Records a new value for this metric and notifies subscribers.
|
|
19
|
+
* @param {object} params
|
|
20
|
+
* @param {number} params.value The metric's value. For most metrics this is itself a ms offset from page origin
|
|
21
|
+
* (i.e. the metric's own real time of occurrence); for score-based metrics like CLS it isn't a time at all.
|
|
22
|
+
* @param {object} [params.attrs] Metric-specific attribution attrs.
|
|
23
|
+
*/
|
|
15
24
|
update ({ value, attrs = {} }) {
|
|
16
25
|
if (value === undefined || value === null || value < 0) return
|
|
26
|
+
const metricTimestamps = { inp: attrs.interactionTime ?? now(), cls: attrs.largestShiftTime ?? now() }
|
|
17
27
|
const state = {
|
|
18
28
|
value: this.roundingMethod(value),
|
|
19
29
|
name: this.name,
|
|
20
30
|
// optional web-vitals attribution fields are omitted rather than being serialized as null attributes
|
|
21
|
-
attrs: Object.fromEntries(Object.entries(attrs).filter(([, attrValue]) => attrValue !== undefined))
|
|
31
|
+
attrs: Object.fromEntries(Object.entries(attrs).filter(([, attrValue]) => attrValue !== undefined)),
|
|
32
|
+
relativeTimestamp: metricTimestamps[this.name] ?? value
|
|
22
33
|
}
|
|
23
34
|
|
|
24
35
|
this.history.push(state)
|
|
@@ -28,8 +28,8 @@ import { createStringAdders } from '../../../common/payloads/payloads'
|
|
|
28
28
|
export class Aggregate extends AggregateBase {
|
|
29
29
|
static featureName = FEATURE_NAME
|
|
30
30
|
|
|
31
|
-
#handleVitalMetric = ({ name, value, attrs }) => {
|
|
32
|
-
this.addTiming(name, value, attrs)
|
|
31
|
+
#handleVitalMetric = ({ name, value, attrs, relativeTimestamp }) => {
|
|
32
|
+
this.addTiming(name, value, attrs, relativeTimestamp)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
constructor (agentRef) {
|
|
@@ -44,7 +44,7 @@ export class Aggregate extends AggregateBase {
|
|
|
44
44
|
|
|
45
45
|
registerHandler('docHidden', msTimestamp => this.endCurrentSession(msTimestamp), this.featureName, this.ee)
|
|
46
46
|
// Add the time of _window pagehide event_ firing to the next PVT harvest == NRDB windowUnload attr:
|
|
47
|
-
registerHandler('winPagehide', msTimestamp => this.addTiming('unload', msTimestamp
|
|
47
|
+
registerHandler('winPagehide', msTimestamp => this.addTiming('unload', msTimestamp), this.featureName, this.ee)
|
|
48
48
|
|
|
49
49
|
this.waitForFlags(([])).then(() => {
|
|
50
50
|
firstPaint.subscribe(this.#handleVitalMetric)
|
|
@@ -56,9 +56,9 @@ export class Aggregate extends AggregateBase {
|
|
|
56
56
|
/* Downstream, the event consumer interprets all timing node value as ms-unit and converts it to seconds via division by 1000. CLS is unitless so this normally is a problem.
|
|
57
57
|
bel.6 schema also doesn't support decimal values, of which cls within [0,1). However, the two nicely cancels out, and we can multiply cls by 1000 to both negate the division
|
|
58
58
|
and send an integer > 1. We effectively lose some precision down to 3 decimal places for this workaround. E.g. (real) 0.749132... -> 749.132...-> 749 -> 0.749 (final) */
|
|
59
|
-
const { name, value, attrs } = cumulativeLayoutShift.current
|
|
59
|
+
const { name, value, attrs, relativeTimestamp } = cumulativeLayoutShift.current
|
|
60
60
|
if (value === undefined) return
|
|
61
|
-
this.addTiming(name, value * 1000, attrs)
|
|
61
|
+
this.addTiming(name, value * 1000, attrs, relativeTimestamp)
|
|
62
62
|
}, true, true) // CLS node should only report on vis change rather than on every change.
|
|
63
63
|
/* NOTE: the capture=true is required -- empirically verified against a real browser (Chrome) for CLS
|
|
64
64
|
timing node to reliably be in the same final harvest as the rest, alongside Harvester#startTimer's
|
|
@@ -75,13 +75,25 @@ export class Aggregate extends AggregateBase {
|
|
|
75
75
|
*/
|
|
76
76
|
endCurrentSession (timestamp) {
|
|
77
77
|
if (!this.curSessEndRecorded) { // TO DO: stage 2 - we don't want to capture this timing twice on page navigating away, but it should run again if we return to page and away *again*
|
|
78
|
-
this.addTiming('pageHide', timestamp
|
|
78
|
+
this.addTiming('pageHide', timestamp)
|
|
79
79
|
this.curSessEndRecorded = true
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Records a page view timing node, decorating it with system attributes (page URL, connection info,
|
|
85
|
+
* CLS-so-far, webdriver detection, and a server-corrected absolute timestamp) before buffering it for harvest.
|
|
86
|
+
* @param {string} name The timing node's name (e.g. 'fp', 'fcp', 'lcp', 'cls', 'fi', 'pageHide', 'unload').
|
|
87
|
+
* @param {number} value The timing node's value; a ms offset from page origin for most nodes, a duration for
|
|
88
|
+
* 'inp', and a unitless score for 'cls'.
|
|
89
|
+
* @param {object} [attrs] Additional custom attributes to attach to the node.
|
|
90
|
+
* @param {number} [relativeTimestamp] The ms offset from page origin at which the underlying event occurred. It is
|
|
91
|
+
* server-corrected into the node's absolute `timestamp` attribute, which is omitted if the time keeper isn't ready
|
|
92
|
+
* or this isn't a finite number. Supplied by VitalMetric for nodes whose `value` isn't itself the event's time
|
|
93
|
+
* (e.g. 'inp' uses the interaction time, 'cls' uses the largest shift time). Defaults to `value`.
|
|
94
|
+
* @returns {{name: string, value: number, attrs: object}} The recorded timing node.
|
|
95
|
+
*/
|
|
96
|
+
addTiming (name, value, attrs = {}, relativeTimestamp = value) {
|
|
85
97
|
attrs.pageUrl = cleanURL(getNavigationEntry()?.name || initialLocation)
|
|
86
98
|
|
|
87
99
|
addConnectionAttributes(attrs) // network conditions may differ from the actual for VitalMetrics when they were captured
|
|
@@ -100,6 +112,11 @@ export class Aggregate extends AggregateBase {
|
|
|
100
112
|
|
|
101
113
|
attrs.webdriverDetected = webdriverDetected
|
|
102
114
|
|
|
115
|
+
const timeKeeper = this.agentRef.runtime.timeKeeper
|
|
116
|
+
if (timeKeeper?.ready && Number.isFinite(relativeTimestamp)) {
|
|
117
|
+
attrs.timestamp = Math.floor(timeKeeper.correctRelativeTimestamp(relativeTimestamp))
|
|
118
|
+
}
|
|
119
|
+
|
|
103
120
|
const timing = {
|
|
104
121
|
name,
|
|
105
122
|
value,
|
|
@@ -134,7 +151,7 @@ export class Aggregate extends AggregateBase {
|
|
|
134
151
|
|
|
135
152
|
#getGlobalCustomAttributes () {
|
|
136
153
|
const reservedAttributes = ['size', 'eid', 'cls', 'type', 'fid', 'elTag', 'elUrl', 'net-type',
|
|
137
|
-
'net-etype', 'net-rtt', 'net-dlink', 'webdriverDetected']
|
|
154
|
+
'net-etype', 'net-rtt', 'net-dlink', 'webdriverDetected', 'timestamp']
|
|
138
155
|
|
|
139
156
|
return Object.fromEntries(
|
|
140
157
|
Object.entries(this.agentRef.info.jsAttributes || {}).filter(([key]) => !reservedAttributes.includes(key))
|