@newrelic/browser-agent 1.296.0 → 1.297.0-rc.0
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 +13 -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/harvest/harvester.js +2 -1
- package/dist/cjs/features/session_replay/aggregate/index.js +10 -41
- package/dist/cjs/features/session_replay/instrument/index.js +4 -4
- package/dist/cjs/features/session_replay/shared/recorder-events.js +2 -2
- package/dist/cjs/features/session_replay/shared/recorder.js +41 -61
- package/dist/cjs/features/session_replay/shared/utils.js +0 -13
- package/dist/cjs/features/utils/aggregate-base.js +6 -5
- package/dist/cjs/features/utils/event-buffer.js +3 -2
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/harvest/harvester.js +2 -2
- package/dist/esm/features/session_replay/aggregate/index.js +10 -41
- package/dist/esm/features/session_replay/instrument/index.js +4 -4
- package/dist/esm/features/session_replay/shared/recorder-events.js +2 -2
- package/dist/esm/features/session_replay/shared/recorder.js +42 -62
- package/dist/esm/features/session_replay/shared/utils.js +0 -12
- package/dist/esm/features/utils/aggregate-base.js +6 -5
- package/dist/esm/features/utils/event-buffer.js +3 -2
- package/dist/types/common/harvest/harvester.d.ts +15 -0
- package/dist/types/common/harvest/harvester.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +0 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder-events.d.ts +1 -1
- package/dist/types/features/session_replay/shared/recorder-events.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder.d.ts +10 -8
- package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/utils.d.ts +0 -8
- package/dist/types/features/session_replay/shared/utils.d.ts.map +1 -1
- package/dist/types/features/utils/aggregate-base.d.ts +2 -0
- package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
- package/dist/types/features/utils/event-buffer.d.ts +2 -1
- package/dist/types/features/utils/event-buffer.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/common/harvest/harvester.js +2 -2
- package/src/features/session_replay/aggregate/index.js +8 -35
- package/src/features/session_replay/instrument/index.js +1 -1
- package/src/features/session_replay/shared/recorder-events.js +2 -2
- package/src/features/session_replay/shared/recorder.js +39 -67
- package/src/features/session_replay/shared/utils.js +0 -13
- package/src/features/utils/aggregate-base.js +6 -4
- package/src/features/utils/event-buffer.js +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,19 @@
|
|
|
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.297.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.296.0...v1.297.0) (2025-09-10)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* Prevent early harvests when in retry period ([#1548](https://github.com/newrelic/newrelic-browser-agent/issues/1548)) ([5ce9e70](https://github.com/newrelic/newrelic-browser-agent/commit/5ce9e705c2d8e55d4f3f93bd704ad78707aa3c84))
|
|
12
|
+
* remove newrelic meta attribute ([#1550](https://github.com/newrelic/newrelic-browser-agent/issues/1550)) ([f165b3e](https://github.com/newrelic/newrelic-browser-agent/commit/f165b3e877fc4fc4c8b2b77c942b791a25d58705))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* Fix exceptions for null bodies arising from empty harvests ([#1553](https://github.com/newrelic/newrelic-browser-agent/issues/1553)) ([b763d03](https://github.com/newrelic/newrelic-browser-agent/commit/b763d03215f043e5be555e2dfe2fba85351d672f))
|
|
18
|
+
|
|
6
19
|
## [1.296.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.295.0...v1.296.0) (2025-08-19)
|
|
7
20
|
|
|
8
21
|
|
|
@@ -17,7 +17,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.DIST_METHOD = exports.BUILD_EN
|
|
|
17
17
|
/**
|
|
18
18
|
* Exposes the version of the agent
|
|
19
19
|
*/
|
|
20
|
-
const VERSION = exports.VERSION = "1.
|
|
20
|
+
const VERSION = exports.VERSION = "1.297.0-rc.0";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Exposes the build type of the agent
|
|
@@ -17,7 +17,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.DIST_METHOD = exports.BUILD_EN
|
|
|
17
17
|
/**
|
|
18
18
|
* Exposes the version of the agent
|
|
19
19
|
*/
|
|
20
|
-
const VERSION = exports.VERSION = "1.
|
|
20
|
+
const VERSION = exports.VERSION = "1.297.0-rc.0";
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Exposes the build type of the agent
|
|
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.Harvester = void 0;
|
|
7
|
+
exports.send = send;
|
|
7
8
|
var _constants = require("../../features/metrics/constants");
|
|
8
9
|
var _features = require("../../loaders/features/features");
|
|
9
10
|
var _env = require("../constants/env.npm");
|
|
@@ -252,7 +253,7 @@ function send(agentRef, {
|
|
|
252
253
|
function cleanPayload(payload = {}) {
|
|
253
254
|
const clean = input => {
|
|
254
255
|
if (typeof Uint8Array !== 'undefined' && input instanceof Uint8Array || Array.isArray(input)) return input;
|
|
255
|
-
if (typeof input === 'string') return input
|
|
256
|
+
if (typeof input === 'string') return input;
|
|
256
257
|
return Object.entries(input || {}).reduce((accumulator, [key, value]) => {
|
|
257
258
|
if (typeof value === 'number' || typeof value === 'string' && value.length > 0 || typeof value === 'object' && Object.keys(value || {}).length > 0) {
|
|
258
259
|
accumulator[key] = value;
|
|
@@ -16,7 +16,6 @@ var _constants2 = require("../../../common/session/constants");
|
|
|
16
16
|
var _stringify = require("../../../common/util/stringify");
|
|
17
17
|
var _stylesheetEvaluator = require("../shared/stylesheet-evaluator");
|
|
18
18
|
var _now = require("../../../common/timing/now");
|
|
19
|
-
var _utils = require("../shared/utils");
|
|
20
19
|
var _agentConstants = require("../../../common/constants/agent-constants");
|
|
21
20
|
var _cleanUrl = require("../../../common/url/clean-url");
|
|
22
21
|
var _featureGates = require("../../utils/feature-gates");
|
|
@@ -101,8 +100,9 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
101
100
|
}
|
|
102
101
|
return;
|
|
103
102
|
}
|
|
104
|
-
this.
|
|
105
|
-
|
|
103
|
+
this.initializeRecording(srMode).then(() => {
|
|
104
|
+
this.drain();
|
|
105
|
+
});
|
|
106
106
|
}).then(() => {
|
|
107
107
|
if (this.mode === _constants2.MODE.OFF) {
|
|
108
108
|
this.recorder?.stopRecording(); // stop any conservative preload recording launched by instrument
|
|
@@ -125,7 +125,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
125
125
|
return Boolean(this.recorder && this.mode === _constants2.MODE.FULL && !this.blocked && this.entitled);
|
|
126
126
|
}
|
|
127
127
|
handleError(e) {
|
|
128
|
-
if (this.recorder) this.recorder.
|
|
128
|
+
if (this.recorder) this.recorder.events.hasError = true;
|
|
129
129
|
// run once
|
|
130
130
|
if (this.mode === _constants2.MODE.ERROR && _runtime.globalScope?.document.visibilityState === 'visible') {
|
|
131
131
|
this.switchToFull();
|
|
@@ -184,7 +184,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
184
184
|
Recorder
|
|
185
185
|
} = await Promise.resolve().then(() => _interopRequireWildcard(require(/* webpackChunkName: "recorder" */'../shared/recorder')));
|
|
186
186
|
this.recorder = new Recorder(this);
|
|
187
|
-
this.recorder.
|
|
187
|
+
this.recorder.events.hasError = this.errorNoticed;
|
|
188
188
|
} catch (err) {
|
|
189
189
|
return this.abort(_constants.ABORT_REASONS.IMPORT);
|
|
190
190
|
}
|
|
@@ -199,10 +199,6 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
199
199
|
// ERROR mode will do this until an error is thrown, and then switch into FULL mode.
|
|
200
200
|
// The makeHarvestPayload should ensure that no payload is returned if we're not in FULL mode...
|
|
201
201
|
|
|
202
|
-
// If theres preloaded events and we are in full mode, just harvest immediately to clear up space and for consistency
|
|
203
|
-
if (this.mode === _constants2.MODE.FULL && this.recorder?.getEvents().type === 'preloaded') {
|
|
204
|
-
this.prepUtils().then(() => this.agentRef.runtime.harvester.triggerHarvestFor(this));
|
|
205
|
-
}
|
|
206
202
|
await this.prepUtils();
|
|
207
203
|
if (!this.agentRef.runtime.isRecording) this.recorder.startRecording();
|
|
208
204
|
this.syncWithSessionManager({
|
|
@@ -241,33 +237,11 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
241
237
|
let len = 0;
|
|
242
238
|
if (!!this.gzipper && !!this.u8) {
|
|
243
239
|
payload.body = this.gzipper(this.u8("[".concat(payload.body.map(({
|
|
244
|
-
__serialized
|
|
245
|
-
|
|
246
|
-
}) => {
|
|
247
|
-
if (e.__newrelic && __serialized) return __serialized;
|
|
248
|
-
const output = {
|
|
249
|
-
...e
|
|
250
|
-
};
|
|
251
|
-
if (!output.__newrelic) {
|
|
252
|
-
output.__newrelic = (0, _utils.buildNRMetaNode)(e.timestamp, this.timeKeeper);
|
|
253
|
-
output.timestamp = this.timeKeeper.correctAbsoluteTimestamp(e.timestamp);
|
|
254
|
-
}
|
|
255
|
-
return (0, _stringify.stringify)(output);
|
|
256
|
-
}).join(','), "]")));
|
|
240
|
+
__serialized
|
|
241
|
+
}) => __serialized).join(','), "]")));
|
|
257
242
|
len = payload.body.length;
|
|
258
243
|
} else {
|
|
259
|
-
payload.body
|
|
260
|
-
__serialized,
|
|
261
|
-
...node
|
|
262
|
-
}) => {
|
|
263
|
-
if (node.__newrelic) return node;
|
|
264
|
-
const output = {
|
|
265
|
-
...node
|
|
266
|
-
};
|
|
267
|
-
output.__newrelic = (0, _utils.buildNRMetaNode)(node.timestamp, this.timeKeeper);
|
|
268
|
-
output.timestamp = this.timeKeeper.correctAbsoluteTimestamp(node.timestamp);
|
|
269
|
-
return output;
|
|
270
|
-
});
|
|
244
|
+
for (let idx in payload.body) delete payload.body[idx].__serialized;
|
|
271
245
|
len = (0, _stringify.stringify)(payload.body).length;
|
|
272
246
|
}
|
|
273
247
|
if (len > _agentConstants.MAX_PAYLOAD_SIZE) {
|
|
@@ -286,11 +260,6 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
286
260
|
}
|
|
287
261
|
return [payloadOutput];
|
|
288
262
|
}
|
|
289
|
-
getCorrectedTimestamp(node) {
|
|
290
|
-
if (!node?.timestamp) return;
|
|
291
|
-
if (node.__newrelic) return node.timestamp;
|
|
292
|
-
return this.timeKeeper.correctAbsoluteTimestamp(node.timestamp);
|
|
293
|
-
}
|
|
294
263
|
|
|
295
264
|
/**
|
|
296
265
|
* returns the timestamps for the earliest and latest nodes in the provided array, even if out of order
|
|
@@ -338,8 +307,8 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
338
307
|
lastEvent
|
|
339
308
|
} = this.getFirstAndLastNodes(events);
|
|
340
309
|
// from rrweb node || from when the harvest cycle started
|
|
341
|
-
const firstTimestamp =
|
|
342
|
-
const lastTimestamp =
|
|
310
|
+
const firstTimestamp = firstEvent?.timestamp || Math.floor(this.timeKeeper.correctAbsoluteTimestamp(recorderEvents.cycleTimestamp));
|
|
311
|
+
const lastTimestamp = lastEvent?.timestamp || Math.floor(this.timeKeeper.correctRelativeTimestamp(relativeNow));
|
|
343
312
|
const agentMetadata = agentRuntime.appMetadata?.agents?.[0] || {};
|
|
344
313
|
return {
|
|
345
314
|
qs: {
|
|
@@ -81,12 +81,12 @@ class Instrument extends _instrumentBase.InstrumentBase {
|
|
|
81
81
|
// If startReplay() has been used by this point, we must record in full mode regardless of session preload:
|
|
82
82
|
// Note: recorder starts here with w/e the mode is at this time, but this may be changed later (see #apiStartOrRestartReplay else-case)
|
|
83
83
|
this.recorder ??= new Recorder({
|
|
84
|
+
...this,
|
|
84
85
|
mode: this.#mode,
|
|
85
|
-
|
|
86
|
+
agentRef: this.#agentRef,
|
|
86
87
|
trigger,
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
});
|
|
88
|
+
timeKeeper: this.#agentRef.runtime.timeKeeper
|
|
89
|
+
}); // if TK exists due to deferred state, pass it
|
|
90
90
|
this.recorder.startRecording();
|
|
91
91
|
this.abortHandler = this.recorder.stopRecording;
|
|
92
92
|
} catch (err) {
|
|
@@ -30,8 +30,8 @@ class RecorderEvents {
|
|
|
30
30
|
/** Payload metadata -- Denotes whether all stylesheet elements were able to be inlined */
|
|
31
31
|
this.inlinedAllStylesheets = shouldInlineStylesheets;
|
|
32
32
|
}
|
|
33
|
-
add(event) {
|
|
34
|
-
this.#events.add(event);
|
|
33
|
+
add(event, evaluatedSize) {
|
|
34
|
+
this.#events.add(event, evaluatedSize);
|
|
35
35
|
}
|
|
36
36
|
get events() {
|
|
37
37
|
return this.#events.get();
|
|
@@ -15,21 +15,16 @@ var _constants3 = require("../../metrics/constants");
|
|
|
15
15
|
var _features = require("../../../loaders/features/features");
|
|
16
16
|
var _utils = require("./utils");
|
|
17
17
|
var _agentConstants = require("../../../common/constants/agent-constants");
|
|
18
|
-
var _aggregateBase = require("../../utils/aggregate-base");
|
|
19
18
|
var _console = require("../../../common/util/console");
|
|
20
19
|
var _invoke = require("../../../common/util/invoke");
|
|
20
|
+
var _registerHandler = require("../../../common/event-emitter/register-handler");
|
|
21
21
|
/**
|
|
22
22
|
* Copyright 2020-2025 New Relic, Inc. All rights reserved.
|
|
23
23
|
* SPDX-License-Identifier: Apache-2.0
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
const RRWEB_DATA_CHANNEL = 'rrweb-data';
|
|
26
27
|
class Recorder {
|
|
27
|
-
/** Each page mutation or event will be stored (raw) in this array. This array will be cleared on each harvest */
|
|
28
|
-
#events;
|
|
29
|
-
/** Backlog used for a 2-part sliding window to guarantee a 15-30s buffer window */
|
|
30
|
-
#backloggedEvents;
|
|
31
|
-
/** array of recorder events -- Will be filled only if forced harvest was triggered and harvester does not exist */
|
|
32
|
-
#preloaded;
|
|
33
28
|
/** flag that if true, blocks events from being "stored". Only set to true when a full snapshot has incomplete nodes (only stylesheets ATM) */
|
|
34
29
|
#fixing = false;
|
|
35
30
|
#warnCSSOnce = (0, _invoke.single)(() => (0, _console.warn)(47)); // notifies user of potential replayer issue if fix_stylesheets is off
|
|
@@ -39,13 +34,12 @@ class Recorder {
|
|
|
39
34
|
this.parent = parent;
|
|
40
35
|
/** A flag that can be set to false by failing conversions to stop the fetching process */
|
|
41
36
|
this.shouldFix = this.parent.agentRef.init.session_replay.fix_stylesheets;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
this
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
/** Only set to true once a snapshot node has been processed. Used to block preload harvests from sending before we know we have a snapshot */
|
|
37
|
+
|
|
38
|
+
/** Each page mutation or event will be stored (raw) in this array. This array will be cleared on each harvest */
|
|
39
|
+
this.events = new _recorderEvents.RecorderEvents(this.shouldFix);
|
|
40
|
+
/** Backlog used for a 2-part sliding window to guarantee a 15-30s buffer window */
|
|
41
|
+
this.backloggedEvents = new _recorderEvents.RecorderEvents(this.shouldFix);
|
|
42
|
+
/** Only set to true once a snapshot node has been processed. Used to block harvests from sending before we know we have a snapshot */
|
|
49
43
|
this.hasSeenSnapshot = false;
|
|
50
44
|
/** Hold on to the last meta node, so that it can be re-inserted if the meta and snapshot nodes are broken up due to harvesting */
|
|
51
45
|
this.lastMeta = false;
|
|
@@ -53,32 +47,27 @@ class Recorder {
|
|
|
53
47
|
this.stopRecording = () => {
|
|
54
48
|
this.parent.agentRef.runtime.isRecording = false;
|
|
55
49
|
};
|
|
50
|
+
(0, _registerHandler.registerHandler)(RRWEB_DATA_CHANNEL, (event, isCheckout) => {
|
|
51
|
+
this.audit(event, isCheckout);
|
|
52
|
+
}, this.parent.featureName, this.parent.ee);
|
|
56
53
|
}
|
|
57
54
|
getEvents() {
|
|
58
|
-
if (this.#preloaded[0]?.events.length) {
|
|
59
|
-
return {
|
|
60
|
-
...this.#preloaded[0],
|
|
61
|
-
events: this.#preloaded[0].events,
|
|
62
|
-
payloadBytesEstimation: this.#preloaded[0].payloadBytesEstimation,
|
|
63
|
-
type: 'preloaded'
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
55
|
return {
|
|
67
|
-
events: [...this
|
|
56
|
+
events: [...this.backloggedEvents.events, ...this.events.events].filter(x => x),
|
|
68
57
|
type: 'standard',
|
|
69
|
-
cycleTimestamp: Math.min(this
|
|
70
|
-
payloadBytesEstimation: this
|
|
71
|
-
hasError: this
|
|
72
|
-
hasMeta: this
|
|
73
|
-
hasSnapshot: this
|
|
74
|
-
inlinedAllStylesheets: !!this
|
|
58
|
+
cycleTimestamp: Math.min(this.backloggedEvents.cycleTimestamp, this.events.cycleTimestamp),
|
|
59
|
+
payloadBytesEstimation: this.backloggedEvents.payloadBytesEstimation + this.events.payloadBytesEstimation,
|
|
60
|
+
hasError: this.backloggedEvents.hasError || this.events.hasError,
|
|
61
|
+
hasMeta: this.backloggedEvents.hasMeta || this.events.hasMeta,
|
|
62
|
+
hasSnapshot: this.backloggedEvents.hasSnapshot || this.events.hasSnapshot,
|
|
63
|
+
inlinedAllStylesheets: !!this.backloggedEvents.events.length && this.backloggedEvents.inlinedAllStylesheets || this.events.inlinedAllStylesheets
|
|
75
64
|
};
|
|
76
65
|
}
|
|
77
66
|
|
|
78
|
-
/** Clears the buffer (this
|
|
67
|
+
/** Clears the buffer (this.events), and resets all payload metadata properties */
|
|
79
68
|
clearBuffer() {
|
|
80
|
-
|
|
81
|
-
this
|
|
69
|
+
this.backloggedEvents = this.parent.mode === _constants2.MODE.ERROR ? this.events : new _recorderEvents.RecorderEvents(this.shouldFix);
|
|
70
|
+
this.events = new _recorderEvents.RecorderEvents(this.shouldFix);
|
|
82
71
|
}
|
|
83
72
|
|
|
84
73
|
/** Begin recording using configured recording lib */
|
|
@@ -101,7 +90,9 @@ class Recorder {
|
|
|
101
90
|
let stop;
|
|
102
91
|
try {
|
|
103
92
|
stop = (0, _rrweb.record)({
|
|
104
|
-
emit:
|
|
93
|
+
emit: (event, isCheckout) => {
|
|
94
|
+
(0, _handle.handle)(RRWEB_DATA_CHANNEL, [event, isCheckout], undefined, this.parent.featureName, this.parent.ee);
|
|
95
|
+
},
|
|
105
96
|
blockClass: block_class,
|
|
106
97
|
ignoreClass: ignore_class,
|
|
107
98
|
maskTextClass: mask_text_class,
|
|
@@ -139,7 +130,7 @@ class Recorder {
|
|
|
139
130
|
/** only run the full fixing behavior (more costly) if fix_stylesheets is configured as on (default behavior) */
|
|
140
131
|
if (!this.shouldFix) {
|
|
141
132
|
if (incompletes > 0) {
|
|
142
|
-
this.
|
|
133
|
+
this.events.inlinedAllStylesheets = false;
|
|
143
134
|
this.#warnCSSOnce();
|
|
144
135
|
(0, _handle.handle)(_constants3.SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Skipped', incompletes], undefined, _features.FEATURE_NAMES.metrics, this.parent.ee);
|
|
145
136
|
}
|
|
@@ -151,7 +142,7 @@ class Recorder {
|
|
|
151
142
|
/** wait for the evaluator to download/replace the incompletes' src code and then take a new snap */
|
|
152
143
|
_stylesheetEvaluator.stylesheetEvaluator.fix().then(failedToFix => {
|
|
153
144
|
if (failedToFix > 0) {
|
|
154
|
-
this.
|
|
145
|
+
this.events.inlinedAllStylesheets = false;
|
|
155
146
|
this.shouldFix = false;
|
|
156
147
|
}
|
|
157
148
|
(0, _handle.handle)(_constants3.SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Failed', failedToFix], undefined, _features.FEATURE_NAMES.metrics, this.parent.ee);
|
|
@@ -165,15 +156,12 @@ class Recorder {
|
|
|
165
156
|
if (!this.#fixing) this.store(event, isCheckout);
|
|
166
157
|
}
|
|
167
158
|
|
|
168
|
-
/** Store a payload in the buffer (this
|
|
159
|
+
/** Store a payload in the buffer (this.events). This should be the callback to the recording lib noticing a mutation */
|
|
169
160
|
store(event, isCheckout) {
|
|
170
|
-
if (!event) return;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
event.__newrelic = (0, _utils.buildNRMetaNode)(event.timestamp, this.parent.timeKeeper);
|
|
175
|
-
event.timestamp = this.parent.timeKeeper.correctAbsoluteTimestamp(event.timestamp);
|
|
176
|
-
}
|
|
161
|
+
if (!event || this.parent.blocked) return;
|
|
162
|
+
|
|
163
|
+
/** because we've waited until draining to process the buffered rrweb events, we can guarantee the timekeeper exists */
|
|
164
|
+
event.timestamp = this.parent.timeKeeper.correctAbsoluteTimestamp(event.timestamp);
|
|
177
165
|
event.__serialized = (0, _stringify.stringify)(event);
|
|
178
166
|
const eventBytes = event.__serialized.length;
|
|
179
167
|
/** The estimated size of the payload after compression */
|
|
@@ -188,26 +176,18 @@ class Recorder {
|
|
|
188
176
|
}
|
|
189
177
|
|
|
190
178
|
// meta event
|
|
191
|
-
|
|
192
|
-
this.currentBufferTarget.hasMeta = true;
|
|
193
|
-
}
|
|
179
|
+
this.events.hasMeta ||= event.type === _constants.RRWEB_EVENT_TYPES.Meta;
|
|
194
180
|
// snapshot event
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
this.currentBufferTarget.add(event);
|
|
181
|
+
this.events.hasSnapshot ||= this.hasSeenSnapshot ||= event.type === _constants.RRWEB_EVENT_TYPES.FullSnapshot;
|
|
182
|
+
|
|
183
|
+
//* dont let the EventBuffer class double evaluate the event data size, it's a performance burden and we have special reasons to do it outside the event buffer */
|
|
184
|
+
this.events.add(event, eventBytes);
|
|
200
185
|
|
|
201
186
|
// We are making an effort to try to keep payloads manageable for unloading. If they reach the unload limit before their interval,
|
|
202
187
|
// it will send immediately. This often happens on the first snapshot, which can be significantly larger than the other payloads.
|
|
203
|
-
if ((
|
|
204
|
-
// if we've made it to the ideal size of ~
|
|
205
|
-
|
|
206
|
-
this.parent.agentRef.runtime.harvester.triggerHarvestFor(this.parent);
|
|
207
|
-
} else {
|
|
208
|
-
// we are still in "preload" and it triggered a "stop point". Make a new set, which will get pointed at on next cycle
|
|
209
|
-
this.#preloaded.push(new _recorderEvents.RecorderEvents(this.shouldFix));
|
|
210
|
-
}
|
|
188
|
+
if ((this.events.hasSnapshot && this.events.hasMeta || payloadSize > _agentConstants.IDEAL_PAYLOAD_SIZE) && this.parent.mode === _constants2.MODE.FULL) {
|
|
189
|
+
// if we've made it to the ideal size of ~16kb before the interval timer, we should send early.
|
|
190
|
+
this.parent.agentRef.runtime.harvester.triggerHarvestFor(this.parent);
|
|
211
191
|
}
|
|
212
192
|
}
|
|
213
193
|
|
|
@@ -221,13 +201,13 @@ class Recorder {
|
|
|
221
201
|
}
|
|
222
202
|
}
|
|
223
203
|
clearTimestamps() {
|
|
224
|
-
this.
|
|
204
|
+
this.events.cycleTimestamp = undefined;
|
|
225
205
|
}
|
|
226
206
|
|
|
227
207
|
/** Estimate the payload size */
|
|
228
208
|
getPayloadSize(newBytes = 0) {
|
|
229
209
|
// the query param padding constant gives us some padding for the other metadata to be safely injected
|
|
230
|
-
return this.estimateCompression(this.
|
|
210
|
+
return this.estimateCompression(this.events.payloadBytesEstimation + newBytes) + _constants.QUERY_PARAM_PADDING;
|
|
231
211
|
}
|
|
232
212
|
|
|
233
213
|
/** Extensive research has yielded about an 88% compression factor on these payloads.
|
|
@@ -3,13 +3,11 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.buildNRMetaNode = buildNRMetaNode;
|
|
7
6
|
exports.customMasker = customMasker;
|
|
8
7
|
exports.hasReplayPrerequisite = hasReplayPrerequisite;
|
|
9
8
|
exports.isPreloadAllowed = isPreloadAllowed;
|
|
10
9
|
var _nreum = require("../../../common/window/nreum");
|
|
11
10
|
var _featureGates = require("../../utils/feature-gates");
|
|
12
|
-
var _runtime = require("../../../common/constants/runtime");
|
|
13
11
|
/**
|
|
14
12
|
* Copyright 2020-2025 New Relic, Inc. All rights reserved.
|
|
15
13
|
* SPDX-License-Identifier: Apache-2.0
|
|
@@ -25,17 +23,6 @@ function hasReplayPrerequisite(agentInit) {
|
|
|
25
23
|
function isPreloadAllowed(agentInit) {
|
|
26
24
|
return agentInit?.session_replay.preload === true && hasReplayPrerequisite(agentInit);
|
|
27
25
|
}
|
|
28
|
-
function buildNRMetaNode(timestamp, timeKeeper) {
|
|
29
|
-
const correctedTimestamp = timeKeeper.correctAbsoluteTimestamp(timestamp);
|
|
30
|
-
return {
|
|
31
|
-
originalTimestamp: timestamp,
|
|
32
|
-
correctedTimestamp,
|
|
33
|
-
timestampDiff: timestamp - correctedTimestamp,
|
|
34
|
-
originTime: _runtime.originTime,
|
|
35
|
-
correctedOriginTime: timeKeeper.correctedOriginTime,
|
|
36
|
-
originTimeDiff: Math.floor(_runtime.originTime - timeKeeper.correctedOriginTime)
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
26
|
function customMasker(text, element) {
|
|
40
27
|
try {
|
|
41
28
|
if (typeof element?.type === 'string') {
|
|
@@ -41,8 +41,9 @@ class AggregateBase extends _featureBase.FeatureBase {
|
|
|
41
41
|
/** @type {Boolean} indicates if custom attributes are combined in each event payload for size estimation purposes. this is set to true in derived classes that need to evaluate custom attributes separately from the event payload */
|
|
42
42
|
this.customAttributesAreSeparate = false;
|
|
43
43
|
/** @type {Boolean} indicates if the feature can harvest early. This is set to false in derived classes that need to block early harvests, like ajax under certain conditions */
|
|
44
|
-
this.canHarvestEarly = true;
|
|
45
|
-
|
|
44
|
+
this.canHarvestEarly = true;
|
|
45
|
+
/** @type {Boolean} indicates if the feature is actively in a retry deferral period */
|
|
46
|
+
this.isRetrying = false;
|
|
46
47
|
this.harvestOpts = {}; // features aggregate classes can define custom opts for when their harvest is called
|
|
47
48
|
|
|
48
49
|
const agentEntityGuid = this.agentRef?.runtime?.appMetadata?.agents?.[0]?.entityGuid;
|
|
@@ -90,7 +91,7 @@ class AggregateBase extends _featureBase.FeatureBase {
|
|
|
90
91
|
* @returns void
|
|
91
92
|
*/
|
|
92
93
|
decideEarlyHarvest() {
|
|
93
|
-
if (!this.canHarvestEarly) return;
|
|
94
|
+
if (!this.canHarvestEarly || this.blocked || this.isRetrying) return;
|
|
94
95
|
const estimatedSize = this.events.byteSize() + (this.customAttributesAreSeparate ? this.agentRef.runtime.jsAttributesMetadata.bytes : 0);
|
|
95
96
|
if (estimatedSize > _agentConstants.IDEAL_PAYLOAD_SIZE) {
|
|
96
97
|
this.agentRef.runtime.harvester.triggerHarvestFor(this);
|
|
@@ -179,8 +180,8 @@ class AggregateBase extends _featureBase.FeatureBase {
|
|
|
179
180
|
* @param {boolean=} result.retry - whether the harvest should be retried
|
|
180
181
|
*/
|
|
181
182
|
postHarvestCleanup(result = {}) {
|
|
182
|
-
|
|
183
|
-
if (
|
|
183
|
+
this.isRetrying = result.sent && result.retry;
|
|
184
|
+
if (this.isRetrying) this.events.reloadSave(this.harvestOpts, result.targetApp?.entityGuid);
|
|
184
185
|
this.events.clearSave(this.harvestOpts, result.targetApp?.entityGuid);
|
|
185
186
|
}
|
|
186
187
|
|
|
@@ -45,10 +45,11 @@ class EventBuffer {
|
|
|
45
45
|
/**
|
|
46
46
|
* Add feature-processed event to our buffer. If this event would cause our total raw size to exceed the set max payload size, it is dropped.
|
|
47
47
|
* @param {any} event - any primitive type or object
|
|
48
|
+
* @param {number} [evaluatedSize] - the evalated size of the event, if already done so before storing in the event buffer
|
|
48
49
|
* @returns {Boolean} true if successfully added; false otherwise
|
|
49
50
|
*/
|
|
50
|
-
add(event) {
|
|
51
|
-
const addSize = (0, _stringify.stringify)(event)?.length || 0; // (estimate) # of bytes a directly stringified event it would take to send
|
|
51
|
+
add(event, evaluatedSize) {
|
|
52
|
+
const addSize = evaluatedSize || (0, _stringify.stringify)(event)?.length || 0; // (estimate) # of bytes a directly stringified event it would take to send
|
|
52
53
|
if (this.#rawBytes + addSize > this.maxPayloadSize) {
|
|
53
54
|
const smTag = inject => "EventBuffer/".concat(inject, "/Dropped/Bytes");
|
|
54
55
|
this.featureAgg?.reportSupportabilityMetric(smTag(this.featureAgg.featureName), addSize); // bytes dropped for this feature will aggregate with this metric tag
|
|
@@ -112,7 +112,7 @@ const warnings = {};
|
|
|
112
112
|
* @param {NetworkSendSpec} param0 Specification for sending data
|
|
113
113
|
* @returns {boolean} True if a network call was made. Note that this does not mean or guarantee that it was successful.
|
|
114
114
|
*/
|
|
115
|
-
function send(agentRef, {
|
|
115
|
+
export function send(agentRef, {
|
|
116
116
|
endpoint,
|
|
117
117
|
targetApp,
|
|
118
118
|
payload,
|
|
@@ -245,7 +245,7 @@ function send(agentRef, {
|
|
|
245
245
|
function cleanPayload(payload = {}) {
|
|
246
246
|
const clean = input => {
|
|
247
247
|
if (typeof Uint8Array !== 'undefined' && input instanceof Uint8Array || Array.isArray(input)) return input;
|
|
248
|
-
if (typeof input === 'string') return input
|
|
248
|
+
if (typeof input === 'string') return input;
|
|
249
249
|
return Object.entries(input || {}).reduce((accumulator, [key, value]) => {
|
|
250
250
|
if (typeof value === 'number' || typeof value === 'string' && value.length > 0 || typeof value === 'object' && Object.keys(value || {}).length > 0) {
|
|
251
251
|
accumulator[key] = value;
|
|
@@ -18,7 +18,6 @@ import { MODE, SESSION_EVENTS, SESSION_EVENT_TYPES } from '../../../common/sessi
|
|
|
18
18
|
import { stringify } from '../../../common/util/stringify';
|
|
19
19
|
import { stylesheetEvaluator } from '../shared/stylesheet-evaluator';
|
|
20
20
|
import { now } from '../../../common/timing/now';
|
|
21
|
-
import { buildNRMetaNode } from '../shared/utils';
|
|
22
21
|
import { MAX_PAYLOAD_SIZE } from '../../../common/constants/agent-constants';
|
|
23
22
|
import { cleanURL } from '../../../common/url/clean-url';
|
|
24
23
|
import { canEnableSessionTracking } from '../../utils/feature-gates';
|
|
@@ -96,8 +95,9 @@ export class Aggregate extends AggregateBase {
|
|
|
96
95
|
}
|
|
97
96
|
return;
|
|
98
97
|
}
|
|
99
|
-
this.
|
|
100
|
-
|
|
98
|
+
this.initializeRecording(srMode).then(() => {
|
|
99
|
+
this.drain();
|
|
100
|
+
});
|
|
101
101
|
}).then(() => {
|
|
102
102
|
if (this.mode === MODE.OFF) {
|
|
103
103
|
this.recorder?.stopRecording(); // stop any conservative preload recording launched by instrument
|
|
@@ -120,7 +120,7 @@ export class Aggregate extends AggregateBase {
|
|
|
120
120
|
return Boolean(this.recorder && this.mode === MODE.FULL && !this.blocked && this.entitled);
|
|
121
121
|
}
|
|
122
122
|
handleError(e) {
|
|
123
|
-
if (this.recorder) this.recorder.
|
|
123
|
+
if (this.recorder) this.recorder.events.hasError = true;
|
|
124
124
|
// run once
|
|
125
125
|
if (this.mode === MODE.ERROR && globalScope?.document.visibilityState === 'visible') {
|
|
126
126
|
this.switchToFull();
|
|
@@ -179,7 +179,7 @@ export class Aggregate extends AggregateBase {
|
|
|
179
179
|
Recorder
|
|
180
180
|
} = await import(/* webpackChunkName: "recorder" */'../shared/recorder');
|
|
181
181
|
this.recorder = new Recorder(this);
|
|
182
|
-
this.recorder.
|
|
182
|
+
this.recorder.events.hasError = this.errorNoticed;
|
|
183
183
|
} catch (err) {
|
|
184
184
|
return this.abort(ABORT_REASONS.IMPORT);
|
|
185
185
|
}
|
|
@@ -194,10 +194,6 @@ export class Aggregate extends AggregateBase {
|
|
|
194
194
|
// ERROR mode will do this until an error is thrown, and then switch into FULL mode.
|
|
195
195
|
// The makeHarvestPayload should ensure that no payload is returned if we're not in FULL mode...
|
|
196
196
|
|
|
197
|
-
// If theres preloaded events and we are in full mode, just harvest immediately to clear up space and for consistency
|
|
198
|
-
if (this.mode === MODE.FULL && this.recorder?.getEvents().type === 'preloaded') {
|
|
199
|
-
this.prepUtils().then(() => this.agentRef.runtime.harvester.triggerHarvestFor(this));
|
|
200
|
-
}
|
|
201
197
|
await this.prepUtils();
|
|
202
198
|
if (!this.agentRef.runtime.isRecording) this.recorder.startRecording();
|
|
203
199
|
this.syncWithSessionManager({
|
|
@@ -236,33 +232,11 @@ export class Aggregate extends AggregateBase {
|
|
|
236
232
|
let len = 0;
|
|
237
233
|
if (!!this.gzipper && !!this.u8) {
|
|
238
234
|
payload.body = this.gzipper(this.u8("[".concat(payload.body.map(({
|
|
239
|
-
__serialized
|
|
240
|
-
|
|
241
|
-
}) => {
|
|
242
|
-
if (e.__newrelic && __serialized) return __serialized;
|
|
243
|
-
const output = {
|
|
244
|
-
...e
|
|
245
|
-
};
|
|
246
|
-
if (!output.__newrelic) {
|
|
247
|
-
output.__newrelic = buildNRMetaNode(e.timestamp, this.timeKeeper);
|
|
248
|
-
output.timestamp = this.timeKeeper.correctAbsoluteTimestamp(e.timestamp);
|
|
249
|
-
}
|
|
250
|
-
return stringify(output);
|
|
251
|
-
}).join(','), "]")));
|
|
235
|
+
__serialized
|
|
236
|
+
}) => __serialized).join(','), "]")));
|
|
252
237
|
len = payload.body.length;
|
|
253
238
|
} else {
|
|
254
|
-
payload.body
|
|
255
|
-
__serialized,
|
|
256
|
-
...node
|
|
257
|
-
}) => {
|
|
258
|
-
if (node.__newrelic) return node;
|
|
259
|
-
const output = {
|
|
260
|
-
...node
|
|
261
|
-
};
|
|
262
|
-
output.__newrelic = buildNRMetaNode(node.timestamp, this.timeKeeper);
|
|
263
|
-
output.timestamp = this.timeKeeper.correctAbsoluteTimestamp(node.timestamp);
|
|
264
|
-
return output;
|
|
265
|
-
});
|
|
239
|
+
for (let idx in payload.body) delete payload.body[idx].__serialized;
|
|
266
240
|
len = stringify(payload.body).length;
|
|
267
241
|
}
|
|
268
242
|
if (len > MAX_PAYLOAD_SIZE) {
|
|
@@ -281,11 +255,6 @@ export class Aggregate extends AggregateBase {
|
|
|
281
255
|
}
|
|
282
256
|
return [payloadOutput];
|
|
283
257
|
}
|
|
284
|
-
getCorrectedTimestamp(node) {
|
|
285
|
-
if (!node?.timestamp) return;
|
|
286
|
-
if (node.__newrelic) return node.timestamp;
|
|
287
|
-
return this.timeKeeper.correctAbsoluteTimestamp(node.timestamp);
|
|
288
|
-
}
|
|
289
258
|
|
|
290
259
|
/**
|
|
291
260
|
* returns the timestamps for the earliest and latest nodes in the provided array, even if out of order
|
|
@@ -333,8 +302,8 @@ export class Aggregate extends AggregateBase {
|
|
|
333
302
|
lastEvent
|
|
334
303
|
} = this.getFirstAndLastNodes(events);
|
|
335
304
|
// from rrweb node || from when the harvest cycle started
|
|
336
|
-
const firstTimestamp =
|
|
337
|
-
const lastTimestamp =
|
|
305
|
+
const firstTimestamp = firstEvent?.timestamp || Math.floor(this.timeKeeper.correctAbsoluteTimestamp(recorderEvents.cycleTimestamp));
|
|
306
|
+
const lastTimestamp = lastEvent?.timestamp || Math.floor(this.timeKeeper.correctRelativeTimestamp(relativeNow));
|
|
338
307
|
const agentMetadata = agentRuntime.appMetadata?.agents?.[0] || {};
|
|
339
308
|
return {
|
|
340
309
|
qs: {
|
|
@@ -76,12 +76,12 @@ export class Instrument extends InstrumentBase {
|
|
|
76
76
|
// If startReplay() has been used by this point, we must record in full mode regardless of session preload:
|
|
77
77
|
// Note: recorder starts here with w/e the mode is at this time, but this may be changed later (see #apiStartOrRestartReplay else-case)
|
|
78
78
|
this.recorder ??= new Recorder({
|
|
79
|
+
...this,
|
|
79
80
|
mode: this.#mode,
|
|
80
|
-
|
|
81
|
+
agentRef: this.#agentRef,
|
|
81
82
|
trigger,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
});
|
|
83
|
+
timeKeeper: this.#agentRef.runtime.timeKeeper
|
|
84
|
+
}); // if TK exists due to deferred state, pass it
|
|
85
85
|
this.recorder.startRecording();
|
|
86
86
|
this.abortHandler = this.recorder.stopRecording;
|
|
87
87
|
} catch (err) {
|
|
@@ -23,8 +23,8 @@ export class RecorderEvents {
|
|
|
23
23
|
/** Payload metadata -- Denotes whether all stylesheet elements were able to be inlined */
|
|
24
24
|
this.inlinedAllStylesheets = shouldInlineStylesheets;
|
|
25
25
|
}
|
|
26
|
-
add(event) {
|
|
27
|
-
this.#events.add(event);
|
|
26
|
+
add(event, evaluatedSize) {
|
|
27
|
+
this.#events.add(event, evaluatedSize);
|
|
28
28
|
}
|
|
29
29
|
get events() {
|
|
30
30
|
return this.#events.get();
|