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