@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.
- package/CHANGELOG.md +15 -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/harvest/harvest.js +1 -0
- 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/features/session_trace/aggregate/index.js +19 -22
- package/dist/cjs/loaders/api/api.js +7 -1
- package/dist/cjs/loaders/configure/configure.js +1 -0
- 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/harvest/harvest.js +1 -0
- 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/features/session_trace/aggregate/index.js +19 -22
- package/dist/esm/loaders/api/api.js +7 -1
- package/dist/esm/loaders/configure/configure.js +1 -0
- 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/features/session_trace/aggregate/index.d.ts +2 -1
- package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
- package/dist/types/loaders/api/api.d.ts.map +1 -1
- package/dist/types/loaders/configure/configure.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/common/constants/runtime.js +1 -1
- package/src/common/harvest/harvest.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/features/session_trace/aggregate/index.js +18 -16
- package/src/loaders/api/api.js +10 -1
- 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.
|
|
259
|
-
// continue interval harvest only
|
|
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.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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.
|
|
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.
|
|
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.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.
|
|
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
|