@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/cjs/common/constants/env.cdn.js +1 -1
  3. package/dist/cjs/common/constants/env.npm.js +1 -1
  4. package/dist/cjs/common/harvest/harvester.js +2 -1
  5. package/dist/cjs/features/session_replay/aggregate/index.js +10 -41
  6. package/dist/cjs/features/session_replay/instrument/index.js +4 -4
  7. package/dist/cjs/features/session_replay/shared/recorder-events.js +2 -2
  8. package/dist/cjs/features/session_replay/shared/recorder.js +41 -61
  9. package/dist/cjs/features/session_replay/shared/utils.js +0 -13
  10. package/dist/cjs/features/utils/aggregate-base.js +6 -5
  11. package/dist/cjs/features/utils/event-buffer.js +3 -2
  12. package/dist/esm/common/constants/env.cdn.js +1 -1
  13. package/dist/esm/common/constants/env.npm.js +1 -1
  14. package/dist/esm/common/harvest/harvester.js +2 -2
  15. package/dist/esm/features/session_replay/aggregate/index.js +10 -41
  16. package/dist/esm/features/session_replay/instrument/index.js +4 -4
  17. package/dist/esm/features/session_replay/shared/recorder-events.js +2 -2
  18. package/dist/esm/features/session_replay/shared/recorder.js +42 -62
  19. package/dist/esm/features/session_replay/shared/utils.js +0 -12
  20. package/dist/esm/features/utils/aggregate-base.js +6 -5
  21. package/dist/esm/features/utils/event-buffer.js +3 -2
  22. package/dist/types/common/harvest/harvester.d.ts +15 -0
  23. package/dist/types/common/harvest/harvester.d.ts.map +1 -1
  24. package/dist/types/features/session_replay/aggregate/index.d.ts +0 -1
  25. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  26. package/dist/types/features/session_replay/shared/recorder-events.d.ts +1 -1
  27. package/dist/types/features/session_replay/shared/recorder-events.d.ts.map +1 -1
  28. package/dist/types/features/session_replay/shared/recorder.d.ts +10 -8
  29. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  30. package/dist/types/features/session_replay/shared/utils.d.ts +0 -8
  31. package/dist/types/features/session_replay/shared/utils.d.ts.map +1 -1
  32. package/dist/types/features/utils/aggregate-base.d.ts +2 -0
  33. package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
  34. package/dist/types/features/utils/event-buffer.d.ts +2 -1
  35. package/dist/types/features/utils/event-buffer.d.ts.map +1 -1
  36. package/package.json +2 -2
  37. package/src/common/harvest/harvester.js +2 -2
  38. package/src/features/session_replay/aggregate/index.js +8 -35
  39. package/src/features/session_replay/instrument/index.js +1 -1
  40. package/src/features/session_replay/shared/recorder-events.js +2 -2
  41. package/src/features/session_replay/shared/recorder.js +39 -67
  42. package/src/features/session_replay/shared/utils.js +0 -13
  43. package/src/features/utils/aggregate-base.js +6 -4
  44. 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.296.0";
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.296.0";
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.length > 0 ? input : null;
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.drain();
105
- this.initializeRecording(srMode);
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.currentBufferTarget.hasError = true;
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.currentBufferTarget.hasError = this.errorNoticed;
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
- ...e
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 = payload.body.map(({
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 = this.getCorrectedTimestamp(firstEvent) || Math.floor(this.timeKeeper.correctAbsoluteTimestamp(recorderEvents.cycleTimestamp));
342
- const lastTimestamp = this.getCorrectedTimestamp(lastEvent) || Math.floor(this.timeKeeper.correctRelativeTimestamp(relativeNow));
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
- agentIdentifier: this.agentIdentifier,
86
+ agentRef: this.#agentRef,
86
87
  trigger,
87
- ee: this.ee,
88
- agentRef: this.#agentRef
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
- /** Event Buffers */
43
- this.#events = new _recorderEvents.RecorderEvents(this.shouldFix);
44
- this.#backloggedEvents = new _recorderEvents.RecorderEvents(this.shouldFix);
45
- this.#preloaded = [new _recorderEvents.RecorderEvents(this.shouldFix)];
46
- /** The pointer to the current bucket holding rrweb events */
47
- this.currentBufferTarget = this.#events;
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.#backloggedEvents.events, ...this.#events.events].filter(x => x),
56
+ events: [...this.backloggedEvents.events, ...this.events.events].filter(x => x),
68
57
  type: 'standard',
69
- cycleTimestamp: Math.min(this.#backloggedEvents.cycleTimestamp, this.#events.cycleTimestamp),
70
- payloadBytesEstimation: this.#backloggedEvents.payloadBytesEstimation + this.#events.payloadBytesEstimation,
71
- hasError: this.#backloggedEvents.hasError || this.#events.hasError,
72
- hasMeta: this.#backloggedEvents.hasMeta || this.#events.hasMeta,
73
- hasSnapshot: this.#backloggedEvents.hasSnapshot || this.#events.hasSnapshot,
74
- inlinedAllStylesheets: !!this.#backloggedEvents.events.length && this.#backloggedEvents.inlinedAllStylesheets || this.#events.inlinedAllStylesheets
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.#events), and resets all payload metadata properties */
67
+ /** Clears the buffer (this.events), and resets all payload metadata properties */
79
68
  clearBuffer() {
80
- if (this.#preloaded[0]?.events.length) this.#preloaded.shift();else if (this.parent.mode === _constants2.MODE.ERROR) this.#backloggedEvents = this.#events;else this.#backloggedEvents = new _recorderEvents.RecorderEvents(this.shouldFix);
81
- this.#events = new _recorderEvents.RecorderEvents(this.shouldFix);
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: this.audit.bind(this),
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.currentBufferTarget.inlinedAllStylesheets = false;
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.currentBufferTarget.inlinedAllStylesheets = false;
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.#events). This should be the callback to the recording lib noticing a mutation */
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
- if (!(this.parent instanceof _aggregateBase.AggregateBase) && this.#preloaded.length) this.currentBufferTarget = this.#preloaded[this.#preloaded.length - 1];else this.currentBufferTarget = this.#events;
172
- if (this.parent.blocked) return;
173
- if (this.parent.timeKeeper?.ready && !event.__newrelic) {
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
- if (event.type === _constants.RRWEB_EVENT_TYPES.Meta) {
192
- this.currentBufferTarget.hasMeta = true;
193
- }
179
+ this.events.hasMeta ||= event.type === _constants.RRWEB_EVENT_TYPES.Meta;
194
180
  // snapshot event
195
- if (event.type === _constants.RRWEB_EVENT_TYPES.FullSnapshot) {
196
- this.currentBufferTarget.hasSnapshot = true;
197
- this.hasSeenSnapshot = true;
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 ((event.type === _constants.RRWEB_EVENT_TYPES.FullSnapshot && this.currentBufferTarget.hasMeta || payloadSize > _agentConstants.IDEAL_PAYLOAD_SIZE) && this.parent.mode === _constants2.MODE.FULL) {
204
- // if we've made it to the ideal size of ~64kb before the interval timer, we should send early.
205
- if (this.parent instanceof _aggregateBase.AggregateBase) {
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.currentBufferTarget.cycleTimestamp = undefined;
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.currentBufferTarget.payloadBytesEstimation + newBytes) + _constants.QUERY_PARAM_PADDING;
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; // this is set to false in derived classes that need to block early harvests, like ajax under certain conditions
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
- const harvestFailed = result.sent && result.retry;
183
- if (harvestFailed) this.events.reloadSave(this.harvestOpts, result.targetApp?.entityGuid);
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
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.296.0";
14
+ export const VERSION = "1.297.0-rc.0";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -11,7 +11,7 @@
11
11
  /**
12
12
  * Exposes the version of the agent
13
13
  */
14
- export const VERSION = "1.296.0";
14
+ export const VERSION = "1.297.0-rc.0";
15
15
 
16
16
  /**
17
17
  * Exposes the build type of the agent
@@ -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.length > 0 ? input : null;
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.drain();
100
- this.initializeRecording(srMode);
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.currentBufferTarget.hasError = true;
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.currentBufferTarget.hasError = this.errorNoticed;
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
- ...e
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 = payload.body.map(({
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 = this.getCorrectedTimestamp(firstEvent) || Math.floor(this.timeKeeper.correctAbsoluteTimestamp(recorderEvents.cycleTimestamp));
337
- const lastTimestamp = this.getCorrectedTimestamp(lastEvent) || Math.floor(this.timeKeeper.correctRelativeTimestamp(relativeNow));
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
- agentIdentifier: this.agentIdentifier,
81
+ agentRef: this.#agentRef,
81
82
  trigger,
82
- ee: this.ee,
83
- agentRef: this.#agentRef
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();