@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.
- package/CHANGELOG.md +22 -0
- package/dist/cjs/common/constants/env.cdn.js +2 -2
- package/dist/cjs/common/constants/env.npm.js +2 -2
- package/dist/cjs/common/constants/runtime.js +1 -1
- package/dist/cjs/common/session/session-entity.js +2 -1
- package/dist/cjs/common/timer/interaction-timer.js +16 -2
- package/dist/cjs/common/timing/time-keeper.js +1 -2
- package/dist/cjs/features/jserrors/aggregate/index.js +16 -6
- package/dist/cjs/features/jserrors/instrument/index.js +8 -3
- package/dist/cjs/features/session_replay/aggregate/index.js +48 -29
- package/dist/cjs/features/session_replay/constants.js +2 -1
- package/dist/cjs/features/session_replay/instrument/index.js +9 -2
- package/dist/cjs/features/session_replay/shared/recorder-events.js +1 -9
- package/dist/cjs/features/session_replay/shared/recorder.js +22 -50
- package/dist/cjs/features/session_replay/shared/utils.js +12 -0
- package/dist/cjs/loaders/api/api.js +7 -1
- package/dist/esm/common/constants/env.cdn.js +2 -2
- package/dist/esm/common/constants/env.npm.js +2 -2
- package/dist/esm/common/constants/runtime.js +1 -1
- package/dist/esm/common/session/session-entity.js +2 -1
- package/dist/esm/common/timer/interaction-timer.js +16 -2
- package/dist/esm/common/timing/time-keeper.js +1 -3
- package/dist/esm/features/jserrors/aggregate/index.js +16 -6
- package/dist/esm/features/jserrors/instrument/index.js +8 -3
- package/dist/esm/features/session_replay/aggregate/index.js +48 -29
- package/dist/esm/features/session_replay/constants.js +2 -1
- package/dist/esm/features/session_replay/instrument/index.js +9 -2
- package/dist/esm/features/session_replay/shared/recorder-events.js +1 -9
- package/dist/esm/features/session_replay/shared/recorder.js +23 -51
- package/dist/esm/features/session_replay/shared/utils.js +11 -0
- package/dist/esm/loaders/api/api.js +7 -1
- package/dist/types/common/constants/runtime.d.ts.map +1 -1
- package/dist/types/common/session/session-entity.d.ts.map +1 -1
- package/dist/types/common/timer/interaction-timer.d.ts +2 -0
- package/dist/types/common/timer/interaction-timer.d.ts.map +1 -1
- package/dist/types/common/timing/time-keeper.d.ts.map +1 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts +2 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/jserrors/instrument/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +5 -2
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/constants.d.ts +1 -0
- package/dist/types/features/session_replay/constants.d.ts.map +1 -1
- package/dist/types/features/session_replay/instrument/index.d.ts +1 -0
- package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder-events.d.ts +0 -8
- package/dist/types/features/session_replay/shared/recorder-events.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder.d.ts +1 -17
- package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/utils.d.ts +8 -0
- package/dist/types/features/session_replay/shared/utils.d.ts.map +1 -1
- package/dist/types/loaders/api/api.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/common/constants/runtime.js +1 -1
- package/src/common/session/session-entity.js +2 -1
- package/src/common/timer/interaction-timer.js +17 -2
- package/src/common/timing/time-keeper.js +1 -3
- package/src/features/jserrors/aggregate/index.js +15 -6
- package/src/features/jserrors/instrument/index.js +9 -4
- package/src/features/session_replay/aggregate/index.js +43 -25
- package/src/features/session_replay/constants.js +2 -1
- package/src/features/session_replay/instrument/index.js +7 -2
- package/src/features/session_replay/shared/recorder-events.js +1 -6
- package/src/features/session_replay/shared/recorder.js +21 -27
- package/src/features/session_replay/shared/utils.js +12 -0
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
-
|
|
66
|
-
|
|
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 =
|
|
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
|
|
88
|
-
|
|
89
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
270
|
-
|
|
271
|
-
|
|
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(
|
|
283
|
+
payload.body = payload.body.map(_ref3 => {
|
|
277
284
|
let {
|
|
278
285
|
__serialized,
|
|
279
286
|
...node
|
|
280
|
-
} =
|
|
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]
|
|
327
|
-
const lastEventTimestamp = events[events.length - 1]
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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.
|
|
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
|