@newrelic/browser-agent 1.248.0 → 1.250.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 (81) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/cjs/common/config/state/init.js +3 -3
  3. package/dist/cjs/common/constants/env.cdn.js +1 -1
  4. package/dist/cjs/common/constants/env.npm.js +1 -1
  5. package/dist/cjs/common/harvest/harvest-scheduler.js +2 -2
  6. package/dist/cjs/common/harvest/harvest.js +4 -3
  7. package/dist/cjs/common/ids/unique-id.js +5 -4
  8. package/dist/cjs/common/session/constants.js +20 -2
  9. package/dist/cjs/common/session/session-entity.js +8 -26
  10. package/dist/cjs/common/url/encode.js +2 -0
  11. package/dist/cjs/features/metrics/aggregate/index.js +3 -1
  12. package/dist/cjs/features/session_replay/aggregate/index.js +114 -277
  13. package/dist/cjs/features/session_replay/constants.js +57 -2
  14. package/dist/cjs/features/session_replay/instrument/index.js +38 -16
  15. package/dist/cjs/features/session_replay/shared/recorder-events.js +31 -0
  16. package/dist/cjs/features/session_replay/shared/recorder.js +155 -0
  17. package/dist/cjs/features/session_replay/{replay-mode.js → shared/replay-mode.js} +5 -5
  18. package/dist/cjs/features/session_trace/aggregate/index.js +25 -25
  19. package/dist/cjs/loaders/agent-base.js +18 -13
  20. package/dist/esm/common/config/state/init.js +3 -3
  21. package/dist/esm/common/constants/env.cdn.js +1 -1
  22. package/dist/esm/common/constants/env.npm.js +1 -1
  23. package/dist/esm/common/harvest/harvest-scheduler.js +1 -1
  24. package/dist/esm/common/harvest/harvest.js +4 -3
  25. package/dist/esm/common/ids/unique-id.js +5 -4
  26. package/dist/esm/common/session/constants.js +16 -1
  27. package/dist/esm/common/session/session-entity.js +2 -16
  28. package/dist/esm/common/url/encode.js +2 -0
  29. package/dist/esm/features/metrics/aggregate/index.js +3 -1
  30. package/dist/esm/features/session_replay/aggregate/index.js +95 -254
  31. package/dist/esm/features/session_replay/constants.js +49 -1
  32. package/dist/esm/features/session_replay/instrument/index.js +24 -1
  33. package/dist/esm/features/session_replay/shared/recorder-events.js +24 -0
  34. package/dist/esm/features/session_replay/shared/recorder.js +148 -0
  35. package/dist/esm/features/session_replay/{replay-mode.js → shared/replay-mode.js} +4 -4
  36. package/dist/esm/features/session_trace/aggregate/index.js +2 -2
  37. package/dist/esm/loaders/agent-base.js +18 -13
  38. package/dist/types/common/config/state/init.d.ts.map +1 -1
  39. package/dist/types/common/harvest/harvest.d.ts +1 -1
  40. package/dist/types/common/harvest/harvest.d.ts.map +1 -1
  41. package/dist/types/common/ids/unique-id.d.ts.map +1 -1
  42. package/dist/types/common/session/constants.d.ts +15 -0
  43. package/dist/types/common/session/session-entity.d.ts +0 -15
  44. package/dist/types/common/session/session-entity.d.ts.map +1 -1
  45. package/dist/types/common/url/encode.d.ts +1 -1
  46. package/dist/types/common/url/encode.d.ts.map +1 -1
  47. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  48. package/dist/types/features/session_replay/aggregate/index.d.ts +7 -63
  49. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  50. package/dist/types/features/session_replay/constants.d.ts +55 -0
  51. package/dist/types/features/session_replay/constants.d.ts.map +1 -1
  52. package/dist/types/features/session_replay/instrument/index.d.ts +2 -0
  53. package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
  54. package/dist/types/features/session_replay/shared/recorder-events.d.ts +21 -0
  55. package/dist/types/features/session_replay/shared/recorder-events.d.ts.map +1 -0
  56. package/dist/types/features/session_replay/shared/recorder.d.ts +40 -0
  57. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -0
  58. package/dist/types/features/session_replay/shared/replay-mode.d.ts.map +1 -0
  59. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  60. package/dist/types/loaders/agent-base.d.ts +2 -1
  61. package/dist/types/loaders/agent-base.d.ts.map +1 -1
  62. package/package.json +1 -5
  63. package/src/common/config/state/init.js +6 -4
  64. package/src/common/harvest/harvest-scheduler.js +1 -1
  65. package/src/common/harvest/harvest.js +4 -3
  66. package/src/common/ids/unique-id.js +5 -4
  67. package/src/common/session/__mocks__/session-entity.js +0 -6
  68. package/src/common/session/constants.js +18 -0
  69. package/src/common/session/session-entity.js +1 -17
  70. package/src/common/url/encode.js +2 -1
  71. package/src/features/metrics/aggregate/index.js +3 -1
  72. package/src/features/session_replay/aggregate/index.js +88 -246
  73. package/src/features/session_replay/constants.js +45 -0
  74. package/src/features/session_replay/instrument/index.js +18 -1
  75. package/src/features/session_replay/shared/recorder-events.js +25 -0
  76. package/src/features/session_replay/shared/recorder.js +145 -0
  77. package/src/features/session_replay/{replay-mode.js → shared/replay-mode.js} +4 -4
  78. package/src/features/session_trace/aggregate/index.js +2 -2
  79. package/src/loaders/agent-base.js +18 -13
  80. package/dist/types/features/session_replay/replay-mode.d.ts.map +0 -1
  81. /package/dist/types/features/session_replay/{replay-mode.d.ts → shared/replay-mode.d.ts} +0 -0
@@ -3,13 +3,11 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.RRWEB_EVENT_TYPES = exports.MAX_PAYLOAD_SIZE = exports.IDEAL_PAYLOAD_SIZE = exports.Aggregate = exports.AVG_COMPRESSION = void 0;
6
+ exports.Aggregate = void 0;
7
7
  var _registerHandler = require("../../../common/event-emitter/register-handler");
8
8
  var _harvestScheduler = require("../../../common/harvest/harvest-scheduler");
9
9
  var _constants = require("../constants");
10
- var _stringify = require("../../../common/util/stringify");
11
10
  var _config = require("../../../common/config/config");
12
- var _sessionEntity = require("../../../common/session/session-entity");
13
11
  var _aggregateBase = require("../../utils/aggregate-base");
14
12
  var _sharedChannel = require("../../../common/constants/shared-channel");
15
13
  var _encode = require("../../../common/url/encode");
@@ -20,6 +18,8 @@ var _handle = require("../../../common/event-emitter/handle");
20
18
  var _features = require("../../../loaders/features/features");
21
19
  var _env = require("../../../common/constants/env.npm");
22
20
  var _now = require("../../../common/timing/now");
21
+ var _constants3 = require("../../../common/session/constants");
22
+ var _stringify = require("../../../common/util/stringify");
23
23
  function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
24
24
  function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } /*
25
25
  * Copyright 2023 New Relic Corporation. All rights reserved.
@@ -31,132 +31,60 @@ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj &&
31
31
  * It is not production ready, and is not intended to be imported or implemented in any build of the browser agent until
32
32
  * functionality is validated and a full user experience is curated.
33
33
  */
34
- const AVG_COMPRESSION = 0.12;
35
- exports.AVG_COMPRESSION = AVG_COMPRESSION;
36
- const RRWEB_EVENT_TYPES = {
37
- DomContentLoaded: 0,
38
- Load: 1,
39
- FullSnapshot: 2,
40
- IncrementalSnapshot: 3,
41
- Meta: 4,
42
- Custom: 5
43
- };
44
- exports.RRWEB_EVENT_TYPES = RRWEB_EVENT_TYPES;
45
- const ABORT_REASONS = {
46
- RESET: {
47
- message: 'Session was reset',
48
- sm: 'Reset'
49
- },
50
- IMPORT: {
51
- message: 'Recorder failed to import',
52
- sm: 'Import'
53
- },
54
- TOO_MANY: {
55
- message: '429: Too Many Requests',
56
- sm: 'Too-Many'
57
- },
58
- TOO_BIG: {
59
- message: 'Payload was too large',
60
- sm: 'Too-Big'
61
- },
62
- CROSS_TAB: {
63
- message: 'Session Entity was set to OFF on another tab',
64
- sm: 'Cross-Tab'
65
- }
66
- };
67
- let recorder, gzipper, u8;
68
-
69
- /** Vortex caps payload sizes at 1MB */
70
- const MAX_PAYLOAD_SIZE = 1000000;
71
- /** Unloading caps around 64kb */
72
- exports.MAX_PAYLOAD_SIZE = MAX_PAYLOAD_SIZE;
73
- const IDEAL_PAYLOAD_SIZE = 64000;
74
- /** Reserved room for query param attrs */
75
- exports.IDEAL_PAYLOAD_SIZE = IDEAL_PAYLOAD_SIZE;
76
- const QUERY_PARAM_PADDING = 5000;
77
- /** Interval between forcing new full snapshots -- 15 seconds in error mode (x2), 5 minutes in full mode */
78
- const CHECKOUT_MS = {
79
- [_sessionEntity.MODE.ERROR]: 15000,
80
- [_sessionEntity.MODE.FULL]: 300000,
81
- [_sessionEntity.MODE.OFF]: 0
82
- };
34
+ let gzipper, u8;
83
35
  class Aggregate extends _aggregateBase.AggregateBase {
84
36
  static featureName = _constants.FEATURE_NAME;
85
- constructor(agentIdentifier, aggregator) {
37
+ // pass the recorder into the aggregator
38
+ constructor(agentIdentifier, aggregator, args) {
86
39
  super(agentIdentifier, aggregator, _constants.FEATURE_NAME);
87
- /** Each page mutation or event will be stored (raw) in this array. This array will be cleared on each harvest */
88
- this.events = [];
89
- /** Backlog used for a 2-part sliding window to guarantee a 15-30s buffer window */
90
- this.backloggedEvents = [];
91
40
  /** The interval to harvest at. This gets overridden if the size of the payload exceeds certain thresholds */
92
41
  this.harvestTimeSeconds = (0, _config.getConfigurationValue)(this.agentIdentifier, 'session_replay.harvestTimeSeconds') || 60;
93
42
  /** Set once the recorder has fully initialized after flag checks and sampling */
94
43
  this.initialized = false;
95
- /** Set once an error has been detected on the page. Never unset */
96
- this.errorNoticed = false;
97
- /** The "mode" to record in. Defaults to "OFF" until flags and sampling are checked. See "MODE" constant. */
98
- this.mode = _sessionEntity.MODE.OFF;
99
44
  /** Set once the feature has been "aborted" to prevent other side-effects from continuing */
100
45
  this.blocked = false;
101
- /** True when actively recording, false when paused or stopped */
102
- this.recording = false;
103
46
  /** can shut off efforts to compress the data */
104
47
  this.shouldCompress = true;
105
-
106
- /** Payload metadata -- Should indicate that the payload being sent has a full DOM snapshot. This can happen
107
- * -- When the recording library begins recording, it starts by taking a DOM snapshot
108
- * -- When visibility changes from "hidden" -> "visible", it must capture a full snapshot for the replay to work correctly across tabs
109
- */
110
- this.hasSnapshot = false;
111
- /** Payload metadata -- Should indicate that the payload being sent has a meta node. The meta node should always precede a snapshot node. */
112
- this.hasMeta = false;
113
- /** Payload metadata -- Should indicate that the payload being sent contains an error. Used for query/filter purposes in UI */
114
- this.hasError = false;
115
-
116
- /** Payload metadata -- Should indicate when a replay blob started recording. Resets each time a harvest occurs.
117
- * cycle timestamps are used as fallbacks if event timestamps cannot be used
118
- */
119
- this.cycleTimestamp = undefined;
120
-
121
- /** A value which increments with every new mutation node reported. Resets after a harvest is sent */
122
- this.payloadBytesEstimation = 0;
123
-
124
- /** 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 */
125
- this.lastMeta = undefined;
48
+ /** the mode to start in. Defaults to off */
49
+ const {
50
+ session
51
+ } = (0, _config.getRuntime)(this.agentIdentifier);
52
+ this.mode = session.state.sessionReplayMode || _constants3.MODE.OFF;
126
53
 
127
54
  /** set by BCS response */
128
55
  this.entitled = false;
56
+ this.recorder = args?.recorder;
57
+ if (this.recorder) this.recorder.parent = this;
129
58
  const shouldSetup = (0, _config.getConfigurationValue)(agentIdentifier, 'privacy.cookies_enabled') === true && (0, _config.getConfigurationValue)(agentIdentifier, 'session_trace.enabled') === true;
130
-
131
- /** The method to stop recording. This defaults to a noop, but is overwritten once the recording library is imported and initialized */
132
- this.stopRecording = () => {/* no-op until set by rrweb initializer */};
133
59
  if (shouldSetup) {
134
60
  // The SessionEntity class can emit a message indicating the session was cleared and reset (expiry, inactivity). This feature must abort and never resume if that occurs.
135
- this.ee.on(_sessionEntity.SESSION_EVENTS.RESET, () => {
136
- this.abort(ABORT_REASONS.RESET);
61
+ this.ee.on(_constants3.SESSION_EVENTS.RESET, () => {
62
+ this.scheduler.runHarvest();
63
+ this.abort(_constants.ABORT_REASONS.RESET);
137
64
  });
138
65
 
139
66
  // The SessionEntity class can emit a message indicating the session was paused (visibility change). This feature must stop recording if that occurs.
140
- this.ee.on(_sessionEntity.SESSION_EVENTS.PAUSE, () => {
141
- this.stopRecording();
67
+ this.ee.on(_constants3.SESSION_EVENTS.PAUSE, () => {
68
+ this.recorder?.stopRecording();
142
69
  });
143
70
  // The SessionEntity class can emit a message indicating the session was resumed (visibility change). This feature must start running again (if already running) if that occurs.
144
- this.ee.on(_sessionEntity.SESSION_EVENTS.RESUME, () => {
71
+ this.ee.on(_constants3.SESSION_EVENTS.RESUME, () => {
72
+ if (!this.recorder) return;
145
73
  // if the mode changed on a different tab, it needs to update this instance to match
146
74
  const {
147
75
  session
148
76
  } = (0, _config.getRuntime)(this.agentIdentifier);
149
77
  this.mode = session.state.sessionReplayMode;
150
- if (!this.initialized || this.mode === _sessionEntity.MODE.OFF) return;
151
- this.startRecording();
78
+ if (!this.initialized || this.mode === _constants3.MODE.OFF) return;
79
+ this.recorder?.startRecording();
152
80
  });
153
- this.ee.on(_sessionEntity.SESSION_EVENTS.UPDATE, (type, data) => {
154
- if (!this.initialized || this.blocked || type !== _sessionEntity.SESSION_EVENT_TYPES.CROSS_TAB) return;
155
- if (this.mode !== _sessionEntity.MODE.OFF && data.sessionReplayMode === _sessionEntity.MODE.OFF) this.abort(ABORT_REASONS.CROSS_TAB);
81
+ this.ee.on(_constants3.SESSION_EVENTS.UPDATE, (type, data) => {
82
+ if (!this.recorder || !this.initialized || this.blocked || type !== _constants3.SESSION_EVENT_TYPES.CROSS_TAB) return;
83
+ if (this.mode !== _constants3.MODE.OFF && data.sessionReplayMode === _constants3.MODE.OFF) this.abort(_constants.ABORT_REASONS.CROSS_TAB);
156
84
  this.mode = data.sessionReplay;
157
85
  });
158
86
 
159
- // Bespoke logic for new endpoint. This will change as downstream dependencies become solidified.
87
+ // Bespoke logic for blobs endpoint.
160
88
  this.scheduler = new _harvestScheduler.HarvestScheduler('browser/blobs', {
161
89
  onFinished: this.onHarvestFinished.bind(this),
162
90
  retryDelay: this.harvestTimeSeconds,
@@ -167,28 +95,29 @@ class Aggregate extends _aggregateBase.AggregateBase {
167
95
  // if it has aborted or BCS returned bad entitlements, do not allow
168
96
  if (this.blocked || !this.entitled) return;
169
97
  // if it isnt already (fully) initialized... initialize it
170
- if (!recorder) this.initializeRecording(false, true, true);
98
+ if (!this.recorder) this.initializeRecording(false, true, true);
171
99
  // its been initialized and imported the recorder but its not recording (mode === off || error)
172
- else if (this.mode !== _sessionEntity.MODE.FULL) this.switchToFull();
100
+ else if (this.mode !== _constants3.MODE.FULL) this.switchToFull();
173
101
  // if it gets all the way to here, that means a full session is already recording... do nothing
174
102
  }, this.featureName, this.ee);
175
103
  (0, _registerHandler.registerHandler)('pauseReplay', () => {
176
- this.forceStop(this.mode !== _sessionEntity.MODE.ERROR);
104
+ this.forceStop(this.mode !== _constants3.MODE.ERROR);
177
105
  }, this.featureName, this.ee);
178
106
 
179
107
  // Wait for an error to be reported. This currently is wrapped around the "Error" feature. This is a feature-feature dependency.
180
108
  // This was to ensure that all errors, including those on the page before load and those handled with "noticeError" are accounted for. Needs evalulation
181
109
  (0, _registerHandler.registerHandler)('errorAgg', e => {
182
- this.hasError = true;
183
110
  this.errorNoticed = true;
111
+ if (this.recorder) this.recorder.currentBufferTarget.hasError = true;
184
112
  // run once
185
- if (this.mode === _sessionEntity.MODE.ERROR && _runtime.globalScope?.document.visibilityState === 'visible') {
113
+ if (this.mode === _constants3.MODE.ERROR && _runtime.globalScope?.document.visibilityState === 'visible') {
186
114
  this.switchToFull();
187
115
  }
188
116
  }, this.featureName, this.ee);
189
117
  this.waitForFlags(['sr']).then(_ref => {
190
118
  let [flagOn] = _ref;
191
119
  this.entitled = flagOn;
120
+ if (!this.entitled && this.recorder?.recording) this.recorder.abort(_constants.ABORT_REASONS.ENTITLEMENTS);
192
121
  this.initializeRecording(Math.random() * 100 < (0, _config.getConfigurationValue)(this.agentIdentifier, 'session_replay.error_sampling_rate'), Math.random() * 100 < (0, _config.getConfigurationValue)(this.agentIdentifier, 'session_replay.sampling_rate'));
193
122
  }).then(() => _sharedChannel.sharedChannel.onReplayReady(this.mode)); // notify watchers that replay started with the mode
194
123
 
@@ -196,11 +125,11 @@ class Aggregate extends _aggregateBase.AggregateBase {
196
125
  }
197
126
  }
198
127
  switchToFull() {
199
- this.mode = _sessionEntity.MODE.FULL;
128
+ this.mode = _constants3.MODE.FULL;
200
129
  // if the error was noticed AFTER the recorder was already imported....
201
- if (recorder && this.initialized) {
202
- this.stopRecording();
203
- this.startRecording();
130
+ if (this.recorder && this.initialized) {
131
+ this.recorder.stopRecording();
132
+ this.recorder.startRecording();
204
133
  this.scheduler.startTimer(this.harvestTimeSeconds);
205
134
  this.syncWithSessionManager({
206
135
  sessionReplayMode: this.mode
@@ -218,42 +147,51 @@ class Aggregate extends _aggregateBase.AggregateBase {
218
147
  */
219
148
  async initializeRecording(errorSample, fullSample, ignoreSession) {
220
149
  this.initialized = true;
221
- if (!this.entitled || this.recording) return;
222
- const {
223
- session
224
- } = (0, _config.getRuntime)(this.agentIdentifier);
150
+ if (!this.entitled) return;
151
+
225
152
  // if theres an existing session replay in progress, there's no need to sample, just check the entitlements response
226
153
  // if not, these sample flags need to be checked
227
154
  // if this isnt the FIRST load of a session AND
228
155
  // we are not actively recording SR... DO NOT import or run the recording library
229
156
  // session replay samples can only be decided on the first load of a session
230
157
  // session replays can continue if already in progress
158
+ const {
159
+ session
160
+ } = (0, _config.getRuntime)(this.agentIdentifier);
231
161
  if (!session.isNew && !ignoreSession) {
232
162
  // inherit the mode of the existing session
233
163
  this.mode = session.state.sessionReplayMode;
234
164
  } else {
235
165
  // The session is new... determine the mode the new session should start in
236
- if (fullSample) this.mode = _sessionEntity.MODE.FULL; // full mode has precedence over error mode
237
- else if (errorSample) this.mode = _sessionEntity.MODE.ERROR;
166
+ if (fullSample) this.mode = _constants3.MODE.FULL; // full mode has precedence over error mode
167
+ else if (errorSample) this.mode = _constants3.MODE.ERROR;
238
168
  // If neither are selected, then don't record (early return)
239
- else return;
169
+ else {
170
+ return;
171
+ }
172
+ }
173
+ if (!this.recorder) {
174
+ try {
175
+ // Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
176
+ const {
177
+ Recorder
178
+ } = await Promise.resolve().then(() => _interopRequireWildcard(require( /* webpackChunkName: "recorder" */'../shared/recorder')));
179
+ this.recorder = new Recorder(this);
180
+ this.recorder.currentBufferTarget.hasError = this.errorNoticed;
181
+ } catch (err) {
182
+ return this.abort(_constants.ABORT_REASONS.IMPORT);
183
+ }
240
184
  }
241
185
 
242
186
  // If an error was noticed before the mode could be set (like in the early lifecycle of the page), immediately set to FULL mode
243
- if (this.mode === _sessionEntity.MODE.ERROR && this.errorNoticed) {
244
- this.mode = _sessionEntity.MODE.FULL;
245
- }
246
- try {
247
- // Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
248
- recorder = (await Promise.resolve().then(() => _interopRequireWildcard(require( /* webpackChunkName: "recorder" */'rrweb')))).record;
249
- } catch (err) {
250
- return this.abort(ABORT_REASONS.IMPORT);
187
+ if (this.mode === _constants3.MODE.ERROR && this.errorNoticed) {
188
+ this.mode = _constants3.MODE.FULL;
251
189
  }
252
190
 
253
191
  // FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
254
192
  // ERROR mode will do this until an error is thrown, and then switch into FULL mode.
255
193
  // If an error happened in ERROR mode before we've gotten to this stage, it will have already set the mode to FULL
256
- if (this.mode === _sessionEntity.MODE.FULL) {
194
+ if (this.mode === _constants3.MODE.FULL && !this.scheduler.started) {
257
195
  // We only report (harvest) in FULL mode
258
196
  this.scheduler.startTimer(this.harvestTimeSeconds);
259
197
  }
@@ -269,24 +207,41 @@ class Aggregate extends _aggregateBase.AggregateBase {
269
207
  // compressor failed to load, but we can still record without compression as a last ditch effort
270
208
  this.shouldCompress = false;
271
209
  }
272
- this.startRecording();
210
+ if (!this.recorder.recording) this.recorder.startRecording();
273
211
  this.syncWithSessionManager({
274
212
  sessionReplayMode: this.mode
275
213
  });
276
214
  }
277
215
  prepareHarvest() {
278
- if (this.events.length === 0 || this.mode !== _sessionEntity.MODE.FULL && !this.blocked) return;
279
- const payload = this.getHarvestContents();
216
+ if (!this.recorder) return;
217
+ const recorderEvents = this.recorder.getEvents();
218
+ // get the event type and use that to trigger another harvest if needed
219
+ if (!recorderEvents.events.length || this.mode !== _constants3.MODE.FULL || this.blocked) return;
220
+ const payload = this.getHarvestContents(recorderEvents);
280
221
  if (!payload.body.length) {
281
- this.clearBuffer();
222
+ this.recorder.clearBuffer();
282
223
  return;
283
224
  }
225
+ let len = 0;
284
226
  if (this.shouldCompress) {
285
- payload.body = gzipper(u8((0, _stringify.stringify)(payload.body)));
227
+ payload.body = gzipper(u8("[".concat(payload.body.map(e => e.__serialized).join(','), "]")));
228
+ len = payload.body.length;
286
229
  this.scheduler.opts.gzip = true;
287
230
  } else {
231
+ payload.body = payload.body.map(_ref2 => {
232
+ let {
233
+ __serialized,
234
+ ...node
235
+ } = _ref2;
236
+ return node;
237
+ });
238
+ len = (0, _stringify.stringify)(payload.body).length;
288
239
  this.scheduler.opts.gzip = false;
289
240
  }
241
+ if (len > _constants.MAX_PAYLOAD_SIZE) {
242
+ this.abort(_constants.ABORT_REASONS.TOO_BIG);
243
+ return;
244
+ }
290
245
  // TODO -- Gracefully handle the buffer for retries.
291
246
  const {
292
247
  session
@@ -294,37 +249,39 @@ class Aggregate extends _aggregateBase.AggregateBase {
294
249
  if (!session.state.sessionReplaySentFirstChunk) this.syncWithSessionManager({
295
250
  sessionReplaySentFirstChunk: true
296
251
  });
297
- this.clearBuffer();
252
+ this.recorder.clearBuffer();
253
+ if (recorderEvents.type === 'preloaded') this.scheduler.runHarvest();
298
254
  return [payload];
299
255
  }
300
- getHarvestContents() {
256
+ getHarvestContents(recorderEvents) {
257
+ recorderEvents ??= this.recorder.getEvents();
258
+ let events = recorderEvents.events;
301
259
  const agentRuntime = (0, _config.getRuntime)(this.agentIdentifier);
302
260
  const info = (0, _config.getInfo)(this.agentIdentifier);
303
261
  const endUserId = info.jsAttributes?.['enduser.id'];
304
- if (this.backloggedEvents.length) this.events = [...this.backloggedEvents, ...this.events];
305
262
 
306
263
  // do not let the first node be a full snapshot node, since this NEEDS to be preceded by a meta node
307
264
  // we will manually inject it if this happens
308
- const payloadStartsWithFullSnapshot = this.events[0]?.type === RRWEB_EVENT_TYPES.FullSnapshot;
309
- if (payloadStartsWithFullSnapshot && !!this.lastMeta) {
310
- this.hasMeta = true;
311
- this.events.unshift(this.lastMeta); // --> pushed the meta from a previous payload into newer payload... but it still has old timestamps
312
- this.lastMeta = undefined;
265
+ const payloadStartsWithFullSnapshot = events?.[0]?.type === _constants.RRWEB_EVENT_TYPES.FullSnapshot;
266
+ if (payloadStartsWithFullSnapshot && !!this.recorder.lastMeta) {
267
+ recorderEvents.hasMeta = true;
268
+ events.unshift(this.recorder.lastMeta); // --> pushed the meta from a previous payload into newer payload... but it still has old timestamps
269
+ this.recorder.lastMeta = undefined;
313
270
  }
314
271
 
315
272
  // do not let the last node be a meta node, since this NEEDS to precede a snapshot
316
273
  // we will manually inject it later if we find a payload that is missing a meta node
317
- const payloadEndsWithMeta = this.events[this.events.length - 1]?.type === RRWEB_EVENT_TYPES.Meta;
274
+ const payloadEndsWithMeta = events[events.length - 1]?.type === _constants.RRWEB_EVENT_TYPES.Meta;
318
275
  if (payloadEndsWithMeta) {
319
- this.lastMeta = this.events[this.events.length - 1];
320
- this.events = this.events.slice(0, this.events.length - 1);
321
- this.hasMeta = !!this.events.find(x => x.type === RRWEB_EVENT_TYPES.Meta);
276
+ this.recorder.lastMeta = events[events.length - 1];
277
+ events = events.slice(0, events.length - 1);
278
+ recorderEvents.hasMeta = !!events.find(x => x.type === _constants.RRWEB_EVENT_TYPES.Meta);
322
279
  }
323
280
  const agentOffset = (0, _config.getRuntime)(this.agentIdentifier).offset;
324
281
  const relativeNow = (0, _now.now)();
325
- const firstEventTimestamp = this.events[0]?.timestamp; // from rrweb node
326
- const lastEventTimestamp = this.events[this.events.length - 1]?.timestamp; // from rrweb node
327
- const firstTimestamp = firstEventTimestamp || this.cycleTimestamp;
282
+ const firstEventTimestamp = events[0]?.timestamp; // from rrweb node
283
+ const lastEventTimestamp = events[events.length - 1]?.timestamp; // from rrweb node
284
+ const firstTimestamp = firstEventTimestamp || recorderEvents.cycleTimestamp;
328
285
  const lastTimestamp = lastEventTimestamp || agentOffset + relativeNow;
329
286
  return {
330
287
  qs: {
@@ -342,148 +299,36 @@ class Aggregate extends _aggregateBase.AggregateBase {
342
299
  'replay.firstTimestampOffset': firstTimestamp - agentOffset,
343
300
  'replay.lastTimestamp': lastTimestamp,
344
301
  'replay.durationMs': lastTimestamp - firstTimestamp,
345
- 'replay.nodes': this.events.length,
302
+ 'replay.nodes': events.length,
346
303
  'session.durationMs': agentRuntime.session.getDuration(),
347
304
  agentVersion: agentRuntime.version,
348
305
  session: agentRuntime.session.state.value,
349
306
  rst: relativeNow,
350
- hasMeta: this.hasMeta,
351
- hasSnapshot: this.hasSnapshot,
352
- hasError: this.hasError,
307
+ hasMeta: recorderEvents.hasMeta || false,
308
+ hasSnapshot: recorderEvents.hasSnapshot || false,
309
+ hasError: recorderEvents.hasError || false,
353
310
  isFirstChunk: agentRuntime.session.state.sessionReplaySentFirstChunk === false,
354
- decompressedBytes: this.payloadBytesEstimation,
311
+ decompressedBytes: recorderEvents.payloadBytesEstimation,
355
312
  'rrweb.version': _env.RRWEB_VERSION,
356
313
  // customer-defined data should go last so that if it exceeds the query param padding limit it will be truncated instead of important attrs
357
314
  ...(endUserId && {
358
315
  'enduser.id': endUserId
359
316
  })
360
317
  // The Query Param is being arbitrarily limited in length here. It is also applied when estimating the size of the payload in getPayloadSize()
361
- }, QUERY_PARAM_PADDING).substring(1) // remove the leading '&'
318
+ }, _constants.QUERY_PARAM_PADDING).substring(1) // remove the leading '&'
362
319
  },
363
320
 
364
- body: this.events
321
+ body: events
365
322
  };
366
323
  }
367
324
  onHarvestFinished(result) {
368
325
  // The mutual decision for now is to stop recording and clear buffers if ingest is experiencing 429 rate limiting
369
326
  if (result.status === 429) {
370
- this.abort(ABORT_REASONS.TOO_MANY);
327
+ this.abort(_constants.ABORT_REASONS.TOO_MANY);
371
328
  }
372
329
  if (this.blocked) this.scheduler.stopTimer(true);
373
330
  }
374
331
 
375
- /** Clears the buffer (this.events), and resets all payload metadata properties */
376
- clearBuffer() {
377
- if (this.mode === _sessionEntity.MODE.ERROR) this.backloggedEvents = this.events;else this.backloggedEvents = [];
378
- this.events = [];
379
- this.hasSnapshot = false;
380
- this.hasMeta = false;
381
- this.hasError = false;
382
- this.payloadBytesEstimation = 0;
383
- this.clearTimestamps();
384
- }
385
-
386
- /** Begin recording using configured recording lib */
387
- startRecording() {
388
- if (!recorder) {
389
- (0, _console.warn)('Recording library was never imported');
390
- return this.abort(ABORT_REASONS.IMPORT);
391
- }
392
- this.recording = true;
393
- const {
394
- block_class,
395
- ignore_class,
396
- mask_text_class,
397
- block_selector,
398
- mask_input_options,
399
- mask_text_selector,
400
- mask_all_inputs,
401
- inline_images,
402
- inline_stylesheet,
403
- collect_fonts
404
- } = (0, _config.getConfigurationValue)(this.agentIdentifier, 'session_replay');
405
- // set up rrweb configurations for maximum privacy --
406
- // https://newrelic.atlassian.net/wiki/spaces/O11Y/pages/2792293280/2023+02+28+Browser+-+Session+Replay#Configuration-options
407
- const stop = recorder({
408
- emit: this.store.bind(this),
409
- blockClass: block_class,
410
- ignoreClass: ignore_class,
411
- maskTextClass: mask_text_class,
412
- blockSelector: block_selector,
413
- maskInputOptions: mask_input_options,
414
- maskTextSelector: mask_text_selector,
415
- maskAllInputs: mask_all_inputs,
416
- inlineImages: inline_images,
417
- inlineStylesheet: inline_stylesheet,
418
- collectFonts: collect_fonts,
419
- checkoutEveryNms: CHECKOUT_MS[this.mode]
420
- });
421
- this.stopRecording = () => {
422
- this.recording = false;
423
- stop();
424
- };
425
- }
426
-
427
- /** Store a payload in the buffer (this.events). This should be the callback to the recording lib noticing a mutation */
428
- store(event, isCheckout) {
429
- this.setTimestamps();
430
- if (this.blocked) return;
431
- const eventBytes = (0, _stringify.stringify)(event).length;
432
- /** The estimated size of the payload after compression */
433
- const payloadSize = this.getPayloadSize(eventBytes);
434
- // Vortex will block payloads at a certain size, we might as well not send.
435
- if (payloadSize > MAX_PAYLOAD_SIZE) {
436
- this.clearBuffer();
437
- return this.abort(ABORT_REASONS.TOO_BIG);
438
- }
439
- // Checkout events are flags by the recording lib that indicate a fullsnapshot was taken every n ms. These are important
440
- // to help reconstruct the replay later and must be included. While waiting and buffering for errors to come through,
441
- // each time we see a new checkout, we can drop the old data.
442
- // we need to check for meta because rrweb will flag it as checkout twice, once for meta, then once for snapshot
443
- if (this.mode === _sessionEntity.MODE.ERROR && isCheckout && event.type === RRWEB_EVENT_TYPES.Meta) {
444
- // we are still waiting for an error to throw, so keep wiping the buffer over time
445
- this.clearBuffer();
446
- }
447
-
448
- // meta event
449
- if (event.type === RRWEB_EVENT_TYPES.Meta) {
450
- this.hasMeta = true;
451
- }
452
- // snapshot event
453
- if (event.type === RRWEB_EVENT_TYPES.FullSnapshot) {
454
- this.hasSnapshot = true;
455
- }
456
- this.events.push(event);
457
- this.payloadBytesEstimation += eventBytes;
458
-
459
- // We are making an effort to try to keep payloads manageable for unloading. If they reach the unload limit before their interval,
460
- // it will send immediately. This often happens on the first snapshot, which can be significantly larger than the other payloads.
461
- if (payloadSize > IDEAL_PAYLOAD_SIZE && this.mode !== _sessionEntity.MODE.ERROR) {
462
- // if we've made it to the ideal size of ~64kb before the interval timer, we should send early.
463
- this.scheduler.runHarvest();
464
- }
465
- }
466
-
467
- /** force the recording lib to take a full DOM snapshot. This needs to occur in certain cases, like visibility changes */
468
- takeFullSnapshot() {
469
- if (!recorder) return;
470
- recorder.takeFullSnapshot();
471
- }
472
- setTimestamps() {
473
- // fallbacks if timestamps cannot be derived from rrweb events
474
- if (!this.cycleTimestamp) this.cycleTimestamp = (0, _config.getRuntime)(this.agentIdentifier).offset + _runtime.globalScope.performance.now();
475
- }
476
- clearTimestamps() {
477
- this.cycleTimestamp = undefined;
478
- }
479
-
480
- /** Estimate the payload size */
481
- getPayloadSize() {
482
- let newBytes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
483
- // the query param padding constant gives us some padding for the other metadata to be safely injected
484
- return this.estimateCompression(this.payloadBytesEstimation + newBytes) + QUERY_PARAM_PADDING;
485
- }
486
-
487
332
  /**
488
333
  * Forces the agent into OFF mode so that changing tabs or navigating
489
334
  * does not restart the recording. This is used when the customer calls
@@ -491,8 +336,8 @@ class Aggregate extends _aggregateBase.AggregateBase {
491
336
  */
492
337
  forceStop(forceHarvest) {
493
338
  if (forceHarvest) this.scheduler.runHarvest();
494
- this.mode = _sessionEntity.MODE.OFF;
495
- this.stopRecording();
339
+ this.mode = _constants3.MODE.OFF;
340
+ this.recorder?.stopRecording?.();
496
341
  this.syncWithSessionManager({
497
342
  sessionReplayMode: this.mode
498
343
  });
@@ -504,22 +349,14 @@ class Aggregate extends _aggregateBase.AggregateBase {
504
349
  (0, _console.warn)("SR aborted -- ".concat(reason.message));
505
350
  (0, _handle.handle)(_constants2.SUPPORTABILITY_METRIC_CHANNEL, ["SessionReplay/Abort/".concat(reason.sm)], undefined, _features.FEATURE_NAMES.metrics, this.ee);
506
351
  this.blocked = true;
507
- this.mode = _sessionEntity.MODE.OFF;
508
- this.stopRecording();
352
+ this.mode = _constants3.MODE.OFF;
353
+ this.recorder?.stopRecording?.();
509
354
  this.syncWithSessionManager({
510
355
  sessionReplayMode: this.mode
511
356
  });
512
- this.clearTimestamps();
357
+ this.recorder?.clearTimestamps?.();
513
358
  this.ee.emit('REPLAY_ABORTED');
514
- }
515
-
516
- /** Extensive research has yielded about an 88% compression factor on these payloads.
517
- * This is an estimation using that factor as to not cause performance issues while evaluating
518
- * https://staging.onenr.io/037jbJWxbjy
519
- * */
520
- estimateCompression(data) {
521
- if (this.shouldCompress) return data * AVG_COMPRESSION;
522
- return data;
359
+ this.recorder?.clearBuffer?.();
523
360
  }
524
361
  syncWithSessionManager() {
525
362
  let state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
@@ -3,7 +3,62 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.FEATURE_NAME = void 0;
6
+ exports.RRWEB_EVENT_TYPES = exports.QUERY_PARAM_PADDING = exports.MAX_PAYLOAD_SIZE = exports.IDEAL_PAYLOAD_SIZE = exports.FEATURE_NAME = exports.CHECKOUT_MS = exports.AVG_COMPRESSION = exports.ABORT_REASONS = void 0;
7
+ var _constants = require("../../common/session/constants");
7
8
  var _features = require("../../loaders/features/features");
8
9
  const FEATURE_NAME = _features.FEATURE_NAMES.sessionReplay;
9
- exports.FEATURE_NAME = FEATURE_NAME;
10
+ exports.FEATURE_NAME = FEATURE_NAME;
11
+ const AVG_COMPRESSION = 0.12;
12
+ exports.AVG_COMPRESSION = AVG_COMPRESSION;
13
+ const RRWEB_EVENT_TYPES = {
14
+ DomContentLoaded: 0,
15
+ Load: 1,
16
+ FullSnapshot: 2,
17
+ IncrementalSnapshot: 3,
18
+ Meta: 4,
19
+ Custom: 5
20
+ };
21
+ /** Vortex caps payload sizes at 1MB */
22
+ exports.RRWEB_EVENT_TYPES = RRWEB_EVENT_TYPES;
23
+ const MAX_PAYLOAD_SIZE = 1000000;
24
+ /** Unloading caps around 64kb */
25
+ exports.MAX_PAYLOAD_SIZE = MAX_PAYLOAD_SIZE;
26
+ const IDEAL_PAYLOAD_SIZE = 64000;
27
+ /** Interval between forcing new full snapshots -- 15 seconds in error mode (x2), 5 minutes in full mode */
28
+ exports.IDEAL_PAYLOAD_SIZE = IDEAL_PAYLOAD_SIZE;
29
+ const CHECKOUT_MS = {
30
+ [_constants.MODE.ERROR]: 15000,
31
+ [_constants.MODE.FULL]: 300000,
32
+ [_constants.MODE.OFF]: 0
33
+ };
34
+ exports.CHECKOUT_MS = CHECKOUT_MS;
35
+ const ABORT_REASONS = {
36
+ RESET: {
37
+ message: 'Session was reset',
38
+ sm: 'Reset'
39
+ },
40
+ IMPORT: {
41
+ message: 'Recorder failed to import',
42
+ sm: 'Import'
43
+ },
44
+ TOO_MANY: {
45
+ message: '429: Too Many Requests',
46
+ sm: 'Too-Many'
47
+ },
48
+ TOO_BIG: {
49
+ message: 'Payload was too large',
50
+ sm: 'Too-Big'
51
+ },
52
+ CROSS_TAB: {
53
+ message: 'Session Entity was set to OFF on another tab',
54
+ sm: 'Cross-Tab'
55
+ },
56
+ ENTITLEMENTS: {
57
+ message: 'Session Replay is not allowed and will not be started',
58
+ sm: 'Entitlement'
59
+ }
60
+ };
61
+ /** Reserved room for query param attrs */
62
+ exports.ABORT_REASONS = ABORT_REASONS;
63
+ const QUERY_PARAM_PADDING = 5000;
64
+ exports.QUERY_PARAM_PADDING = QUERY_PARAM_PADDING;