@newrelic/browser-agent 1.249.0 → 1.251.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 (143) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/cjs/common/config/state/init.js +1 -3
  3. package/dist/cjs/common/config/state/originals.js +1 -2
  4. package/dist/cjs/common/constants/env.cdn.js +4 -8
  5. package/dist/cjs/common/constants/env.js +4 -8
  6. package/dist/cjs/common/constants/env.npm.js +4 -8
  7. package/dist/cjs/common/constants/runtime.js +13 -24
  8. package/dist/cjs/common/constants/shared-channel.js +2 -3
  9. package/dist/cjs/common/event-emitter/contextual-ee.js +2 -4
  10. package/dist/cjs/common/event-emitter/handle.js +1 -2
  11. package/dist/cjs/common/harvest/harvest-scheduler.js +4 -4
  12. package/dist/cjs/common/harvest/harvest.js +6 -5
  13. package/dist/cjs/common/harvest/types.js +1 -2
  14. package/dist/cjs/common/ids/bundle-id.js +1 -2
  15. package/dist/cjs/common/ids/unique-id.js +1 -1
  16. package/dist/cjs/common/session/constants.js +19 -7
  17. package/dist/cjs/common/session/session-entity.js +8 -26
  18. package/dist/cjs/common/timer/interaction-timer.js +0 -1
  19. package/dist/cjs/common/timing/nav-timing.js +1 -2
  20. package/dist/cjs/common/url/encode.js +2 -0
  21. package/dist/cjs/common/util/feature-flags.js +1 -2
  22. package/dist/cjs/common/vitals/constants.js +2 -3
  23. package/dist/cjs/common/vitals/cumulative-layout-shift.js +1 -2
  24. package/dist/cjs/common/vitals/first-contentful-paint.js +1 -2
  25. package/dist/cjs/common/vitals/first-input-delay.js +1 -2
  26. package/dist/cjs/common/vitals/first-paint.js +1 -2
  27. package/dist/cjs/common/vitals/interaction-to-next-paint.js +1 -2
  28. package/dist/cjs/common/vitals/largest-contentful-paint.js +1 -2
  29. package/dist/cjs/common/vitals/long-task.js +1 -3
  30. package/dist/cjs/common/vitals/time-to-first-byte.js +1 -2
  31. package/dist/cjs/common/window/nreum.js +1 -2
  32. package/dist/cjs/common/wrap/wrap-function.js +2 -4
  33. package/dist/cjs/features/ajax/aggregate/index.js +0 -2
  34. package/dist/cjs/features/ajax/constants.js +1 -2
  35. package/dist/cjs/features/jserrors/aggregate/index.js +0 -1
  36. package/dist/cjs/features/jserrors/aggregate/string-hash-code.js +0 -1
  37. package/dist/cjs/features/jserrors/constants.js +1 -2
  38. package/dist/cjs/features/metrics/aggregate/index.js +3 -1
  39. package/dist/cjs/features/metrics/constants.js +5 -10
  40. package/dist/cjs/features/page_action/constants.js +1 -2
  41. package/dist/cjs/features/page_view_event/aggregate/index.js +2 -2
  42. package/dist/cjs/features/page_view_event/constants.js +1 -2
  43. package/dist/cjs/features/page_view_event/instrument/index.js +2 -2
  44. package/dist/cjs/features/page_view_timing/constants.js +1 -2
  45. package/dist/cjs/features/session_replay/aggregate/index.js +128 -289
  46. package/dist/cjs/features/session_replay/constants.js +50 -3
  47. package/dist/cjs/features/session_replay/instrument/index.js +30 -8
  48. package/dist/cjs/features/session_replay/shared/recorder-events.js +33 -0
  49. package/dist/cjs/features/session_replay/shared/recorder.js +201 -0
  50. package/dist/cjs/features/session_replay/{replay-mode.js → shared/replay-mode.js} +5 -5
  51. package/dist/cjs/features/session_replay/shared/stylesheet-evaluator.js +94 -0
  52. package/dist/cjs/features/session_trace/aggregate/index.js +25 -27
  53. package/dist/cjs/features/session_trace/constants.js +8 -16
  54. package/dist/cjs/features/session_trace/instrument/index.js +2 -2
  55. package/dist/cjs/features/spa/aggregate/index.js +2 -2
  56. package/dist/cjs/features/spa/aggregate/interaction-node.js +0 -1
  57. package/dist/cjs/features/spa/constants.js +22 -44
  58. package/dist/cjs/features/spa/instrument/index.js +2 -2
  59. package/dist/cjs/features/utils/instrument-base.js +6 -7
  60. package/dist/cjs/features/utils/lazy-feature-loader.js +2 -2
  61. package/dist/cjs/loaders/agent-base.js +61 -15
  62. package/dist/cjs/loaders/agent.js +0 -38
  63. package/dist/cjs/loaders/api/api.js +7 -7
  64. package/dist/cjs/loaders/api/interaction-types.js +1 -2
  65. package/dist/cjs/loaders/features/features.js +3 -5
  66. package/dist/cjs/loaders/micro-agent.js +2 -2
  67. package/dist/esm/common/config/state/init.js +1 -3
  68. package/dist/esm/common/constants/env.cdn.js +1 -1
  69. package/dist/esm/common/constants/env.npm.js +1 -1
  70. package/dist/esm/common/harvest/harvest-scheduler.js +1 -1
  71. package/dist/esm/common/harvest/harvest.js +4 -3
  72. package/dist/esm/common/ids/unique-id.js +1 -1
  73. package/dist/esm/common/session/constants.js +16 -1
  74. package/dist/esm/common/session/session-entity.js +2 -16
  75. package/dist/esm/common/timer/interaction-timer.js +0 -1
  76. package/dist/esm/common/url/encode.js +2 -0
  77. package/dist/esm/common/vitals/long-task.js +0 -1
  78. package/dist/esm/features/ajax/aggregate/index.js +0 -2
  79. package/dist/esm/features/jserrors/aggregate/index.js +0 -1
  80. package/dist/esm/features/jserrors/aggregate/string-hash-code.js +0 -1
  81. package/dist/esm/features/metrics/aggregate/index.js +3 -1
  82. package/dist/esm/features/session_replay/aggregate/index.js +98 -255
  83. package/dist/esm/features/session_replay/constants.js +49 -1
  84. package/dist/esm/features/session_replay/instrument/index.js +24 -1
  85. package/dist/esm/features/session_replay/shared/recorder-events.js +26 -0
  86. package/dist/esm/features/session_replay/shared/recorder.js +194 -0
  87. package/dist/esm/features/session_replay/{replay-mode.js → shared/replay-mode.js} +4 -4
  88. package/dist/esm/features/session_replay/shared/stylesheet-evaluator.js +88 -0
  89. package/dist/esm/features/session_trace/aggregate/index.js +2 -4
  90. package/dist/esm/features/spa/aggregate/interaction-node.js +0 -1
  91. package/dist/esm/features/utils/instrument-base.js +0 -1
  92. package/dist/esm/loaders/agent-base.js +62 -15
  93. package/dist/esm/loaders/agent.js +0 -38
  94. package/dist/types/common/harvest/harvest.d.ts +1 -1
  95. package/dist/types/common/harvest/harvest.d.ts.map +1 -1
  96. package/dist/types/common/session/constants.d.ts +15 -0
  97. package/dist/types/common/session/session-entity.d.ts +0 -15
  98. package/dist/types/common/session/session-entity.d.ts.map +1 -1
  99. package/dist/types/common/url/encode.d.ts +1 -1
  100. package/dist/types/common/url/encode.d.ts.map +1 -1
  101. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  102. package/dist/types/features/session_replay/aggregate/index.d.ts +7 -63
  103. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  104. package/dist/types/features/session_replay/constants.d.ts +55 -0
  105. package/dist/types/features/session_replay/constants.d.ts.map +1 -1
  106. package/dist/types/features/session_replay/instrument/index.d.ts +2 -0
  107. package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
  108. package/dist/types/features/session_replay/shared/recorder-events.d.ts +23 -0
  109. package/dist/types/features/session_replay/shared/recorder-events.d.ts.map +1 -0
  110. package/dist/types/features/session_replay/shared/recorder.d.ts +53 -0
  111. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -0
  112. package/dist/types/features/session_replay/shared/replay-mode.d.ts.map +1 -0
  113. package/dist/types/features/session_replay/shared/stylesheet-evaluator.d.ts +22 -0
  114. package/dist/types/features/session_replay/shared/stylesheet-evaluator.d.ts.map +1 -0
  115. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  116. package/dist/types/loaders/agent-base.d.ts +48 -12
  117. package/dist/types/loaders/agent-base.d.ts.map +1 -1
  118. package/dist/types/loaders/agent.d.ts +0 -33
  119. package/dist/types/loaders/agent.d.ts.map +1 -1
  120. package/package.json +49 -49
  121. package/src/common/config/state/init.js +1 -1
  122. package/src/common/harvest/harvest-scheduler.js +1 -1
  123. package/src/common/harvest/harvest.js +4 -3
  124. package/src/common/ids/__mocks__/bundle-id.js +1 -1
  125. package/src/common/ids/__mocks__/unique-id.js +2 -2
  126. package/src/common/ids/unique-id.js +1 -1
  127. package/src/common/session/__mocks__/session-entity.js +0 -6
  128. package/src/common/session/constants.js +18 -0
  129. package/src/common/session/session-entity.js +1 -17
  130. package/src/common/url/encode.js +2 -1
  131. package/src/features/metrics/aggregate/index.js +3 -1
  132. package/src/features/session_replay/aggregate/index.js +91 -246
  133. package/src/features/session_replay/constants.js +45 -0
  134. package/src/features/session_replay/instrument/index.js +18 -1
  135. package/src/features/session_replay/shared/recorder-events.js +27 -0
  136. package/src/features/session_replay/shared/recorder.js +190 -0
  137. package/src/features/session_replay/{replay-mode.js → shared/replay-mode.js} +4 -4
  138. package/src/features/session_replay/shared/stylesheet-evaluator.js +84 -0
  139. package/src/features/session_trace/aggregate/index.js +2 -2
  140. package/src/loaders/agent-base.js +59 -15
  141. package/src/loaders/agent.js +0 -38
  142. package/dist/types/features/session_replay/replay-mode.d.ts.map +0 -1
  143. /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,143 +18,74 @@ 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");
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
- 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
- * Copyright 2023 New Relic Corporation. All rights reserved.
26
- * SPDX-License-Identifier: Apache-2.0
27
- */ /**
28
- * @file Records, aggregates, and harvests session replay data.
29
- *
30
- * NOTE: This code is under development and dormant. It will not download to instrumented pages or record any data.
31
- * It is not production ready, and is not intended to be imported or implemented in any build of the browser agent until
32
- * functionality is validated and a full user experience is curated.
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
- };
21
+ var _constants3 = require("../../../common/session/constants");
22
+ var _stringify = require("../../../common/util/stringify");
23
+ var _stylesheetEvaluator = require("../shared/stylesheet-evaluator");
24
+ function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
25
+ function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } /*
26
+ * Copyright 2023 New Relic Corporation. All rights reserved.
27
+ * SPDX-License-Identifier: Apache-2.0
28
+ */ /**
29
+ * @file Records, aggregates, and harvests session replay data.
30
+ *
31
+ * NOTE: This code is under development and dormant. It will not download to instrumented pages or record any data.
32
+ * It is not production ready, and is not intended to be imported or implemented in any build of the browser agent until
33
+ * functionality is validated and a full user experience is curated.
34
+ */
35
+ let gzipper, u8;
83
36
  class Aggregate extends _aggregateBase.AggregateBase {
84
37
  static featureName = _constants.FEATURE_NAME;
85
- constructor(agentIdentifier, aggregator) {
38
+ // pass the recorder into the aggregator
39
+ constructor(agentIdentifier, aggregator, args) {
86
40
  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
41
  /** The interval to harvest at. This gets overridden if the size of the payload exceeds certain thresholds */
92
42
  this.harvestTimeSeconds = (0, _config.getConfigurationValue)(this.agentIdentifier, 'session_replay.harvestTimeSeconds') || 60;
93
43
  /** Set once the recorder has fully initialized after flag checks and sampling */
94
44
  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
45
  /** Set once the feature has been "aborted" to prevent other side-effects from continuing */
100
46
  this.blocked = false;
101
- /** True when actively recording, false when paused or stopped */
102
- this.recording = false;
103
47
  /** can shut off efforts to compress the data */
104
48
  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;
49
+ /** the mode to start in. Defaults to off */
50
+ const {
51
+ session
52
+ } = (0, _config.getRuntime)(this.agentIdentifier);
53
+ this.mode = session.state.sessionReplayMode || _constants3.MODE.OFF;
126
54
 
127
55
  /** set by BCS response */
128
56
  this.entitled = false;
57
+ this.recorder = args?.recorder;
58
+ if (this.recorder) this.recorder.parent = this;
129
59
  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
60
  if (shouldSetup) {
134
61
  // 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);
62
+ this.ee.on(_constants3.SESSION_EVENTS.RESET, () => {
63
+ this.scheduler.runHarvest();
64
+ this.abort(_constants.ABORT_REASONS.RESET);
137
65
  });
138
66
 
139
67
  // 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();
68
+ this.ee.on(_constants3.SESSION_EVENTS.PAUSE, () => {
69
+ this.recorder?.stopRecording();
142
70
  });
143
71
  // 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, () => {
72
+ this.ee.on(_constants3.SESSION_EVENTS.RESUME, () => {
73
+ if (!this.recorder) return;
145
74
  // if the mode changed on a different tab, it needs to update this instance to match
146
75
  const {
147
76
  session
148
77
  } = (0, _config.getRuntime)(this.agentIdentifier);
149
78
  this.mode = session.state.sessionReplayMode;
150
- if (!this.initialized || this.mode === _sessionEntity.MODE.OFF) return;
151
- this.startRecording();
79
+ if (!this.initialized || this.mode === _constants3.MODE.OFF) return;
80
+ this.recorder?.startRecording();
152
81
  });
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);
82
+ this.ee.on(_constants3.SESSION_EVENTS.UPDATE, (type, data) => {
83
+ if (!this.recorder || !this.initialized || this.blocked || type !== _constants3.SESSION_EVENT_TYPES.CROSS_TAB) return;
84
+ if (this.mode !== _constants3.MODE.OFF && data.sessionReplayMode === _constants3.MODE.OFF) this.abort(_constants.ABORT_REASONS.CROSS_TAB);
156
85
  this.mode = data.sessionReplay;
157
86
  });
158
87
 
159
- // Bespoke logic for new endpoint. This will change as downstream dependencies become solidified.
88
+ // Bespoke logic for blobs endpoint.
160
89
  this.scheduler = new _harvestScheduler.HarvestScheduler('browser/blobs', {
161
90
  onFinished: this.onHarvestFinished.bind(this),
162
91
  retryDelay: this.harvestTimeSeconds,
@@ -167,28 +96,29 @@ class Aggregate extends _aggregateBase.AggregateBase {
167
96
  // if it has aborted or BCS returned bad entitlements, do not allow
168
97
  if (this.blocked || !this.entitled) return;
169
98
  // if it isnt already (fully) initialized... initialize it
170
- if (!recorder) this.initializeRecording(false, true, true);
99
+ if (!this.recorder) this.initializeRecording(false, true, true);
171
100
  // its been initialized and imported the recorder but its not recording (mode === off || error)
172
- else if (this.mode !== _sessionEntity.MODE.FULL) this.switchToFull();
101
+ else if (this.mode !== _constants3.MODE.FULL) this.switchToFull();
173
102
  // if it gets all the way to here, that means a full session is already recording... do nothing
174
103
  }, this.featureName, this.ee);
175
104
  (0, _registerHandler.registerHandler)('pauseReplay', () => {
176
- this.forceStop(this.mode !== _sessionEntity.MODE.ERROR);
105
+ this.forceStop(this.mode !== _constants3.MODE.ERROR);
177
106
  }, this.featureName, this.ee);
178
107
 
179
108
  // Wait for an error to be reported. This currently is wrapped around the "Error" feature. This is a feature-feature dependency.
180
109
  // 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
110
  (0, _registerHandler.registerHandler)('errorAgg', e => {
182
- this.hasError = true;
183
111
  this.errorNoticed = true;
112
+ if (this.recorder) this.recorder.currentBufferTarget.hasError = true;
184
113
  // run once
185
- if (this.mode === _sessionEntity.MODE.ERROR && _runtime.globalScope?.document.visibilityState === 'visible') {
114
+ if (this.mode === _constants3.MODE.ERROR && _runtime.globalScope?.document.visibilityState === 'visible') {
186
115
  this.switchToFull();
187
116
  }
188
117
  }, this.featureName, this.ee);
189
118
  this.waitForFlags(['sr']).then(_ref => {
190
119
  let [flagOn] = _ref;
191
120
  this.entitled = flagOn;
121
+ if (!this.entitled && this.recorder?.recording) this.recorder.abort(_constants.ABORT_REASONS.ENTITLEMENTS);
192
122
  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
123
  }).then(() => _sharedChannel.sharedChannel.onReplayReady(this.mode)); // notify watchers that replay started with the mode
194
124
 
@@ -196,11 +126,11 @@ class Aggregate extends _aggregateBase.AggregateBase {
196
126
  }
197
127
  }
198
128
  switchToFull() {
199
- this.mode = _sessionEntity.MODE.FULL;
129
+ this.mode = _constants3.MODE.FULL;
200
130
  // if the error was noticed AFTER the recorder was already imported....
201
- if (recorder && this.initialized) {
202
- this.stopRecording();
203
- this.startRecording();
131
+ if (this.recorder && this.initialized) {
132
+ this.recorder.stopRecording();
133
+ this.recorder.startRecording();
204
134
  this.scheduler.startTimer(this.harvestTimeSeconds);
205
135
  this.syncWithSessionManager({
206
136
  sessionReplayMode: this.mode
@@ -218,42 +148,51 @@ class Aggregate extends _aggregateBase.AggregateBase {
218
148
  */
219
149
  async initializeRecording(errorSample, fullSample, ignoreSession) {
220
150
  this.initialized = true;
221
- if (!this.entitled || this.recording) return;
222
- const {
223
- session
224
- } = (0, _config.getRuntime)(this.agentIdentifier);
151
+ if (!this.entitled) return;
152
+
225
153
  // if theres an existing session replay in progress, there's no need to sample, just check the entitlements response
226
154
  // if not, these sample flags need to be checked
227
155
  // if this isnt the FIRST load of a session AND
228
156
  // we are not actively recording SR... DO NOT import or run the recording library
229
157
  // session replay samples can only be decided on the first load of a session
230
158
  // session replays can continue if already in progress
159
+ const {
160
+ session
161
+ } = (0, _config.getRuntime)(this.agentIdentifier);
231
162
  if (!session.isNew && !ignoreSession) {
232
163
  // inherit the mode of the existing session
233
164
  this.mode = session.state.sessionReplayMode;
234
165
  } else {
235
166
  // 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;
167
+ if (fullSample) this.mode = _constants3.MODE.FULL; // full mode has precedence over error mode
168
+ else if (errorSample) this.mode = _constants3.MODE.ERROR;
238
169
  // If neither are selected, then don't record (early return)
239
- else return;
170
+ else {
171
+ return;
172
+ }
173
+ }
174
+ if (!this.recorder) {
175
+ try {
176
+ // Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
177
+ const {
178
+ Recorder
179
+ } = await Promise.resolve().then(() => _interopRequireWildcard(require( /* webpackChunkName: "recorder" */'../shared/recorder')));
180
+ this.recorder = new Recorder(this);
181
+ this.recorder.currentBufferTarget.hasError = this.errorNoticed;
182
+ } catch (err) {
183
+ return this.abort(_constants.ABORT_REASONS.IMPORT);
184
+ }
240
185
  }
241
186
 
242
187
  // 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);
188
+ if (this.mode === _constants3.MODE.ERROR && this.errorNoticed) {
189
+ this.mode = _constants3.MODE.FULL;
251
190
  }
252
191
 
253
192
  // FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
254
193
  // ERROR mode will do this until an error is thrown, and then switch into FULL mode.
255
194
  // 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) {
195
+ if (this.mode === _constants3.MODE.FULL && !this.scheduler.started) {
257
196
  // We only report (harvest) in FULL mode
258
197
  this.scheduler.startTimer(this.harvestTimeSeconds);
259
198
  }
@@ -269,24 +208,41 @@ class Aggregate extends _aggregateBase.AggregateBase {
269
208
  // compressor failed to load, but we can still record without compression as a last ditch effort
270
209
  this.shouldCompress = false;
271
210
  }
272
- this.startRecording();
211
+ if (!this.recorder.recording) this.recorder.startRecording();
273
212
  this.syncWithSessionManager({
274
213
  sessionReplayMode: this.mode
275
214
  });
276
215
  }
277
216
  prepareHarvest() {
278
- if (this.events.length === 0 || this.mode !== _sessionEntity.MODE.FULL && !this.blocked) return;
279
- const payload = this.getHarvestContents();
217
+ if (!this.recorder) return;
218
+ const recorderEvents = this.recorder.getEvents();
219
+ // get the event type and use that to trigger another harvest if needed
220
+ if (!recorderEvents.events.length || this.mode !== _constants3.MODE.FULL || this.blocked) return;
221
+ const payload = this.getHarvestContents(recorderEvents);
280
222
  if (!payload.body.length) {
281
- this.clearBuffer();
223
+ this.recorder.clearBuffer();
282
224
  return;
283
225
  }
226
+ let len = 0;
284
227
  if (this.shouldCompress) {
285
- payload.body = gzipper(u8((0, _stringify.stringify)(payload.body)));
228
+ payload.body = gzipper(u8("[".concat(payload.body.map(e => e.__serialized).join(','), "]")));
229
+ len = payload.body.length;
286
230
  this.scheduler.opts.gzip = true;
287
231
  } else {
232
+ payload.body = payload.body.map(_ref2 => {
233
+ let {
234
+ __serialized,
235
+ ...node
236
+ } = _ref2;
237
+ return node;
238
+ });
239
+ len = (0, _stringify.stringify)(payload.body).length;
288
240
  this.scheduler.opts.gzip = false;
289
241
  }
242
+ if (len > _constants.MAX_PAYLOAD_SIZE) {
243
+ this.abort(_constants.ABORT_REASONS.TOO_BIG);
244
+ return;
245
+ }
290
246
  // TODO -- Gracefully handle the buffer for retries.
291
247
  const {
292
248
  session
@@ -294,37 +250,39 @@ class Aggregate extends _aggregateBase.AggregateBase {
294
250
  if (!session.state.sessionReplaySentFirstChunk) this.syncWithSessionManager({
295
251
  sessionReplaySentFirstChunk: true
296
252
  });
297
- this.clearBuffer();
253
+ this.recorder.clearBuffer();
254
+ if (recorderEvents.type === 'preloaded') this.scheduler.runHarvest();
298
255
  return [payload];
299
256
  }
300
- getHarvestContents() {
257
+ getHarvestContents(recorderEvents) {
258
+ recorderEvents ??= this.recorder.getEvents();
259
+ let events = recorderEvents.events;
301
260
  const agentRuntime = (0, _config.getRuntime)(this.agentIdentifier);
302
261
  const info = (0, _config.getInfo)(this.agentIdentifier);
303
262
  const endUserId = info.jsAttributes?.['enduser.id'];
304
- if (this.backloggedEvents.length) this.events = [...this.backloggedEvents, ...this.events];
305
263
 
306
264
  // do not let the first node be a full snapshot node, since this NEEDS to be preceded by a meta node
307
265
  // 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;
266
+ const payloadStartsWithFullSnapshot = events?.[0]?.type === _constants.RRWEB_EVENT_TYPES.FullSnapshot;
267
+ if (payloadStartsWithFullSnapshot && !!this.recorder.lastMeta) {
268
+ recorderEvents.hasMeta = true;
269
+ events.unshift(this.recorder.lastMeta); // --> pushed the meta from a previous payload into newer payload... but it still has old timestamps
270
+ this.recorder.lastMeta = undefined;
313
271
  }
314
272
 
315
273
  // do not let the last node be a meta node, since this NEEDS to precede a snapshot
316
274
  // 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;
275
+ const payloadEndsWithMeta = events[events.length - 1]?.type === _constants.RRWEB_EVENT_TYPES.Meta;
318
276
  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);
277
+ this.recorder.lastMeta = events[events.length - 1];
278
+ events = events.slice(0, events.length - 1);
279
+ recorderEvents.hasMeta = !!events.find(x => x.type === _constants.RRWEB_EVENT_TYPES.Meta);
322
280
  }
323
281
  const agentOffset = (0, _config.getRuntime)(this.agentIdentifier).offset;
324
282
  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;
283
+ const firstEventTimestamp = events[0]?.timestamp; // from rrweb node
284
+ const lastEventTimestamp = events[events.length - 1]?.timestamp; // from rrweb node
285
+ const firstTimestamp = firstEventTimestamp || recorderEvents.cycleTimestamp;
328
286
  const lastTimestamp = lastEventTimestamp || agentOffset + relativeNow;
329
287
  return {
330
288
  qs: {
@@ -342,148 +300,37 @@ class Aggregate extends _aggregateBase.AggregateBase {
342
300
  'replay.firstTimestampOffset': firstTimestamp - agentOffset,
343
301
  'replay.lastTimestamp': lastTimestamp,
344
302
  'replay.durationMs': lastTimestamp - firstTimestamp,
345
- 'replay.nodes': this.events.length,
303
+ 'replay.nodes': events.length,
346
304
  'session.durationMs': agentRuntime.session.getDuration(),
347
305
  agentVersion: agentRuntime.version,
348
306
  session: agentRuntime.session.state.value,
349
307
  rst: relativeNow,
350
- hasMeta: this.hasMeta,
351
- hasSnapshot: this.hasSnapshot,
352
- hasError: this.hasError,
308
+ hasMeta: recorderEvents.hasMeta || false,
309
+ hasSnapshot: recorderEvents.hasSnapshot || false,
310
+ hasError: recorderEvents.hasError || false,
353
311
  isFirstChunk: agentRuntime.session.state.sessionReplaySentFirstChunk === false,
354
- decompressedBytes: this.payloadBytesEstimation,
312
+ decompressedBytes: recorderEvents.payloadBytesEstimation,
313
+ invalidStylesheetsDetected: _stylesheetEvaluator.stylesheetEvaluator.invalidStylesheetsDetected,
314
+ inlinedAllStylesheets: recorderEvents.inlinedAllStylesheets,
355
315
  'rrweb.version': _env.RRWEB_VERSION,
356
316
  // 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
317
  ...(endUserId && {
358
318
  'enduser.id': endUserId
359
319
  })
360
320
  // 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 '&'
321
+ }, _constants.QUERY_PARAM_PADDING).substring(1) // remove the leading '&'
362
322
  },
363
-
364
- body: this.events
323
+ body: events
365
324
  };
366
325
  }
367
326
  onHarvestFinished(result) {
368
327
  // The mutual decision for now is to stop recording and clear buffers if ingest is experiencing 429 rate limiting
369
328
  if (result.status === 429) {
370
- this.abort(ABORT_REASONS.TOO_MANY);
329
+ this.abort(_constants.ABORT_REASONS.TOO_MANY);
371
330
  }
372
331
  if (this.blocked) this.scheduler.stopTimer(true);
373
332
  }
374
333
 
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
334
  /**
488
335
  * Forces the agent into OFF mode so that changing tabs or navigating
489
336
  * does not restart the recording. This is used when the customer calls
@@ -491,8 +338,8 @@ class Aggregate extends _aggregateBase.AggregateBase {
491
338
  */
492
339
  forceStop(forceHarvest) {
493
340
  if (forceHarvest) this.scheduler.runHarvest();
494
- this.mode = _sessionEntity.MODE.OFF;
495
- this.stopRecording();
341
+ this.mode = _constants3.MODE.OFF;
342
+ this.recorder?.stopRecording?.();
496
343
  this.syncWithSessionManager({
497
344
  sessionReplayMode: this.mode
498
345
  });
@@ -504,22 +351,14 @@ class Aggregate extends _aggregateBase.AggregateBase {
504
351
  (0, _console.warn)("SR aborted -- ".concat(reason.message));
505
352
  (0, _handle.handle)(_constants2.SUPPORTABILITY_METRIC_CHANNEL, ["SessionReplay/Abort/".concat(reason.sm)], undefined, _features.FEATURE_NAMES.metrics, this.ee);
506
353
  this.blocked = true;
507
- this.mode = _sessionEntity.MODE.OFF;
508
- this.stopRecording();
354
+ this.mode = _constants3.MODE.OFF;
355
+ this.recorder?.stopRecording?.();
509
356
  this.syncWithSessionManager({
510
357
  sessionReplayMode: this.mode
511
358
  });
512
- this.clearTimestamps();
359
+ this.recorder?.clearTimestamps?.();
513
360
  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;
361
+ this.recorder?.clearBuffer?.();
523
362
  }
524
363
  syncWithSessionManager() {
525
364
  let state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};