@newrelic/browser-agent 1.255.0 → 1.256.1

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 (66) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/cjs/common/constants/env.cdn.js +2 -2
  3. package/dist/cjs/common/constants/env.npm.js +2 -2
  4. package/dist/cjs/common/constants/runtime.js +1 -1
  5. package/dist/cjs/common/session/session-entity.js +2 -1
  6. package/dist/cjs/common/timer/interaction-timer.js +16 -2
  7. package/dist/cjs/common/timing/time-keeper.js +1 -2
  8. package/dist/cjs/features/jserrors/aggregate/index.js +16 -6
  9. package/dist/cjs/features/jserrors/instrument/index.js +8 -3
  10. package/dist/cjs/features/session_replay/aggregate/index.js +48 -29
  11. package/dist/cjs/features/session_replay/constants.js +2 -1
  12. package/dist/cjs/features/session_replay/instrument/index.js +9 -2
  13. package/dist/cjs/features/session_replay/shared/recorder-events.js +1 -9
  14. package/dist/cjs/features/session_replay/shared/recorder.js +22 -50
  15. package/dist/cjs/features/session_replay/shared/utils.js +12 -0
  16. package/dist/cjs/loaders/api/api.js +7 -1
  17. package/dist/esm/common/constants/env.cdn.js +2 -2
  18. package/dist/esm/common/constants/env.npm.js +2 -2
  19. package/dist/esm/common/constants/runtime.js +1 -1
  20. package/dist/esm/common/session/session-entity.js +2 -1
  21. package/dist/esm/common/timer/interaction-timer.js +16 -2
  22. package/dist/esm/common/timing/time-keeper.js +1 -3
  23. package/dist/esm/features/jserrors/aggregate/index.js +16 -6
  24. package/dist/esm/features/jserrors/instrument/index.js +8 -3
  25. package/dist/esm/features/session_replay/aggregate/index.js +48 -29
  26. package/dist/esm/features/session_replay/constants.js +2 -1
  27. package/dist/esm/features/session_replay/instrument/index.js +9 -2
  28. package/dist/esm/features/session_replay/shared/recorder-events.js +1 -9
  29. package/dist/esm/features/session_replay/shared/recorder.js +23 -51
  30. package/dist/esm/features/session_replay/shared/utils.js +11 -0
  31. package/dist/esm/loaders/api/api.js +7 -1
  32. package/dist/types/common/constants/runtime.d.ts.map +1 -1
  33. package/dist/types/common/session/session-entity.d.ts.map +1 -1
  34. package/dist/types/common/timer/interaction-timer.d.ts +2 -0
  35. package/dist/types/common/timer/interaction-timer.d.ts.map +1 -1
  36. package/dist/types/common/timing/time-keeper.d.ts.map +1 -1
  37. package/dist/types/features/jserrors/aggregate/index.d.ts +2 -1
  38. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  39. package/dist/types/features/jserrors/instrument/index.d.ts.map +1 -1
  40. package/dist/types/features/session_replay/aggregate/index.d.ts +5 -2
  41. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  42. package/dist/types/features/session_replay/constants.d.ts +1 -0
  43. package/dist/types/features/session_replay/constants.d.ts.map +1 -1
  44. package/dist/types/features/session_replay/instrument/index.d.ts +1 -0
  45. package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
  46. package/dist/types/features/session_replay/shared/recorder-events.d.ts +0 -8
  47. package/dist/types/features/session_replay/shared/recorder-events.d.ts.map +1 -1
  48. package/dist/types/features/session_replay/shared/recorder.d.ts +1 -17
  49. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  50. package/dist/types/features/session_replay/shared/utils.d.ts +8 -0
  51. package/dist/types/features/session_replay/shared/utils.d.ts.map +1 -1
  52. package/dist/types/loaders/api/api.d.ts.map +1 -1
  53. package/package.json +2 -2
  54. package/src/common/constants/runtime.js +1 -1
  55. package/src/common/session/session-entity.js +2 -1
  56. package/src/common/timer/interaction-timer.js +17 -2
  57. package/src/common/timing/time-keeper.js +1 -3
  58. package/src/features/jserrors/aggregate/index.js +15 -6
  59. package/src/features/jserrors/instrument/index.js +9 -4
  60. package/src/features/session_replay/aggregate/index.js +43 -25
  61. package/src/features/session_replay/constants.js +2 -1
  62. package/src/features/session_replay/instrument/index.js +7 -2
  63. package/src/features/session_replay/shared/recorder-events.js +1 -6
  64. package/src/features/session_replay/shared/recorder.js +21 -27
  65. package/src/features/session_replay/shared/utils.js +12 -0
  66. package/src/loaders/api/api.js +10 -1
@@ -18,6 +18,7 @@ var _nreum = require("../../common/window/nreum");
18
18
  var _apiMethods = require("./api-methods");
19
19
  var _constants2 = require("../../features/session_replay/constants");
20
20
  var _now = require("../../common/timing/now");
21
+ var _constants3 = require("../../common/session/constants");
21
22
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
22
23
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } /*
23
24
  * Copyright 2020 New Relic Corporation. All rights reserved.
@@ -46,12 +47,17 @@ function setTopLevelCallers() {
46
47
  return returnVals.length > 1 ? returnVals : returnVals[0];
47
48
  }
48
49
  }
50
+ const replayRunning = {};
49
51
  function setAPI(agentIdentifier, forceDrain) {
50
52
  let runSoftNavOverSpa = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
51
53
  if (!forceDrain) (0, _drain.registerDrain)(agentIdentifier, 'api');
52
54
  const apiInterface = {};
53
55
  var instanceEE = _contextualEe.ee.get(agentIdentifier);
54
56
  var tracerEE = instanceEE.get('tracer');
57
+ replayRunning[agentIdentifier] = _constants3.MODE.OFF;
58
+ instanceEE.on(_constants2.SR_EVENT_EMITTER_TYPES.REPLAY_RUNNING, isRunning => {
59
+ replayRunning[agentIdentifier] = isRunning;
60
+ });
55
61
  var prefix = 'api-';
56
62
  var spaPrefix = prefix + 'ixn-';
57
63
 
@@ -194,7 +200,7 @@ function setAPI(agentIdentifier, forceDrain) {
194
200
  apiInterface.noticeError = function (err, customAttributes) {
195
201
  if (typeof err === 'string') err = new Error(err);
196
202
  (0, _handle.handle)(_constants.SUPPORTABILITY_METRIC_CHANNEL, ['API/noticeError/called'], undefined, _features.FEATURE_NAMES.metrics, instanceEE);
197
- (0, _handle.handle)('err', [err, (0, _now.now)(), false, customAttributes], undefined, _features.FEATURE_NAMES.jserrors, instanceEE);
203
+ (0, _handle.handle)('err', [err, (0, _now.now)(), false, customAttributes, !!replayRunning[agentIdentifier]], undefined, _features.FEATURE_NAMES.jserrors, instanceEE);
198
204
  };
199
205
 
200
206
  // theres no window.load event on non-browser scopes, lazy load immediately
@@ -6,7 +6,7 @@
6
6
  /**
7
7
  * Exposes the version of the agent
8
8
  */
9
- export const VERSION = "1.255.0";
9
+ export const VERSION = "1.256.1";
10
10
 
11
11
  /**
12
12
  * Exposes the build type of the agent
@@ -22,4 +22,4 @@ export const DIST_METHOD = 'CDN';
22
22
  /**
23
23
  * Exposes the lib version of rrweb
24
24
  */
25
- export const RRWEB_VERSION = "2.0.0-alpha.11";
25
+ export const RRWEB_VERSION = "2.0.0-alpha.12";
@@ -6,7 +6,7 @@
6
6
  /**
7
7
  * Exposes the version of the agent
8
8
  */
9
- export const VERSION = "1.255.0";
9
+ export const VERSION = "1.256.1";
10
10
 
11
11
  /**
12
12
  * Exposes the build type of the agent
@@ -23,4 +23,4 @@ export const DIST_METHOD = 'NPM';
23
23
  /**
24
24
  * Exposes the lib version of rrweb
25
25
  */
26
- export const RRWEB_VERSION = "2.0.0-alpha.11";
26
+ export const RRWEB_VERSION = "2.0.0-alpha.12";
@@ -38,4 +38,4 @@ export const ffVersion = (() => {
38
38
  export const isIE = Boolean(isBrowserScope && window.document.documentMode); // deprecated property that only works in IE
39
39
 
40
40
  export const supportsSendBeacon = !!globalScope.navigator?.sendBeacon;
41
- export const offset = Math.floor(globalScope?.performance?.timeOrigin || globalScope?.performance?.timing?.navigationStart || Date.now());
41
+ export const offset = Math.floor(Date.now() - performance.now());
@@ -122,7 +122,8 @@ export class SessionEntity {
122
122
  this.write(getModeledObject(this.state, model));
123
123
  },
124
124
  ee: this.ee,
125
- refreshEvents: ['click', 'keydown', 'scroll']
125
+ refreshEvents: ['click', 'keydown', 'scroll'],
126
+ readStorage: () => this.storage.get(this.lookupKey)
126
127
  }, this.state.inactiveAt - Date.now());
127
128
  } else {
128
129
  this.state.inactiveAt = Infinity;
@@ -9,6 +9,9 @@ export class InteractionTimer extends Timer {
9
9
  this.onRefresh = typeof opts.onRefresh === 'function' ? opts.onRefresh : () => {/* noop */};
10
10
  this.onResume = typeof opts.onResume === 'function' ? opts.onResume : () => {/* noop */};
11
11
 
12
+ /** used to double-check LS state at resume time */
13
+ this.readStorage = opts.readStorage;
14
+
12
15
  // used by pause/resume
13
16
  this.remainingMs = undefined;
14
17
  if (!opts.refreshEvents) opts.refreshEvents = ['click', 'keydown', 'scroll'];
@@ -62,8 +65,19 @@ export class InteractionTimer extends Timer {
62
65
  this.remainingMs = this.initialMs - (Date.now() - this.startTimestamp);
63
66
  }
64
67
  resume() {
65
- this.refresh();
66
- this.onResume(); // emit resume event after state updated
68
+ try {
69
+ const lsData = this.readStorage();
70
+ const obj = typeof lsData === 'string' ? JSON.parse(lsData) : lsData;
71
+ if (isExpired(obj.expiresAt) || isExpired(obj.inactiveAt)) this.end();else {
72
+ this.refresh();
73
+ this.onResume(); // emit resume event after state updated
74
+ }
75
+ } catch (err) {
76
+ this.end();
77
+ }
78
+ function isExpired(timestamp) {
79
+ return Date.now() > timestamp;
80
+ }
67
81
  }
68
82
  refresh(cb, ms) {
69
83
  this.clear();
@@ -1,5 +1,3 @@
1
- import { globalScope } from '../constants/runtime';
2
-
3
1
  /**
4
2
  * Class used to adjust the timestamp of harvested data to New Relic server time. This
5
3
  * is done by tracking the performance timings of the RUM call and applying a calculation
@@ -32,7 +30,7 @@ export class TimeKeeper {
32
30
  */
33
31
  #ready = false;
34
32
  constructor() {
35
- this.#originTime = globalScope.performance.timeOrigin || globalScope.performance.timing.navigationStart;
33
+ this.#originTime = Date.now() - performance.now();
36
34
  }
37
35
  get ready() {
38
36
  return this.#ready;
@@ -37,9 +37,13 @@ export class Aggregate extends AggregateBase {
37
37
  this.bufferedErrorsUnderSpa = {};
38
38
  this.currentBody = undefined;
39
39
  this.errorOnPage = false;
40
+ this.replayAborted = false;
40
41
 
41
42
  // this will need to change to match whatever ee we use in the instrument
42
43
  this.ee.on('interactionDone', (interaction, wasSaved) => this.onInteractionDone(interaction, wasSaved));
44
+ this.ee.on('REPLAY_ABORTED', () => {
45
+ this.replayAborted = true;
46
+ });
43
47
  register('err', function () {
44
48
  return _this.storeError(...arguments);
45
49
  }, this.featureName, this.ee);
@@ -84,9 +88,16 @@ export class Aggregate extends AggregateBase {
84
88
  if (releaseIds !== '{}') {
85
89
  payload.qs.ri = releaseIds;
86
90
  }
87
- if (body && body.err && body.err.length && !this.errorOnPage) {
88
- payload.qs.pve = '1';
89
- this.errorOnPage = true;
91
+ if (body && body.err && body.err.length) {
92
+ if (this.replayAborted) {
93
+ body.err.forEach(e => {
94
+ delete e.params?.hasReplay;
95
+ });
96
+ }
97
+ if (!this.errorOnPage) {
98
+ payload.qs.pve = '1';
99
+ this.errorOnPage = true;
100
+ }
90
101
  }
91
102
  return payload;
92
103
  }
@@ -131,7 +142,7 @@ export class Aggregate extends AggregateBase {
131
142
  }
132
143
  return canonicalStackString;
133
144
  }
134
- storeError(err, time, internal, customAttributes) {
145
+ storeError(err, time, internal, customAttributes, hasReplay) {
135
146
  // are we in an interaction
136
147
  time = time || now();
137
148
  const agentRuntime = getRuntime(this.agentIdentifier);
@@ -182,7 +193,7 @@ export class Aggregate extends AggregateBase {
182
193
  params.pageview = 1;
183
194
  this.pageviewReported[bucketHash] = true;
184
195
  }
185
- if (agentRuntime?.session?.state?.sessionReplayMode) params.hasReplay = true;
196
+ if (hasReplay && !this.replayAborted) params.hasReplay = hasReplay;
186
197
  params.firstOccurrenceTimestamp = this.observedAt[bucketHash];
187
198
  params.timestamp = this.observedAt[bucketHash];
188
199
  var type = internal ? 'ierr' : 'err';
@@ -193,7 +204,6 @@ export class Aggregate extends AggregateBase {
193
204
  // Trace sends the error in its payload, and both trace & replay simply listens for any error to occur.
194
205
  const jsErrorEvent = [type, bucketHash, params, newMetrics, customAttributes];
195
206
  handle('errorAgg', jsErrorEvent, undefined, FEATURE_NAMES.sessionTrace, this.ee);
196
- handle('errorAgg', jsErrorEvent, undefined, FEATURE_NAMES.sessionReplay, this.ee);
197
207
  // still send EE events for other features such as above, but stop this one from aggregating internal data
198
208
  if (this.blocked) return;
199
209
  const softNavInUse = Boolean(getNREUMInitializedAgent(this.agentIdentifier)?.features[FEATURE_NAMES.softNav]);
@@ -12,9 +12,11 @@ import { eventListenerOpts } from '../../../common/event-listener/event-listener
12
12
  import { stringify } from '../../../common/util/stringify';
13
13
  import { UncaughtError } from './uncaught-error';
14
14
  import { now } from '../../../common/timing/now';
15
+ import { SR_EVENT_EMITTER_TYPES } from '../../session_replay/constants';
15
16
  export class Instrument extends InstrumentBase {
16
17
  static featureName = FEATURE_NAME;
17
18
  #seenErrors = new Set();
19
+ #replayRunning = false;
18
20
  constructor(agentIdentifier, aggregator) {
19
21
  let auto = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
20
22
  super(agentIdentifier, aggregator, FEATURE_NAME, auto);
@@ -31,13 +33,16 @@ export class Instrument extends InstrumentBase {
31
33
  });
32
34
  this.ee.on('internal-error', error => {
33
35
  if (!this.abortHandler) return;
34
- handle('ierr', [this.#castError(error), now(), true], undefined, FEATURE_NAMES.jserrors, this.ee);
36
+ handle('ierr', [this.#castError(error), now(), true, {}, this.#replayRunning], undefined, FEATURE_NAMES.jserrors, this.ee);
37
+ });
38
+ this.ee.on(SR_EVENT_EMITTER_TYPES.REPLAY_RUNNING, isRunning => {
39
+ this.#replayRunning = isRunning;
35
40
  });
36
41
  globalScope.addEventListener('unhandledrejection', promiseRejectionEvent => {
37
42
  if (!this.abortHandler) return;
38
43
  handle('err', [this.#castPromiseRejectionEvent(promiseRejectionEvent), now(), false, {
39
44
  unhandledPromiseRejection: 1
40
- }], undefined, FEATURE_NAMES.jserrors, this.ee);
45
+ }, this.#replayRunning], undefined, FEATURE_NAMES.jserrors, this.ee);
41
46
  }, eventListenerOpts(false, this.removeOnAbort?.signal));
42
47
  globalScope.addEventListener('error', errorEvent => {
43
48
  if (!this.abortHandler) return;
@@ -50,7 +55,7 @@ export class Instrument extends InstrumentBase {
50
55
  this.#seenErrors.delete(errorEvent.error);
51
56
  return;
52
57
  }
53
- handle('err', [this.#castErrorEvent(errorEvent), now()], undefined, FEATURE_NAMES.jserrors, this.ee);
58
+ handle('err', [this.#castErrorEvent(errorEvent), now(), false, {}, this.#replayRunning], undefined, FEATURE_NAMES.jserrors, this.ee);
54
59
  }, eventListenerOpts(false, this.removeOnAbort?.signal));
55
60
  this.abortHandler = this.#abort; // we also use this as a flag to denote that the feature is active or on and handling errors
56
61
  this.importAggregator();
@@ -28,8 +28,11 @@ import { stringify } from '../../../common/util/stringify';
28
28
  import { stylesheetEvaluator } from '../shared/stylesheet-evaluator';
29
29
  import { deregisterDrain } from '../../../common/drain/drain';
30
30
  import { now } from '../../../common/timing/now';
31
+ import { buildNRMetaNode } from '../shared/utils';
31
32
  export class Aggregate extends AggregateBase {
32
33
  static featureName = FEATURE_NAME;
34
+ mode = MODE.OFF;
35
+
33
36
  // pass the recorder into the aggregator
34
37
  constructor(agentIdentifier, aggregator, args) {
35
38
  super(agentIdentifier, aggregator, FEATURE_NAME);
@@ -43,23 +46,18 @@ export class Aggregate extends AggregateBase {
43
46
  this.gzipper = undefined;
44
47
  /** populated with the u8 string lib async */
45
48
  this.u8 = undefined;
46
- /** the mode to start in. Defaults to off */
47
- const {
48
- session
49
- } = getRuntime(this.agentIdentifier);
50
- this.mode = session.state.sessionReplayMode || MODE.OFF;
51
49
 
52
50
  /** set by BCS response */
53
51
  this.entitled = false;
54
52
  /** set at BCS response, stored in runtime */
55
53
  this.timeKeeper = undefined;
56
54
  this.recorder = args?.recorder;
57
- if (this.recorder) this.recorder.parent = this;
55
+ this.preloaded = !!this.recorder;
56
+ this.errorNoticed = args?.errorNoticed || false;
58
57
  handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/Enabled'], undefined, FEATURE_NAMES.metrics, this.ee);
59
58
 
60
59
  // The SessionEntity class can emit a message indicating the session was cleared and reset (expiry, inactivity). This feature must abort and never resume if that occurs.
61
60
  this.ee.on(SESSION_EVENTS.RESET, () => {
62
- this.scheduler.runHarvest();
63
61
  this.abort(ABORT_REASONS.RESET);
64
62
  });
65
63
 
@@ -103,17 +101,6 @@ export class Aggregate extends AggregateBase {
103
101
  registerHandler(SR_EVENT_EMITTER_TYPES.PAUSE, () => {
104
102
  this.forceStop(this.mode !== MODE.ERROR);
105
103
  }, this.featureName, this.ee);
106
-
107
- // Wait for an error to be reported. This currently is wrapped around the "Error" feature. This is a feature-feature dependency.
108
- // This was to ensure that all errors, including those on the page before load and those handled with "noticeError" are accounted for. Needs evalulation
109
- registerHandler('errorAgg', e => {
110
- this.errorNoticed = true;
111
- if (this.recorder) this.recorder.currentBufferTarget.hasError = true;
112
- // run once
113
- if (this.mode === MODE.ERROR && globalScope?.document.visibilityState === 'visible') {
114
- this.switchToFull();
115
- }
116
- }, this.featureName, this.ee);
117
104
  const {
118
105
  error_sampling_rate,
119
106
  sampling_rate,
@@ -154,6 +141,13 @@ export class Aggregate extends AggregateBase {
154
141
  handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/SamplingRate/Value', sampling_rate], undefined, FEATURE_NAMES.metrics, this.ee);
155
142
  handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/ErrorSamplingRate/Value', error_sampling_rate], undefined, FEATURE_NAMES.metrics, this.ee);
156
143
  }
144
+ handleError(e) {
145
+ if (this.recorder) this.recorder.currentBufferTarget.hasError = true;
146
+ // run once
147
+ if (this.mode === MODE.ERROR && globalScope?.document.visibilityState === 'visible') {
148
+ this.switchToFull();
149
+ }
150
+ }
157
151
  switchToFull() {
158
152
  this.mode = MODE.FULL;
159
153
  // if the error was noticed AFTER the recorder was already imported....
@@ -218,12 +212,13 @@ export class Aggregate extends AggregateBase {
218
212
  } catch (err) {
219
213
  return this.abort(ABORT_REASONS.IMPORT);
220
214
  }
215
+ } else {
216
+ this.recorder.parent = this;
221
217
  }
222
218
 
223
219
  // If an error was noticed before the mode could be set (like in the early lifecycle of the page), immediately set to FULL mode
224
- if (this.mode === MODE.ERROR && this.errorNoticed) {
225
- this.mode = MODE.FULL;
226
- }
220
+ if (this.mode === MODE.ERROR && this.errorNoticed) this.mode = MODE.FULL;
221
+ if (!this.preloaded) this.ee.on('err', e => this.handleError(e));
227
222
 
228
223
  // FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
229
224
  // ERROR mode will do this until an error is thrown, and then switch into FULL mode.
@@ -264,21 +259,39 @@ export class Aggregate extends AggregateBase {
264
259
  this.recorder.clearBuffer();
265
260
  return;
266
261
  }
262
+ handle(SUPPORTABILITY_METRIC_CHANNEL, ['SessionReplay/Harvest/Attempts'], undefined, FEATURE_NAMES.metrics, this.ee);
267
263
  let len = 0;
268
264
  if (!!this.gzipper && !!this.u8) {
269
- payload.body = this.gzipper(this.u8("[".concat(payload.body.map(e => {
270
- if (e.__serialized) return e.__serialized;
271
- return stringify(e);
265
+ payload.body = this.gzipper(this.u8("[".concat(payload.body.map(_ref2 => {
266
+ let {
267
+ __serialized,
268
+ ...e
269
+ } = _ref2;
270
+ if (e.__newrelic && __serialized) return __serialized;
271
+ const output = {
272
+ ...e
273
+ };
274
+ if (!output.__newrelic) {
275
+ output.__newrelic = buildNRMetaNode(e.timestamp, this.timeKeeper);
276
+ output.timestamp = this.timeKeeper.correctAbsoluteTimestamp(e.timestamp);
277
+ }
278
+ return stringify(output);
272
279
  }).join(','), "]")));
273
280
  len = payload.body.length;
274
281
  this.scheduler.opts.gzip = true;
275
282
  } else {
276
- payload.body = payload.body.map(_ref2 => {
283
+ payload.body = payload.body.map(_ref3 => {
277
284
  let {
278
285
  __serialized,
279
286
  ...node
280
- } = _ref2;
281
- return node;
287
+ } = _ref3;
288
+ if (node.__newrelic) return node;
289
+ const output = {
290
+ ...node
291
+ };
292
+ output.__newrelic = buildNRMetaNode(node.timestamp, this.timeKeeper);
293
+ output.timestamp = this.timeKeeper.correctAbsoluteTimestamp(node.timestamp);
294
+ return output;
282
295
  });
283
296
  len = stringify(payload.body).length;
284
297
  this.scheduler.opts.gzip = false;
@@ -298,6 +311,11 @@ export class Aggregate extends AggregateBase {
298
311
  if (recorderEvents.type === 'preloaded') this.scheduler.runHarvest(opts);
299
312
  return [payload];
300
313
  }
314
+ getCorrectedTimestamp(node) {
315
+ if (!node.timestamp) return;
316
+ if (node.__newrelic) return node.timestamp;
317
+ return this.timeKeeper.correctAbsoluteTimestamp(node.timestamp);
318
+ }
301
319
  getHarvestContents(recorderEvents) {
302
320
  recorderEvents ??= this.recorder.getEvents();
303
321
  let events = recorderEvents.events;
@@ -323,8 +341,8 @@ export class Aggregate extends AggregateBase {
323
341
  recorderEvents.hasMeta = !!events.find(x => x.type === RRWEB_EVENT_TYPES.Meta);
324
342
  }
325
343
  const relativeNow = now();
326
- const firstEventTimestamp = events[0]?.timestamp; // from rrweb node
327
- const lastEventTimestamp = events[events.length - 1]?.timestamp; // from rrweb node
344
+ const firstEventTimestamp = this.getCorrectedTimestamp(events[0]); // from rrweb node
345
+ const lastEventTimestamp = this.getCorrectedTimestamp(events[events.length - 1]); // from rrweb node
328
346
  const firstTimestamp = firstEventTimestamp || this.timeKeeper.correctAbsoluteTimestamp(recorderEvents.cycleTimestamp); // from rrweb node || from when the harvest cycle started
329
347
  const lastTimestamp = lastEventTimestamp || this.timeKeeper.convertRelativeTimestamp(relativeNow);
330
348
  const agentMetadata = agentRuntime.appMetadata?.agents?.[0] || {};
@@ -359,6 +377,7 @@ export class Aggregate extends AggregateBase {
359
377
  invalidStylesheetsDetected: stylesheetEvaluator.invalidStylesheetsDetected,
360
378
  inlinedAllStylesheets: recorderEvents.inlinedAllStylesheets,
361
379
  'rrweb.version': RRWEB_VERSION,
380
+ 'payload.type': recorderEvents.type,
362
381
  // customer-defined data should go last so that if it exceeds the query param padding limit it will be truncated instead of important attrs
363
382
  ...(endUserId && {
364
383
  'enduser.id': endUserId
@@ -3,7 +3,8 @@ import { FEATURE_NAMES } from '../../loaders/features/features';
3
3
  export const FEATURE_NAME = FEATURE_NAMES.sessionReplay;
4
4
  export const SR_EVENT_EMITTER_TYPES = {
5
5
  RECORD: 'recordReplay',
6
- PAUSE: 'pauseReplay'
6
+ PAUSE: 'pauseReplay',
7
+ REPLAY_RUNNING: 'replayRunning'
7
8
  };
8
9
  export const AVG_COMPRESSION = 0.12;
9
10
  export const RRWEB_EVENT_TYPES = {
@@ -23,6 +23,11 @@ export class Instrument extends InstrumentBase {
23
23
  session = JSON.parse(localStorage.getItem("".concat(PREFIX, "_").concat(DEFAULT_KEY)));
24
24
  } catch (err) {}
25
25
  if (this.#canPreloadRecorder(session)) {
26
+ /** If this is preloaded, set up a buffer, if not, later when sampling we will set up a .on for live events */
27
+ this.ee.on('err', e => {
28
+ this.errorNoticed = true;
29
+ if (this.featAggregate) this.featAggregate.handleError();
30
+ });
26
31
  this.#startRecording(session?.sessionReplayMode);
27
32
  } else {
28
33
  this.importAggregator();
@@ -49,12 +54,14 @@ export class Instrument extends InstrumentBase {
49
54
  } = await import( /* webpackChunkName: "recorder" */'../shared/recorder');
50
55
  this.recorder = new Recorder({
51
56
  mode,
52
- agentIdentifier: this.agentIdentifier
57
+ agentIdentifier: this.agentIdentifier,
58
+ ee: this.ee
53
59
  });
54
60
  this.recorder.startRecording();
55
61
  this.abortHandler = this.recorder.stopRecording;
56
62
  this.importAggregator({
57
- recorder: this.recorder
63
+ recorder: this.recorder,
64
+ errorNoticed: this.errorNoticed
58
65
  });
59
66
  }
60
67
  }
@@ -1,19 +1,11 @@
1
1
  export class RecorderEvents {
2
- constructor(_ref) {
3
- let {
4
- canCorrectTimestamps
5
- } = _ref;
2
+ constructor() {
6
3
  /** The buffer to hold recorder event nodes */
7
4
  this.events = [];
8
5
  /** Payload metadata -- Should indicate when a replay blob started recording. Resets each time a harvest occurs.
9
6
  * cycle timestamps are used as fallbacks if event timestamps cannot be used
10
7
  */
11
8
  this.cycleTimestamp = Date.now();
12
- /** Payload metadata -- Whether timestamps can be corrected, defaults as false, can be set to true if timekeeper is present at init time. Used to determine
13
- * if harvest needs to re-loop through nodes and correct them before sending. Ideal behavior is to correct them as they flow into the recorder
14
- * to prevent re-looping, but is not always possible since the timekeeper is not set until after page load and the recorder can be preloaded.
15
- */
16
- this.canCorrectTimestamps = !!canCorrectTimestamps;
17
9
  /** A value which increments with every new mutation node reported. Resets after a harvest is sent */
18
10
  this.payloadBytesEstimation = 0;
19
11
  /** Payload metadata -- Should indicate that the payload being sent has a full DOM snapshot. This can happen
@@ -1,6 +1,6 @@
1
1
  import { record as recorder } from 'rrweb';
2
2
  import { stringify } from '../../../common/util/stringify';
3
- import { AVG_COMPRESSION, CHECKOUT_MS, IDEAL_PAYLOAD_SIZE, QUERY_PARAM_PADDING, RRWEB_EVENT_TYPES } from '../constants';
3
+ import { AVG_COMPRESSION, CHECKOUT_MS, IDEAL_PAYLOAD_SIZE, QUERY_PARAM_PADDING, RRWEB_EVENT_TYPES, SR_EVENT_EMITTER_TYPES } from '../constants';
4
4
  import { getConfigurationValue } from '../../../common/config/config';
5
5
  import { RecorderEvents } from './recorder-events';
6
6
  import { MODE } from '../../../common/session/constants';
@@ -8,6 +8,7 @@ import { stylesheetEvaluator } from './stylesheet-evaluator';
8
8
  import { handle } from '../../../common/event-emitter/handle';
9
9
  import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants';
10
10
  import { FEATURE_NAMES } from '../../../loaders/features/features';
11
+ import { buildNRMetaNode } from './utils';
11
12
  export class Recorder {
12
13
  /** Each page mutation or event will be stored (raw) in this array. This array will be cleared on each harvest */
13
14
  #events;
@@ -18,15 +19,9 @@ export class Recorder {
18
19
  /** flag that if true, blocks events from being "stored". Only set to true when a full snapshot has incomplete nodes (only stylesheets ATM) */
19
20
  #fixing = false;
20
21
  constructor(parent) {
21
- this.#events = new RecorderEvents({
22
- canCorrectTimestamps: !!parent.timeKeeper?.ready
23
- });
24
- this.#backloggedEvents = new RecorderEvents({
25
- canCorrectTimestamps: !!parent.timeKeeper?.ready
26
- });
27
- this.#preloaded = [new RecorderEvents({
28
- canCorrectTimestamps: !!parent.timeKeeper?.ready
29
- })];
22
+ this.#events = new RecorderEvents();
23
+ this.#backloggedEvents = new RecorderEvents();
24
+ this.#preloaded = [new RecorderEvents()];
30
25
  /** True when actively recording, false when paused or stopped */
31
26
  this.recording = false;
32
27
  /** The pointer to the current bucket holding rrweb events */
@@ -43,18 +38,12 @@ export class Recorder {
43
38
  this.stopRecording = () => {/* no-op until set by rrweb initializer */};
44
39
  }
45
40
  getEvents() {
46
- if (this.#preloaded[0]?.events.length) {
47
- const preloadedEvents = this.returnCorrectTimestamps(this.#preloaded[0]);
48
- return {
49
- ...this.#preloaded[0],
50
- events: preloadedEvents,
51
- type: 'preloaded'
52
- };
53
- }
54
- const backloggedEvents = this.returnCorrectTimestamps(this.#backloggedEvents);
55
- const events = this.returnCorrectTimestamps(this.#events);
41
+ if (this.#preloaded[0]?.events.length) return {
42
+ ...this.#preloaded[0],
43
+ type: 'preloaded'
44
+ };
56
45
  return {
57
- events: [...backloggedEvents, ...events].filter(x => x),
46
+ events: [...this.#backloggedEvents.events, ...this.#events.events].filter(x => x),
58
47
  type: 'standard',
59
48
  cycleTimestamp: Math.min(this.#backloggedEvents.cycleTimestamp, this.#events.cycleTimestamp),
60
49
  payloadBytesEstimation: this.#backloggedEvents.payloadBytesEstimation + this.#events.payloadBytesEstimation,
@@ -65,34 +54,10 @@ export class Recorder {
65
54
  };
66
55
  }
67
56
 
68
- /**
69
- * Returns time-corrected events. If the events were correctable from the beginning, this correction will have already been applied.
70
- * @param {SessionReplayEvent[]} events The array of buffered SR nodes
71
- * @returns {CorrectedSessionReplayEvent[]}
72
- */
73
- returnCorrectTimestamps(events) {
74
- if (!this.parent.timeKeeper?.ready) return events.events;
75
- return events.canCorrectTimestamps ? events.events : events.events.map(_ref => {
76
- let {
77
- __serialized,
78
- timestamp,
79
- ...e
80
- } = _ref;
81
- return {
82
- timestamp: this.parent.timeKeeper.correctAbsoluteTimestamp(timestamp),
83
- ...e
84
- };
85
- });
86
- }
87
-
88
57
  /** Clears the buffer (this.#events), and resets all payload metadata properties */
89
58
  clearBuffer() {
90
- if (this.#preloaded[0]?.events.length) this.#preloaded.shift();else if (this.parent.mode === MODE.ERROR) this.#backloggedEvents = this.#events;else this.#backloggedEvents = new RecorderEvents({
91
- canCorrectTimestamps: !!this.parent.timeKeeper?.ready
92
- });
93
- this.#events = new RecorderEvents({
94
- canCorrectTimestamps: !!this.parent.timeKeeper?.ready
95
- });
59
+ if (this.#preloaded[0]?.events.length) this.#preloaded.shift();else if (this.parent.mode === MODE.ERROR) this.#backloggedEvents = this.#events;else this.#backloggedEvents = new RecorderEvents();
60
+ this.#events = new RecorderEvents();
96
61
  }
97
62
 
98
63
  /** Begin recording using configured recording lib */
@@ -110,6 +75,10 @@ export class Recorder {
110
75
  inline_images,
111
76
  collect_fonts
112
77
  } = getConfigurationValue(this.parent.agentIdentifier, 'session_replay');
78
+ const customMasker = (text, element) => {
79
+ if (element?.type?.toLowerCase() !== 'password' && (element?.dataset.nrUnmask !== undefined || element?.classList.contains('nr-unmask'))) return text;
80
+ return '*'.repeat(text.length);
81
+ };
113
82
  // set up rrweb configurations for maximum privacy --
114
83
  // https://newrelic.atlassian.net/wiki/spaces/O11Y/pages/2792293280/2023+02+28+Browser+-+Session+Replay#Configuration-options
115
84
  const stop = recorder({
@@ -120,14 +89,18 @@ export class Recorder {
120
89
  blockSelector: block_selector,
121
90
  maskInputOptions: mask_input_options,
122
91
  maskTextSelector: mask_text_selector,
92
+ maskTextFn: customMasker,
123
93
  maskAllInputs: mask_all_inputs,
94
+ maskInputFn: customMasker,
124
95
  inlineStylesheet: inline_stylesheet,
125
96
  inlineImages: inline_images,
126
97
  collectFonts: collect_fonts,
127
98
  checkoutEveryNms: CHECKOUT_MS[this.parent.mode]
128
99
  });
100
+ this.parent.ee.emit(SR_EVENT_EMITTER_TYPES.REPLAY_RUNNING, [true, this.parent.mode]);
129
101
  this.stopRecording = () => {
130
102
  this.recording = false;
103
+ this.parent.ee.emit(SR_EVENT_EMITTER_TYPES.REPLAY_RUNNING, [false, this.parent.mode]);
131
104
  stop();
132
105
  };
133
106
  }
@@ -171,7 +144,8 @@ export class Recorder {
171
144
  if (!event) return;
172
145
  if (!this.parent.scheduler && this.#preloaded.length) this.currentBufferTarget = this.#preloaded[this.#preloaded.length - 1];else this.currentBufferTarget = this.#events;
173
146
  if (this.parent.blocked) return;
174
- if (this.currentBufferTarget.canCorrectTimestamps) {
147
+ if (this.parent.timeKeeper?.ready && !event.__newrelic) {
148
+ event.__newrelic = buildNRMetaNode(event.timestamp, this.parent.timeKeeper);
175
149
  event.timestamp = this.parent.timeKeeper.correctAbsoluteTimestamp(event.timestamp);
176
150
  }
177
151
  event.__serialized = stringify(event);
@@ -206,9 +180,7 @@ export class Recorder {
206
180
  this.parent.scheduler.runHarvest();
207
181
  } else {
208
182
  // we are still in "preload" and it triggered a "stop point". Make a new set, which will get pointed at on next cycle
209
- this.#preloaded.push(new RecorderEvents({
210
- canCorrectTimestamps: !!this.parent.timeKeeper?.ready
211
- }));
183
+ this.#preloaded.push(new RecorderEvents());
212
184
  }
213
185
  }
214
186
  }
@@ -14,4 +14,15 @@ export function isPreloadAllowed(agentId) {
14
14
  export function canImportReplayAgg(agentId, sessionMgr) {
15
15
  if (!hasReplayPrerequisite(agentId)) return false;
16
16
  return !!sessionMgr?.isNew || !!sessionMgr?.state.sessionReplayMode; // Session Replay should only try to run if already running from a previous page, or at the beginning of a session
17
+ }
18
+ export function buildNRMetaNode(timestamp, timeKeeper) {
19
+ const correctedTimestamp = timeKeeper.correctAbsoluteTimestamp(timestamp);
20
+ return {
21
+ originalTimestamp: timestamp,
22
+ correctedTimestamp,
23
+ timestampDiff: timestamp - correctedTimestamp,
24
+ timeKeeperOriginTime: timeKeeper.originTime,
25
+ timeKeeperCorrectedOriginTime: timeKeeper.correctedOriginTime,
26
+ timeKeeperDiff: Math.floor(timeKeeper.originTime - timeKeeper.correctedOriginTime)
27
+ };
17
28
  }
@@ -15,6 +15,7 @@ import { gosCDN } from '../../common/window/nreum';
15
15
  import { apiMethods, asyncApiMethods } from './api-methods';
16
16
  import { SR_EVENT_EMITTER_TYPES } from '../../features/session_replay/constants';
17
17
  import { now } from '../../common/timing/now';
18
+ import { MODE } from '../../common/session/constants';
18
19
  export function setTopLevelCallers() {
19
20
  const nr = gosCDN();
20
21
  apiMethods.forEach(f => {
@@ -38,12 +39,17 @@ export function setTopLevelCallers() {
38
39
  return returnVals.length > 1 ? returnVals : returnVals[0];
39
40
  }
40
41
  }
42
+ const replayRunning = {};
41
43
  export function setAPI(agentIdentifier, forceDrain) {
42
44
  let runSoftNavOverSpa = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
43
45
  if (!forceDrain) registerDrain(agentIdentifier, 'api');
44
46
  const apiInterface = {};
45
47
  var instanceEE = ee.get(agentIdentifier);
46
48
  var tracerEE = instanceEE.get('tracer');
49
+ replayRunning[agentIdentifier] = MODE.OFF;
50
+ instanceEE.on(SR_EVENT_EMITTER_TYPES.REPLAY_RUNNING, isRunning => {
51
+ replayRunning[agentIdentifier] = isRunning;
52
+ });
47
53
  var prefix = 'api-';
48
54
  var spaPrefix = prefix + 'ixn-';
49
55
 
@@ -186,7 +192,7 @@ export function setAPI(agentIdentifier, forceDrain) {
186
192
  apiInterface.noticeError = function (err, customAttributes) {
187
193
  if (typeof err === 'string') err = new Error(err);
188
194
  handle(SUPPORTABILITY_METRIC_CHANNEL, ['API/noticeError/called'], undefined, FEATURE_NAMES.metrics, instanceEE);
189
- handle('err', [err, now(), false, customAttributes], undefined, FEATURE_NAMES.jserrors, instanceEE);
195
+ handle('err', [err, now(), false, customAttributes, !!replayRunning[agentIdentifier]], undefined, FEATURE_NAMES.jserrors, instanceEE);
190
196
  };
191
197
 
192
198
  // theres no window.load event on non-browser scopes, lazy load immediately