@newrelic/browser-agent 1.240.0 → 1.241.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/dist/cjs/common/config/state/init.js +19 -17
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/features/metrics/aggregate/index.js +10 -7
- package/dist/cjs/features/page_view_timing/aggregate/index.js +9 -9
- package/dist/cjs/features/session_replay/aggregate/index.js +81 -35
- package/dist/cjs/loaders/browser-agent.js +2 -1
- package/dist/esm/common/config/state/init.js +19 -17
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/features/metrics/aggregate/index.js +10 -7
- package/dist/esm/features/page_view_timing/aggregate/index.js +9 -9
- package/dist/esm/features/session_replay/aggregate/index.js +81 -35
- package/dist/esm/loaders/browser-agent.js +2 -1
- package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +16 -4
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/loaders/browser-agent.d.ts.map +1 -1
- package/package.json +2 -3
- package/src/common/config/state/init.js +17 -17
- package/src/common/harvest/harvest-scheduler.test.js +2 -2
- package/src/features/metrics/aggregate/index.js +5 -3
- package/src/features/page_view_timing/aggregate/index.js +7 -6
- package/src/features/session_replay/aggregate/index.component-test.js +10 -10
- package/src/features/session_replay/aggregate/index.js +62 -29
- package/src/loaders/browser-agent.js +3 -1
|
@@ -11,8 +11,8 @@ var _nreum = require("../../window/nreum");
|
|
|
11
11
|
var _configurable = require("./configurable");
|
|
12
12
|
const model = () => {
|
|
13
13
|
const hiddenState = {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
block_selector: '[data-nr-block]',
|
|
15
|
+
mask_input_options: {
|
|
16
16
|
password: true
|
|
17
17
|
}
|
|
18
18
|
};
|
|
@@ -86,35 +86,37 @@ const model = () => {
|
|
|
86
86
|
autoStart: true,
|
|
87
87
|
enabled: false,
|
|
88
88
|
harvestTimeSeconds: 60,
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
sampling_rate: 50,
|
|
90
|
+
// float from 0 - 100
|
|
91
|
+
error_sampling_rate: 50,
|
|
92
|
+
// float from 0 - 100
|
|
91
93
|
// recording config settings
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
mask_text_selector: '*',
|
|
95
|
+
mask_all_inputs: true,
|
|
94
96
|
// these properties only have getters because they are enforcable constants and should error if someone tries to override them
|
|
95
|
-
get
|
|
97
|
+
get block_class() {
|
|
96
98
|
return 'nr-block';
|
|
97
99
|
},
|
|
98
|
-
get
|
|
100
|
+
get ignore_class() {
|
|
99
101
|
return 'nr-ignore';
|
|
100
102
|
},
|
|
101
|
-
get
|
|
103
|
+
get mask_text_class() {
|
|
102
104
|
return 'nr-mask';
|
|
103
105
|
},
|
|
104
106
|
// props with a getter and setter are used to extend enforcable constants with customer input
|
|
105
107
|
// we must preserve data-nr-block no matter what else the customer sets
|
|
106
|
-
get
|
|
107
|
-
return hiddenState.
|
|
108
|
+
get block_selector() {
|
|
109
|
+
return hiddenState.block_selector;
|
|
108
110
|
},
|
|
109
|
-
set
|
|
110
|
-
hiddenState.
|
|
111
|
+
set block_selector(val) {
|
|
112
|
+
hiddenState.block_selector += ",".concat(val);
|
|
111
113
|
},
|
|
112
114
|
// password: must always be present and true no matter what customer sets
|
|
113
|
-
get
|
|
114
|
-
return hiddenState.
|
|
115
|
+
get mask_input_options() {
|
|
116
|
+
return hiddenState.mask_input_options;
|
|
115
117
|
},
|
|
116
|
-
set
|
|
117
|
-
hiddenState.
|
|
118
|
+
set mask_input_options(val) {
|
|
119
|
+
hiddenState.mask_input_options = {
|
|
118
120
|
...val,
|
|
119
121
|
password: true
|
|
120
122
|
};
|
|
@@ -33,13 +33,16 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
33
33
|
this.singleChecks(); // checks that are run only one time, at script load
|
|
34
34
|
this.eachSessionChecks(); // the start of every time user engages with page
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
this.ee.on("drain-".concat(this.featureName), () => {
|
|
37
|
+
// *cli, Mar 23 - Per NR-94597, this feature should only harvest ONCE at the (potential) EoL time of the page.
|
|
38
|
+
scheduler = new _harvestScheduler.HarvestScheduler('jserrors', {
|
|
39
|
+
onUnload: () => this.unload()
|
|
40
|
+
}, this);
|
|
41
|
+
scheduler.harvest.on('jserrors', () => ({
|
|
42
|
+
body: this.aggregator.take(['cm', 'sm'])
|
|
43
|
+
}));
|
|
44
|
+
}); // this is needed to ensure EoL is "on" and sent
|
|
45
|
+
|
|
43
46
|
this.drain();
|
|
44
47
|
}
|
|
45
48
|
storeSupportabilityMetrics(name, value) {
|
|
@@ -58,21 +58,21 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
58
58
|
|
|
59
59
|
/* It's important that CWV api, like "onLCP", is called before this scheduler is initialized. The reason is because they listen to the same
|
|
60
60
|
on vis change or pagehide events, and we'd want ex. onLCP to record the timing (win the race) before we try to send "final harvest". */
|
|
61
|
-
|
|
62
|
-
onFinished: function () {
|
|
63
|
-
return _this.onHarvestFinished(...arguments);
|
|
64
|
-
},
|
|
65
|
-
getPayload: function () {
|
|
66
|
-
return _this.prepareHarvest(...arguments);
|
|
67
|
-
}
|
|
68
|
-
}, this);
|
|
69
|
-
(0, _registerHandler.registerHandler)('timing', (name, value, attrs) => this.addTiming(name, value, attrs), this.featureName, this.ee); // notice CLS is added to all timings via 4th param
|
|
61
|
+
|
|
70
62
|
(0, _registerHandler.registerHandler)('docHidden', msTimestamp => this.endCurrentSession(msTimestamp), this.featureName, this.ee);
|
|
71
63
|
(0, _registerHandler.registerHandler)('winPagehide', msTimestamp => this.recordPageUnload(msTimestamp), this.featureName, this.ee);
|
|
72
64
|
const initialHarvestSeconds = (0, _config.getConfigurationValue)(this.agentIdentifier, 'page_view_timing.initialHarvestSeconds') || 10;
|
|
73
65
|
const harvestTimeSeconds = (0, _config.getConfigurationValue)(this.agentIdentifier, 'page_view_timing.harvestTimeSeconds') || 30;
|
|
74
66
|
// send initial data sooner, then start regular
|
|
75
67
|
this.ee.on("drain-".concat(this.featureName), () => {
|
|
68
|
+
this.scheduler = new _harvestScheduler.HarvestScheduler('events', {
|
|
69
|
+
onFinished: function () {
|
|
70
|
+
return _this.onHarvestFinished(...arguments);
|
|
71
|
+
},
|
|
72
|
+
getPayload: function () {
|
|
73
|
+
return _this.prepareHarvest(...arguments);
|
|
74
|
+
}
|
|
75
|
+
}, this);
|
|
76
76
|
this.scheduler.startTimer(harvestTimeSeconds, initialHarvestSeconds);
|
|
77
77
|
});
|
|
78
78
|
this.drain();
|
|
@@ -14,6 +14,7 @@ var _aggregateBase = require("../../utils/aggregate-base");
|
|
|
14
14
|
var _sharedChannel = require("../../../common/constants/shared-channel");
|
|
15
15
|
var _encode = require("../../../common/url/encode");
|
|
16
16
|
var _console = require("../../../common/util/console");
|
|
17
|
+
var _runtime = require("../../../common/constants/runtime");
|
|
17
18
|
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); }
|
|
18
19
|
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; } /*
|
|
19
20
|
* Copyright 2023 New Relic Corporation. All rights reserved.
|
|
@@ -72,17 +73,30 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
72
73
|
* -- When visibility changes from "hidden" -> "visible", it must capture a full snapshot for the replay to work correctly across tabs
|
|
73
74
|
*/
|
|
74
75
|
this.hasSnapshot = false;
|
|
76
|
+
/** Payload metadata -- Should indicate that the payload being sent has a meta node. The meta node should always precede a snapshot node. */
|
|
77
|
+
this.hasMeta = false;
|
|
75
78
|
/** Payload metadata -- Should indicate that the payload being sent contains an error. Used for query/filter purposes in UI */
|
|
76
79
|
this.hasError = false;
|
|
77
80
|
|
|
78
|
-
/** Payload metadata -- Should indicate when a replay blob started recording. Resets each time a harvest occurs.
|
|
81
|
+
/** Payload metadata -- Should indicate when a replay blob started recording. Resets each time a harvest occurs.
|
|
82
|
+
* cycle timestamps are used as fallbacks if event timestamps cannot be used
|
|
83
|
+
*/
|
|
79
84
|
this.timestamp = {
|
|
80
|
-
|
|
81
|
-
|
|
85
|
+
event: {
|
|
86
|
+
first: undefined,
|
|
87
|
+
last: undefined
|
|
88
|
+
},
|
|
89
|
+
cycle: {
|
|
90
|
+
first: undefined,
|
|
91
|
+
last: undefined
|
|
92
|
+
}
|
|
82
93
|
};
|
|
83
94
|
|
|
84
95
|
/** A value which increments with every new mutation node reported. Resets after a harvest is sent */
|
|
85
96
|
this.payloadBytesEstimation = 0;
|
|
97
|
+
|
|
98
|
+
/** 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 */
|
|
99
|
+
this.lastMeta = undefined;
|
|
86
100
|
const shouldSetup = (0, _config.getConfigurationValue)(agentIdentifier, 'privacy.cookies_enabled') === true && (0, _config.getConfigurationValue)(agentIdentifier, 'session_trace.enabled') === true;
|
|
87
101
|
|
|
88
102
|
/** The method to stop recording. This defaults to a noop, but is overwritten once the recording library is imported and initialized */
|
|
@@ -132,7 +146,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
132
146
|
}, this.featureName, this.ee);
|
|
133
147
|
this.waitForFlags(['sr']).then(_ref => {
|
|
134
148
|
let [flagOn] = _ref;
|
|
135
|
-
return this.initializeRecording(flagOn, Math.random() < (0, _config.getConfigurationValue)(this.agentIdentifier, 'session_replay.
|
|
149
|
+
return this.initializeRecording(flagOn, 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'));
|
|
136
150
|
}).then(() => _sharedChannel.sharedChannel.onReplayReady(this.mode)); // notify watchers that replay started with the mode
|
|
137
151
|
|
|
138
152
|
this.drain();
|
|
@@ -173,6 +187,12 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
173
187
|
if (this.mode === _sessionEntity.MODE.ERROR && this.errorNoticed) {
|
|
174
188
|
this.mode = _sessionEntity.MODE.FULL;
|
|
175
189
|
}
|
|
190
|
+
try {
|
|
191
|
+
// Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
|
|
192
|
+
recorder = (await Promise.resolve().then(() => _interopRequireWildcard(require( /* webpackChunkName: "recorder" */'rrweb')))).record;
|
|
193
|
+
} catch (err) {
|
|
194
|
+
return this.abort();
|
|
195
|
+
}
|
|
176
196
|
|
|
177
197
|
// FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
|
|
178
198
|
// ERROR mode will do this until an error is thrown, and then switch into FULL mode.
|
|
@@ -181,12 +201,6 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
181
201
|
// We only report (harvest) in FULL mode
|
|
182
202
|
this.scheduler.startTimer(this.harvestTimeSeconds);
|
|
183
203
|
}
|
|
184
|
-
try {
|
|
185
|
-
// Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
|
|
186
|
-
recorder = (await Promise.resolve().then(() => _interopRequireWildcard(require( /* webpackChunkName: "recorder" */'rrweb')))).record;
|
|
187
|
-
} catch (err) {
|
|
188
|
-
return this.abort();
|
|
189
|
-
}
|
|
190
204
|
try {
|
|
191
205
|
// Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
|
|
192
206
|
const {
|
|
@@ -221,6 +235,8 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
221
235
|
getHarvestContents() {
|
|
222
236
|
const agentRuntime = (0, _config.getRuntime)(this.agentIdentifier);
|
|
223
237
|
const info = (0, _config.getInfo)(this.agentIdentifier);
|
|
238
|
+
const firstTimestamp = this.timestamp.event.first || this.timestamp.cycle.first;
|
|
239
|
+
const lastTimestamp = this.timestamp.event.last || this.timestamp.cycle.last;
|
|
224
240
|
return {
|
|
225
241
|
qs: {
|
|
226
242
|
browser_monitoring_key: info.licenseKey,
|
|
@@ -231,11 +247,12 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
231
247
|
...(this.shouldCompress && {
|
|
232
248
|
content_encoding: 'gzip'
|
|
233
249
|
}),
|
|
234
|
-
'replay.firstTimestamp':
|
|
235
|
-
'replay.lastTimestamp':
|
|
236
|
-
'replay.durationMs':
|
|
250
|
+
'replay.firstTimestamp': firstTimestamp,
|
|
251
|
+
'replay.lastTimestamp': lastTimestamp,
|
|
252
|
+
'replay.durationMs': lastTimestamp - firstTimestamp,
|
|
237
253
|
agentVersion: agentRuntime.version,
|
|
238
254
|
session: agentRuntime.session.state.value,
|
|
255
|
+
hasMeta: this.hasMeta,
|
|
239
256
|
hasSnapshot: this.hasSnapshot,
|
|
240
257
|
hasError: this.hasError,
|
|
241
258
|
isFirstChunk: this.isFirstChunk,
|
|
@@ -260,6 +277,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
260
277
|
this.events = [];
|
|
261
278
|
this.isFirstChunk = false;
|
|
262
279
|
this.hasSnapshot = false;
|
|
280
|
+
this.hasMeta = false;
|
|
263
281
|
this.hasError = false;
|
|
264
282
|
this.payloadBytesEstimation = 0;
|
|
265
283
|
this.clearTimestamps();
|
|
@@ -271,27 +289,30 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
271
289
|
(0, _console.warn)('Recording library was never imported');
|
|
272
290
|
return this.abort();
|
|
273
291
|
}
|
|
292
|
+
this.clearTimestamps();
|
|
293
|
+
// set the fallbacks as early as possible
|
|
294
|
+
this.setTimestamps();
|
|
274
295
|
this.recording = true;
|
|
275
296
|
const {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
297
|
+
block_class,
|
|
298
|
+
ignore_class,
|
|
299
|
+
mask_text_class,
|
|
300
|
+
block_selector,
|
|
301
|
+
mask_input_options,
|
|
302
|
+
mask_text_selector,
|
|
303
|
+
mask_all_inputs
|
|
283
304
|
} = (0, _config.getConfigurationValue)(this.agentIdentifier, 'session_replay');
|
|
284
305
|
// set up rrweb configurations for maximum privacy --
|
|
285
306
|
// https://newrelic.atlassian.net/wiki/spaces/O11Y/pages/2792293280/2023+02+28+Browser+-+Session+Replay#Configuration-options
|
|
286
307
|
const stop = recorder({
|
|
287
308
|
emit: this.store.bind(this),
|
|
288
|
-
blockClass,
|
|
289
|
-
ignoreClass,
|
|
290
|
-
maskTextClass,
|
|
291
|
-
blockSelector,
|
|
292
|
-
maskInputOptions,
|
|
293
|
-
maskTextSelector,
|
|
294
|
-
maskAllInputs,
|
|
309
|
+
blockClass: block_class,
|
|
310
|
+
ignoreClass: ignore_class,
|
|
311
|
+
maskTextClass: mask_text_class,
|
|
312
|
+
blockSelector: block_selector,
|
|
313
|
+
maskInputOptions: mask_input_options,
|
|
314
|
+
maskTextSelector: mask_text_selector,
|
|
315
|
+
maskAllInputs: mask_all_inputs,
|
|
295
316
|
checkoutEveryNms: CHECKOUT_MS[this.mode]
|
|
296
317
|
});
|
|
297
318
|
this.stopRecording = () => {
|
|
@@ -302,6 +323,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
302
323
|
|
|
303
324
|
/** Store a payload in the buffer (this.events). This should be the callback to the recording lib noticing a mutation */
|
|
304
325
|
store(event, isCheckout) {
|
|
326
|
+
this.setTimestamps(event);
|
|
305
327
|
if (this.blocked) return;
|
|
306
328
|
const eventBytes = (0, _stringify.stringify)(event).length;
|
|
307
329
|
/** The estimated size of the payload after compression */
|
|
@@ -318,8 +340,22 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
318
340
|
// we are still waiting for an error to throw, so keep wiping the buffer over time
|
|
319
341
|
this.clearBuffer();
|
|
320
342
|
}
|
|
321
|
-
|
|
322
|
-
|
|
343
|
+
|
|
344
|
+
// meta event
|
|
345
|
+
if (event.type === 4) {
|
|
346
|
+
this.hasMeta = true;
|
|
347
|
+
this.lastMeta = event;
|
|
348
|
+
}
|
|
349
|
+
// snapshot event
|
|
350
|
+
if (event.type === 2) {
|
|
351
|
+
this.hasSnapshot = true;
|
|
352
|
+
// small chance that the meta event got separated from its matching snapshot across payload harvests
|
|
353
|
+
// it needs to precede the snapshot, so shove it in first.
|
|
354
|
+
if (!this.hasMeta) {
|
|
355
|
+
this.events.push(this.lastMeta);
|
|
356
|
+
this.hasMeta = true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
323
359
|
this.events.push(event);
|
|
324
360
|
this.payloadBytesEstimation += eventBytes;
|
|
325
361
|
|
|
@@ -336,15 +372,25 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
336
372
|
if (!recorder) return;
|
|
337
373
|
recorder.takeFullSnapshot();
|
|
338
374
|
}
|
|
339
|
-
setTimestamps(
|
|
340
|
-
if
|
|
341
|
-
|
|
342
|
-
this.timestamp.
|
|
375
|
+
setTimestamps(event) {
|
|
376
|
+
// fallbacks if timestamps cannot be derived from rrweb events
|
|
377
|
+
this.timestamp.cycle.last = (0, _config.getRuntime)(this.agentIdentifier).offset + _runtime.globalScope.performance.now();
|
|
378
|
+
if (!this.timestamp.cycle.first) this.timestamp.cycle.first = this.timestamp.cycle.last;
|
|
379
|
+
// timestamps based on rrweb events
|
|
380
|
+
if (!event || !event.timestamp) return;
|
|
381
|
+
if (!this.timestamp.event.first) this.timestamp.event.first = event.timestamp;
|
|
382
|
+
this.timestamp.event.last = event.timestamp;
|
|
343
383
|
}
|
|
344
384
|
clearTimestamps() {
|
|
345
385
|
this.timestamp = {
|
|
346
|
-
|
|
347
|
-
|
|
386
|
+
event: {
|
|
387
|
+
first: undefined,
|
|
388
|
+
last: undefined
|
|
389
|
+
},
|
|
390
|
+
cycle: {
|
|
391
|
+
first: undefined,
|
|
392
|
+
last: undefined
|
|
393
|
+
}
|
|
348
394
|
};
|
|
349
395
|
}
|
|
350
396
|
|
|
@@ -13,6 +13,7 @@ var _instrument5 = require("../features/ajax/instrument");
|
|
|
13
13
|
var _instrument6 = require("../features/session_trace/instrument");
|
|
14
14
|
var _instrument7 = require("../features/spa/instrument");
|
|
15
15
|
var _instrument8 = require("../features/page_action/instrument");
|
|
16
|
+
var _instrument9 = require("../features/session_replay/instrument");
|
|
16
17
|
/**
|
|
17
18
|
* An agent class with all feature modules available. Features may be disabled and enabled via runtime configuration.
|
|
18
19
|
* The BrowserAgent class is the most convenient and reliable option for most use cases.
|
|
@@ -21,7 +22,7 @@ class BrowserAgent extends _agent.Agent {
|
|
|
21
22
|
constructor(args) {
|
|
22
23
|
super({
|
|
23
24
|
...args,
|
|
24
|
-
features: [_instrument5.Instrument, _instrument.Instrument, _instrument2.Instrument, _instrument6.Instrument, _instrument3.Instrument, _instrument8.Instrument, _instrument4.Instrument, _instrument7.Instrument],
|
|
25
|
+
features: [_instrument5.Instrument, _instrument.Instrument, _instrument2.Instrument, _instrument6.Instrument, _instrument3.Instrument, _instrument8.Instrument, _instrument4.Instrument, _instrument7.Instrument, _instrument9.Instrument],
|
|
25
26
|
loaderType: 'browser-agent'
|
|
26
27
|
});
|
|
27
28
|
}
|
|
@@ -3,8 +3,8 @@ import { gosNREUMInitializedAgents } from '../../window/nreum';
|
|
|
3
3
|
import { getModeledObject } from './configurable';
|
|
4
4
|
const model = () => {
|
|
5
5
|
const hiddenState = {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
block_selector: '[data-nr-block]',
|
|
7
|
+
mask_input_options: {
|
|
8
8
|
password: true
|
|
9
9
|
}
|
|
10
10
|
};
|
|
@@ -78,35 +78,37 @@ const model = () => {
|
|
|
78
78
|
autoStart: true,
|
|
79
79
|
enabled: false,
|
|
80
80
|
harvestTimeSeconds: 60,
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
sampling_rate: 50,
|
|
82
|
+
// float from 0 - 100
|
|
83
|
+
error_sampling_rate: 50,
|
|
84
|
+
// float from 0 - 100
|
|
83
85
|
// recording config settings
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
mask_text_selector: '*',
|
|
87
|
+
mask_all_inputs: true,
|
|
86
88
|
// these properties only have getters because they are enforcable constants and should error if someone tries to override them
|
|
87
|
-
get
|
|
89
|
+
get block_class() {
|
|
88
90
|
return 'nr-block';
|
|
89
91
|
},
|
|
90
|
-
get
|
|
92
|
+
get ignore_class() {
|
|
91
93
|
return 'nr-ignore';
|
|
92
94
|
},
|
|
93
|
-
get
|
|
95
|
+
get mask_text_class() {
|
|
94
96
|
return 'nr-mask';
|
|
95
97
|
},
|
|
96
98
|
// props with a getter and setter are used to extend enforcable constants with customer input
|
|
97
99
|
// we must preserve data-nr-block no matter what else the customer sets
|
|
98
|
-
get
|
|
99
|
-
return hiddenState.
|
|
100
|
+
get block_selector() {
|
|
101
|
+
return hiddenState.block_selector;
|
|
100
102
|
},
|
|
101
|
-
set
|
|
102
|
-
hiddenState.
|
|
103
|
+
set block_selector(val) {
|
|
104
|
+
hiddenState.block_selector += ",".concat(val);
|
|
103
105
|
},
|
|
104
106
|
// password: must always be present and true no matter what customer sets
|
|
105
|
-
get
|
|
106
|
-
return hiddenState.
|
|
107
|
+
get mask_input_options() {
|
|
108
|
+
return hiddenState.mask_input_options;
|
|
107
109
|
},
|
|
108
|
-
set
|
|
109
|
-
hiddenState.
|
|
110
|
+
set mask_input_options(val) {
|
|
111
|
+
hiddenState.mask_input_options = {
|
|
110
112
|
...val,
|
|
111
113
|
password: true
|
|
112
114
|
};
|
|
@@ -27,13 +27,16 @@ export class Aggregate extends AggregateBase {
|
|
|
27
27
|
this.singleChecks(); // checks that are run only one time, at script load
|
|
28
28
|
this.eachSessionChecks(); // the start of every time user engages with page
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
30
|
+
this.ee.on("drain-".concat(this.featureName), () => {
|
|
31
|
+
// *cli, Mar 23 - Per NR-94597, this feature should only harvest ONCE at the (potential) EoL time of the page.
|
|
32
|
+
scheduler = new HarvestScheduler('jserrors', {
|
|
33
|
+
onUnload: () => this.unload()
|
|
34
|
+
}, this);
|
|
35
|
+
scheduler.harvest.on('jserrors', () => ({
|
|
36
|
+
body: this.aggregator.take(['cm', 'sm'])
|
|
37
|
+
}));
|
|
38
|
+
}); // this is needed to ensure EoL is "on" and sent
|
|
39
|
+
|
|
37
40
|
this.drain();
|
|
38
41
|
}
|
|
39
42
|
storeSupportabilityMetrics(name, value) {
|
|
@@ -52,21 +52,21 @@ export class Aggregate extends AggregateBase {
|
|
|
52
52
|
|
|
53
53
|
/* It's important that CWV api, like "onLCP", is called before this scheduler is initialized. The reason is because they listen to the same
|
|
54
54
|
on vis change or pagehide events, and we'd want ex. onLCP to record the timing (win the race) before we try to send "final harvest". */
|
|
55
|
-
|
|
56
|
-
onFinished: function () {
|
|
57
|
-
return _this.onHarvestFinished(...arguments);
|
|
58
|
-
},
|
|
59
|
-
getPayload: function () {
|
|
60
|
-
return _this.prepareHarvest(...arguments);
|
|
61
|
-
}
|
|
62
|
-
}, this);
|
|
63
|
-
registerHandler('timing', (name, value, attrs) => this.addTiming(name, value, attrs), this.featureName, this.ee); // notice CLS is added to all timings via 4th param
|
|
55
|
+
|
|
64
56
|
registerHandler('docHidden', msTimestamp => this.endCurrentSession(msTimestamp), this.featureName, this.ee);
|
|
65
57
|
registerHandler('winPagehide', msTimestamp => this.recordPageUnload(msTimestamp), this.featureName, this.ee);
|
|
66
58
|
const initialHarvestSeconds = getConfigurationValue(this.agentIdentifier, 'page_view_timing.initialHarvestSeconds') || 10;
|
|
67
59
|
const harvestTimeSeconds = getConfigurationValue(this.agentIdentifier, 'page_view_timing.harvestTimeSeconds') || 30;
|
|
68
60
|
// send initial data sooner, then start regular
|
|
69
61
|
this.ee.on("drain-".concat(this.featureName), () => {
|
|
62
|
+
this.scheduler = new HarvestScheduler('events', {
|
|
63
|
+
onFinished: function () {
|
|
64
|
+
return _this.onHarvestFinished(...arguments);
|
|
65
|
+
},
|
|
66
|
+
getPayload: function () {
|
|
67
|
+
return _this.prepareHarvest(...arguments);
|
|
68
|
+
}
|
|
69
|
+
}, this);
|
|
70
70
|
this.scheduler.startTimer(harvestTimeSeconds, initialHarvestSeconds);
|
|
71
71
|
});
|
|
72
72
|
this.drain();
|
|
@@ -20,6 +20,7 @@ import { AggregateBase } from '../../utils/aggregate-base';
|
|
|
20
20
|
import { sharedChannel } from '../../../common/constants/shared-channel';
|
|
21
21
|
import { obj as encodeObj } from '../../../common/url/encode';
|
|
22
22
|
import { warn } from '../../../common/util/console';
|
|
23
|
+
import { globalScope } from '../../../common/constants/runtime';
|
|
23
24
|
|
|
24
25
|
// would be better to get this dynamically in some way
|
|
25
26
|
export const RRWEB_VERSION = '2.0.0-alpha.8';
|
|
@@ -64,17 +65,30 @@ export class Aggregate extends AggregateBase {
|
|
|
64
65
|
* -- When visibility changes from "hidden" -> "visible", it must capture a full snapshot for the replay to work correctly across tabs
|
|
65
66
|
*/
|
|
66
67
|
this.hasSnapshot = false;
|
|
68
|
+
/** Payload metadata -- Should indicate that the payload being sent has a meta node. The meta node should always precede a snapshot node. */
|
|
69
|
+
this.hasMeta = false;
|
|
67
70
|
/** Payload metadata -- Should indicate that the payload being sent contains an error. Used for query/filter purposes in UI */
|
|
68
71
|
this.hasError = false;
|
|
69
72
|
|
|
70
|
-
/** Payload metadata -- Should indicate when a replay blob started recording. Resets each time a harvest occurs.
|
|
73
|
+
/** Payload metadata -- Should indicate when a replay blob started recording. Resets each time a harvest occurs.
|
|
74
|
+
* cycle timestamps are used as fallbacks if event timestamps cannot be used
|
|
75
|
+
*/
|
|
71
76
|
this.timestamp = {
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
event: {
|
|
78
|
+
first: undefined,
|
|
79
|
+
last: undefined
|
|
80
|
+
},
|
|
81
|
+
cycle: {
|
|
82
|
+
first: undefined,
|
|
83
|
+
last: undefined
|
|
84
|
+
}
|
|
74
85
|
};
|
|
75
86
|
|
|
76
87
|
/** A value which increments with every new mutation node reported. Resets after a harvest is sent */
|
|
77
88
|
this.payloadBytesEstimation = 0;
|
|
89
|
+
|
|
90
|
+
/** 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 */
|
|
91
|
+
this.lastMeta = undefined;
|
|
78
92
|
const shouldSetup = getConfigurationValue(agentIdentifier, 'privacy.cookies_enabled') === true && getConfigurationValue(agentIdentifier, 'session_trace.enabled') === true;
|
|
79
93
|
|
|
80
94
|
/** The method to stop recording. This defaults to a noop, but is overwritten once the recording library is imported and initialized */
|
|
@@ -124,7 +138,7 @@ export class Aggregate extends AggregateBase {
|
|
|
124
138
|
}, this.featureName, this.ee);
|
|
125
139
|
this.waitForFlags(['sr']).then(_ref => {
|
|
126
140
|
let [flagOn] = _ref;
|
|
127
|
-
return this.initializeRecording(flagOn, Math.random() < getConfigurationValue(this.agentIdentifier, 'session_replay.
|
|
141
|
+
return this.initializeRecording(flagOn, Math.random() * 100 < getConfigurationValue(this.agentIdentifier, 'session_replay.error_sampling_rate'), Math.random() * 100 < getConfigurationValue(this.agentIdentifier, 'session_replay.sampling_rate'));
|
|
128
142
|
}).then(() => sharedChannel.onReplayReady(this.mode)); // notify watchers that replay started with the mode
|
|
129
143
|
|
|
130
144
|
this.drain();
|
|
@@ -165,6 +179,12 @@ export class Aggregate extends AggregateBase {
|
|
|
165
179
|
if (this.mode === MODE.ERROR && this.errorNoticed) {
|
|
166
180
|
this.mode = MODE.FULL;
|
|
167
181
|
}
|
|
182
|
+
try {
|
|
183
|
+
// Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
|
|
184
|
+
recorder = (await import( /* webpackChunkName: "recorder" */'rrweb')).record;
|
|
185
|
+
} catch (err) {
|
|
186
|
+
return this.abort();
|
|
187
|
+
}
|
|
168
188
|
|
|
169
189
|
// FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
|
|
170
190
|
// ERROR mode will do this until an error is thrown, and then switch into FULL mode.
|
|
@@ -173,12 +193,6 @@ export class Aggregate extends AggregateBase {
|
|
|
173
193
|
// We only report (harvest) in FULL mode
|
|
174
194
|
this.scheduler.startTimer(this.harvestTimeSeconds);
|
|
175
195
|
}
|
|
176
|
-
try {
|
|
177
|
-
// Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
|
|
178
|
-
recorder = (await import( /* webpackChunkName: "recorder" */'rrweb')).record;
|
|
179
|
-
} catch (err) {
|
|
180
|
-
return this.abort();
|
|
181
|
-
}
|
|
182
196
|
try {
|
|
183
197
|
// Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
|
|
184
198
|
const {
|
|
@@ -213,6 +227,8 @@ export class Aggregate extends AggregateBase {
|
|
|
213
227
|
getHarvestContents() {
|
|
214
228
|
const agentRuntime = getRuntime(this.agentIdentifier);
|
|
215
229
|
const info = getInfo(this.agentIdentifier);
|
|
230
|
+
const firstTimestamp = this.timestamp.event.first || this.timestamp.cycle.first;
|
|
231
|
+
const lastTimestamp = this.timestamp.event.last || this.timestamp.cycle.last;
|
|
216
232
|
return {
|
|
217
233
|
qs: {
|
|
218
234
|
browser_monitoring_key: info.licenseKey,
|
|
@@ -223,11 +239,12 @@ export class Aggregate extends AggregateBase {
|
|
|
223
239
|
...(this.shouldCompress && {
|
|
224
240
|
content_encoding: 'gzip'
|
|
225
241
|
}),
|
|
226
|
-
'replay.firstTimestamp':
|
|
227
|
-
'replay.lastTimestamp':
|
|
228
|
-
'replay.durationMs':
|
|
242
|
+
'replay.firstTimestamp': firstTimestamp,
|
|
243
|
+
'replay.lastTimestamp': lastTimestamp,
|
|
244
|
+
'replay.durationMs': lastTimestamp - firstTimestamp,
|
|
229
245
|
agentVersion: agentRuntime.version,
|
|
230
246
|
session: agentRuntime.session.state.value,
|
|
247
|
+
hasMeta: this.hasMeta,
|
|
231
248
|
hasSnapshot: this.hasSnapshot,
|
|
232
249
|
hasError: this.hasError,
|
|
233
250
|
isFirstChunk: this.isFirstChunk,
|
|
@@ -252,6 +269,7 @@ export class Aggregate extends AggregateBase {
|
|
|
252
269
|
this.events = [];
|
|
253
270
|
this.isFirstChunk = false;
|
|
254
271
|
this.hasSnapshot = false;
|
|
272
|
+
this.hasMeta = false;
|
|
255
273
|
this.hasError = false;
|
|
256
274
|
this.payloadBytesEstimation = 0;
|
|
257
275
|
this.clearTimestamps();
|
|
@@ -263,27 +281,30 @@ export class Aggregate extends AggregateBase {
|
|
|
263
281
|
warn('Recording library was never imported');
|
|
264
282
|
return this.abort();
|
|
265
283
|
}
|
|
284
|
+
this.clearTimestamps();
|
|
285
|
+
// set the fallbacks as early as possible
|
|
286
|
+
this.setTimestamps();
|
|
266
287
|
this.recording = true;
|
|
267
288
|
const {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
289
|
+
block_class,
|
|
290
|
+
ignore_class,
|
|
291
|
+
mask_text_class,
|
|
292
|
+
block_selector,
|
|
293
|
+
mask_input_options,
|
|
294
|
+
mask_text_selector,
|
|
295
|
+
mask_all_inputs
|
|
275
296
|
} = getConfigurationValue(this.agentIdentifier, 'session_replay');
|
|
276
297
|
// set up rrweb configurations for maximum privacy --
|
|
277
298
|
// https://newrelic.atlassian.net/wiki/spaces/O11Y/pages/2792293280/2023+02+28+Browser+-+Session+Replay#Configuration-options
|
|
278
299
|
const stop = recorder({
|
|
279
300
|
emit: this.store.bind(this),
|
|
280
|
-
blockClass,
|
|
281
|
-
ignoreClass,
|
|
282
|
-
maskTextClass,
|
|
283
|
-
blockSelector,
|
|
284
|
-
maskInputOptions,
|
|
285
|
-
maskTextSelector,
|
|
286
|
-
maskAllInputs,
|
|
301
|
+
blockClass: block_class,
|
|
302
|
+
ignoreClass: ignore_class,
|
|
303
|
+
maskTextClass: mask_text_class,
|
|
304
|
+
blockSelector: block_selector,
|
|
305
|
+
maskInputOptions: mask_input_options,
|
|
306
|
+
maskTextSelector: mask_text_selector,
|
|
307
|
+
maskAllInputs: mask_all_inputs,
|
|
287
308
|
checkoutEveryNms: CHECKOUT_MS[this.mode]
|
|
288
309
|
});
|
|
289
310
|
this.stopRecording = () => {
|
|
@@ -294,6 +315,7 @@ export class Aggregate extends AggregateBase {
|
|
|
294
315
|
|
|
295
316
|
/** Store a payload in the buffer (this.events). This should be the callback to the recording lib noticing a mutation */
|
|
296
317
|
store(event, isCheckout) {
|
|
318
|
+
this.setTimestamps(event);
|
|
297
319
|
if (this.blocked) return;
|
|
298
320
|
const eventBytes = stringify(event).length;
|
|
299
321
|
/** The estimated size of the payload after compression */
|
|
@@ -310,8 +332,22 @@ export class Aggregate extends AggregateBase {
|
|
|
310
332
|
// we are still waiting for an error to throw, so keep wiping the buffer over time
|
|
311
333
|
this.clearBuffer();
|
|
312
334
|
}
|
|
313
|
-
|
|
314
|
-
|
|
335
|
+
|
|
336
|
+
// meta event
|
|
337
|
+
if (event.type === 4) {
|
|
338
|
+
this.hasMeta = true;
|
|
339
|
+
this.lastMeta = event;
|
|
340
|
+
}
|
|
341
|
+
// snapshot event
|
|
342
|
+
if (event.type === 2) {
|
|
343
|
+
this.hasSnapshot = true;
|
|
344
|
+
// small chance that the meta event got separated from its matching snapshot across payload harvests
|
|
345
|
+
// it needs to precede the snapshot, so shove it in first.
|
|
346
|
+
if (!this.hasMeta) {
|
|
347
|
+
this.events.push(this.lastMeta);
|
|
348
|
+
this.hasMeta = true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
315
351
|
this.events.push(event);
|
|
316
352
|
this.payloadBytesEstimation += eventBytes;
|
|
317
353
|
|
|
@@ -328,15 +364,25 @@ export class Aggregate extends AggregateBase {
|
|
|
328
364
|
if (!recorder) return;
|
|
329
365
|
recorder.takeFullSnapshot();
|
|
330
366
|
}
|
|
331
|
-
setTimestamps(
|
|
332
|
-
if
|
|
333
|
-
|
|
334
|
-
this.timestamp.
|
|
367
|
+
setTimestamps(event) {
|
|
368
|
+
// fallbacks if timestamps cannot be derived from rrweb events
|
|
369
|
+
this.timestamp.cycle.last = getRuntime(this.agentIdentifier).offset + globalScope.performance.now();
|
|
370
|
+
if (!this.timestamp.cycle.first) this.timestamp.cycle.first = this.timestamp.cycle.last;
|
|
371
|
+
// timestamps based on rrweb events
|
|
372
|
+
if (!event || !event.timestamp) return;
|
|
373
|
+
if (!this.timestamp.event.first) this.timestamp.event.first = event.timestamp;
|
|
374
|
+
this.timestamp.event.last = event.timestamp;
|
|
335
375
|
}
|
|
336
376
|
clearTimestamps() {
|
|
337
377
|
this.timestamp = {
|
|
338
|
-
|
|
339
|
-
|
|
378
|
+
event: {
|
|
379
|
+
first: undefined,
|
|
380
|
+
last: undefined
|
|
381
|
+
},
|
|
382
|
+
cycle: {
|
|
383
|
+
first: undefined,
|
|
384
|
+
last: undefined
|
|
385
|
+
}
|
|
340
386
|
};
|
|
341
387
|
}
|
|
342
388
|
|
|
@@ -7,6 +7,7 @@ import { Instrument as InstrumentXhr } from '../features/ajax/instrument';
|
|
|
7
7
|
import { Instrument as InstrumentSessionTrace } from '../features/session_trace/instrument';
|
|
8
8
|
import { Instrument as InstrumentSpa } from '../features/spa/instrument';
|
|
9
9
|
import { Instrument as InstrumentPageAction } from '../features/page_action/instrument';
|
|
10
|
+
import { Instrument as InstrumentSessionReplay } from '../features/session_replay/instrument';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* An agent class with all feature modules available. Features may be disabled and enabled via runtime configuration.
|
|
@@ -16,7 +17,7 @@ export class BrowserAgent extends Agent {
|
|
|
16
17
|
constructor(args) {
|
|
17
18
|
super({
|
|
18
19
|
...args,
|
|
19
|
-
features: [InstrumentXhr, InstrumentPageViewEvent, InstrumentPageViewTiming, InstrumentSessionTrace, InstrumentMetrics, InstrumentPageAction, InstrumentErrors, InstrumentSpa],
|
|
20
|
+
features: [InstrumentXhr, InstrumentPageViewEvent, InstrumentPageViewTiming, InstrumentSessionTrace, InstrumentMetrics, InstrumentPageAction, InstrumentErrors, InstrumentSpa, InstrumentSessionReplay],
|
|
20
21
|
loaderType: 'browser-agent'
|
|
21
22
|
});
|
|
22
23
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/metrics/aggregate/index.js"],"names":[],"mappings":"AAYA;IACE,2BAAiC;IACjC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/metrics/aggregate/index.js"],"names":[],"mappings":"AAYA;IACE,2BAAiC;IACjC,mDAwBC;IAED,wDAKC;IAED,iDAKC;IAED,qBAqCC;IAED,0BAOC;IAED,eA0CC;IAvCG,mCAAyB;CAwC9B;8BAvI6B,4BAA4B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/page_view_timing/aggregate/index.js"],"names":[],"mappings":"AAuBA;IACE,2BAAiC;IAMjC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/page_view_timing/aggregate/index.js"],"names":[],"mappings":"AAuBA;IACE,2BAAiC;IAMjC,mDAoCC;IAjCC,eAAiB;IACjB,mBAAqB;IACrB,4BAA+B;IAuB7B,4BAGQ;IAOZ;;;OAGG;IACH,6BAFW,MAAM,QAOhB;IAED;;OAEG;IACH,uCAUC;IAED,mDAqBC;IAED,qCAKC;IAED,gDAWC;IAGD;;;;kBAWC;IAGD,8BAuBC;;CACF;8BAnK6B,4BAA4B;iCANzB,2CAA2C"}
|
|
@@ -28,15 +28,27 @@ export class Aggregate extends AggregateBase {
|
|
|
28
28
|
* -- When visibility changes from "hidden" -> "visible", it must capture a full snapshot for the replay to work correctly across tabs
|
|
29
29
|
*/
|
|
30
30
|
hasSnapshot: boolean;
|
|
31
|
+
/** Payload metadata -- Should indicate that the payload being sent has a meta node. The meta node should always precede a snapshot node. */
|
|
32
|
+
hasMeta: boolean;
|
|
31
33
|
/** Payload metadata -- Should indicate that the payload being sent contains an error. Used for query/filter purposes in UI */
|
|
32
34
|
hasError: boolean;
|
|
33
|
-
/** Payload metadata -- Should indicate when a replay blob started recording. Resets each time a harvest occurs.
|
|
35
|
+
/** Payload metadata -- Should indicate when a replay blob started recording. Resets each time a harvest occurs.
|
|
36
|
+
* cycle timestamps are used as fallbacks if event timestamps cannot be used
|
|
37
|
+
*/
|
|
34
38
|
timestamp: {
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
event: {
|
|
40
|
+
first: undefined;
|
|
41
|
+
last: undefined;
|
|
42
|
+
};
|
|
43
|
+
cycle: {
|
|
44
|
+
first: undefined;
|
|
45
|
+
last: undefined;
|
|
46
|
+
};
|
|
37
47
|
};
|
|
38
48
|
/** A value which increments with every new mutation node reported. Resets after a harvest is sent */
|
|
39
49
|
payloadBytesEstimation: number;
|
|
50
|
+
/** Hold on to the last meta node, so that it can be re-inserted if the meta and snapshot nodes are broken up due to harvesting */
|
|
51
|
+
lastMeta: any;
|
|
40
52
|
/** The method to stop recording. This defaults to a noop, but is overwritten once the recording library is imported and initialized */
|
|
41
53
|
stopRecording: () => void;
|
|
42
54
|
scheduler: HarvestScheduler | undefined;
|
|
@@ -77,7 +89,7 @@ export class Aggregate extends AggregateBase {
|
|
|
77
89
|
store(event: any, isCheckout: any): void;
|
|
78
90
|
/** force the recording lib to take a full DOM snapshot. This needs to occur in certain cases, like visibility changes */
|
|
79
91
|
takeFullSnapshot(): void;
|
|
80
|
-
setTimestamps(
|
|
92
|
+
setTimestamps(event: any): void;
|
|
81
93
|
clearTimestamps(): void;
|
|
82
94
|
/** Estimate the payload size */
|
|
83
95
|
getPayloadSize(newBytes?: number): any;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/aggregate/index.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/aggregate/index.js"],"names":[],"mappings":"AAyBA,4CAA4C;AAE5C,mCAAmC;AAInC,uCAAuC;AACvC,uCAAuC;AACvC,iCAAiC;AACjC,uCAAuC;AAIvC;IACE,2BAAiC;IACjC,mDAmGC;IAjGC,iHAAiH;IACjH,cAAgB;IAChB,8GAA8G;IAC9G,wBAAgH;IAChH,iFAAiF;IACjF,qBAAwB;IACxB,mEAAmE;IACnE,sBAAyB;IACzB,6GAA6G;IAC7G,aAAoB;IAGpB,iEAAiE;IACjE,mBAAsB;IACtB,gDAAgD;IAChD,wBAA0B;IAE1B,gGAAgG;IAChG,sBAAyB;IACzB;;;MAGE;IACF,qBAAwB;IACxB,4IAA4I;IAC5I,iBAAoB;IACpB,+HAA+H;IAC/H,kBAAqB;IAErB;;OAEG;IACH;;;;;;;;;MAA+G;IAE/G,qGAAqG;IACrG,+BAA+B;IAE/B,kIAAkI;IAClI,cAAyB;IAOzB,uIAAuI;IACvI,0BAAyE;IAiBvE,wCAKQ;IA+BZ;;;;;;OAMG;IACH,kCALW,OAAO,eACP,OAAO,cACP,OAAO,GACL,IAAI,CAyDhB;IAED;;;;;;;;;oBAaC;IAED;;;;;;;;;MA4BC;IAED,qCAOC;IAED,kFAAkF;IAClF,oBAQC;IAED,qDAAqD;IACrD,uBA4BC;IAED,yHAAyH;IACzH,yCA4CC;IAED,0HAA0H;IAC1H,yBAGC;IAED,gCAQC;IAED,wBAEC;IAED,gCAAgC;IAChC,uCAGC;IAED,yDAAyD;IACzD,cAOC;IAED;;;SAGK;IACL,oCAGC;IAED,yCAGC;CACF;8BA3X6B,4BAA4B;iCALzB,2CAA2C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-agent.d.ts","sourceRoot":"","sources":["../../../src/loaders/browser-agent.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"browser-agent.d.ts","sourceRoot":"","sources":["../../../src/loaders/browser-agent.js"],"names":[],"mappings":"AAYA;;;GAGG;AACH;IACE,uBAgBC;CACF;sBAlCqB,SAAS"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@newrelic/browser-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.241.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"author": "New Relic Browser Agent Team <browser-agent@newrelic.com>",
|
|
6
6
|
"description": "New Relic Browser Agent",
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
"dependencies": {
|
|
177
177
|
"core-js": "^3.26.0",
|
|
178
178
|
"fflate": "^0.7.4",
|
|
179
|
-
"rrweb": "
|
|
179
|
+
"rrweb": "2.0.0-alpha.8",
|
|
180
180
|
"web-vitals": "^3.1.0"
|
|
181
181
|
},
|
|
182
182
|
"devDependencies": {
|
|
@@ -236,7 +236,6 @@
|
|
|
236
236
|
"jest-extended": "^3.2.4",
|
|
237
237
|
"jung": "^2.1.0",
|
|
238
238
|
"just-debounce": "^1.0.0",
|
|
239
|
-
"newrelic": "^9.7.5",
|
|
240
239
|
"node-fetch": "^3.3.0",
|
|
241
240
|
"npm-run-all": "^4.1.5",
|
|
242
241
|
"object-inspect": "^1.5.0",
|
|
@@ -4,8 +4,8 @@ import { getModeledObject } from './configurable'
|
|
|
4
4
|
|
|
5
5
|
const model = () => {
|
|
6
6
|
const hiddenState = {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
block_selector: '[data-nr-block]',
|
|
8
|
+
mask_input_options: { password: true }
|
|
9
9
|
}
|
|
10
10
|
return {
|
|
11
11
|
proxy: {
|
|
@@ -40,29 +40,29 @@ const model = () => {
|
|
|
40
40
|
autoStart: true,
|
|
41
41
|
enabled: false,
|
|
42
42
|
harvestTimeSeconds: 60,
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
sampling_rate: 50, // float from 0 - 100
|
|
44
|
+
error_sampling_rate: 50, // float from 0 - 100
|
|
45
45
|
// recording config settings
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
mask_text_selector: '*',
|
|
47
|
+
mask_all_inputs: true,
|
|
48
48
|
// these properties only have getters because they are enforcable constants and should error if someone tries to override them
|
|
49
|
-
get
|
|
50
|
-
get
|
|
51
|
-
get
|
|
49
|
+
get block_class () { return 'nr-block' },
|
|
50
|
+
get ignore_class () { return 'nr-ignore' },
|
|
51
|
+
get mask_text_class () { return 'nr-mask' },
|
|
52
52
|
// props with a getter and setter are used to extend enforcable constants with customer input
|
|
53
53
|
// we must preserve data-nr-block no matter what else the customer sets
|
|
54
|
-
get
|
|
55
|
-
return hiddenState.
|
|
54
|
+
get block_selector () {
|
|
55
|
+
return hiddenState.block_selector
|
|
56
56
|
},
|
|
57
|
-
set
|
|
58
|
-
hiddenState.
|
|
57
|
+
set block_selector (val) {
|
|
58
|
+
hiddenState.block_selector += `,${val}`
|
|
59
59
|
},
|
|
60
60
|
// password: must always be present and true no matter what customer sets
|
|
61
|
-
get
|
|
62
|
-
return hiddenState.
|
|
61
|
+
get mask_input_options () {
|
|
62
|
+
return hiddenState.mask_input_options
|
|
63
63
|
},
|
|
64
|
-
set
|
|
65
|
-
hiddenState.
|
|
64
|
+
set mask_input_options (val) {
|
|
65
|
+
hiddenState.mask_input_options = { ...val, password: true }
|
|
66
66
|
}
|
|
67
67
|
},
|
|
68
68
|
spa: { enabled: true, harvestTimeSeconds: 10, autoStart: true }
|
|
@@ -36,7 +36,7 @@ describe('unload', () => {
|
|
|
36
36
|
expect(subscribeToEOL).toHaveBeenCalledWith(expect.any(Function))
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
test('should run onUnload callback', () => {
|
|
39
|
+
test('should run onUnload callback when started', () => {
|
|
40
40
|
harvestSchedulerInstance.opts.onUnload = jest.fn()
|
|
41
41
|
|
|
42
42
|
eolSubscribeFn()
|
|
@@ -44,7 +44,7 @@ describe('unload', () => {
|
|
|
44
44
|
expect(harvestSchedulerInstance.opts.onUnload).toHaveBeenCalledTimes(1)
|
|
45
45
|
})
|
|
46
46
|
|
|
47
|
-
test('should run harvest when not aborted', () => {
|
|
47
|
+
test('should run harvest when started and not aborted', () => {
|
|
48
48
|
harvestSchedulerInstance.aborted = false
|
|
49
49
|
|
|
50
50
|
eolSubscribeFn()
|
|
@@ -29,9 +29,11 @@ export class Aggregate extends AggregateBase {
|
|
|
29
29
|
this.singleChecks() // checks that are run only one time, at script load
|
|
30
30
|
this.eachSessionChecks() // the start of every time user engages with page
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
this.ee.on(`drain-${this.featureName}`, () => {
|
|
33
|
+
// *cli, Mar 23 - Per NR-94597, this feature should only harvest ONCE at the (potential) EoL time of the page.
|
|
34
|
+
scheduler = new HarvestScheduler('jserrors', { onUnload: () => this.unload() }, this)
|
|
35
|
+
scheduler.harvest.on('jserrors', () => ({ body: this.aggregator.take(['cm', 'sm']) }))
|
|
36
|
+
}) // this is needed to ensure EoL is "on" and sent
|
|
35
37
|
|
|
36
38
|
this.drain()
|
|
37
39
|
}
|
|
@@ -48,19 +48,20 @@ export class Aggregate extends AggregateBase {
|
|
|
48
48
|
|
|
49
49
|
/* It's important that CWV api, like "onLCP", is called before this scheduler is initialized. The reason is because they listen to the same
|
|
50
50
|
on vis change or pagehide events, and we'd want ex. onLCP to record the timing (win the race) before we try to send "final harvest". */
|
|
51
|
-
this.scheduler = new HarvestScheduler('events', {
|
|
52
|
-
onFinished: (...args) => this.onHarvestFinished(...args),
|
|
53
|
-
getPayload: (...args) => this.prepareHarvest(...args)
|
|
54
|
-
}, this)
|
|
55
51
|
|
|
56
|
-
registerHandler('timing', (name, value, attrs) => this.addTiming(name, value, attrs), this.featureName, this.ee) // notice CLS is added to all timings via 4th param
|
|
57
52
|
registerHandler('docHidden', msTimestamp => this.endCurrentSession(msTimestamp), this.featureName, this.ee)
|
|
58
53
|
registerHandler('winPagehide', msTimestamp => this.recordPageUnload(msTimestamp), this.featureName, this.ee)
|
|
59
54
|
|
|
60
55
|
const initialHarvestSeconds = getConfigurationValue(this.agentIdentifier, 'page_view_timing.initialHarvestSeconds') || 10
|
|
61
56
|
const harvestTimeSeconds = getConfigurationValue(this.agentIdentifier, 'page_view_timing.harvestTimeSeconds') || 30
|
|
62
57
|
// send initial data sooner, then start regular
|
|
63
|
-
this.ee.on(`drain-${this.featureName}`, () => {
|
|
58
|
+
this.ee.on(`drain-${this.featureName}`, () => {
|
|
59
|
+
this.scheduler = new HarvestScheduler('events', {
|
|
60
|
+
onFinished: (...args) => this.onHarvestFinished(...args),
|
|
61
|
+
getPayload: (...args) => this.prepareHarvest(...args)
|
|
62
|
+
}, this)
|
|
63
|
+
this.scheduler.startTimer(harvestTimeSeconds, initialHarvestSeconds)
|
|
64
|
+
})
|
|
64
65
|
|
|
65
66
|
this.drain()
|
|
66
67
|
}
|
|
@@ -39,7 +39,7 @@ class LocalMemory {
|
|
|
39
39
|
let sr, session
|
|
40
40
|
const agentIdentifier = 'abcd'
|
|
41
41
|
const info = { licenseKey: 1234, applicationID: 9876 }
|
|
42
|
-
const init = { session_replay: { enabled: true,
|
|
42
|
+
const init = { session_replay: { enabled: true, sampling_rate: 100, error_sampling_rate: 0 } }
|
|
43
43
|
|
|
44
44
|
const anyQuery = {
|
|
45
45
|
browser_monitoring_key: info.licenseKey,
|
|
@@ -99,14 +99,14 @@ describe('Session Replay', () => {
|
|
|
99
99
|
})
|
|
100
100
|
|
|
101
101
|
test('Session SR mode matches SR mode -- ERROR', async () => {
|
|
102
|
-
setConfiguration(agentIdentifier, { session_replay: {
|
|
102
|
+
setConfiguration(agentIdentifier, { session_replay: { sampling_rate: 0, error_sampling_rate: 100 } })
|
|
103
103
|
sr.ee.emit('rumresp-sr', [true])
|
|
104
104
|
await wait(1)
|
|
105
105
|
expect(session.state.sessionReplay).toEqual(sr.mode)
|
|
106
106
|
})
|
|
107
107
|
|
|
108
108
|
test('Session SR mode matches SR mode -- OFF', async () => {
|
|
109
|
-
setConfiguration(agentIdentifier, { session_replay: {
|
|
109
|
+
setConfiguration(agentIdentifier, { session_replay: { sampling_rate: 0, error_sampling_rate: 0 } })
|
|
110
110
|
sr.ee.emit('rumresp-sr', [true])
|
|
111
111
|
await wait(1)
|
|
112
112
|
expect(session.state.sessionReplay).toEqual(sr.mode)
|
|
@@ -149,28 +149,28 @@ describe('Session Replay', () => {
|
|
|
149
149
|
|
|
150
150
|
describe('Session Replay Sample -> Mode Behaviors', () => {
|
|
151
151
|
test('New Session -- Full 1 Error 1 === FULL', async () => {
|
|
152
|
-
setConfiguration(agentIdentifier, { session_replay: {
|
|
152
|
+
setConfiguration(agentIdentifier, { session_replay: { error_sampling_rate: 100, sampling_rate: 100 } })
|
|
153
153
|
sr.ee.emit('rumresp-sr', [true])
|
|
154
154
|
await wait(1)
|
|
155
155
|
expect(sr.mode).toEqual(MODE.FULL)
|
|
156
156
|
})
|
|
157
157
|
|
|
158
158
|
test('New Session -- Full 1 Error 0 === FULL', async () => {
|
|
159
|
-
setConfiguration(agentIdentifier, { session_replay: {
|
|
159
|
+
setConfiguration(agentIdentifier, { session_replay: { error_sampling_rate: 0, sampling_rate: 100 } })
|
|
160
160
|
sr.ee.emit('rumresp-sr', [true])
|
|
161
161
|
await wait(1)
|
|
162
162
|
expect(sr.mode).toEqual(MODE.FULL)
|
|
163
163
|
})
|
|
164
164
|
|
|
165
165
|
test('New Session -- Full 0 Error 1 === ERROR', async () => {
|
|
166
|
-
setConfiguration(agentIdentifier, { session_replay: {
|
|
166
|
+
setConfiguration(agentIdentifier, { session_replay: { error_sampling_rate: 100, sampling_rate: 0 } })
|
|
167
167
|
sr.ee.emit('rumresp-sr', [true])
|
|
168
168
|
await wait(1)
|
|
169
169
|
expect(sr.mode).toEqual(MODE.ERROR)
|
|
170
170
|
})
|
|
171
171
|
|
|
172
172
|
test('New Session -- Full 0 Error 0 === OFF', async () => {
|
|
173
|
-
setConfiguration(agentIdentifier, { session_replay: {
|
|
173
|
+
setConfiguration(agentIdentifier, { session_replay: { error_sampling_rate: 0, sampling_rate: 0 } })
|
|
174
174
|
sr.ee.emit('rumresp-sr', [true])
|
|
175
175
|
await wait(1)
|
|
176
176
|
expect(sr.mode).toEqual(MODE.OFF)
|
|
@@ -182,7 +182,7 @@ describe('Session Replay', () => {
|
|
|
182
182
|
expect(session.isNew).toBeFalsy()
|
|
183
183
|
primeSessionAndReplay(session)
|
|
184
184
|
// configure to get "error" sample ---> but should inherit FULL from session manager
|
|
185
|
-
setConfiguration(agentIdentifier, { session_replay: {
|
|
185
|
+
setConfiguration(agentIdentifier, { session_replay: { error_sampling_rate: 100, sampling_rate: 0 } })
|
|
186
186
|
sr.ee.emit('rumresp-sr', [true])
|
|
187
187
|
await wait(1)
|
|
188
188
|
expect(sr.mode).toEqual(MODE.FULL)
|
|
@@ -191,7 +191,7 @@ describe('Session Replay', () => {
|
|
|
191
191
|
|
|
192
192
|
describe('Session Replay Error Mode Behaviors', () => {
|
|
193
193
|
test('An error BEFORE rrweb import starts running in FULL from beginning', async () => {
|
|
194
|
-
setConfiguration(agentIdentifier, { session_replay: {
|
|
194
|
+
setConfiguration(agentIdentifier, { session_replay: { error_sampling_rate: 100, sampling_rate: 0 } })
|
|
195
195
|
sr.ee.emit('errorAgg')
|
|
196
196
|
sr.ee.emit('rumresp-sr', [true])
|
|
197
197
|
await wait(1)
|
|
@@ -200,7 +200,7 @@ describe('Session Replay', () => {
|
|
|
200
200
|
})
|
|
201
201
|
|
|
202
202
|
test('An error AFTER rrweb import changes mode and starts harvester', async () => {
|
|
203
|
-
setConfiguration(agentIdentifier, { session_replay: {
|
|
203
|
+
setConfiguration(agentIdentifier, { session_replay: { error_sampling_rate: 100, sampling_rate: 0 } })
|
|
204
204
|
sr.ee.emit('rumresp-sr', [true])
|
|
205
205
|
await wait(1)
|
|
206
206
|
expect(sr.mode).toEqual(MODE.ERROR)
|
|
@@ -20,6 +20,7 @@ import { AggregateBase } from '../../utils/aggregate-base'
|
|
|
20
20
|
import { sharedChannel } from '../../../common/constants/shared-channel'
|
|
21
21
|
import { obj as encodeObj } from '../../../common/url/encode'
|
|
22
22
|
import { warn } from '../../../common/util/console'
|
|
23
|
+
import { globalScope } from '../../../common/constants/runtime'
|
|
23
24
|
|
|
24
25
|
// would be better to get this dynamically in some way
|
|
25
26
|
export const RRWEB_VERSION = '2.0.0-alpha.8'
|
|
@@ -63,15 +64,22 @@ export class Aggregate extends AggregateBase {
|
|
|
63
64
|
* -- When visibility changes from "hidden" -> "visible", it must capture a full snapshot for the replay to work correctly across tabs
|
|
64
65
|
*/
|
|
65
66
|
this.hasSnapshot = false
|
|
67
|
+
/** Payload metadata -- Should indicate that the payload being sent has a meta node. The meta node should always precede a snapshot node. */
|
|
68
|
+
this.hasMeta = false
|
|
66
69
|
/** Payload metadata -- Should indicate that the payload being sent contains an error. Used for query/filter purposes in UI */
|
|
67
70
|
this.hasError = false
|
|
68
71
|
|
|
69
|
-
/** Payload metadata -- Should indicate when a replay blob started recording. Resets each time a harvest occurs.
|
|
70
|
-
|
|
72
|
+
/** Payload metadata -- Should indicate when a replay blob started recording. Resets each time a harvest occurs.
|
|
73
|
+
* cycle timestamps are used as fallbacks if event timestamps cannot be used
|
|
74
|
+
*/
|
|
75
|
+
this.timestamp = { event: { first: undefined, last: undefined }, cycle: { first: undefined, last: undefined } }
|
|
71
76
|
|
|
72
77
|
/** A value which increments with every new mutation node reported. Resets after a harvest is sent */
|
|
73
78
|
this.payloadBytesEstimation = 0
|
|
74
79
|
|
|
80
|
+
/** 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 */
|
|
81
|
+
this.lastMeta = undefined
|
|
82
|
+
|
|
75
83
|
const shouldSetup = (
|
|
76
84
|
getConfigurationValue(agentIdentifier, 'privacy.cookies_enabled') === true &&
|
|
77
85
|
getConfigurationValue(agentIdentifier, 'session_trace.enabled') === true
|
|
@@ -123,8 +131,8 @@ export class Aggregate extends AggregateBase {
|
|
|
123
131
|
|
|
124
132
|
this.waitForFlags(['sr']).then(([flagOn]) => this.initializeRecording(
|
|
125
133
|
flagOn,
|
|
126
|
-
Math.random() < getConfigurationValue(this.agentIdentifier, 'session_replay.
|
|
127
|
-
Math.random() < getConfigurationValue(this.agentIdentifier, 'session_replay.
|
|
134
|
+
(Math.random() * 100) < getConfigurationValue(this.agentIdentifier, 'session_replay.error_sampling_rate'),
|
|
135
|
+
(Math.random() * 100) < getConfigurationValue(this.agentIdentifier, 'session_replay.sampling_rate')
|
|
128
136
|
)).then(() => sharedChannel.onReplayReady(this.mode)) // notify watchers that replay started with the mode
|
|
129
137
|
|
|
130
138
|
this.drain()
|
|
@@ -164,6 +172,13 @@ export class Aggregate extends AggregateBase {
|
|
|
164
172
|
this.mode = MODE.FULL
|
|
165
173
|
}
|
|
166
174
|
|
|
175
|
+
try {
|
|
176
|
+
// Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
|
|
177
|
+
recorder = (await import(/* webpackChunkName: "recorder" */'rrweb')).record
|
|
178
|
+
} catch (err) {
|
|
179
|
+
return this.abort()
|
|
180
|
+
}
|
|
181
|
+
|
|
167
182
|
// FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
|
|
168
183
|
// ERROR mode will do this until an error is thrown, and then switch into FULL mode.
|
|
169
184
|
// If an error happened in ERROR mode before we've gotten to this stage, it will have already set the mode to FULL
|
|
@@ -172,13 +187,6 @@ export class Aggregate extends AggregateBase {
|
|
|
172
187
|
this.scheduler.startTimer(this.harvestTimeSeconds)
|
|
173
188
|
}
|
|
174
189
|
|
|
175
|
-
try {
|
|
176
|
-
// Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
|
|
177
|
-
recorder = (await import(/* webpackChunkName: "recorder" */'rrweb')).record
|
|
178
|
-
} catch (err) {
|
|
179
|
-
return this.abort()
|
|
180
|
-
}
|
|
181
|
-
|
|
182
190
|
try {
|
|
183
191
|
// Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
|
|
184
192
|
const { gzipSync, strToU8 } = await import(/* webpackChunkName: "compressor" */'fflate')
|
|
@@ -213,6 +221,8 @@ export class Aggregate extends AggregateBase {
|
|
|
213
221
|
getHarvestContents () {
|
|
214
222
|
const agentRuntime = getRuntime(this.agentIdentifier)
|
|
215
223
|
const info = getInfo(this.agentIdentifier)
|
|
224
|
+
const firstTimestamp = this.timestamp.event.first || this.timestamp.cycle.first
|
|
225
|
+
const lastTimestamp = this.timestamp.event.last || this.timestamp.cycle.last
|
|
216
226
|
return {
|
|
217
227
|
qs: {
|
|
218
228
|
browser_monitoring_key: info.licenseKey,
|
|
@@ -221,11 +231,12 @@ export class Aggregate extends AggregateBase {
|
|
|
221
231
|
protocol_version: '0',
|
|
222
232
|
attributes: encodeObj({
|
|
223
233
|
...(this.shouldCompress && { content_encoding: 'gzip' }),
|
|
224
|
-
'replay.firstTimestamp':
|
|
225
|
-
'replay.lastTimestamp':
|
|
226
|
-
'replay.durationMs':
|
|
234
|
+
'replay.firstTimestamp': firstTimestamp,
|
|
235
|
+
'replay.lastTimestamp': lastTimestamp,
|
|
236
|
+
'replay.durationMs': lastTimestamp - firstTimestamp,
|
|
227
237
|
agentVersion: agentRuntime.version,
|
|
228
238
|
session: agentRuntime.session.state.value,
|
|
239
|
+
hasMeta: this.hasMeta,
|
|
229
240
|
hasSnapshot: this.hasSnapshot,
|
|
230
241
|
hasError: this.hasError,
|
|
231
242
|
isFirstChunk: this.isFirstChunk,
|
|
@@ -251,6 +262,7 @@ export class Aggregate extends AggregateBase {
|
|
|
251
262
|
this.events = []
|
|
252
263
|
this.isFirstChunk = false
|
|
253
264
|
this.hasSnapshot = false
|
|
265
|
+
this.hasMeta = false
|
|
254
266
|
this.hasError = false
|
|
255
267
|
this.payloadBytesEstimation = 0
|
|
256
268
|
this.clearTimestamps()
|
|
@@ -262,19 +274,22 @@ export class Aggregate extends AggregateBase {
|
|
|
262
274
|
warn('Recording library was never imported')
|
|
263
275
|
return this.abort()
|
|
264
276
|
}
|
|
277
|
+
this.clearTimestamps()
|
|
278
|
+
// set the fallbacks as early as possible
|
|
279
|
+
this.setTimestamps()
|
|
265
280
|
this.recording = true
|
|
266
|
-
const {
|
|
281
|
+
const { block_class, ignore_class, mask_text_class, block_selector, mask_input_options, mask_text_selector, mask_all_inputs } = getConfigurationValue(this.agentIdentifier, 'session_replay')
|
|
267
282
|
// set up rrweb configurations for maximum privacy --
|
|
268
283
|
// https://newrelic.atlassian.net/wiki/spaces/O11Y/pages/2792293280/2023+02+28+Browser+-+Session+Replay#Configuration-options
|
|
269
284
|
const stop = recorder({
|
|
270
285
|
emit: this.store.bind(this),
|
|
271
|
-
blockClass,
|
|
272
|
-
ignoreClass,
|
|
273
|
-
maskTextClass,
|
|
274
|
-
blockSelector,
|
|
275
|
-
maskInputOptions,
|
|
276
|
-
maskTextSelector,
|
|
277
|
-
maskAllInputs,
|
|
286
|
+
blockClass: block_class,
|
|
287
|
+
ignoreClass: ignore_class,
|
|
288
|
+
maskTextClass: mask_text_class,
|
|
289
|
+
blockSelector: block_selector,
|
|
290
|
+
maskInputOptions: mask_input_options,
|
|
291
|
+
maskTextSelector: mask_text_selector,
|
|
292
|
+
maskAllInputs: mask_all_inputs,
|
|
278
293
|
checkoutEveryNms: CHECKOUT_MS[this.mode]
|
|
279
294
|
})
|
|
280
295
|
|
|
@@ -286,6 +301,7 @@ export class Aggregate extends AggregateBase {
|
|
|
286
301
|
|
|
287
302
|
/** Store a payload in the buffer (this.events). This should be the callback to the recording lib noticing a mutation */
|
|
288
303
|
store (event, isCheckout) {
|
|
304
|
+
this.setTimestamps(event)
|
|
289
305
|
if (this.blocked) return
|
|
290
306
|
const eventBytes = stringify(event).length
|
|
291
307
|
/** The estimated size of the payload after compression */
|
|
@@ -303,8 +319,21 @@ export class Aggregate extends AggregateBase {
|
|
|
303
319
|
this.clearBuffer()
|
|
304
320
|
}
|
|
305
321
|
|
|
306
|
-
|
|
307
|
-
if (event.type ===
|
|
322
|
+
// meta event
|
|
323
|
+
if (event.type === 4) {
|
|
324
|
+
this.hasMeta = true
|
|
325
|
+
this.lastMeta = event
|
|
326
|
+
}
|
|
327
|
+
// snapshot event
|
|
328
|
+
if (event.type === 2) {
|
|
329
|
+
this.hasSnapshot = true
|
|
330
|
+
// small chance that the meta event got separated from its matching snapshot across payload harvests
|
|
331
|
+
// it needs to precede the snapshot, so shove it in first.
|
|
332
|
+
if (!this.hasMeta) {
|
|
333
|
+
this.events.push(this.lastMeta)
|
|
334
|
+
this.hasMeta = true
|
|
335
|
+
}
|
|
336
|
+
}
|
|
308
337
|
|
|
309
338
|
this.events.push(event)
|
|
310
339
|
this.payloadBytesEstimation += eventBytes
|
|
@@ -323,14 +352,18 @@ export class Aggregate extends AggregateBase {
|
|
|
323
352
|
recorder.takeFullSnapshot()
|
|
324
353
|
}
|
|
325
354
|
|
|
326
|
-
setTimestamps (
|
|
327
|
-
if
|
|
328
|
-
|
|
329
|
-
this.timestamp.
|
|
355
|
+
setTimestamps (event) {
|
|
356
|
+
// fallbacks if timestamps cannot be derived from rrweb events
|
|
357
|
+
this.timestamp.cycle.last = getRuntime(this.agentIdentifier).offset + globalScope.performance.now()
|
|
358
|
+
if (!this.timestamp.cycle.first) this.timestamp.cycle.first = this.timestamp.cycle.last
|
|
359
|
+
// timestamps based on rrweb events
|
|
360
|
+
if (!event || !event.timestamp) return
|
|
361
|
+
if (!this.timestamp.event.first) this.timestamp.event.first = event.timestamp
|
|
362
|
+
this.timestamp.event.last = event.timestamp
|
|
330
363
|
}
|
|
331
364
|
|
|
332
365
|
clearTimestamps () {
|
|
333
|
-
this.timestamp = { first: undefined, last: undefined }
|
|
366
|
+
this.timestamp = { event: { first: undefined, last: undefined }, cycle: { first: undefined, last: undefined } }
|
|
334
367
|
}
|
|
335
368
|
|
|
336
369
|
/** Estimate the payload size */
|
|
@@ -8,6 +8,7 @@ import { Instrument as InstrumentXhr } from '../features/ajax/instrument'
|
|
|
8
8
|
import { Instrument as InstrumentSessionTrace } from '../features/session_trace/instrument'
|
|
9
9
|
import { Instrument as InstrumentSpa } from '../features/spa/instrument'
|
|
10
10
|
import { Instrument as InstrumentPageAction } from '../features/page_action/instrument'
|
|
11
|
+
import { Instrument as InstrumentSessionReplay } from '../features/session_replay/instrument'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* An agent class with all feature modules available. Features may be disabled and enabled via runtime configuration.
|
|
@@ -25,7 +26,8 @@ export class BrowserAgent extends Agent {
|
|
|
25
26
|
InstrumentMetrics,
|
|
26
27
|
InstrumentPageAction,
|
|
27
28
|
InstrumentErrors,
|
|
28
|
-
InstrumentSpa
|
|
29
|
+
InstrumentSpa,
|
|
30
|
+
InstrumentSessionReplay
|
|
29
31
|
],
|
|
30
32
|
loaderType: 'browser-agent'
|
|
31
33
|
})
|