@newrelic/browser-agent 1.255.0 → 1.256.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 (78) hide show
  1. package/CHANGELOG.md +15 -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/harvest/harvest.js +1 -0
  6. package/dist/cjs/common/session/session-entity.js +2 -1
  7. package/dist/cjs/common/timer/interaction-timer.js +16 -2
  8. package/dist/cjs/common/timing/time-keeper.js +1 -2
  9. package/dist/cjs/features/jserrors/aggregate/index.js +16 -6
  10. package/dist/cjs/features/jserrors/instrument/index.js +8 -3
  11. package/dist/cjs/features/session_replay/aggregate/index.js +48 -29
  12. package/dist/cjs/features/session_replay/constants.js +2 -1
  13. package/dist/cjs/features/session_replay/instrument/index.js +9 -2
  14. package/dist/cjs/features/session_replay/shared/recorder-events.js +1 -9
  15. package/dist/cjs/features/session_replay/shared/recorder.js +22 -50
  16. package/dist/cjs/features/session_replay/shared/utils.js +12 -0
  17. package/dist/cjs/features/session_trace/aggregate/index.js +19 -22
  18. package/dist/cjs/loaders/api/api.js +7 -1
  19. package/dist/cjs/loaders/configure/configure.js +1 -0
  20. package/dist/esm/common/constants/env.cdn.js +2 -2
  21. package/dist/esm/common/constants/env.npm.js +2 -2
  22. package/dist/esm/common/constants/runtime.js +1 -1
  23. package/dist/esm/common/harvest/harvest.js +1 -0
  24. package/dist/esm/common/session/session-entity.js +2 -1
  25. package/dist/esm/common/timer/interaction-timer.js +16 -2
  26. package/dist/esm/common/timing/time-keeper.js +1 -3
  27. package/dist/esm/features/jserrors/aggregate/index.js +16 -6
  28. package/dist/esm/features/jserrors/instrument/index.js +8 -3
  29. package/dist/esm/features/session_replay/aggregate/index.js +48 -29
  30. package/dist/esm/features/session_replay/constants.js +2 -1
  31. package/dist/esm/features/session_replay/instrument/index.js +9 -2
  32. package/dist/esm/features/session_replay/shared/recorder-events.js +1 -9
  33. package/dist/esm/features/session_replay/shared/recorder.js +23 -51
  34. package/dist/esm/features/session_replay/shared/utils.js +11 -0
  35. package/dist/esm/features/session_trace/aggregate/index.js +19 -22
  36. package/dist/esm/loaders/api/api.js +7 -1
  37. package/dist/esm/loaders/configure/configure.js +1 -0
  38. package/dist/types/common/constants/runtime.d.ts.map +1 -1
  39. package/dist/types/common/session/session-entity.d.ts.map +1 -1
  40. package/dist/types/common/timer/interaction-timer.d.ts +2 -0
  41. package/dist/types/common/timer/interaction-timer.d.ts.map +1 -1
  42. package/dist/types/common/timing/time-keeper.d.ts.map +1 -1
  43. package/dist/types/features/jserrors/aggregate/index.d.ts +2 -1
  44. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  45. package/dist/types/features/jserrors/instrument/index.d.ts.map +1 -1
  46. package/dist/types/features/session_replay/aggregate/index.d.ts +5 -2
  47. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  48. package/dist/types/features/session_replay/constants.d.ts +1 -0
  49. package/dist/types/features/session_replay/constants.d.ts.map +1 -1
  50. package/dist/types/features/session_replay/instrument/index.d.ts +1 -0
  51. package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
  52. package/dist/types/features/session_replay/shared/recorder-events.d.ts +0 -8
  53. package/dist/types/features/session_replay/shared/recorder-events.d.ts.map +1 -1
  54. package/dist/types/features/session_replay/shared/recorder.d.ts +1 -17
  55. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  56. package/dist/types/features/session_replay/shared/utils.d.ts +8 -0
  57. package/dist/types/features/session_replay/shared/utils.d.ts.map +1 -1
  58. package/dist/types/features/session_trace/aggregate/index.d.ts +2 -1
  59. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  60. package/dist/types/loaders/api/api.d.ts.map +1 -1
  61. package/dist/types/loaders/configure/configure.d.ts.map +1 -1
  62. package/package.json +2 -2
  63. package/src/common/constants/runtime.js +1 -1
  64. package/src/common/harvest/harvest.js +1 -1
  65. package/src/common/session/session-entity.js +2 -1
  66. package/src/common/timer/interaction-timer.js +17 -2
  67. package/src/common/timing/time-keeper.js +1 -3
  68. package/src/features/jserrors/aggregate/index.js +15 -6
  69. package/src/features/jserrors/instrument/index.js +9 -4
  70. package/src/features/session_replay/aggregate/index.js +43 -25
  71. package/src/features/session_replay/constants.js +2 -1
  72. package/src/features/session_replay/instrument/index.js +7 -2
  73. package/src/features/session_replay/shared/recorder-events.js +1 -6
  74. package/src/features/session_replay/shared/recorder.js +21 -27
  75. package/src/features/session_replay/shared/utils.js +12 -0
  76. package/src/features/session_trace/aggregate/index.js +18 -16
  77. package/src/loaders/api/api.js +10 -1
  78. package/src/loaders/configure/configure.js +1 -0
@@ -57,10 +57,11 @@ class Aggregate extends _aggregateBase.AggregateBase {
57
57
  // Very unlikely, but in case the existing XMLHttpRequest.prototype object on the page couldn't be wrapped.
58
58
  if (!this.agentRuntime.xhrWrappable) return;
59
59
  this.resourceObserver = argsObj?.resourceObserver; // undefined if observer couldn't be created
60
- this.ptid = '';
60
+ this.ptid = this.agentRuntime.ptid;
61
61
  this.trace = {};
62
62
  this.nodeCount = 0;
63
63
  this.sentTrace = null;
64
+ this.everSent = false;
64
65
  this.harvestTimeSeconds = (0, _config.getConfigurationValue)(agentIdentifier, 'session_trace.harvestTimeSeconds') || 10;
65
66
  this.maxNodesPerHarvest = (0, _config.getConfigurationValue)(agentIdentifier, 'session_trace.maxNodesPerHarvest') || 1000;
66
67
  /**
@@ -112,9 +113,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
112
113
  this.isStandalone = false;
113
114
  if (prevMode === _constants2.MODE.ERROR && this.#scheduler) {
114
115
  this.trimSTNs(ERROR_MODE_SECONDS_WINDOW); // up until now, Trace would've been just buffering nodes up to max, which needs to be trimmed to last X seconds
115
- this.#scheduler.runHarvest({
116
- needResponse: true
117
- });
116
+ this.#scheduler.runHarvest({});
118
117
  } else {
119
118
  controlTraceOp(_constants2.MODE.FULL);
120
119
  }
@@ -137,7 +136,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
137
136
  sessionTraceMode: _constants2.MODE.OFF
138
137
  });
139
138
  operationalGate.permanentlyDecide(false);
140
- if (mostRecentModeKnown === _constants2.MODE.FULL) this.#scheduler?.runHarvest(); // allow queued nodes (past opGate) to final harvest, unless they were buffered in other modes
139
+ if (mostRecentModeKnown === _constants2.MODE.FULL) this.#scheduler?.runHarvest({}); // allow queued nodes (past opGate) to final harvest, unless they were buffered in other modes
141
140
  this.#scheduler?.stopTimer(true); // the 'true' arg here will forcibly block any future call to runHarvest, so the last runHarvest above must be prior
142
141
  this.#scheduler = null;
143
142
  };
@@ -156,9 +155,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
156
155
  - if trace switches to Full mode, harvest should start (prev: Error) if not already running (prev: Full). */
157
156
  this.ee.on(_constants2.SESSION_EVENTS.RESUME, () => {
158
157
  const updatedTraceMode = sessionEntity.state.sessionTraceMode;
159
- if (updatedTraceMode === _constants2.MODE.OFF) stopTracePerm();else if (updatedTraceMode === _constants2.MODE.FULL && this.#scheduler && !this.#scheduler.started) this.#scheduler.runHarvest({
160
- needResponse: true
161
- });
158
+ if (updatedTraceMode === _constants2.MODE.OFF) stopTracePerm();else if (updatedTraceMode === _constants2.MODE.FULL && this.#scheduler && !this.#scheduler.started) this.#scheduler.runHarvest({});
162
159
  mostRecentModeKnown = updatedTraceMode;
163
160
  });
164
161
  this.ee.on(_constants2.SESSION_EVENTS.PAUSE, () => {
@@ -249,15 +246,12 @@ class Aggregate extends _aggregateBase.AggregateBase {
249
246
  retryDelay: this.harvestTimeSeconds
250
247
  }, this);
251
248
  this.#scheduler.harvest.on('resources', this.#prepareHarvest.bind(this));
252
- if (dontStartHarvestYet === false) this.#scheduler.runHarvest({
253
- needResponse: true
254
- }); // sends first stn harvest immediately
249
+ if (dontStartHarvestYet === false) this.#scheduler.runHarvest({}); // sends first stn harvest immediately
255
250
  startupBuffer.decide(true); // signal to ALLOW & process data in EE's buffer into internal nodes queued for next harvest
256
251
  }
257
252
  #onHarvestFinished(result) {
258
- if (result.sent && result.responseText && !this.ptid) {
259
- // continue interval harvest only if ptid was returned by server on the first
260
- this.agentRuntime.ptid = this.ptid = result.responseText;
253
+ if (result.sent && !result.failed && !this.#scheduler.started) {
254
+ // continue interval harvest only after first call
261
255
  this.#scheduler.startTimer(this.harvestTimeSeconds);
262
256
  }
263
257
  if (result.sent && result.retry && this.sentTrace) {
@@ -273,15 +267,18 @@ class Aggregate extends _aggregateBase.AggregateBase {
273
267
  }
274
268
  #prepareHarvest(options) {
275
269
  if (this.isStandalone) {
276
- if (this.ptid && (0, _now.now)() >= MAX_TRACE_DURATION) {
277
- // Perform a final harvest once we hit or exceed the max session trace time
278
- options.isFinalHarvest = true;
279
- this.operationalGate.permanentlyDecide(false);
280
- this.#scheduler.stopTimer(true);
281
- } else if (this.ptid && this.nodeCount <= REQ_THRESHOLD_TO_SEND && !options.isFinalHarvest) {
282
- // Only harvest when more than some threshold of nodes are pending, after the very first harvest, with the exception of the last outgoing harvest.
283
- return;
270
+ if (this.#scheduler.started) {
271
+ if ((0, _now.now)() >= MAX_TRACE_DURATION) {
272
+ // Perform a final harvest once we hit or exceed the max session trace time
273
+ options.isFinalHarvest = true;
274
+ this.operationalGate.permanentlyDecide(false);
275
+ this.#scheduler.stopTimer(true);
276
+ } else if (this.nodeCount <= REQ_THRESHOLD_TO_SEND && !options.isFinalHarvest) {
277
+ // Only harvest when more than some threshold of nodes are pending, after the very first harvest, with the exception of the last outgoing harvest.
278
+ return;
279
+ }
284
280
  }
281
+ // else, we must be on the very first harvest (standalone mode), so go to next square
285
282
  } else {
286
283
  // -- *cli May '26 - Update: Not rate limiting backgrounded pages either for now.
287
284
  // if (this.ptid && document.visibilityState === 'hidden' && this.nodeCount <= REQ_THRESHOLD_TO_SEND) return
@@ -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
@@ -60,6 +60,7 @@ function configure(agent) {
60
60
  agent.runSoftNavOverSpa &&= updatedInit.soft_navigations.enabled === true && updatedInit.feature_flags.includes('soft_nav');
61
61
  }
62
62
  runtime.denyList = [...(updatedInit.ajax.deny_list || []), ...(updatedInit.ajax.block_internal ? internalTrafficList : [])];
63
+ runtime.ptid = agent.agentIdentifier;
63
64
  (0, _config.setRuntime)(agent.agentIdentifier, runtime);
64
65
  if (agent.api === undefined) agent.api = (0, _api.setAPI)(agent.agentIdentifier, forceDrain, agent.runSoftNavOverSpa);
65
66
  if (agent.exposed === undefined) agent.exposed = exposed;
@@ -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.0";
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.0";
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());
@@ -178,6 +178,7 @@ export class Harvest extends SharedContext {
178
178
  const cbResult = {
179
179
  sent: this.status !== 0,
180
180
  status: this.status,
181
+ failed: this.status === 0 || this.status >= 400,
181
182
  xhr: this,
182
183
  fullUrl
183
184
  };
@@ -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