@newrelic/browser-agent 1.320.0 → 1.320.1-rc.1
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/CHANGELOG.md +8 -0
- package/dist/cjs/common/config/runtime.js +1 -0
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/event-listener/event-listener-opts.js +1 -1
- package/dist/cjs/common/harvest/connector.js +200 -0
- package/dist/cjs/common/harvest/harvester.js +5 -8
- package/dist/cjs/common/harvest/send.js +8 -7
- package/dist/cjs/common/payloads/payloads.js +3 -2
- package/dist/cjs/common/serialize/bel-serializer.js +17 -4
- package/dist/cjs/common/timer/interaction-timer.js +2 -2
- package/dist/cjs/common/unload/eol.js +3 -3
- package/dist/cjs/common/util/console.js +2 -1
- package/dist/cjs/common/util/feature-flags.js +2 -1
- package/dist/cjs/common/v2/mfe-vitals.js +34 -11
- package/dist/cjs/common/window/page-visibility.js +1 -1
- package/dist/cjs/features/ajax/aggregate/index.js +19 -8
- package/dist/cjs/features/page_view_event/aggregate/index.js +2 -6
- package/dist/cjs/features/page_view_event/aggregate_v2/index.js +109 -0
- package/dist/cjs/features/page_view_event/instrument/index.js +6 -1
- package/dist/cjs/features/page_view_timing/aggregate/index.js +33 -14
- package/dist/cjs/features/session_replay/instrument/index.js +9 -0
- package/dist/cjs/features/session_replay/shared/recorder.js +2 -1
- package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +16 -4
- package/dist/cjs/features/soft_navigations/aggregate/interaction.js +10 -3
- package/dist/cjs/features/utils/aggregate-base.js +0 -12
- package/dist/cjs/features/utils/instrument-base.js +72 -29
- package/dist/cjs/loaders/agent.js +2 -1
- package/dist/cjs/loaders/api/consent.js +2 -2
- package/dist/cjs/loaders/features/features.js +2 -1
- package/dist/esm/common/config/runtime.js +1 -0
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/event-listener/event-listener-opts.js +1 -1
- package/dist/esm/common/harvest/connector.js +193 -0
- package/dist/esm/common/harvest/harvester.js +5 -8
- package/dist/esm/common/harvest/send.js +10 -9
- package/dist/esm/common/payloads/payloads.js +3 -2
- package/dist/esm/common/serialize/bel-serializer.js +17 -4
- package/dist/esm/common/timer/interaction-timer.js +2 -2
- package/dist/esm/common/unload/eol.js +3 -3
- package/dist/esm/common/util/console.js +2 -1
- package/dist/esm/common/util/feature-flags.js +2 -1
- package/dist/esm/common/v2/mfe-vitals.js +34 -11
- package/dist/esm/common/window/page-visibility.js +1 -1
- package/dist/esm/features/ajax/aggregate/index.js +19 -8
- package/dist/esm/features/page_view_event/aggregate/index.js +2 -6
- package/dist/esm/features/page_view_event/aggregate_v2/index.js +100 -0
- package/dist/esm/features/page_view_event/instrument/index.js +6 -1
- package/dist/esm/features/page_view_timing/aggregate/index.js +33 -14
- package/dist/esm/features/session_replay/instrument/index.js +9 -0
- package/dist/esm/features/session_replay/shared/recorder.js +2 -1
- package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +16 -4
- package/dist/esm/features/soft_navigations/aggregate/interaction.js +10 -3
- package/dist/esm/features/utils/aggregate-base.js +0 -12
- package/dist/esm/features/utils/instrument-base.js +71 -28
- package/dist/esm/loaders/agent.js +2 -1
- package/dist/esm/loaders/api/consent.js +2 -2
- package/dist/esm/loaders/features/features.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/common/config/runtime.d.ts.map +1 -1
- package/dist/types/common/event-listener/event-listener-opts.d.ts +1 -1
- package/dist/types/common/harvest/connector.d.ts +6 -0
- package/dist/types/common/harvest/connector.d.ts.map +1 -0
- package/dist/types/common/harvest/harvester.d.ts.map +1 -1
- package/dist/types/common/harvest/send.d.ts.map +1 -1
- package/dist/types/common/payloads/payloads.d.ts +3 -2
- package/dist/types/common/payloads/payloads.d.ts.map +1 -1
- package/dist/types/common/serialize/bel-serializer.d.ts +9 -1
- package/dist/types/common/serialize/bel-serializer.d.ts.map +1 -1
- package/dist/types/common/unload/eol.d.ts +1 -1
- package/dist/types/common/unload/eol.d.ts.map +1 -1
- package/dist/types/common/util/console.d.ts +2 -1
- package/dist/types/common/util/console.d.ts.map +1 -1
- package/dist/types/common/v2/mfe-vitals.d.ts.map +1 -1
- package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/page_view_event/aggregate_v2/index.d.ts +20 -0
- package/dist/types/features/page_view_event/aggregate_v2/index.d.ts.map +1 -0
- package/dist/types/features/page_view_event/instrument/index.d.ts.map +1 -1
- package/dist/types/features/page_view_timing/aggregate/index.d.ts +0 -1
- package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/instrument/index.d.ts +1 -0
- package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
- package/dist/types/features/utils/aggregate-base.d.ts +0 -5
- package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
- package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
- package/dist/types/loaders/agent.d.ts.map +1 -1
- package/dist/types/loaders/features/features.d.ts +1 -0
- package/dist/types/loaders/features/features.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/common/config/runtime.js +1 -0
- package/src/common/event-listener/event-listener-opts.js +1 -1
- package/src/common/harvest/connector.js +168 -0
- package/src/common/harvest/harvester.js +7 -9
- package/src/common/harvest/send.js +9 -7
- package/src/common/payloads/payloads.js +3 -2
- package/src/common/serialize/bel-serializer.js +13 -4
- package/src/common/timer/interaction-timer.js +2 -2
- package/src/common/unload/eol.js +3 -3
- package/src/common/util/console.js +2 -1
- package/src/common/util/feature-flags.js +1 -1
- package/src/common/v2/mfe-vitals.js +39 -12
- package/src/common/window/page-visibility.js +1 -1
- package/src/features/ajax/aggregate/index.js +12 -9
- package/src/features/page_view_event/aggregate/index.js +2 -6
- package/src/features/page_view_event/aggregate_v2/index.js +97 -0
- package/src/features/page_view_event/instrument/index.js +6 -1
- package/src/features/page_view_timing/aggregate/index.js +24 -15
- package/src/features/session_replay/instrument/index.js +9 -0
- package/src/features/session_replay/shared/recorder.js +2 -1
- package/src/features/soft_navigations/aggregate/ajax-node.js +10 -7
- package/src/features/soft_navigations/aggregate/interaction.js +6 -5
- package/src/features/utils/aggregate-base.js +0 -12
- package/src/features/utils/instrument-base.js +71 -25
- package/src/loaders/agent.js +2 -1
- package/src/loaders/api/consent.js +2 -2
- package/src/loaders/features/features.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [1.320.1](https://github.com/newrelic/newrelic-browser-agent/compare/v1.320.0...v1.320.1) (2026-08-12)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* Apply hard cap to MFE FCP ([#1828](https://github.com/newrelic/newrelic-browser-agent/issues/1828)) ([095a1d6](https://github.com/newrelic/newrelic-browser-agent/commit/095a1d66a917b5044f64757f61ee57607b2879ae))
|
|
12
|
+
* Avoid obfuscating keys and reserved system vals ([#1817](https://github.com/newrelic/newrelic-browser-agent/issues/1817)) ([1f19198](https://github.com/newrelic/newrelic-browser-agent/commit/1f1919809498e6aa8f1fc2ada0c00c3872cb30b8))
|
|
13
|
+
|
|
6
14
|
## [1.320.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.319.0...v1.320.0) (2026-08-07)
|
|
7
15
|
|
|
8
16
|
|
|
@@ -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.320.
|
|
20
|
+
const VERSION = exports.VERSION = "1.320.1-rc.1";
|
|
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.320.
|
|
20
|
+
const VERSION = exports.VERSION = "1.320.1-rc.1";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Exposes the build type of the agent
|
|
@@ -7,7 +7,7 @@ exports.documentAddEventListener = documentAddEventListener;
|
|
|
7
7
|
exports.eventListenerOpts = eventListenerOpts;
|
|
8
8
|
exports.windowAddEventListener = windowAddEventListener;
|
|
9
9
|
/**
|
|
10
|
-
* Copyright 2020-
|
|
10
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
11
11
|
* SPDX-License-Identifier: Apache-2.0
|
|
12
12
|
*/
|
|
13
13
|
function eventListenerOpts(useCapture, abortSignal) {
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.Connector = void 0;
|
|
7
|
+
var _runtime = require("../constants/runtime");
|
|
8
|
+
var _send = require("./send");
|
|
9
|
+
var _timeKeeper = require("../timing/time-keeper");
|
|
10
|
+
var _featureFlags = require("../util/feature-flags");
|
|
11
|
+
var _now = require("../timing/now");
|
|
12
|
+
var _console = require("../util/console");
|
|
13
|
+
var _env = require("../constants/env.npm");
|
|
14
|
+
var _features = require("../../loaders/features/features");
|
|
15
|
+
var _handle = require("../event-emitter/handle");
|
|
16
|
+
var _constants = require("../../features/metrics/constants");
|
|
17
|
+
/**
|
|
18
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
19
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const MAX_CONNECT_ATTEMPTS = 3;
|
|
23
|
+
const CONNECT_RETRY_BASE_MS = 3000; // affects each retry window proportionally (attempt 2: 0-3s, 3: 0-6s, 4: 0-12s, etc)
|
|
24
|
+
|
|
25
|
+
class Connector {
|
|
26
|
+
#agentRef;
|
|
27
|
+
#connectStartTime;
|
|
28
|
+
#numOfAttempts = 0;
|
|
29
|
+
constructor(agentRef) {
|
|
30
|
+
if (!agentRef.init.feature_flags.includes('rum_v2')) return; // the Connector does not run in v1; legacy PVE has its own BCS handling
|
|
31
|
+
this.#agentRef = agentRef;
|
|
32
|
+
agentRef.runtime.timeKeeper = new _timeKeeper.TimeKeeper(agentRef.runtime.session);
|
|
33
|
+
if (_runtime.isBrowserScope) {
|
|
34
|
+
const cached = agentRef.runtime.session && agentRef.runtime.session.state.cachedRumResponse;
|
|
35
|
+
if (cached) {
|
|
36
|
+
// `cachedRumResponse` is stored flat (`{ app, ...flags }`, matching the old v1 RUM response), so re-nest it before applying.
|
|
37
|
+
const {
|
|
38
|
+
app,
|
|
39
|
+
...config
|
|
40
|
+
} = cached;
|
|
41
|
+
this.#applyConnectResponse({
|
|
42
|
+
app,
|
|
43
|
+
config
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
this.makeConnectRequest();
|
|
49
|
+
}
|
|
50
|
+
makeConnectRequest(retryHeaders) {
|
|
51
|
+
this.#connectStartTime = (0, _now.now)();
|
|
52
|
+
return (0, _send.send)(this.#agentRef, {
|
|
53
|
+
endpoint: "browser/connect/2/".concat(this.#agentRef.info.licenseKey),
|
|
54
|
+
payload: {
|
|
55
|
+
body: {},
|
|
56
|
+
qs: {
|
|
57
|
+
a: this.#agentRef.info.applicationID,
|
|
58
|
+
v: _env.VERSION,
|
|
59
|
+
s: this.#agentRef.runtime.session?.state.value || '0'
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
localOpts: {
|
|
63
|
+
sendEmptyBody: true,
|
|
64
|
+
headers: retryHeaders
|
|
65
|
+
},
|
|
66
|
+
raw: true,
|
|
67
|
+
featureName: _features.CONNECT,
|
|
68
|
+
cbFinished: this.#processConnectResponse.bind(this)
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
#applyConnectResponse({
|
|
72
|
+
app,
|
|
73
|
+
config
|
|
74
|
+
}) {
|
|
75
|
+
if (!Object.keys(this.#agentRef.runtime.appMetadata).length) this.#agentRef.runtime.appMetadata = app;
|
|
76
|
+
(0, _featureFlags.activateFeatures)(config, this.#agentRef);
|
|
77
|
+
}
|
|
78
|
+
#processConnectResponse({
|
|
79
|
+
sent,
|
|
80
|
+
status,
|
|
81
|
+
retry,
|
|
82
|
+
xhr,
|
|
83
|
+
responseText
|
|
84
|
+
}) {
|
|
85
|
+
const connectEndTime = (0, _now.now)();
|
|
86
|
+
const session = this.#agentRef.runtime.session;
|
|
87
|
+
if (session) {
|
|
88
|
+
// Double check that there isn't a response saved (e.g. from another agent/browser tab) btwn the async time of the request and now in a race-condition.
|
|
89
|
+
const cachedResp = session.state.cachedRumResponse;
|
|
90
|
+
if (cachedResp) {
|
|
91
|
+
// `cachedRumResponse` is stored flat (`{ app, ...flags }`, matching the old v1 RUM response), so re-nest it before applying.
|
|
92
|
+
const {
|
|
93
|
+
app,
|
|
94
|
+
...config
|
|
95
|
+
} = cachedResp;
|
|
96
|
+
this.#applyConnectResponse({
|
|
97
|
+
app,
|
|
98
|
+
config
|
|
99
|
+
});
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const shouldRetry = sent && retry; // important to note: no retry if HTTP status is 0
|
|
104
|
+
if (shouldRetry && ++this.#numOfAttempts < MAX_CONNECT_ATTEMPTS) {
|
|
105
|
+
// We use an exponential backoff calculation for the retry to avoid thundering herd, skipping any further processing.
|
|
106
|
+
const delay = Math.floor(Math.random() * CONNECT_RETRY_BASE_MS * 2 ** (this.#numOfAttempts - 1));
|
|
107
|
+
const retryHeaders = [{
|
|
108
|
+
key: 'X-Retry-Count',
|
|
109
|
+
value: this.#numOfAttempts
|
|
110
|
+
}, {
|
|
111
|
+
key: 'X-Previous-Status',
|
|
112
|
+
value: status
|
|
113
|
+
}];
|
|
114
|
+
setTimeout(() => this.makeConnectRequest(retryHeaders), delay);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (status >= 400 || status === 0) {
|
|
118
|
+
(0, _console.warn)(18, status);
|
|
119
|
+
|
|
120
|
+
// Get estimated payload size of our backlog.
|
|
121
|
+
const textEncoder = new TextEncoder();
|
|
122
|
+
const payloadSize = Object.values(newrelic.ee.backlog).reduce((acc, value) => {
|
|
123
|
+
if (!value) return acc;
|
|
124
|
+
const encoded = textEncoder.encode(value);
|
|
125
|
+
return acc + encoded.byteLength;
|
|
126
|
+
}, 0);
|
|
127
|
+
|
|
128
|
+
// Send SMs about the (last) failed connect request, then abort agent.
|
|
129
|
+
const BCSError = 'BCS/Error/';
|
|
130
|
+
const sm = [{
|
|
131
|
+
params: {
|
|
132
|
+
name: BCSError + status
|
|
133
|
+
},
|
|
134
|
+
stats: {
|
|
135
|
+
c: 1
|
|
136
|
+
}
|
|
137
|
+
}, {
|
|
138
|
+
params: {
|
|
139
|
+
name: BCSError + 'Duration/Ms'
|
|
140
|
+
},
|
|
141
|
+
stats: {
|
|
142
|
+
c: 1,
|
|
143
|
+
t: connectEndTime - this.#connectStartTime
|
|
144
|
+
}
|
|
145
|
+
}, {
|
|
146
|
+
params: {
|
|
147
|
+
name: BCSError + 'Dropped/Bytes'
|
|
148
|
+
},
|
|
149
|
+
stats: {
|
|
150
|
+
c: 1,
|
|
151
|
+
t: payloadSize
|
|
152
|
+
}
|
|
153
|
+
}];
|
|
154
|
+
(0, _send.send)(this.#agentRef, {
|
|
155
|
+
// note we have to send it this way because the Metrics feature will never be harvested under this branch of code (BCS failure)
|
|
156
|
+
endpoint: _features.FEATURE_TO_ENDPOINT[_features.FEATURE_NAMES.metrics],
|
|
157
|
+
payload: {
|
|
158
|
+
body: {
|
|
159
|
+
sm
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
featureName: _features.FEATURE_NAMES.metrics
|
|
163
|
+
});
|
|
164
|
+
this.#agentRef.ee.abort();
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
let resp;
|
|
168
|
+
try {
|
|
169
|
+
resp = JSON.parse(responseText);
|
|
170
|
+
} catch (error) {
|
|
171
|
+
(0, _console.warn)(53, error);
|
|
172
|
+
this.#agentRef.ee.abort();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// store the response for other pages later in same session or in parallel tabs; flatten `.config` into the old v1 RUM response shape
|
|
176
|
+
if (session) session.write({
|
|
177
|
+
cachedRumResponse: {
|
|
178
|
+
app: resp.app,
|
|
179
|
+
...resp.config
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
try {
|
|
183
|
+
const wasReady = this.#agentRef.runtime.timeKeeper.ready;
|
|
184
|
+
this.#agentRef.runtime.timeKeeper.processRumRequest(xhr, this.#connectStartTime, connectEndTime, resp.app.nrServerTime);
|
|
185
|
+
if (!this.#agentRef.runtime.timeKeeper.ready) throw new Error('TimeKeeper not ready');
|
|
186
|
+
|
|
187
|
+
// If timeKeeper's origin time is ahead of nrServerTime, then the timestamp is invalid. Report a supportability metric.
|
|
188
|
+
const timeDiff = this.#agentRef.runtime.timeKeeper.correctedOriginTime - resp.app.nrServerTime;
|
|
189
|
+
if (wasReady && timeDiff > 0) {
|
|
190
|
+
(0, _handle.handle)(_constants.SUPPORTABILITY_METRIC_CHANNEL, ['Generic/TimeKeeper/InvalidTimestamp/Seen', timeDiff], undefined, _features.FEATURE_NAMES.metrics, this.#agentRef.ee);
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
(0, _console.warn)(17, error);
|
|
194
|
+
this.#agentRef.ee.abort();
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
this.#applyConnectResponse(resp);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
exports.Connector = Connector;
|
|
@@ -26,11 +26,13 @@ class Harvester {
|
|
|
26
26
|
constructor(agentRef) {
|
|
27
27
|
this.agentRef = agentRef;
|
|
28
28
|
|
|
29
|
-
// Create obfuscator for harvest metadata (referrer URL, etc.)
|
|
30
29
|
// No event type specified - applies to all harvests regardless of feature
|
|
31
30
|
this.obfuscator = new _obfuscate.Obfuscator(agentRef);
|
|
31
|
+
}
|
|
32
|
+
startTimer(harvestInterval = this.agentRef.init.harvest.interval) {
|
|
33
|
+
if (this.#started) return;
|
|
34
|
+
this.#started = true;
|
|
32
35
|
(0, _eol.subscribeToEOL)(() => {
|
|
33
|
-
// do one last harvest round or check
|
|
34
36
|
this.initializedAggregates.forEach(aggregateInst => {
|
|
35
37
|
// let all features wrap up things needed to do before ANY harvest in case there's last minute cross-feature data dependencies
|
|
36
38
|
if (typeof aggregateInst.harvestOpts.beforeUnload === 'function') aggregateInst.harvestOpts.beforeUnload();
|
|
@@ -38,12 +40,7 @@ class Harvester {
|
|
|
38
40
|
this.initializedAggregates.forEach(aggregateInst => this.triggerHarvestFor(aggregateInst, {
|
|
39
41
|
isFinalHarvest: true
|
|
40
42
|
}));
|
|
41
|
-
|
|
42
|
-
}, false);
|
|
43
|
-
}
|
|
44
|
-
startTimer(harvestInterval = this.agentRef.init.harvest.interval) {
|
|
45
|
-
if (this.#started) return;
|
|
46
|
-
this.#started = true;
|
|
43
|
+
});
|
|
47
44
|
const onHarvestInterval = () => {
|
|
48
45
|
this.initializedAggregates.forEach(aggregateInst => this.triggerHarvestFor(aggregateInst));
|
|
49
46
|
setTimeout(onHarvestInterval, harvestInterval * 1000); // repeat in X seconds
|
|
@@ -37,7 +37,7 @@ function send(agentRef, {
|
|
|
37
37
|
endpoint,
|
|
38
38
|
payload,
|
|
39
39
|
localOpts = {},
|
|
40
|
-
submitMethod,
|
|
40
|
+
submitMethod = (0, _submitData.getSubmitMethod)(),
|
|
41
41
|
cbFinished,
|
|
42
42
|
raw,
|
|
43
43
|
featureName,
|
|
@@ -58,13 +58,15 @@ function send(agentRef, {
|
|
|
58
58
|
}
|
|
59
59
|
const protocol = agentRef.init.ssl === false ? 'http' : 'https';
|
|
60
60
|
const perceivedBeacon = agentRef.init.proxy.beacon || agentRef.info.errorBeacon;
|
|
61
|
-
const
|
|
61
|
+
const includeEndpointName = endpoint !== _features.RUM || endpointVersion !== 1;
|
|
62
|
+
const url = raw ? "".concat(protocol, "://").concat(perceivedBeacon, "/").concat(endpoint) : "".concat(protocol, "://").concat(perceivedBeacon).concat(includeEndpointName ? '/' + endpoint : '', "/").concat(endpointVersion, "/").concat(agentRef.info.licenseKey);
|
|
62
63
|
const baseParams = !raw ? baseQueryString(agentRef, qs, endpoint, harvesterObfuscator) : '';
|
|
63
64
|
let payloadParams = (0, _encode.obj)(qs, agentRef.runtime.maxBytes);
|
|
64
65
|
if (baseParams === '' && payloadParams.startsWith('&')) {
|
|
65
66
|
payloadParams = payloadParams.substring(1);
|
|
66
67
|
}
|
|
67
|
-
const
|
|
68
|
+
const queryString = "".concat(baseParams).concat(payloadParams);
|
|
69
|
+
const fullUrl = queryString ? "".concat(url, "?").concat(queryString) : url; // omit the '?' if there are no query params
|
|
68
70
|
const gzip = !!qs?.attributes?.includes('gzip');
|
|
69
71
|
|
|
70
72
|
// all gzipped data is already in the correct format and needs no transformation
|
|
@@ -79,7 +81,7 @@ function send(agentRef, {
|
|
|
79
81
|
const headers = [{
|
|
80
82
|
key: 'content-type',
|
|
81
83
|
value: 'text/plain'
|
|
82
|
-
}];
|
|
84
|
+
}, ...(localOpts.headers || [])];
|
|
83
85
|
|
|
84
86
|
/* Since workers don't support sendBeacon right now, they can only use XHR method.
|
|
85
87
|
Because they still do permit synch XHR, the idea is that at final harvest time (worker is closing),
|
|
@@ -128,6 +130,7 @@ function send(agentRef, {
|
|
|
128
130
|
}
|
|
129
131
|
function trackHarvestMetadata() {
|
|
130
132
|
try {
|
|
133
|
+
if (featureName === _features.CONNECT) return;
|
|
131
134
|
if (featureName === _features.FEATURE_NAMES.jserrors && !body?.err) return;
|
|
132
135
|
const hasReplay = baseParams.includes('hr=1');
|
|
133
136
|
const hasTrace = baseParams.includes('ht=1');
|
|
@@ -213,9 +216,7 @@ function baseQueryString(agentRef, qs, endpoint, harvesterObfuscator) {
|
|
|
213
216
|
const session = agentRef.runtime.session;
|
|
214
217
|
const hr = !!session?.state.sessionReplaySentFirstChunk && session?.state.sessionReplayMode === 1 && endpoint !== _features.JSERRORS;
|
|
215
218
|
const ht = !!session?.state.traceHarvestStarted && session?.state.sessionTraceMode === 1 && ![_features.LOGS, _features.BLOBS].includes(endpoint);
|
|
216
|
-
const qps = ['a=' + agentRef.info.applicationID, (0, _encode.param)('sa', agentRef.info.sa ? '' + agentRef.info.sa : ''), (0, _encode.param)('v', _env.VERSION), transactionNameParam(), (0, _encode.param)('ct', agentRef.runtime.customTransaction), '&rst=' + (0, _now.now)(), '&
|
|
217
|
-
// ck param DEPRECATED - still expected by backend
|
|
218
|
-
'&s=' + (session?.state.value || '0'),
|
|
219
|
+
const qps = ['a=' + agentRef.info.applicationID, (0, _encode.param)('sa', agentRef.info.sa ? '' + agentRef.info.sa : ''), (0, _encode.param)('v', _env.VERSION), transactionNameParam(), (0, _encode.param)('ct', agentRef.runtime.customTransaction), '&rst=' + (0, _now.now)(), '&s=' + (session?.state.value || '0'),
|
|
219
220
|
// the 0 id encaps all untrackable and default traffic
|
|
220
221
|
(0, _encode.param)('ref', ref), (0, _encode.param)('ptid', agentRef.runtime.ptid ? '' + agentRef.runtime.ptid : '')];
|
|
221
222
|
if (hr) qps.push((0, _encode.param)('hr', '1', qs));
|
|
@@ -123,11 +123,11 @@ function truncateAsString(data) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
* Creates string adder functions for BEL serialization with obfuscation and optional truncation.
|
|
126
|
+
* Creates string adder functions for BEL serialization with obfuscation and optional truncation, as well as raw string.
|
|
127
127
|
* This ensures a single string table is used while providing separate handling for regular vs payload attributes.
|
|
128
128
|
* @param {Function} getAddStringContext - Function that creates a new string table context
|
|
129
129
|
* @param {Object} obfuscator - Optional obfuscator instance for string obfuscation
|
|
130
|
-
* @returns {{addString: Function, addStringWithTruncation: Function}} Object containing
|
|
130
|
+
* @returns {{addString: Function, addStringRaw: Function, addStringWithTruncation: Function}} Object containing various string adder functions
|
|
131
131
|
*/
|
|
132
132
|
function createStringAdders(getAddStringContext, obfuscator) {
|
|
133
133
|
const addStringRaw = getAddStringContext();
|
|
@@ -140,6 +140,7 @@ function createStringAdders(getAddStringContext, obfuscator) {
|
|
|
140
140
|
};
|
|
141
141
|
return {
|
|
142
142
|
addString: str => processString(str, false),
|
|
143
|
+
addStringRaw: str => addStringRaw(str),
|
|
143
144
|
addStringWithTruncation: str => processString(str, true)
|
|
144
145
|
};
|
|
145
146
|
}
|
|
@@ -40,19 +40,32 @@ function getAddStringContext(obfuscator, truncator) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Attributes are serialized according to their types, using the provided serializers. Only the first 64 attributes will be added to the payload.
|
|
46
|
+
* @param attrs
|
|
47
|
+
* @param serializers
|
|
48
|
+
* @param {function} serializers.addKey -function for serializing attribute keys
|
|
49
|
+
* @param {function} serializers.addVal - function for serializing attribute values
|
|
50
|
+
* @returns {*[]}
|
|
51
|
+
*/
|
|
52
|
+
function addCustomAttributes(attrs, serializers) {
|
|
53
|
+
const {
|
|
54
|
+
addKey,
|
|
55
|
+
addVal
|
|
56
|
+
} = serializers;
|
|
44
57
|
var attrParts = [];
|
|
45
58
|
Object.entries(attrs || {}).forEach(([key, val]) => {
|
|
46
59
|
if (attrParts.length >= MAX_ATTRIBUTES) return;
|
|
47
60
|
var type = 5;
|
|
48
61
|
var serializedValue;
|
|
49
62
|
// add key to string table first
|
|
50
|
-
key =
|
|
63
|
+
key = addKey(key);
|
|
51
64
|
switch (typeof val) {
|
|
52
65
|
case 'object':
|
|
53
66
|
if (val) {
|
|
54
67
|
// serialize objects to strings
|
|
55
|
-
serializedValue =
|
|
68
|
+
serializedValue = addVal((0, _stringify.stringify)(val));
|
|
56
69
|
} else {
|
|
57
70
|
// null attribute type
|
|
58
71
|
type = 9;
|
|
@@ -71,7 +84,7 @@ function addCustomAttributes(attrs, addString) {
|
|
|
71
84
|
type = 9;
|
|
72
85
|
break;
|
|
73
86
|
default:
|
|
74
|
-
serializedValue =
|
|
87
|
+
serializedValue = addVal(val);
|
|
75
88
|
}
|
|
76
89
|
attrParts.push([type, key + (serializedValue ? ',' + serializedValue : '')]);
|
|
77
90
|
});
|
|
@@ -9,7 +9,7 @@ var _pageVisibility = require("../window/page-visibility");
|
|
|
9
9
|
var _invoke = require("../util/invoke");
|
|
10
10
|
var _runtime = require("../constants/runtime");
|
|
11
11
|
/**
|
|
12
|
-
* Copyright 2020-
|
|
12
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
13
13
|
* SPDX-License-Identifier: Apache-2.0
|
|
14
14
|
*/
|
|
15
15
|
|
|
@@ -59,7 +59,7 @@ class InteractionTimer extends _timer.Timer {
|
|
|
59
59
|
if (state === 'hidden') this.pause();
|
|
60
60
|
// vis change --> visible is treated like a new interaction with the page
|
|
61
61
|
else this.resume();
|
|
62
|
-
}, false,
|
|
62
|
+
}, false, this.abortController?.signal);
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
abort() {
|
|
@@ -7,7 +7,7 @@ exports.subscribeToEOL = subscribeToEOL;
|
|
|
7
7
|
var _runtime = require("../constants/runtime");
|
|
8
8
|
var _pageVisibility = require("../window/page-visibility");
|
|
9
9
|
/**
|
|
10
|
-
* Copyright 2020-
|
|
10
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
11
11
|
* SPDX-License-Identifier: Apache-2.0
|
|
12
12
|
*/
|
|
13
13
|
|
|
@@ -29,9 +29,9 @@ if (_runtime.isWorkerScope) {
|
|
|
29
29
|
* This is used, for example, to submit a final harvest and send all remaining data on best-effort.
|
|
30
30
|
* @param {function} cb - func to run before or during the last reliable event or time of an env's life span
|
|
31
31
|
*/
|
|
32
|
-
function subscribeToEOL(cb
|
|
32
|
+
function subscribeToEOL(cb) {
|
|
33
33
|
if (_runtime.isBrowserScope) {
|
|
34
|
-
(0, _pageVisibility.subscribeToVisibilityChange)(cb, true
|
|
34
|
+
(0, _pageVisibility.subscribeToVisibilityChange)(cb, true); // when user switches tab or hides window, esp. mobile scenario
|
|
35
35
|
} else if (_runtime.isWorkerScope) {
|
|
36
36
|
_runtime.globalScope.cleanupTasks.push(cb); // close() should run these tasks before quitting thread
|
|
37
37
|
}
|
|
@@ -73,7 +73,7 @@ var _globalEvent = require("../dispatch/global-event");
|
|
|
73
73
|
* | 50 | Failed to connect. Cannot allow registered API. |
|
|
74
74
|
* | 51 | Container agent is not available to register with. Can not connect. |
|
|
75
75
|
* | 52 | Unexpected problem encountered. There should be at least one app for harvest! |
|
|
76
|
-
* | 53 |
|
|
76
|
+
* | 53 | Failed to parse connect response. |
|
|
77
77
|
* | 54 | An experimental feature is being used. Support can not be offered for issues. |
|
|
78
78
|
* | 55 | Register API has been disabled on the container agent. |
|
|
79
79
|
* | 56 | Could not find a matching entity to store data. |
|
|
@@ -99,6 +99,7 @@ var _globalEvent = require("../dispatch/global-event");
|
|
|
99
99
|
* | 76 | Agent rejected post message, could not match with existing entity. |
|
|
100
100
|
* | 77 | Agent rejected post message, could not validate origin. |
|
|
101
101
|
* | 78 | RegisteredIframeEntity could not determine parent origin and will not register, to avoid trusting messages from any origin. |
|
|
102
|
+
* | 79 | Unable to initialize Connector and/or Harvester. |
|
|
102
103
|
*
|
|
103
104
|
* @param {number} code The warning code to emit, which will be used to link to the warning code documentation
|
|
104
105
|
* @param {*} [secondary] Secondary data to include, usually an extra message, error or object
|
|
@@ -25,7 +25,8 @@ function activateFeatures(flags, agentRef) {
|
|
|
25
25
|
// let any window level subscribers know that the agent is running, per install docs
|
|
26
26
|
(0, _globalEvent.dispatchGlobalEvent)({
|
|
27
27
|
loaded: true,
|
|
28
|
-
drained:
|
|
28
|
+
drained: !agentRef.init.feature_flags.includes('rum_v2'),
|
|
29
|
+
// v2 RUM always call activateFeatures before any group is drained (in waitForFlags)
|
|
29
30
|
type: 'lifecycle',
|
|
30
31
|
name: 'load',
|
|
31
32
|
feature: undefined,
|
|
@@ -132,25 +132,33 @@ const observePerformance = (observers, config, onEntry) => {
|
|
|
132
132
|
function trackMFEVitals(target, timings) {
|
|
133
133
|
let fcpObservedAt;
|
|
134
134
|
let lcpObservedAt;
|
|
135
|
+
// Once disconnect() runs, no further vitals data should be written - even from an observer callback that was
|
|
136
|
+
// already in flight when disconnect() was called.
|
|
137
|
+
let disconnected = false;
|
|
135
138
|
const getTimeRelativeToScriptStart = capturedAt => {
|
|
136
139
|
if (capturedAt == null) return capturedAt;
|
|
137
140
|
return capturedAt - (timings?.scriptStart || timings?.registeredAt || 0);
|
|
138
141
|
};
|
|
139
142
|
const vitals = {
|
|
140
143
|
fcp: {
|
|
144
|
+
// A clock freeze/tab suspend between script registration and first paint can make this value huge once the
|
|
145
|
+
// tab resumes. Rather than report a multi-hour "FCP", drop it so bad data doesn't reach the backend.
|
|
141
146
|
get value() {
|
|
142
|
-
|
|
147
|
+
const relative = getTimeRelativeToScriptStart(fcpObservedAt);
|
|
148
|
+
return relative > _scriptTrackerConstants.CORRELATION_STALE_THRESHOLD_MS ? undefined : relative;
|
|
143
149
|
},
|
|
144
150
|
set value(v) {
|
|
145
|
-
fcpObservedAt = v;
|
|
151
|
+
if (!disconnected) fcpObservedAt = v;
|
|
146
152
|
}
|
|
147
153
|
},
|
|
148
154
|
lcp: {
|
|
155
|
+
// LCP can never occur before FCP, so if FCP was dropped as invalid, LCP is derived from the same untrustworthy clock and must be dropped too.
|
|
149
156
|
get value() {
|
|
157
|
+
if (vitals.fcp.value === undefined) return undefined;
|
|
150
158
|
return getTimeRelativeToScriptStart(lcpObservedAt);
|
|
151
159
|
},
|
|
152
160
|
set value(v) {
|
|
153
|
-
lcpObservedAt = v;
|
|
161
|
+
if (!disconnected) lcpObservedAt = v;
|
|
154
162
|
}
|
|
155
163
|
},
|
|
156
164
|
cls: {
|
|
@@ -170,15 +178,22 @@ function trackMFEVitals(target, timings) {
|
|
|
170
178
|
if (!fcpObservedAt) vitals.disconnect();
|
|
171
179
|
}, _scriptTrackerConstants.CORRELATION_STALE_THRESHOLD_MS);
|
|
172
180
|
const populateVitalMinimums = () => {
|
|
173
|
-
|
|
181
|
+
if (disconnected) return;
|
|
182
|
+
if (fcpObservedAt == null) {
|
|
183
|
+
const capturedAt = (0, _now.now)();
|
|
184
|
+
// If the very first observed render already exceeds the window, the clock is untrustworthy for this
|
|
185
|
+
// MFE (e.g. tab was frozen/suspended between registration and paint) - shut everything down instead
|
|
186
|
+
// of seeding vitals with data derived from it.
|
|
187
|
+
if (getTimeRelativeToScriptStart(capturedAt) > _scriptTrackerConstants.CORRELATION_STALE_THRESHOLD_MS) {
|
|
188
|
+
vitals.disconnect();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
fcpObservedAt = capturedAt;
|
|
192
|
+
}
|
|
174
193
|
lcpObservedAt ??= (0, _now.now)();
|
|
175
194
|
vitals.cls.value ??= 0;
|
|
176
195
|
};
|
|
177
196
|
|
|
178
|
-
// if the MFE has already rendered something on the page before we could set up listeners, just populate vital minimums immediately
|
|
179
|
-
const existingRoots = _runtime.globalScope.document?.querySelectorAll("[data-nr-mfe-id=\"".concat(escapeSelectorValue(target.id), "\"]"));
|
|
180
|
-
if (Array.from(existingRoots || []).some(x => isMatch(x.dataset, target))) populateVitalMinimums();
|
|
181
|
-
|
|
182
197
|
// Track FCP - first contentful paint
|
|
183
198
|
const fcpObs = observeMutations(target, (_, obs) => {
|
|
184
199
|
// An observed "FCP" means _something_ rendered, so at minimum we can populate all the baseline values for the vitals
|
|
@@ -193,12 +208,13 @@ function trackMFEVitals(target, timings) {
|
|
|
193
208
|
const observedElements = new Set();
|
|
194
209
|
try {
|
|
195
210
|
resizeObs = new _runtime.globalScope.ResizeObserver(entries => {
|
|
211
|
+
if (disconnected) return;
|
|
196
212
|
entries.forEach(entry => {
|
|
197
213
|
try {
|
|
198
214
|
const size = entry.contentRect.width * entry.contentRect.height;
|
|
199
215
|
if (size > largestSize) {
|
|
200
216
|
largestSize = size;
|
|
201
|
-
|
|
217
|
+
vitals.lcp.value = (0, _now.now)();
|
|
202
218
|
}
|
|
203
219
|
resizeObs.unobserve(entry.target);
|
|
204
220
|
} catch (e) {
|
|
@@ -259,7 +275,7 @@ function trackMFEVitals(target, timings) {
|
|
|
259
275
|
type: 'layout-shift',
|
|
260
276
|
buffered: true
|
|
261
277
|
}, entry => {
|
|
262
|
-
if (entry.hadRecentInput) return;
|
|
278
|
+
if (disconnected || entry.hadRecentInput) return;
|
|
263
279
|
(entry.sources || []).some(source => {
|
|
264
280
|
if (isInMFE(source.node, target)) {
|
|
265
281
|
// an observed "CLS" means _something_ rendered for the MFE, so at minimum we can make sure all the baseline values are populated for the vitals
|
|
@@ -277,7 +293,7 @@ function trackMFEVitals(target, timings) {
|
|
|
277
293
|
buffered: true,
|
|
278
294
|
durationThreshold: 40
|
|
279
295
|
}, entry => {
|
|
280
|
-
if (!entry.interactionId || !isInMFE(entry.target, target)) return;
|
|
296
|
+
if (disconnected || !entry.interactionId || !isInMFE(entry.target, target)) return;
|
|
281
297
|
if (vitals.inp.value === undefined || entry.duration > vitals.inp.value) {
|
|
282
298
|
// an observed "INP" means _something_ rendered for the MFE, so at minimum we can make sure all the baseline values are populated for the vitals
|
|
283
299
|
populateVitalMinimums();
|
|
@@ -295,6 +311,7 @@ function trackMFEVitals(target, timings) {
|
|
|
295
311
|
|
|
296
312
|
// Disconnect all observers
|
|
297
313
|
vitals.disconnect = () => {
|
|
314
|
+
disconnected = true;
|
|
298
315
|
// Disconnect all observers
|
|
299
316
|
observers.forEach(obs => {
|
|
300
317
|
try {
|
|
@@ -341,5 +358,11 @@ function trackMFEVitals(target, timings) {
|
|
|
341
358
|
passive: true
|
|
342
359
|
});
|
|
343
360
|
});
|
|
361
|
+
|
|
362
|
+
// If the MFE has already rendered something on the page before we could set up listeners, just populate vital
|
|
363
|
+
// minimums immediately. Done last, once `observers` is fully populated and `vitals.disconnect` has its real
|
|
364
|
+
// implementation, so a stale-clock detection here can actually tear everything down instead of no-op'ing.
|
|
365
|
+
const existingRoots = _runtime.globalScope.document?.querySelectorAll("[data-nr-mfe-id=\"".concat(escapeSelectorValue(target.id), "\"]"));
|
|
366
|
+
if (Array.from(existingRoots || []).some(x => isMatch(x.dataset, target))) populateVitalMinimums();
|
|
344
367
|
return vitals;
|
|
345
368
|
}
|
|
@@ -7,7 +7,7 @@ exports.subscribeToPageUnload = subscribeToPageUnload;
|
|
|
7
7
|
exports.subscribeToVisibilityChange = subscribeToVisibilityChange;
|
|
8
8
|
var _eventListenerOpts = require("../event-listener/event-listener-opts");
|
|
9
9
|
/**
|
|
10
|
-
* Copyright 2020-
|
|
10
|
+
* Copyright 2020-2026 New Relic, Inc. All rights reserved.
|
|
11
11
|
* SPDX-License-Identifier: Apache-2.0
|
|
12
12
|
*/
|
|
13
13
|
|