@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.
- package/CHANGELOG.md +27 -0
- package/dist/cjs/common/config/state/init.js +3 -3
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/harvest/harvest-scheduler.js +2 -2
- package/dist/cjs/common/harvest/harvest.js +4 -3
- package/dist/cjs/common/ids/unique-id.js +5 -4
- package/dist/cjs/common/session/constants.js +20 -2
- package/dist/cjs/common/session/session-entity.js +8 -26
- package/dist/cjs/common/url/encode.js +2 -0
- package/dist/cjs/features/metrics/aggregate/index.js +3 -1
- package/dist/cjs/features/session_replay/aggregate/index.js +114 -277
- package/dist/cjs/features/session_replay/constants.js +57 -2
- package/dist/cjs/features/session_replay/instrument/index.js +38 -16
- package/dist/cjs/features/session_replay/shared/recorder-events.js +31 -0
- package/dist/cjs/features/session_replay/shared/recorder.js +155 -0
- package/dist/cjs/features/session_replay/{replay-mode.js → shared/replay-mode.js} +5 -5
- package/dist/cjs/features/session_trace/aggregate/index.js +25 -25
- package/dist/cjs/loaders/agent-base.js +18 -13
- package/dist/esm/common/config/state/init.js +3 -3
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/harvest/harvest-scheduler.js +1 -1
- package/dist/esm/common/harvest/harvest.js +4 -3
- package/dist/esm/common/ids/unique-id.js +5 -4
- package/dist/esm/common/session/constants.js +16 -1
- package/dist/esm/common/session/session-entity.js +2 -16
- package/dist/esm/common/url/encode.js +2 -0
- package/dist/esm/features/metrics/aggregate/index.js +3 -1
- package/dist/esm/features/session_replay/aggregate/index.js +95 -254
- package/dist/esm/features/session_replay/constants.js +49 -1
- package/dist/esm/features/session_replay/instrument/index.js +24 -1
- package/dist/esm/features/session_replay/shared/recorder-events.js +24 -0
- package/dist/esm/features/session_replay/shared/recorder.js +148 -0
- package/dist/esm/features/session_replay/{replay-mode.js → shared/replay-mode.js} +4 -4
- package/dist/esm/features/session_trace/aggregate/index.js +2 -2
- package/dist/esm/loaders/agent-base.js +18 -13
- package/dist/types/common/config/state/init.d.ts.map +1 -1
- package/dist/types/common/harvest/harvest.d.ts +1 -1
- package/dist/types/common/harvest/harvest.d.ts.map +1 -1
- package/dist/types/common/ids/unique-id.d.ts.map +1 -1
- package/dist/types/common/session/constants.d.ts +15 -0
- package/dist/types/common/session/session-entity.d.ts +0 -15
- package/dist/types/common/session/session-entity.d.ts.map +1 -1
- package/dist/types/common/url/encode.d.ts +1 -1
- package/dist/types/common/url/encode.d.ts.map +1 -1
- package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +7 -63
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/constants.d.ts +55 -0
- package/dist/types/features/session_replay/constants.d.ts.map +1 -1
- package/dist/types/features/session_replay/instrument/index.d.ts +2 -0
- package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder-events.d.ts +21 -0
- package/dist/types/features/session_replay/shared/recorder-events.d.ts.map +1 -0
- package/dist/types/features/session_replay/shared/recorder.d.ts +40 -0
- package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -0
- package/dist/types/features/session_replay/shared/replay-mode.d.ts.map +1 -0
- package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
- package/dist/types/loaders/agent-base.d.ts +2 -1
- package/dist/types/loaders/agent-base.d.ts.map +1 -1
- package/package.json +1 -5
- package/src/common/config/state/init.js +6 -4
- package/src/common/harvest/harvest-scheduler.js +1 -1
- package/src/common/harvest/harvest.js +4 -3
- package/src/common/ids/unique-id.js +5 -4
- package/src/common/session/__mocks__/session-entity.js +0 -6
- package/src/common/session/constants.js +18 -0
- package/src/common/session/session-entity.js +1 -17
- package/src/common/url/encode.js +2 -1
- package/src/features/metrics/aggregate/index.js +3 -1
- package/src/features/session_replay/aggregate/index.js +88 -246
- package/src/features/session_replay/constants.js +45 -0
- package/src/features/session_replay/instrument/index.js +18 -1
- package/src/features/session_replay/shared/recorder-events.js +25 -0
- package/src/features/session_replay/shared/recorder.js +145 -0
- package/src/features/session_replay/{replay-mode.js → shared/replay-mode.js} +4 -4
- package/src/features/session_trace/aggregate/index.js +2 -2
- package/src/loaders/agent-base.js +18 -13
- package/dist/types/features/session_replay/replay-mode.d.ts.map +0 -1
- /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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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(
|
|
136
|
-
this.
|
|
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(
|
|
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(
|
|
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 ===
|
|
151
|
-
this.startRecording();
|
|
78
|
+
if (!this.initialized || this.mode === _constants3.MODE.OFF) return;
|
|
79
|
+
this.recorder?.startRecording();
|
|
152
80
|
});
|
|
153
|
-
this.ee.on(
|
|
154
|
-
if (!this.initialized || this.blocked || type !==
|
|
155
|
-
if (this.mode !==
|
|
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
|
|
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 !==
|
|
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 !==
|
|
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 ===
|
|
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 =
|
|
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
|
|
222
|
-
|
|
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 =
|
|
237
|
-
else if (errorSample) this.mode =
|
|
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
|
|
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 ===
|
|
244
|
-
this.mode =
|
|
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 ===
|
|
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 (
|
|
279
|
-
const
|
|
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(
|
|
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 =
|
|
309
|
-
if (payloadStartsWithFullSnapshot && !!this.lastMeta) {
|
|
310
|
-
|
|
311
|
-
|
|
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 =
|
|
274
|
+
const payloadEndsWithMeta = events[events.length - 1]?.type === _constants.RRWEB_EVENT_TYPES.Meta;
|
|
318
275
|
if (payloadEndsWithMeta) {
|
|
319
|
-
this.lastMeta =
|
|
320
|
-
|
|
321
|
-
|
|
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 =
|
|
326
|
-
const lastEventTimestamp =
|
|
327
|
-
const firstTimestamp = firstEventTimestamp ||
|
|
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':
|
|
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:
|
|
351
|
-
hasSnapshot:
|
|
352
|
-
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:
|
|
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:
|
|
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 =
|
|
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 =
|
|
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;
|