@newrelic/browser-agent 1.248.0 → 1.250.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 +27 -0
- package/dist/cjs/common/config/state/init.js +3 -3
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/harvest/harvest-scheduler.js +2 -2
- package/dist/cjs/common/harvest/harvest.js +4 -3
- package/dist/cjs/common/ids/unique-id.js +5 -4
- package/dist/cjs/common/session/constants.js +20 -2
- package/dist/cjs/common/session/session-entity.js +8 -26
- package/dist/cjs/common/url/encode.js +2 -0
- package/dist/cjs/features/metrics/aggregate/index.js +3 -1
- package/dist/cjs/features/session_replay/aggregate/index.js +114 -277
- package/dist/cjs/features/session_replay/constants.js +57 -2
- package/dist/cjs/features/session_replay/instrument/index.js +38 -16
- package/dist/cjs/features/session_replay/shared/recorder-events.js +31 -0
- package/dist/cjs/features/session_replay/shared/recorder.js +155 -0
- package/dist/cjs/features/session_replay/{replay-mode.js → shared/replay-mode.js} +5 -5
- package/dist/cjs/features/session_trace/aggregate/index.js +25 -25
- package/dist/cjs/loaders/agent-base.js +18 -13
- package/dist/esm/common/config/state/init.js +3 -3
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/harvest/harvest-scheduler.js +1 -1
- package/dist/esm/common/harvest/harvest.js +4 -3
- package/dist/esm/common/ids/unique-id.js +5 -4
- package/dist/esm/common/session/constants.js +16 -1
- package/dist/esm/common/session/session-entity.js +2 -16
- package/dist/esm/common/url/encode.js +2 -0
- package/dist/esm/features/metrics/aggregate/index.js +3 -1
- package/dist/esm/features/session_replay/aggregate/index.js +95 -254
- package/dist/esm/features/session_replay/constants.js +49 -1
- package/dist/esm/features/session_replay/instrument/index.js +24 -1
- package/dist/esm/features/session_replay/shared/recorder-events.js +24 -0
- package/dist/esm/features/session_replay/shared/recorder.js +148 -0
- package/dist/esm/features/session_replay/{replay-mode.js → shared/replay-mode.js} +4 -4
- package/dist/esm/features/session_trace/aggregate/index.js +2 -2
- package/dist/esm/loaders/agent-base.js +18 -13
- package/dist/types/common/config/state/init.d.ts.map +1 -1
- package/dist/types/common/harvest/harvest.d.ts +1 -1
- package/dist/types/common/harvest/harvest.d.ts.map +1 -1
- package/dist/types/common/ids/unique-id.d.ts.map +1 -1
- package/dist/types/common/session/constants.d.ts +15 -0
- package/dist/types/common/session/session-entity.d.ts +0 -15
- package/dist/types/common/session/session-entity.d.ts.map +1 -1
- package/dist/types/common/url/encode.d.ts +1 -1
- package/dist/types/common/url/encode.d.ts.map +1 -1
- package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +7 -63
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/constants.d.ts +55 -0
- package/dist/types/features/session_replay/constants.d.ts.map +1 -1
- package/dist/types/features/session_replay/instrument/index.d.ts +2 -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 +21 -0
- package/dist/types/features/session_replay/shared/recorder-events.d.ts.map +1 -0
- package/dist/types/features/session_replay/shared/recorder.d.ts +40 -0
- package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -0
- package/dist/types/features/session_replay/shared/replay-mode.d.ts.map +1 -0
- package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
- package/dist/types/loaders/agent-base.d.ts +2 -1
- package/dist/types/loaders/agent-base.d.ts.map +1 -1
- package/package.json +1 -5
- package/src/common/config/state/init.js +6 -4
- package/src/common/harvest/harvest-scheduler.js +1 -1
- package/src/common/harvest/harvest.js +4 -3
- package/src/common/ids/unique-id.js +5 -4
- package/src/common/session/__mocks__/session-entity.js +0 -6
- package/src/common/session/constants.js +18 -0
- package/src/common/session/session-entity.js +1 -17
- package/src/common/url/encode.js +2 -1
- package/src/features/metrics/aggregate/index.js +3 -1
- package/src/features/session_replay/aggregate/index.js +88 -246
- package/src/features/session_replay/constants.js +45 -0
- package/src/features/session_replay/instrument/index.js +18 -1
- package/src/features/session_replay/shared/recorder-events.js +25 -0
- package/src/features/session_replay/shared/recorder.js +145 -0
- package/src/features/session_replay/{replay-mode.js → shared/replay-mode.js} +4 -4
- package/src/features/session_trace/aggregate/index.js +2 -2
- package/src/loaders/agent-base.js +18 -13
- package/dist/types/features/session_replay/replay-mode.d.ts.map +0 -1
- /package/dist/types/features/session_replay/{replay-mode.d.ts → shared/replay-mode.d.ts} +0 -0
|
@@ -41,12 +41,13 @@ export function generateUuid() {
|
|
|
41
41
|
let randomValueTable;
|
|
42
42
|
let randomValueIndex = 0;
|
|
43
43
|
if (crypto && crypto.getRandomValues) {
|
|
44
|
+
// For a UUID, we only need 30 characters since two characters are pre-defined
|
|
44
45
|
// eslint-disable-next-line
|
|
45
|
-
randomValueTable = crypto.getRandomValues(new Uint8Array(
|
|
46
|
+
randomValueTable = crypto.getRandomValues(new Uint8Array(30));
|
|
46
47
|
}
|
|
47
48
|
return uuidv4Template.split('').map(templateInput => {
|
|
48
49
|
if (templateInput === 'x') {
|
|
49
|
-
return getRandomValue(randomValueTable, ++
|
|
50
|
+
return getRandomValue(randomValueTable, randomValueIndex++).toString(16);
|
|
50
51
|
} else if (templateInput === 'y') {
|
|
51
52
|
// this is the uuid variant per spec (8, 9, a, b)
|
|
52
53
|
// % 4, then shift to get values 8-11
|
|
@@ -69,11 +70,11 @@ export function generateRandomHexString(length) {
|
|
|
69
70
|
let randomValueIndex = 0;
|
|
70
71
|
if (crypto && crypto.getRandomValues) {
|
|
71
72
|
// eslint-disable-next-line
|
|
72
|
-
randomValueTable = crypto.getRandomValues(new Uint8Array(
|
|
73
|
+
randomValueTable = crypto.getRandomValues(new Uint8Array(length));
|
|
73
74
|
}
|
|
74
75
|
const chars = [];
|
|
75
76
|
for (var i = 0; i < length; i++) {
|
|
76
|
-
chars.push(getRandomValue(randomValueTable, ++
|
|
77
|
+
chars.push(getRandomValue(randomValueTable, randomValueIndex++).toString(16));
|
|
77
78
|
}
|
|
78
79
|
return chars.join('');
|
|
79
80
|
}
|
|
@@ -1,3 +1,18 @@
|
|
|
1
1
|
export const PREFIX = 'NRBA';
|
|
2
2
|
export const DEFAULT_EXPIRES_MS = 14400000;
|
|
3
|
-
export const DEFAULT_INACTIVE_MS = 1800000;
|
|
3
|
+
export const DEFAULT_INACTIVE_MS = 1800000;
|
|
4
|
+
export const SESSION_EVENTS = {
|
|
5
|
+
PAUSE: 'session-pause',
|
|
6
|
+
RESET: 'session-reset',
|
|
7
|
+
RESUME: 'session-resume',
|
|
8
|
+
UPDATE: 'session-update'
|
|
9
|
+
};
|
|
10
|
+
export const SESSION_EVENT_TYPES = {
|
|
11
|
+
SAME_TAB: 'same-tab',
|
|
12
|
+
CROSS_TAB: 'cross-tab'
|
|
13
|
+
};
|
|
14
|
+
export const MODE = {
|
|
15
|
+
OFF: 0,
|
|
16
|
+
FULL: 1,
|
|
17
|
+
ERROR: 2
|
|
18
|
+
};
|
|
@@ -4,7 +4,7 @@ import { stringify } from '../util/stringify';
|
|
|
4
4
|
import { ee } from '../event-emitter/contextual-ee';
|
|
5
5
|
import { Timer } from '../timer/timer';
|
|
6
6
|
import { isBrowserScope } from '../constants/runtime';
|
|
7
|
-
import { DEFAULT_EXPIRES_MS, DEFAULT_INACTIVE_MS, PREFIX } from './constants';
|
|
7
|
+
import { DEFAULT_EXPIRES_MS, DEFAULT_INACTIVE_MS, MODE, PREFIX, SESSION_EVENTS, SESSION_EVENT_TYPES } from './constants';
|
|
8
8
|
import { InteractionTimer } from '../timer/interaction-timer';
|
|
9
9
|
import { wrapEvents } from '../wrap';
|
|
10
10
|
import { getModeledObject } from '../config/state/configurable';
|
|
@@ -12,11 +12,7 @@ import { handle } from '../event-emitter/handle';
|
|
|
12
12
|
import { SUPPORTABILITY_METRIC_CHANNEL } from '../../features/metrics/constants';
|
|
13
13
|
import { FEATURE_NAMES } from '../../loaders/features/features';
|
|
14
14
|
import { windowAddEventListener } from '../event-listener/event-listener-opts';
|
|
15
|
-
|
|
16
|
-
OFF: 0,
|
|
17
|
-
FULL: 1,
|
|
18
|
-
ERROR: 2
|
|
19
|
-
};
|
|
15
|
+
|
|
20
16
|
// this is what can be stored in local storage (not enforced but probably should be)
|
|
21
17
|
// these values should sync between local storage and the parent class props
|
|
22
18
|
const model = {
|
|
@@ -30,16 +26,6 @@ const model = {
|
|
|
30
26
|
traceHarvestStarted: false,
|
|
31
27
|
custom: {}
|
|
32
28
|
};
|
|
33
|
-
export const SESSION_EVENTS = {
|
|
34
|
-
PAUSE: 'session-pause',
|
|
35
|
-
RESET: 'session-reset',
|
|
36
|
-
RESUME: 'session-resume',
|
|
37
|
-
UPDATE: 'session-update'
|
|
38
|
-
};
|
|
39
|
-
export const SESSION_EVENT_TYPES = {
|
|
40
|
-
SAME_TAB: 'same-tab',
|
|
41
|
-
CROSS_TAB: 'cross-tab'
|
|
42
|
-
};
|
|
43
29
|
export class SessionEntity {
|
|
44
30
|
/**
|
|
45
31
|
* Create a self-managing Session Entity. This entity is scoped to the agent identifier which triggered it, allowing for multiple simultaneous session objects to exist.
|
|
@@ -64,6 +64,8 @@ export function obj(payload, maxBytes) {
|
|
|
64
64
|
|
|
65
65
|
// Constructs an HTTP parameter to add to the BAM router URL
|
|
66
66
|
export function param(name, value) {
|
|
67
|
+
let base = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
|
|
68
|
+
if (Object.keys(base).includes(name)) return ''; // we assume if feature supplied a matching qp to the base, we should honor what the feature sent over the default
|
|
67
69
|
if (value && typeof value === 'string') {
|
|
68
70
|
return '&' + name + '=' + qs(value);
|
|
69
71
|
}
|
|
@@ -95,10 +95,12 @@ export class Aggregate extends AggregateBase {
|
|
|
95
95
|
|
|
96
96
|
// Check if proxy for either chunks or beacon is being used
|
|
97
97
|
const {
|
|
98
|
-
proxy
|
|
98
|
+
proxy,
|
|
99
|
+
privacy
|
|
99
100
|
} = getConfiguration(this.agentIdentifier);
|
|
100
101
|
if (proxy.assets) this.storeSupportabilityMetrics('Config/AssetsUrl/Changed');
|
|
101
102
|
if (proxy.beacon) this.storeSupportabilityMetrics('Config/BeaconUrl/Changed');
|
|
103
|
+
if (!(isBrowserScope && privacy.cookies_enabled)) this.storeSupportabilityMetrics('Config/SessionTracking/Disabled');
|
|
102
104
|
}
|
|
103
105
|
eachSessionChecks() {
|
|
104
106
|
if (!isBrowserScope) return;
|
|
@@ -12,10 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
import { registerHandler } from '../../../common/event-emitter/register-handler';
|
|
14
14
|
import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler';
|
|
15
|
-
import { FEATURE_NAME } from '../constants';
|
|
16
|
-
import { stringify } from '../../../common/util/stringify';
|
|
15
|
+
import { ABORT_REASONS, FEATURE_NAME, MAX_PAYLOAD_SIZE, QUERY_PARAM_PADDING, RRWEB_EVENT_TYPES } from '../constants';
|
|
17
16
|
import { getConfigurationValue, getInfo, getRuntime } from '../../../common/config/config';
|
|
18
|
-
import { SESSION_EVENTS, MODE, SESSION_EVENT_TYPES } from '../../../common/session/session-entity';
|
|
19
17
|
import { AggregateBase } from '../../utils/aggregate-base';
|
|
20
18
|
import { sharedChannel } from '../../../common/constants/shared-channel';
|
|
21
19
|
import { obj as encodeObj } from '../../../common/url/encode';
|
|
@@ -26,128 +24,62 @@ import { handle } from '../../../common/event-emitter/handle';
|
|
|
26
24
|
import { FEATURE_NAMES } from '../../../loaders/features/features';
|
|
27
25
|
import { RRWEB_VERSION } from "../../../common/constants/env.npm";
|
|
28
26
|
import { now } from '../../../common/timing/now';
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Load: 1,
|
|
33
|
-
FullSnapshot: 2,
|
|
34
|
-
IncrementalSnapshot: 3,
|
|
35
|
-
Meta: 4,
|
|
36
|
-
Custom: 5
|
|
37
|
-
};
|
|
38
|
-
const ABORT_REASONS = {
|
|
39
|
-
RESET: {
|
|
40
|
-
message: 'Session was reset',
|
|
41
|
-
sm: 'Reset'
|
|
42
|
-
},
|
|
43
|
-
IMPORT: {
|
|
44
|
-
message: 'Recorder failed to import',
|
|
45
|
-
sm: 'Import'
|
|
46
|
-
},
|
|
47
|
-
TOO_MANY: {
|
|
48
|
-
message: '429: Too Many Requests',
|
|
49
|
-
sm: 'Too-Many'
|
|
50
|
-
},
|
|
51
|
-
TOO_BIG: {
|
|
52
|
-
message: 'Payload was too large',
|
|
53
|
-
sm: 'Too-Big'
|
|
54
|
-
},
|
|
55
|
-
CROSS_TAB: {
|
|
56
|
-
message: 'Session Entity was set to OFF on another tab',
|
|
57
|
-
sm: 'Cross-Tab'
|
|
58
|
-
}
|
|
59
|
-
};
|
|
60
|
-
let recorder, gzipper, u8;
|
|
61
|
-
|
|
62
|
-
/** Vortex caps payload sizes at 1MB */
|
|
63
|
-
export const MAX_PAYLOAD_SIZE = 1000000;
|
|
64
|
-
/** Unloading caps around 64kb */
|
|
65
|
-
export const IDEAL_PAYLOAD_SIZE = 64000;
|
|
66
|
-
/** Reserved room for query param attrs */
|
|
67
|
-
const QUERY_PARAM_PADDING = 5000;
|
|
68
|
-
/** Interval between forcing new full snapshots -- 15 seconds in error mode (x2), 5 minutes in full mode */
|
|
69
|
-
const CHECKOUT_MS = {
|
|
70
|
-
[MODE.ERROR]: 15000,
|
|
71
|
-
[MODE.FULL]: 300000,
|
|
72
|
-
[MODE.OFF]: 0
|
|
73
|
-
};
|
|
27
|
+
import { MODE, SESSION_EVENTS, SESSION_EVENT_TYPES } from '../../../common/session/constants';
|
|
28
|
+
import { stringify } from '../../../common/util/stringify';
|
|
29
|
+
let gzipper, u8;
|
|
74
30
|
export class Aggregate extends AggregateBase {
|
|
75
31
|
static featureName = FEATURE_NAME;
|
|
76
|
-
|
|
32
|
+
// pass the recorder into the aggregator
|
|
33
|
+
constructor(agentIdentifier, aggregator, args) {
|
|
77
34
|
super(agentIdentifier, aggregator, FEATURE_NAME);
|
|
78
|
-
/** Each page mutation or event will be stored (raw) in this array. This array will be cleared on each harvest */
|
|
79
|
-
this.events = [];
|
|
80
|
-
/** Backlog used for a 2-part sliding window to guarantee a 15-30s buffer window */
|
|
81
|
-
this.backloggedEvents = [];
|
|
82
35
|
/** The interval to harvest at. This gets overridden if the size of the payload exceeds certain thresholds */
|
|
83
36
|
this.harvestTimeSeconds = getConfigurationValue(this.agentIdentifier, 'session_replay.harvestTimeSeconds') || 60;
|
|
84
37
|
/** Set once the recorder has fully initialized after flag checks and sampling */
|
|
85
38
|
this.initialized = false;
|
|
86
|
-
/** Set once an error has been detected on the page. Never unset */
|
|
87
|
-
this.errorNoticed = false;
|
|
88
|
-
/** The "mode" to record in. Defaults to "OFF" until flags and sampling are checked. See "MODE" constant. */
|
|
89
|
-
this.mode = MODE.OFF;
|
|
90
39
|
/** Set once the feature has been "aborted" to prevent other side-effects from continuing */
|
|
91
40
|
this.blocked = false;
|
|
92
|
-
/** True when actively recording, false when paused or stopped */
|
|
93
|
-
this.recording = false;
|
|
94
41
|
/** can shut off efforts to compress the data */
|
|
95
42
|
this.shouldCompress = true;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
this.hasSnapshot = false;
|
|
102
|
-
/** Payload metadata -- Should indicate that the payload being sent has a meta node. The meta node should always precede a snapshot node. */
|
|
103
|
-
this.hasMeta = false;
|
|
104
|
-
/** Payload metadata -- Should indicate that the payload being sent contains an error. Used for query/filter purposes in UI */
|
|
105
|
-
this.hasError = false;
|
|
106
|
-
|
|
107
|
-
/** Payload metadata -- Should indicate when a replay blob started recording. Resets each time a harvest occurs.
|
|
108
|
-
* cycle timestamps are used as fallbacks if event timestamps cannot be used
|
|
109
|
-
*/
|
|
110
|
-
this.cycleTimestamp = undefined;
|
|
111
|
-
|
|
112
|
-
/** A value which increments with every new mutation node reported. Resets after a harvest is sent */
|
|
113
|
-
this.payloadBytesEstimation = 0;
|
|
114
|
-
|
|
115
|
-
/** Hold on to the last meta node, so that it can be re-inserted if the meta and snapshot nodes are broken up due to harvesting */
|
|
116
|
-
this.lastMeta = undefined;
|
|
43
|
+
/** the mode to start in. Defaults to off */
|
|
44
|
+
const {
|
|
45
|
+
session
|
|
46
|
+
} = getRuntime(this.agentIdentifier);
|
|
47
|
+
this.mode = session.state.sessionReplayMode || MODE.OFF;
|
|
117
48
|
|
|
118
49
|
/** set by BCS response */
|
|
119
50
|
this.entitled = false;
|
|
51
|
+
this.recorder = args?.recorder;
|
|
52
|
+
if (this.recorder) this.recorder.parent = this;
|
|
120
53
|
const shouldSetup = getConfigurationValue(agentIdentifier, 'privacy.cookies_enabled') === true && getConfigurationValue(agentIdentifier, 'session_trace.enabled') === true;
|
|
121
|
-
|
|
122
|
-
/** The method to stop recording. This defaults to a noop, but is overwritten once the recording library is imported and initialized */
|
|
123
|
-
this.stopRecording = () => {/* no-op until set by rrweb initializer */};
|
|
124
54
|
if (shouldSetup) {
|
|
125
55
|
// 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.
|
|
126
56
|
this.ee.on(SESSION_EVENTS.RESET, () => {
|
|
57
|
+
this.scheduler.runHarvest();
|
|
127
58
|
this.abort(ABORT_REASONS.RESET);
|
|
128
59
|
});
|
|
129
60
|
|
|
130
61
|
// The SessionEntity class can emit a message indicating the session was paused (visibility change). This feature must stop recording if that occurs.
|
|
131
62
|
this.ee.on(SESSION_EVENTS.PAUSE, () => {
|
|
132
|
-
this.stopRecording();
|
|
63
|
+
this.recorder?.stopRecording();
|
|
133
64
|
});
|
|
134
65
|
// The SessionEntity class can emit a message indicating the session was resumed (visibility change). This feature must start running again (if already running) if that occurs.
|
|
135
66
|
this.ee.on(SESSION_EVENTS.RESUME, () => {
|
|
67
|
+
if (!this.recorder) return;
|
|
136
68
|
// if the mode changed on a different tab, it needs to update this instance to match
|
|
137
69
|
const {
|
|
138
70
|
session
|
|
139
71
|
} = getRuntime(this.agentIdentifier);
|
|
140
72
|
this.mode = session.state.sessionReplayMode;
|
|
141
73
|
if (!this.initialized || this.mode === MODE.OFF) return;
|
|
142
|
-
this.startRecording();
|
|
74
|
+
this.recorder?.startRecording();
|
|
143
75
|
});
|
|
144
76
|
this.ee.on(SESSION_EVENTS.UPDATE, (type, data) => {
|
|
145
|
-
if (!this.initialized || this.blocked || type !== SESSION_EVENT_TYPES.CROSS_TAB) return;
|
|
77
|
+
if (!this.recorder || !this.initialized || this.blocked || type !== SESSION_EVENT_TYPES.CROSS_TAB) return;
|
|
146
78
|
if (this.mode !== MODE.OFF && data.sessionReplayMode === MODE.OFF) this.abort(ABORT_REASONS.CROSS_TAB);
|
|
147
79
|
this.mode = data.sessionReplay;
|
|
148
80
|
});
|
|
149
81
|
|
|
150
|
-
// Bespoke logic for
|
|
82
|
+
// Bespoke logic for blobs endpoint.
|
|
151
83
|
this.scheduler = new HarvestScheduler('browser/blobs', {
|
|
152
84
|
onFinished: this.onHarvestFinished.bind(this),
|
|
153
85
|
retryDelay: this.harvestTimeSeconds,
|
|
@@ -158,7 +90,7 @@ export class Aggregate extends AggregateBase {
|
|
|
158
90
|
// if it has aborted or BCS returned bad entitlements, do not allow
|
|
159
91
|
if (this.blocked || !this.entitled) return;
|
|
160
92
|
// if it isnt already (fully) initialized... initialize it
|
|
161
|
-
if (!recorder) this.initializeRecording(false, true, true);
|
|
93
|
+
if (!this.recorder) this.initializeRecording(false, true, true);
|
|
162
94
|
// its been initialized and imported the recorder but its not recording (mode === off || error)
|
|
163
95
|
else if (this.mode !== MODE.FULL) this.switchToFull();
|
|
164
96
|
// if it gets all the way to here, that means a full session is already recording... do nothing
|
|
@@ -170,8 +102,8 @@ export class Aggregate extends AggregateBase {
|
|
|
170
102
|
// Wait for an error to be reported. This currently is wrapped around the "Error" feature. This is a feature-feature dependency.
|
|
171
103
|
// This was to ensure that all errors, including those on the page before load and those handled with "noticeError" are accounted for. Needs evalulation
|
|
172
104
|
registerHandler('errorAgg', e => {
|
|
173
|
-
this.hasError = true;
|
|
174
105
|
this.errorNoticed = true;
|
|
106
|
+
if (this.recorder) this.recorder.currentBufferTarget.hasError = true;
|
|
175
107
|
// run once
|
|
176
108
|
if (this.mode === MODE.ERROR && globalScope?.document.visibilityState === 'visible') {
|
|
177
109
|
this.switchToFull();
|
|
@@ -180,6 +112,7 @@ export class Aggregate extends AggregateBase {
|
|
|
180
112
|
this.waitForFlags(['sr']).then(_ref => {
|
|
181
113
|
let [flagOn] = _ref;
|
|
182
114
|
this.entitled = flagOn;
|
|
115
|
+
if (!this.entitled && this.recorder?.recording) this.recorder.abort(ABORT_REASONS.ENTITLEMENTS);
|
|
183
116
|
this.initializeRecording(Math.random() * 100 < getConfigurationValue(this.agentIdentifier, 'session_replay.error_sampling_rate'), Math.random() * 100 < getConfigurationValue(this.agentIdentifier, 'session_replay.sampling_rate'));
|
|
184
117
|
}).then(() => sharedChannel.onReplayReady(this.mode)); // notify watchers that replay started with the mode
|
|
185
118
|
|
|
@@ -189,9 +122,9 @@ export class Aggregate extends AggregateBase {
|
|
|
189
122
|
switchToFull() {
|
|
190
123
|
this.mode = MODE.FULL;
|
|
191
124
|
// if the error was noticed AFTER the recorder was already imported....
|
|
192
|
-
if (recorder && this.initialized) {
|
|
193
|
-
this.stopRecording();
|
|
194
|
-
this.startRecording();
|
|
125
|
+
if (this.recorder && this.initialized) {
|
|
126
|
+
this.recorder.stopRecording();
|
|
127
|
+
this.recorder.startRecording();
|
|
195
128
|
this.scheduler.startTimer(this.harvestTimeSeconds);
|
|
196
129
|
this.syncWithSessionManager({
|
|
197
130
|
sessionReplayMode: this.mode
|
|
@@ -209,16 +142,17 @@ export class Aggregate extends AggregateBase {
|
|
|
209
142
|
*/
|
|
210
143
|
async initializeRecording(errorSample, fullSample, ignoreSession) {
|
|
211
144
|
this.initialized = true;
|
|
212
|
-
if (!this.entitled
|
|
213
|
-
|
|
214
|
-
session
|
|
215
|
-
} = getRuntime(this.agentIdentifier);
|
|
145
|
+
if (!this.entitled) return;
|
|
146
|
+
|
|
216
147
|
// if theres an existing session replay in progress, there's no need to sample, just check the entitlements response
|
|
217
148
|
// if not, these sample flags need to be checked
|
|
218
149
|
// if this isnt the FIRST load of a session AND
|
|
219
150
|
// we are not actively recording SR... DO NOT import or run the recording library
|
|
220
151
|
// session replay samples can only be decided on the first load of a session
|
|
221
152
|
// session replays can continue if already in progress
|
|
153
|
+
const {
|
|
154
|
+
session
|
|
155
|
+
} = getRuntime(this.agentIdentifier);
|
|
222
156
|
if (!session.isNew && !ignoreSession) {
|
|
223
157
|
// inherit the mode of the existing session
|
|
224
158
|
this.mode = session.state.sessionReplayMode;
|
|
@@ -227,24 +161,32 @@ export class Aggregate extends AggregateBase {
|
|
|
227
161
|
if (fullSample) this.mode = MODE.FULL; // full mode has precedence over error mode
|
|
228
162
|
else if (errorSample) this.mode = MODE.ERROR;
|
|
229
163
|
// If neither are selected, then don't record (early return)
|
|
230
|
-
else
|
|
164
|
+
else {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (!this.recorder) {
|
|
169
|
+
try {
|
|
170
|
+
// Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
|
|
171
|
+
const {
|
|
172
|
+
Recorder
|
|
173
|
+
} = await import( /* webpackChunkName: "recorder" */'../shared/recorder');
|
|
174
|
+
this.recorder = new Recorder(this);
|
|
175
|
+
this.recorder.currentBufferTarget.hasError = this.errorNoticed;
|
|
176
|
+
} catch (err) {
|
|
177
|
+
return this.abort(ABORT_REASONS.IMPORT);
|
|
178
|
+
}
|
|
231
179
|
}
|
|
232
180
|
|
|
233
181
|
// If an error was noticed before the mode could be set (like in the early lifecycle of the page), immediately set to FULL mode
|
|
234
182
|
if (this.mode === MODE.ERROR && this.errorNoticed) {
|
|
235
183
|
this.mode = MODE.FULL;
|
|
236
184
|
}
|
|
237
|
-
try {
|
|
238
|
-
// Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
|
|
239
|
-
recorder = (await import( /* webpackChunkName: "recorder" */'rrweb')).record;
|
|
240
|
-
} catch (err) {
|
|
241
|
-
return this.abort(ABORT_REASONS.IMPORT);
|
|
242
|
-
}
|
|
243
185
|
|
|
244
186
|
// FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
|
|
245
187
|
// ERROR mode will do this until an error is thrown, and then switch into FULL mode.
|
|
246
188
|
// If an error happened in ERROR mode before we've gotten to this stage, it will have already set the mode to FULL
|
|
247
|
-
if (this.mode === MODE.FULL) {
|
|
189
|
+
if (this.mode === MODE.FULL && !this.scheduler.started) {
|
|
248
190
|
// We only report (harvest) in FULL mode
|
|
249
191
|
this.scheduler.startTimer(this.harvestTimeSeconds);
|
|
250
192
|
}
|
|
@@ -260,24 +202,41 @@ export class Aggregate extends AggregateBase {
|
|
|
260
202
|
// compressor failed to load, but we can still record without compression as a last ditch effort
|
|
261
203
|
this.shouldCompress = false;
|
|
262
204
|
}
|
|
263
|
-
this.startRecording();
|
|
205
|
+
if (!this.recorder.recording) this.recorder.startRecording();
|
|
264
206
|
this.syncWithSessionManager({
|
|
265
207
|
sessionReplayMode: this.mode
|
|
266
208
|
});
|
|
267
209
|
}
|
|
268
210
|
prepareHarvest() {
|
|
269
|
-
if (
|
|
270
|
-
const
|
|
211
|
+
if (!this.recorder) return;
|
|
212
|
+
const recorderEvents = this.recorder.getEvents();
|
|
213
|
+
// get the event type and use that to trigger another harvest if needed
|
|
214
|
+
if (!recorderEvents.events.length || this.mode !== MODE.FULL || this.blocked) return;
|
|
215
|
+
const payload = this.getHarvestContents(recorderEvents);
|
|
271
216
|
if (!payload.body.length) {
|
|
272
|
-
this.clearBuffer();
|
|
217
|
+
this.recorder.clearBuffer();
|
|
273
218
|
return;
|
|
274
219
|
}
|
|
220
|
+
let len = 0;
|
|
275
221
|
if (this.shouldCompress) {
|
|
276
|
-
payload.body = gzipper(u8(
|
|
222
|
+
payload.body = gzipper(u8("[".concat(payload.body.map(e => e.__serialized).join(','), "]")));
|
|
223
|
+
len = payload.body.length;
|
|
277
224
|
this.scheduler.opts.gzip = true;
|
|
278
225
|
} else {
|
|
226
|
+
payload.body = payload.body.map(_ref2 => {
|
|
227
|
+
let {
|
|
228
|
+
__serialized,
|
|
229
|
+
...node
|
|
230
|
+
} = _ref2;
|
|
231
|
+
return node;
|
|
232
|
+
});
|
|
233
|
+
len = stringify(payload.body).length;
|
|
279
234
|
this.scheduler.opts.gzip = false;
|
|
280
235
|
}
|
|
236
|
+
if (len > MAX_PAYLOAD_SIZE) {
|
|
237
|
+
this.abort(ABORT_REASONS.TOO_BIG);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
281
240
|
// TODO -- Gracefully handle the buffer for retries.
|
|
282
241
|
const {
|
|
283
242
|
session
|
|
@@ -285,37 +244,39 @@ export class Aggregate extends AggregateBase {
|
|
|
285
244
|
if (!session.state.sessionReplaySentFirstChunk) this.syncWithSessionManager({
|
|
286
245
|
sessionReplaySentFirstChunk: true
|
|
287
246
|
});
|
|
288
|
-
this.clearBuffer();
|
|
247
|
+
this.recorder.clearBuffer();
|
|
248
|
+
if (recorderEvents.type === 'preloaded') this.scheduler.runHarvest();
|
|
289
249
|
return [payload];
|
|
290
250
|
}
|
|
291
|
-
getHarvestContents() {
|
|
251
|
+
getHarvestContents(recorderEvents) {
|
|
252
|
+
recorderEvents ??= this.recorder.getEvents();
|
|
253
|
+
let events = recorderEvents.events;
|
|
292
254
|
const agentRuntime = getRuntime(this.agentIdentifier);
|
|
293
255
|
const info = getInfo(this.agentIdentifier);
|
|
294
256
|
const endUserId = info.jsAttributes?.['enduser.id'];
|
|
295
|
-
if (this.backloggedEvents.length) this.events = [...this.backloggedEvents, ...this.events];
|
|
296
257
|
|
|
297
258
|
// do not let the first node be a full snapshot node, since this NEEDS to be preceded by a meta node
|
|
298
259
|
// we will manually inject it if this happens
|
|
299
|
-
const payloadStartsWithFullSnapshot =
|
|
300
|
-
if (payloadStartsWithFullSnapshot && !!this.lastMeta) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
this.lastMeta = undefined;
|
|
260
|
+
const payloadStartsWithFullSnapshot = events?.[0]?.type === RRWEB_EVENT_TYPES.FullSnapshot;
|
|
261
|
+
if (payloadStartsWithFullSnapshot && !!this.recorder.lastMeta) {
|
|
262
|
+
recorderEvents.hasMeta = true;
|
|
263
|
+
events.unshift(this.recorder.lastMeta); // --> pushed the meta from a previous payload into newer payload... but it still has old timestamps
|
|
264
|
+
this.recorder.lastMeta = undefined;
|
|
304
265
|
}
|
|
305
266
|
|
|
306
267
|
// do not let the last node be a meta node, since this NEEDS to precede a snapshot
|
|
307
268
|
// we will manually inject it later if we find a payload that is missing a meta node
|
|
308
|
-
const payloadEndsWithMeta =
|
|
269
|
+
const payloadEndsWithMeta = events[events.length - 1]?.type === RRWEB_EVENT_TYPES.Meta;
|
|
309
270
|
if (payloadEndsWithMeta) {
|
|
310
|
-
this.lastMeta =
|
|
311
|
-
|
|
312
|
-
|
|
271
|
+
this.recorder.lastMeta = events[events.length - 1];
|
|
272
|
+
events = events.slice(0, events.length - 1);
|
|
273
|
+
recorderEvents.hasMeta = !!events.find(x => x.type === RRWEB_EVENT_TYPES.Meta);
|
|
313
274
|
}
|
|
314
275
|
const agentOffset = getRuntime(this.agentIdentifier).offset;
|
|
315
276
|
const relativeNow = now();
|
|
316
|
-
const firstEventTimestamp =
|
|
317
|
-
const lastEventTimestamp =
|
|
318
|
-
const firstTimestamp = firstEventTimestamp ||
|
|
277
|
+
const firstEventTimestamp = events[0]?.timestamp; // from rrweb node
|
|
278
|
+
const lastEventTimestamp = events[events.length - 1]?.timestamp; // from rrweb node
|
|
279
|
+
const firstTimestamp = firstEventTimestamp || recorderEvents.cycleTimestamp;
|
|
319
280
|
const lastTimestamp = lastEventTimestamp || agentOffset + relativeNow;
|
|
320
281
|
return {
|
|
321
282
|
qs: {
|
|
@@ -333,16 +294,16 @@ export class Aggregate extends AggregateBase {
|
|
|
333
294
|
'replay.firstTimestampOffset': firstTimestamp - agentOffset,
|
|
334
295
|
'replay.lastTimestamp': lastTimestamp,
|
|
335
296
|
'replay.durationMs': lastTimestamp - firstTimestamp,
|
|
336
|
-
'replay.nodes':
|
|
297
|
+
'replay.nodes': events.length,
|
|
337
298
|
'session.durationMs': agentRuntime.session.getDuration(),
|
|
338
299
|
agentVersion: agentRuntime.version,
|
|
339
300
|
session: agentRuntime.session.state.value,
|
|
340
301
|
rst: relativeNow,
|
|
341
|
-
hasMeta:
|
|
342
|
-
hasSnapshot:
|
|
343
|
-
hasError:
|
|
302
|
+
hasMeta: recorderEvents.hasMeta || false,
|
|
303
|
+
hasSnapshot: recorderEvents.hasSnapshot || false,
|
|
304
|
+
hasError: recorderEvents.hasError || false,
|
|
344
305
|
isFirstChunk: agentRuntime.session.state.sessionReplaySentFirstChunk === false,
|
|
345
|
-
decompressedBytes:
|
|
306
|
+
decompressedBytes: recorderEvents.payloadBytesEstimation,
|
|
346
307
|
'rrweb.version': RRWEB_VERSION,
|
|
347
308
|
// customer-defined data should go last so that if it exceeds the query param padding limit it will be truncated instead of important attrs
|
|
348
309
|
...(endUserId && {
|
|
@@ -352,7 +313,7 @@ export class Aggregate extends AggregateBase {
|
|
|
352
313
|
}, QUERY_PARAM_PADDING).substring(1) // remove the leading '&'
|
|
353
314
|
},
|
|
354
315
|
|
|
355
|
-
body:
|
|
316
|
+
body: events
|
|
356
317
|
};
|
|
357
318
|
}
|
|
358
319
|
onHarvestFinished(result) {
|
|
@@ -363,118 +324,6 @@ export class Aggregate extends AggregateBase {
|
|
|
363
324
|
if (this.blocked) this.scheduler.stopTimer(true);
|
|
364
325
|
}
|
|
365
326
|
|
|
366
|
-
/** Clears the buffer (this.events), and resets all payload metadata properties */
|
|
367
|
-
clearBuffer() {
|
|
368
|
-
if (this.mode === MODE.ERROR) this.backloggedEvents = this.events;else this.backloggedEvents = [];
|
|
369
|
-
this.events = [];
|
|
370
|
-
this.hasSnapshot = false;
|
|
371
|
-
this.hasMeta = false;
|
|
372
|
-
this.hasError = false;
|
|
373
|
-
this.payloadBytesEstimation = 0;
|
|
374
|
-
this.clearTimestamps();
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/** Begin recording using configured recording lib */
|
|
378
|
-
startRecording() {
|
|
379
|
-
if (!recorder) {
|
|
380
|
-
warn('Recording library was never imported');
|
|
381
|
-
return this.abort(ABORT_REASONS.IMPORT);
|
|
382
|
-
}
|
|
383
|
-
this.recording = true;
|
|
384
|
-
const {
|
|
385
|
-
block_class,
|
|
386
|
-
ignore_class,
|
|
387
|
-
mask_text_class,
|
|
388
|
-
block_selector,
|
|
389
|
-
mask_input_options,
|
|
390
|
-
mask_text_selector,
|
|
391
|
-
mask_all_inputs,
|
|
392
|
-
inline_images,
|
|
393
|
-
inline_stylesheet,
|
|
394
|
-
collect_fonts
|
|
395
|
-
} = getConfigurationValue(this.agentIdentifier, 'session_replay');
|
|
396
|
-
// set up rrweb configurations for maximum privacy --
|
|
397
|
-
// https://newrelic.atlassian.net/wiki/spaces/O11Y/pages/2792293280/2023+02+28+Browser+-+Session+Replay#Configuration-options
|
|
398
|
-
const stop = recorder({
|
|
399
|
-
emit: this.store.bind(this),
|
|
400
|
-
blockClass: block_class,
|
|
401
|
-
ignoreClass: ignore_class,
|
|
402
|
-
maskTextClass: mask_text_class,
|
|
403
|
-
blockSelector: block_selector,
|
|
404
|
-
maskInputOptions: mask_input_options,
|
|
405
|
-
maskTextSelector: mask_text_selector,
|
|
406
|
-
maskAllInputs: mask_all_inputs,
|
|
407
|
-
inlineImages: inline_images,
|
|
408
|
-
inlineStylesheet: inline_stylesheet,
|
|
409
|
-
collectFonts: collect_fonts,
|
|
410
|
-
checkoutEveryNms: CHECKOUT_MS[this.mode]
|
|
411
|
-
});
|
|
412
|
-
this.stopRecording = () => {
|
|
413
|
-
this.recording = false;
|
|
414
|
-
stop();
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/** Store a payload in the buffer (this.events). This should be the callback to the recording lib noticing a mutation */
|
|
419
|
-
store(event, isCheckout) {
|
|
420
|
-
this.setTimestamps();
|
|
421
|
-
if (this.blocked) return;
|
|
422
|
-
const eventBytes = stringify(event).length;
|
|
423
|
-
/** The estimated size of the payload after compression */
|
|
424
|
-
const payloadSize = this.getPayloadSize(eventBytes);
|
|
425
|
-
// Vortex will block payloads at a certain size, we might as well not send.
|
|
426
|
-
if (payloadSize > MAX_PAYLOAD_SIZE) {
|
|
427
|
-
this.clearBuffer();
|
|
428
|
-
return this.abort(ABORT_REASONS.TOO_BIG);
|
|
429
|
-
}
|
|
430
|
-
// Checkout events are flags by the recording lib that indicate a fullsnapshot was taken every n ms. These are important
|
|
431
|
-
// to help reconstruct the replay later and must be included. While waiting and buffering for errors to come through,
|
|
432
|
-
// each time we see a new checkout, we can drop the old data.
|
|
433
|
-
// we need to check for meta because rrweb will flag it as checkout twice, once for meta, then once for snapshot
|
|
434
|
-
if (this.mode === MODE.ERROR && isCheckout && event.type === RRWEB_EVENT_TYPES.Meta) {
|
|
435
|
-
// we are still waiting for an error to throw, so keep wiping the buffer over time
|
|
436
|
-
this.clearBuffer();
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// meta event
|
|
440
|
-
if (event.type === RRWEB_EVENT_TYPES.Meta) {
|
|
441
|
-
this.hasMeta = true;
|
|
442
|
-
}
|
|
443
|
-
// snapshot event
|
|
444
|
-
if (event.type === RRWEB_EVENT_TYPES.FullSnapshot) {
|
|
445
|
-
this.hasSnapshot = true;
|
|
446
|
-
}
|
|
447
|
-
this.events.push(event);
|
|
448
|
-
this.payloadBytesEstimation += eventBytes;
|
|
449
|
-
|
|
450
|
-
// We are making an effort to try to keep payloads manageable for unloading. If they reach the unload limit before their interval,
|
|
451
|
-
// it will send immediately. This often happens on the first snapshot, which can be significantly larger than the other payloads.
|
|
452
|
-
if (payloadSize > IDEAL_PAYLOAD_SIZE && this.mode !== MODE.ERROR) {
|
|
453
|
-
// if we've made it to the ideal size of ~64kb before the interval timer, we should send early.
|
|
454
|
-
this.scheduler.runHarvest();
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
/** force the recording lib to take a full DOM snapshot. This needs to occur in certain cases, like visibility changes */
|
|
459
|
-
takeFullSnapshot() {
|
|
460
|
-
if (!recorder) return;
|
|
461
|
-
recorder.takeFullSnapshot();
|
|
462
|
-
}
|
|
463
|
-
setTimestamps() {
|
|
464
|
-
// fallbacks if timestamps cannot be derived from rrweb events
|
|
465
|
-
if (!this.cycleTimestamp) this.cycleTimestamp = getRuntime(this.agentIdentifier).offset + globalScope.performance.now();
|
|
466
|
-
}
|
|
467
|
-
clearTimestamps() {
|
|
468
|
-
this.cycleTimestamp = undefined;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
/** Estimate the payload size */
|
|
472
|
-
getPayloadSize() {
|
|
473
|
-
let newBytes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;
|
|
474
|
-
// the query param padding constant gives us some padding for the other metadata to be safely injected
|
|
475
|
-
return this.estimateCompression(this.payloadBytesEstimation + newBytes) + QUERY_PARAM_PADDING;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
327
|
/**
|
|
479
328
|
* Forces the agent into OFF mode so that changing tabs or navigating
|
|
480
329
|
* does not restart the recording. This is used when the customer calls
|
|
@@ -483,7 +332,7 @@ export class Aggregate extends AggregateBase {
|
|
|
483
332
|
forceStop(forceHarvest) {
|
|
484
333
|
if (forceHarvest) this.scheduler.runHarvest();
|
|
485
334
|
this.mode = MODE.OFF;
|
|
486
|
-
this.stopRecording();
|
|
335
|
+
this.recorder?.stopRecording?.();
|
|
487
336
|
this.syncWithSessionManager({
|
|
488
337
|
sessionReplayMode: this.mode
|
|
489
338
|
});
|
|
@@ -496,21 +345,13 @@ export class Aggregate extends AggregateBase {
|
|
|
496
345
|
handle(SUPPORTABILITY_METRIC_CHANNEL, ["SessionReplay/Abort/".concat(reason.sm)], undefined, FEATURE_NAMES.metrics, this.ee);
|
|
497
346
|
this.blocked = true;
|
|
498
347
|
this.mode = MODE.OFF;
|
|
499
|
-
this.stopRecording();
|
|
348
|
+
this.recorder?.stopRecording?.();
|
|
500
349
|
this.syncWithSessionManager({
|
|
501
350
|
sessionReplayMode: this.mode
|
|
502
351
|
});
|
|
503
|
-
this.clearTimestamps();
|
|
352
|
+
this.recorder?.clearTimestamps?.();
|
|
504
353
|
this.ee.emit('REPLAY_ABORTED');
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
/** Extensive research has yielded about an 88% compression factor on these payloads.
|
|
508
|
-
* This is an estimation using that factor as to not cause performance issues while evaluating
|
|
509
|
-
* https://staging.onenr.io/037jbJWxbjy
|
|
510
|
-
* */
|
|
511
|
-
estimateCompression(data) {
|
|
512
|
-
if (this.shouldCompress) return data * AVG_COMPRESSION;
|
|
513
|
-
return data;
|
|
354
|
+
this.recorder?.clearBuffer?.();
|
|
514
355
|
}
|
|
515
356
|
syncWithSessionManager() {
|
|
516
357
|
let state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
|