@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.
Files changed (27) hide show
  1. package/dist/cjs/common/config/state/init.js +19 -17
  2. package/dist/cjs/common/constants/env.cdn.js +1 -1
  3. package/dist/cjs/common/constants/env.npm.js +1 -1
  4. package/dist/cjs/features/metrics/aggregate/index.js +10 -7
  5. package/dist/cjs/features/page_view_timing/aggregate/index.js +9 -9
  6. package/dist/cjs/features/session_replay/aggregate/index.js +81 -35
  7. package/dist/cjs/loaders/browser-agent.js +2 -1
  8. package/dist/esm/common/config/state/init.js +19 -17
  9. package/dist/esm/common/constants/env.cdn.js +1 -1
  10. package/dist/esm/common/constants/env.npm.js +1 -1
  11. package/dist/esm/features/metrics/aggregate/index.js +10 -7
  12. package/dist/esm/features/page_view_timing/aggregate/index.js +9 -9
  13. package/dist/esm/features/session_replay/aggregate/index.js +81 -35
  14. package/dist/esm/loaders/browser-agent.js +2 -1
  15. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  16. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  17. package/dist/types/features/session_replay/aggregate/index.d.ts +16 -4
  18. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  19. package/dist/types/loaders/browser-agent.d.ts.map +1 -1
  20. package/package.json +2 -3
  21. package/src/common/config/state/init.js +17 -17
  22. package/src/common/harvest/harvest-scheduler.test.js +2 -2
  23. package/src/features/metrics/aggregate/index.js +5 -3
  24. package/src/features/page_view_timing/aggregate/index.js +7 -6
  25. package/src/features/session_replay/aggregate/index.component-test.js +10 -10
  26. package/src/features/session_replay/aggregate/index.js +62 -29
  27. 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
- blockSelector: '[data-nr-block]',
15
- maskInputOptions: {
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
- sampleRate: 0.1,
90
- errorSampleRate: 0.1,
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
- maskTextSelector: '*',
93
- maskAllInputs: true,
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 blockClass() {
97
+ get block_class() {
96
98
  return 'nr-block';
97
99
  },
98
- get ignoreClass() {
100
+ get ignore_class() {
99
101
  return 'nr-ignore';
100
102
  },
101
- get maskTextClass() {
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 blockSelector() {
107
- return hiddenState.blockSelector;
108
+ get block_selector() {
109
+ return hiddenState.block_selector;
108
110
  },
109
- set blockSelector(val) {
110
- hiddenState.blockSelector += ",".concat(val);
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 maskInputOptions() {
114
- return hiddenState.maskInputOptions;
115
+ get mask_input_options() {
116
+ return hiddenState.mask_input_options;
115
117
  },
116
- set maskInputOptions(val) {
117
- hiddenState.maskInputOptions = {
118
+ set mask_input_options(val) {
119
+ hiddenState.mask_input_options = {
118
120
  ...val,
119
121
  password: true
120
122
  };
@@ -12,7 +12,7 @@ exports.VERSION = exports.DIST_METHOD = exports.BUILD_ENV = void 0;
12
12
  /**
13
13
  * Exposes the version of the agent
14
14
  */
15
- const VERSION = "1.240.0";
15
+ const VERSION = "1.241.0";
16
16
 
17
17
  /**
18
18
  * Exposes the build type of the agent
@@ -12,7 +12,7 @@ exports.VERSION = exports.DIST_METHOD = exports.BUILD_ENV = void 0;
12
12
  /**
13
13
  * Exposes the version of the agent
14
14
  */
15
- const VERSION = "1.240.0";
15
+ const VERSION = "1.241.0";
16
16
 
17
17
  /**
18
18
  * Exposes the build type of the agent
@@ -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
- // *cli, Mar 23 - Per NR-94597, this feature should only harvest ONCE at the (potential) EoL time of the page.
37
- scheduler = new _harvestScheduler.HarvestScheduler('jserrors', {
38
- onUnload: () => this.unload()
39
- }, this);
40
- scheduler.harvest.on('jserrors', () => ({
41
- body: this.aggregator.take(['cm', 'sm'])
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
- this.scheduler = new _harvestScheduler.HarvestScheduler('events', {
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
- first: undefined,
81
- last: undefined
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.errorSampleRate'), Math.random() < (0, _config.getConfigurationValue)(this.agentIdentifier, 'session_replay.sampleRate'));
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': this.timestamp.first,
235
- 'replay.lastTimestamp': this.timestamp.last,
236
- 'replay.durationMs': this.timestamp.last - this.timestamp.first,
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
- blockClass,
277
- ignoreClass,
278
- maskTextClass,
279
- blockSelector,
280
- maskInputOptions,
281
- maskTextSelector,
282
- maskAllInputs
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
- this.setTimestamps(event);
322
- if (event.type === 2) this.hasSnapshot = true;
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(rrwebEvent) {
340
- if (!rrwebEvent) return;
341
- if (!this.timestamp.first) this.timestamp.first = rrwebEvent.timestamp;
342
- this.timestamp.last = rrwebEvent.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
- first: undefined,
347
- last: undefined
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
- blockSelector: '[data-nr-block]',
7
- maskInputOptions: {
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
- sampleRate: 0.1,
82
- errorSampleRate: 0.1,
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
- maskTextSelector: '*',
85
- maskAllInputs: true,
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 blockClass() {
89
+ get block_class() {
88
90
  return 'nr-block';
89
91
  },
90
- get ignoreClass() {
92
+ get ignore_class() {
91
93
  return 'nr-ignore';
92
94
  },
93
- get maskTextClass() {
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 blockSelector() {
99
- return hiddenState.blockSelector;
100
+ get block_selector() {
101
+ return hiddenState.block_selector;
100
102
  },
101
- set blockSelector(val) {
102
- hiddenState.blockSelector += ",".concat(val);
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 maskInputOptions() {
106
- return hiddenState.maskInputOptions;
107
+ get mask_input_options() {
108
+ return hiddenState.mask_input_options;
107
109
  },
108
- set maskInputOptions(val) {
109
- hiddenState.maskInputOptions = {
110
+ set mask_input_options(val) {
111
+ hiddenState.mask_input_options = {
110
112
  ...val,
111
113
  password: true
112
114
  };
@@ -6,7 +6,7 @@
6
6
  /**
7
7
  * Exposes the version of the agent
8
8
  */
9
- export const VERSION = "1.240.0";
9
+ export const VERSION = "1.241.0";
10
10
 
11
11
  /**
12
12
  * Exposes the build type of the agent
@@ -6,7 +6,7 @@
6
6
  /**
7
7
  * Exposes the version of the agent
8
8
  */
9
- export const VERSION = "1.240.0";
9
+ export const VERSION = "1.241.0";
10
10
 
11
11
  /**
12
12
  * Exposes the build type of the agent
@@ -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
- // *cli, Mar 23 - Per NR-94597, this feature should only harvest ONCE at the (potential) EoL time of the page.
31
- scheduler = new HarvestScheduler('jserrors', {
32
- onUnload: () => this.unload()
33
- }, this);
34
- scheduler.harvest.on('jserrors', () => ({
35
- body: this.aggregator.take(['cm', 'sm'])
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
- this.scheduler = new HarvestScheduler('events', {
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
- first: undefined,
73
- last: undefined
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.errorSampleRate'), Math.random() < getConfigurationValue(this.agentIdentifier, 'session_replay.sampleRate'));
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': this.timestamp.first,
227
- 'replay.lastTimestamp': this.timestamp.last,
228
- 'replay.durationMs': this.timestamp.last - this.timestamp.first,
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
- blockClass,
269
- ignoreClass,
270
- maskTextClass,
271
- blockSelector,
272
- maskInputOptions,
273
- maskTextSelector,
274
- maskAllInputs
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
- this.setTimestamps(event);
314
- if (event.type === 2) this.hasSnapshot = true;
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(rrwebEvent) {
332
- if (!rrwebEvent) return;
333
- if (!this.timestamp.first) this.timestamp.first = rrwebEvent.timestamp;
334
- this.timestamp.last = rrwebEvent.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
- first: undefined,
339
- last: undefined
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,mDAsBC;IAED,wDAKC;IAED,iDAKC;IAED,qBAqCC;IAED,0BAOC;IAED,eA0CC;IAvCG,mCAAyB;CAwC9B;8BArI6B,4BAA4B"}
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,mDAmCC;IAhCC,eAAiB;IACjB,mBAAqB;IACrB,4BAA+B;IAe/B,4BAGQ;IAcV;;;OAGG;IACH,6BAFW,MAAM,QAOhB;IAED;;OAEG;IACH,uCAUC;IAED,mDAqBC;IAED,qCAKC;IAED,gDAWC;IAGD;;;;kBAWC;IAGD,8BAuBC;;CACF;8BAlK6B,4BAA4B;iCANzB,2CAA2C"}
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
- first: undefined;
36
- last: undefined;
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(rrwebEvent: any): void;
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":"AAwBA,4CAA4C;AAE5C,mCAAmC;AAInC,uCAAuC;AACvC,uCAAuC;AACvC,iCAAiC;AACjC,uCAAuC;AAIvC;IACE,2BAAiC;IACjC,mDA4FC;IA1FC,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,+HAA+H;IAC/H,kBAAqB;IAErB,oHAAoH;IACpH;;;MAAsD;IAEtD,qGAAqG;IACrG,+BAA+B;IAO/B,uIAAuI;IACvI,0BAAyE;IAiBvE,wCAKQ;IA+BZ;;;;;;OAMG;IACH,kCALW,OAAO,eACP,OAAO,cACP,OAAO,GACL,IAAI,CAyDhB;IAED;;;;;;;;;oBAaC;IAED;;;;;;;;;MAyBC;IAED,qCAOC;IAED,kFAAkF;IAClF,oBAOC;IAED,qDAAqD;IACrD,uBAyBC;IAED,yHAAyH;IACzH,yCA8BC;IAED,0HAA0H;IAC1H,yBAGC;IAED,qCAIC;IAED,wBAEC;IAED,gCAAgC;IAChC,uCAGC;IAED,yDAAyD;IACzD,cAOC;IAED;;;SAGK;IACL,oCAGC;IAED,yCAGC;CACF;8BA1V6B,4BAA4B;iCALzB,2CAA2C"}
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":"AAWA;;;GAGG;AACH;IACE,uBAeC;CACF;sBAhCqB,SAAS"}
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.240.0",
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": "^2.0.0-alpha.8",
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
- blockSelector: '[data-nr-block]',
8
- maskInputOptions: { password: true }
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
- sampleRate: 0.1,
44
- errorSampleRate: 0.1,
43
+ sampling_rate: 50, // float from 0 - 100
44
+ error_sampling_rate: 50, // float from 0 - 100
45
45
  // recording config settings
46
- maskTextSelector: '*',
47
- maskAllInputs: true,
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 blockClass () { return 'nr-block' },
50
- get ignoreClass () { return 'nr-ignore' },
51
- get maskTextClass () { return 'nr-mask' },
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 blockSelector () {
55
- return hiddenState.blockSelector
54
+ get block_selector () {
55
+ return hiddenState.block_selector
56
56
  },
57
- set blockSelector (val) {
58
- hiddenState.blockSelector += `,${val}`
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 maskInputOptions () {
62
- return hiddenState.maskInputOptions
61
+ get mask_input_options () {
62
+ return hiddenState.mask_input_options
63
63
  },
64
- set maskInputOptions (val) {
65
- hiddenState.maskInputOptions = { ...val, password: true }
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
- // *cli, Mar 23 - Per NR-94597, this feature should only harvest ONCE at the (potential) EoL time of the page.
33
- scheduler = new HarvestScheduler('jserrors', { onUnload: () => this.unload() }, this)
34
- scheduler.harvest.on('jserrors', () => ({ body: this.aggregator.take(['cm', 'sm']) }))
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}`, () => { this.scheduler.startTimer(harvestTimeSeconds, initialHarvestSeconds) })
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, sampleRate: 1, errorSampleRate: 0 } }
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: { sampleRate: 0, errorSampleRate: 1 } })
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: { sampleRate: 0, errorSampleRate: 0 } })
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: { errorSampleRate: 1, sampleRate: 1 } })
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: { errorSampleRate: 0, sampleRate: 1 } })
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: { errorSampleRate: 1, sampleRate: 0 } })
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: { errorSampleRate: 0, sampleRate: 0 } })
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: { errorSampleRate: 1, sampleRate: 0 } })
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: { errorSampleRate: 1, sampleRate: 0 } })
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: { errorSampleRate: 1, sampleRate: 0 } })
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
- this.timestamp = { first: undefined, last: undefined }
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.errorSampleRate'),
127
- Math.random() < getConfigurationValue(this.agentIdentifier, 'session_replay.sampleRate')
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': this.timestamp.first,
225
- 'replay.lastTimestamp': this.timestamp.last,
226
- 'replay.durationMs': this.timestamp.last - this.timestamp.first,
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 { blockClass, ignoreClass, maskTextClass, blockSelector, maskInputOptions, maskTextSelector, maskAllInputs } = getConfigurationValue(this.agentIdentifier, 'session_replay')
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
- this.setTimestamps(event)
307
- if (event.type === 2) this.hasSnapshot = true
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 (rrwebEvent) {
327
- if (!rrwebEvent) return
328
- if (!this.timestamp.first) this.timestamp.first = rrwebEvent.timestamp
329
- this.timestamp.last = rrwebEvent.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
  })