@newrelic/browser-agent 1.266.0 → 1.267.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/config/init.js +1 -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/timing/time-keeper.js +5 -12
- package/dist/cjs/common/util/stringify.js +2 -0
- package/dist/cjs/common/vitals/interaction-to-next-paint.js +11 -3
- package/dist/cjs/common/vitals/largest-contentful-paint.js +3 -1
- package/dist/cjs/features/page_view_event/aggregate/index.js +9 -9
- package/dist/cjs/features/session_replay/aggregate/index.js +1 -3
- package/dist/cjs/features/session_replay/shared/recorder.js +22 -14
- package/dist/cjs/features/session_replay/shared/stylesheet-evaluator.js +5 -4
- package/dist/cjs/features/utils/event-buffer.js +2 -1
- package/dist/cjs/loaders/browser-agent.js +2 -1
- package/dist/esm/common/config/init.js +1 -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/timing/time-keeper.js +5 -11
- package/dist/esm/common/util/stringify.js +2 -0
- package/dist/esm/common/vitals/interaction-to-next-paint.js +11 -3
- package/dist/esm/common/vitals/largest-contentful-paint.js +3 -1
- package/dist/esm/features/page_view_event/aggregate/index.js +9 -9
- package/dist/esm/features/session_replay/aggregate/index.js +1 -3
- package/dist/esm/features/session_replay/shared/recorder.js +22 -14
- package/dist/esm/features/session_replay/shared/stylesheet-evaluator.js +5 -4
- package/dist/esm/features/utils/event-buffer.js +2 -1
- package/dist/esm/loaders/browser-agent.js +2 -1
- package/dist/types/common/config/init.d.ts.map +1 -1
- package/dist/types/common/timing/time-keeper.d.ts +2 -1
- package/dist/types/common/timing/time-keeper.d.ts.map +1 -1
- package/dist/types/common/util/stringify.d.ts.map +1 -1
- package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder.d.ts +2 -2
- package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/stylesheet-evaluator.d.ts.map +1 -1
- package/dist/types/features/utils/event-buffer.d.ts +2 -1
- package/dist/types/features/utils/event-buffer.d.ts.map +1 -1
- package/dist/types/loaders/browser-agent.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/common/config/init.js +1 -2
- package/src/common/timing/time-keeper.js +5 -12
- package/src/common/util/stringify.js +2 -0
- package/src/common/vitals/interaction-to-next-paint.js +9 -3
- package/src/common/vitals/largest-contentful-paint.js +2 -1
- package/src/features/page_view_event/aggregate/index.js +9 -11
- package/src/features/session_replay/aggregate/index.js +2 -3
- package/src/features/session_replay/shared/recorder.js +23 -14
- package/src/features/session_replay/shared/stylesheet-evaluator.js +5 -4
- package/src/features/utils/event-buffer.js +2 -1
- package/src/loaders/browser-agent.js +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [1.267.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.266.0...v1.267.0) (2024-09-23)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* add soft navigations to Browser-Agent loader ([#1191](https://github.com/newrelic/newrelic-browser-agent/issues/1191)) ([fd033a7](https://github.com/newrelic/newrelic-browser-agent/commit/fd033a7be52fed01e5a67a5f8060cf6ec080c769))
|
|
12
|
+
* Improve notifications of stylesheet status for session replay ([#1190](https://github.com/newrelic/newrelic-browser-agent/issues/1190)) ([a21b939](https://github.com/newrelic/newrelic-browser-agent/commit/a21b939884697fc4951e6d4cdaceecaa9b000810))
|
|
13
|
+
* Update TimeKeeper Source of Truth ([#1181](https://github.com/newrelic/newrelic-browser-agent/issues/1181)) ([60d63cf](https://github.com/newrelic/newrelic-browser-agent/commit/60d63cf416529d700abaa6fa36ba3e64402b35a9))
|
|
14
|
+
* Upgrade to web-vitals v4 ([#1193](https://github.com/newrelic/newrelic-browser-agent/issues/1193)) ([81349b8](https://github.com/newrelic/newrelic-browser-agent/commit/81349b82f5befd88f425cce1dee06f08fcea8a05))
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* Improve reliability of customMasker ([#1197](https://github.com/newrelic/newrelic-browser-agent/issues/1197)) ([9f2ef1f](https://github.com/newrelic/newrelic-browser-agent/commit/9f2ef1f7d0e5dceaf61745a47849a3b2cc930c53))
|
|
19
|
+
* Stringify now returns an empty string if failed to transform ([#1198](https://github.com/newrelic/newrelic-browser-agent/issues/1198)) ([310937a](https://github.com/newrelic/newrelic-browser-agent/commit/310937ae20a004f91f3ccb4ba6b1d1b230d92632))
|
|
20
|
+
|
|
6
21
|
## [1.266.0](https://github.com/newrelic/newrelic-browser-agent/compare/v1.265.1...v1.266.0) (2024-09-16)
|
|
7
22
|
|
|
8
23
|
|
|
@@ -117,10 +117,8 @@ const model = () => {
|
|
|
117
117
|
// serialize fonts for collection without public asset url, this is currently broken in RRWeb -- https://github.com/rrweb-io/rrweb/issues/1304. When fixed, revisit with test cases
|
|
118
118
|
inline_images: false,
|
|
119
119
|
// serialize images for collection without public asset url -- right now this is only useful for testing as it easily generates payloads too large to be harvested
|
|
120
|
-
inline_stylesheet: true,
|
|
121
|
-
// serialize css for collection without public asset url
|
|
122
120
|
fix_stylesheets: true,
|
|
123
|
-
// fetch missing stylesheet resources for inlining
|
|
121
|
+
// fetch missing stylesheet resources for inlining
|
|
124
122
|
// recording config settings
|
|
125
123
|
mask_all_inputs: true,
|
|
126
124
|
// this has a getter/setter to facilitate validation of the selectors
|
|
@@ -12,7 +12,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.DIST_METHOD = exports.BUILD_EN
|
|
|
12
12
|
/**
|
|
13
13
|
* Exposes the version of the agent
|
|
14
14
|
*/
|
|
15
|
-
const VERSION = exports.VERSION = "1.
|
|
15
|
+
const VERSION = exports.VERSION = "1.267.0";
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Exposes the build type of the agent
|
|
@@ -12,7 +12,7 @@ exports.VERSION = exports.RRWEB_VERSION = exports.DIST_METHOD = exports.BUILD_EN
|
|
|
12
12
|
/**
|
|
13
13
|
* Exposes the version of the agent
|
|
14
14
|
*/
|
|
15
|
-
const VERSION = exports.VERSION = "1.
|
|
15
|
+
const VERSION = exports.VERSION = "1.267.0";
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Exposes the build type of the agent
|
|
@@ -6,8 +6,6 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.TimeKeeper = void 0;
|
|
7
7
|
var _runtime = require("../constants/runtime");
|
|
8
8
|
var _runtime2 = require("../config/runtime");
|
|
9
|
-
const rfc2616Regex = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), ([0-3][0-9]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ([0-9]{4}) ([01][0-9]|2[0-3])(:[0-5][0-9]){2} GMT$/;
|
|
10
|
-
|
|
11
9
|
/**
|
|
12
10
|
* Class used to adjust the timestamp of harvested data to New Relic server time. This
|
|
13
11
|
* is done by tracking the performance timings of the RUM call and applying a calculation
|
|
@@ -58,26 +56,21 @@ class TimeKeeper {
|
|
|
58
56
|
* @param rumRequest {XMLHttpRequest} The xhr for the rum request
|
|
59
57
|
* @param startTime {number} The start time of the RUM request
|
|
60
58
|
* @param endTime {number} The end time of the RUM request
|
|
59
|
+
* @param nrServerTime {number} the unix number value of the NR server time in MS, returned in the RUM request body
|
|
61
60
|
*/
|
|
62
|
-
processRumRequest(rumRequest, startTime, endTime) {
|
|
61
|
+
processRumRequest(rumRequest, startTime, endTime, nrServerTime) {
|
|
63
62
|
this.processStoredDiff(); // Check session entity for stored time diff
|
|
64
63
|
if (this.#ready) return; // Server time calculated from session entity
|
|
65
64
|
|
|
66
|
-
|
|
67
|
-
if (!responseDateHeader) {
|
|
68
|
-
throw new Error('Missing date header on rum response.');
|
|
69
|
-
}
|
|
70
|
-
if (!rfc2616Regex.test(responseDateHeader)) {
|
|
71
|
-
throw new Error('Date header invalid format.');
|
|
72
|
-
}
|
|
65
|
+
if (!nrServerTime) throw new Error('nrServerTime not found');
|
|
73
66
|
const medianRumOffset = (endTime - startTime) / 2;
|
|
74
67
|
const serverOffset = startTime + medianRumOffset;
|
|
75
68
|
|
|
76
69
|
// Corrected page origin time
|
|
77
|
-
this.#correctedOriginTime = Math.floor(
|
|
70
|
+
this.#correctedOriginTime = Math.floor(nrServerTime - serverOffset);
|
|
78
71
|
this.#localTimeDiff = _runtime.originTime - this.#correctedOriginTime;
|
|
79
72
|
if (isNaN(this.#correctedOriginTime)) {
|
|
80
|
-
throw new Error('
|
|
73
|
+
throw new Error('Failed to correct browser time to server time');
|
|
81
74
|
}
|
|
82
75
|
this.#session?.write({
|
|
83
76
|
serverTimeDiff: this.#localTimeDiff
|
|
@@ -18,9 +18,17 @@ if (_runtime.isBrowserScope) {
|
|
|
18
18
|
}) => {
|
|
19
19
|
const attrs = {
|
|
20
20
|
metricId: id,
|
|
21
|
-
eventTarget: attribution.
|
|
22
|
-
|
|
23
|
-
eventTime: attribution.
|
|
21
|
+
eventTarget: attribution.interactionTarget,
|
|
22
|
+
// event* attrs deprecated in v4, kept for NR backwards compatibility
|
|
23
|
+
eventTime: attribution.interactionTime,
|
|
24
|
+
// event* attrs deprecated in v4, kept for NR backwards compatibility
|
|
25
|
+
interactionTarget: attribution.interactionTarget,
|
|
26
|
+
interactionTime: attribution.interactionTime,
|
|
27
|
+
interactionType: attribution.interactionType,
|
|
28
|
+
inputDelay: attribution.inputDelay,
|
|
29
|
+
nextPaintTime: attribution.nextPaintTime,
|
|
30
|
+
processingDuration: attribution.processingDuration,
|
|
31
|
+
presentationDelay: attribution.presentationDelay,
|
|
24
32
|
loadState: attribution.loadState
|
|
25
33
|
};
|
|
26
34
|
interactionToNextPaint.update({
|
|
@@ -26,7 +26,9 @@ if (_runtime.isBrowserScope) {
|
|
|
26
26
|
element: attribution.element,
|
|
27
27
|
timeToFirstByte: attribution.timeToFirstByte,
|
|
28
28
|
resourceLoadDelay: attribution.resourceLoadDelay,
|
|
29
|
-
|
|
29
|
+
resourceLoadDuration: attribution.resourceLoadDuration,
|
|
30
|
+
resourceLoadTime: attribution.resourceLoadDuration,
|
|
31
|
+
// kept for NR backwards compatibility, deprecated in v3->v4
|
|
30
32
|
elementRenderDelay: attribution.elementRenderDelay
|
|
31
33
|
};
|
|
32
34
|
if (attribution.url) attrs.elUrl = (0, _cleanUrl.cleanURL)(attribution.url);
|
|
@@ -142,20 +142,20 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
142
142
|
this.ee.abort();
|
|
143
143
|
return;
|
|
144
144
|
}
|
|
145
|
-
try {
|
|
146
|
-
this.timeKeeper.processRumRequest(xhr, rumStartTime, rumEndTime);
|
|
147
|
-
if (!this.timeKeeper.ready) throw new Error('TimeKeeper not ready');
|
|
148
|
-
agentRuntime.timeKeeper = this.timeKeeper;
|
|
149
|
-
} catch (error) {
|
|
150
|
-
this.ee.abort();
|
|
151
|
-
(0, _console.warn)(17, error);
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
145
|
try {
|
|
155
146
|
const {
|
|
156
147
|
app,
|
|
157
148
|
...flags
|
|
158
149
|
} = JSON.parse(responseText);
|
|
150
|
+
try {
|
|
151
|
+
this.timeKeeper.processRumRequest(xhr, rumStartTime, rumEndTime, app.nrServerTime);
|
|
152
|
+
if (!this.timeKeeper.ready) throw new Error('TimeKeeper not ready');
|
|
153
|
+
agentRuntime.timeKeeper = this.timeKeeper;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
this.ee.abort();
|
|
156
|
+
(0, _console.warn)(17, error);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
159
|
agentRuntime.appMetadata = app;
|
|
160
160
|
(0, _featureFlags.activateFeatures)(flags, this.agentIdentifier);
|
|
161
161
|
this.drain();
|
|
@@ -108,7 +108,6 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
108
108
|
block_selector,
|
|
109
109
|
mask_text_selector,
|
|
110
110
|
mask_all_inputs,
|
|
111
|
-
inline_stylesheet,
|
|
112
111
|
inline_images,
|
|
113
112
|
collect_fonts
|
|
114
113
|
} = (0, _init.getConfigurationValue)(this.agentIdentifier, 'session_replay');
|
|
@@ -135,7 +134,6 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
135
134
|
/** Detect if the default configs have been altered and report a SM. This is useful to evaluate what the reasonable defaults are across a customer base over time */
|
|
136
135
|
if (!autoStart) (0, _handle.handle)(_constants2.SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/AutoStart/Modified'], undefined, _features.FEATURE_NAMES.metrics, this.ee);
|
|
137
136
|
if (collect_fonts === true) (0, _handle.handle)(_constants2.SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/CollectFonts/Modified'], undefined, _features.FEATURE_NAMES.metrics, this.ee);
|
|
138
|
-
if (inline_stylesheet !== true) (0, _handle.handle)(_constants2.SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/InlineStylesheet/Modified'], undefined, _features.FEATURE_NAMES.metrics, this.ee);
|
|
139
137
|
if (inline_images === true) (0, _handle.handle)(_constants2.SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/InlineImages/Modifed'], undefined, _features.FEATURE_NAMES.metrics, this.ee);
|
|
140
138
|
if (mask_all_inputs !== true) (0, _handle.handle)(_constants2.SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/MaskAllInputs/Modified'], undefined, _features.FEATURE_NAMES.metrics, this.ee);
|
|
141
139
|
if (block_selector !== '[data-nr-block]') (0, _handle.handle)(_constants2.SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/BlockSelector/Modified'], undefined, _features.FEATURE_NAMES.metrics, this.ee);
|
|
@@ -253,7 +251,7 @@ class Aggregate extends _aggregateBase.AggregateBase {
|
|
|
253
251
|
prepareHarvest({
|
|
254
252
|
opts
|
|
255
253
|
} = {}) {
|
|
256
|
-
if (!this.recorder || !this.timeKeeper?.ready) return;
|
|
254
|
+
if (!this.recorder || !this.timeKeeper?.ready || !this.recorder.hasSeenSnapshot) return;
|
|
257
255
|
const recorderEvents = this.recorder.getEvents();
|
|
258
256
|
// get the event type and use that to trigger another harvest if needed
|
|
259
257
|
if (!recorderEvents.events.length || this.mode !== _constants3.MODE.FULL || this.blocked) return;
|
|
@@ -33,14 +33,14 @@ class Recorder {
|
|
|
33
33
|
this.recording = false;
|
|
34
34
|
/** The pointer to the current bucket holding rrweb events */
|
|
35
35
|
this.currentBufferTarget = this.#events;
|
|
36
|
+
/** Only set to true once a snapshot node has been processed. Used to block preload harvests from sending before we know we have a snapshot */
|
|
37
|
+
this.hasSeenSnapshot = false;
|
|
36
38
|
/** 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 */
|
|
37
39
|
this.lastMeta = false;
|
|
38
40
|
/** The parent class that instantiated the recorder */
|
|
39
41
|
this.parent = parent;
|
|
40
|
-
/** Config to inform to inline stylesheet contents (true default) */
|
|
41
|
-
this.shouldInlineStylesheets = (0, _init.getConfigurationValue)(this.parent.agentIdentifier, 'session_replay.inline_stylesheet');
|
|
42
42
|
/** A flag that can be set to false by failing conversions to stop the fetching process */
|
|
43
|
-
this.shouldFix =
|
|
43
|
+
this.shouldFix = (0, _init.getConfigurationValue)(this.parent.agentIdentifier, 'session_replay.fix_stylesheets');
|
|
44
44
|
/** The method to stop recording. This defaults to a noop, but is overwritten once the recording library is imported and initialized */
|
|
45
45
|
this.stopRecording = () => {/* no-op until set by rrweb initializer */};
|
|
46
46
|
}
|
|
@@ -82,13 +82,16 @@ class Recorder {
|
|
|
82
82
|
mask_input_options,
|
|
83
83
|
mask_text_selector,
|
|
84
84
|
mask_all_inputs,
|
|
85
|
-
inline_stylesheet,
|
|
86
85
|
inline_images,
|
|
87
86
|
collect_fonts
|
|
88
87
|
} = (0, _init.getConfigurationValue)(this.parent.agentIdentifier, 'session_replay');
|
|
89
88
|
const customMasker = (text, element) => {
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
try {
|
|
90
|
+
if (typeof element?.type === 'string' && element.type.toLowerCase() !== 'password' && (element?.dataset?.nrUnmask !== undefined || element?.classList?.contains('nr-unmask'))) return text;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
// likely an element was passed to this handler that was invalid and was missing attributes or methods
|
|
93
|
+
}
|
|
94
|
+
return '*'.repeat(text?.length || 0);
|
|
92
95
|
};
|
|
93
96
|
// set up rrweb configurations for maximum privacy --
|
|
94
97
|
// https://newrelic.atlassian.net/wiki/spaces/O11Y/pages/2792293280/2023+02+28+Browser+-+Session+Replay#Configuration-options
|
|
@@ -103,7 +106,7 @@ class Recorder {
|
|
|
103
106
|
maskTextFn: customMasker,
|
|
104
107
|
maskAllInputs: mask_all_inputs,
|
|
105
108
|
maskInputFn: customMasker,
|
|
106
|
-
inlineStylesheet:
|
|
109
|
+
inlineStylesheet: true,
|
|
107
110
|
inlineImages: inline_images,
|
|
108
111
|
collectFonts: collect_fonts,
|
|
109
112
|
checkoutEveryNms: _constants.CHECKOUT_MS[this.parent.mode],
|
|
@@ -130,13 +133,17 @@ class Recorder {
|
|
|
130
133
|
* @param {*} isCheckout - Flag indicating if the payload was triggered as a checkout
|
|
131
134
|
*/
|
|
132
135
|
audit(event, isCheckout) {
|
|
133
|
-
/** only run the audit if inline_stylesheets is configured as on (default behavior) */
|
|
134
|
-
if (this.shouldInlineStylesheets === false || !this.shouldFix) {
|
|
135
|
-
this.currentBufferTarget.inlinedAllStylesheets = false;
|
|
136
|
-
return this.store(event, isCheckout);
|
|
137
|
-
}
|
|
138
136
|
/** An count of stylesheet objects that were blocked from accessing contents via JS */
|
|
139
137
|
const incompletes = _stylesheetEvaluator.stylesheetEvaluator.evaluate();
|
|
138
|
+
const missingInlineSMTag = 'SessionReplay/Payload/Missing-Inline-Css/';
|
|
139
|
+
/** only run the full fixing behavior (more costly) if fix_stylesheets is configured as on (default behavior) */
|
|
140
|
+
if (!this.shouldFix) {
|
|
141
|
+
if (incompletes > 0) {
|
|
142
|
+
this.currentBufferTarget.inlinedAllStylesheets = false;
|
|
143
|
+
(0, _handle.handle)(_constants3.SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Skipped', incompletes], undefined, _features.FEATURE_NAMES.metrics, this.parent.ee);
|
|
144
|
+
}
|
|
145
|
+
return this.store(event, isCheckout);
|
|
146
|
+
}
|
|
140
147
|
/** Only stop ignoring data if already ignoring and a new valid snapshap is taking place (0 incompletes and we get a meta node for the snap) */
|
|
141
148
|
if (!incompletes && this.#fixing && event.type === _constants.RRWEB_EVENT_TYPES.Meta) this.#fixing = false;
|
|
142
149
|
if (incompletes > 0) {
|
|
@@ -146,8 +153,8 @@ class Recorder {
|
|
|
146
153
|
this.currentBufferTarget.inlinedAllStylesheets = false;
|
|
147
154
|
this.shouldFix = false;
|
|
148
155
|
}
|
|
149
|
-
(0, _handle.handle)(_constants3.SUPPORTABILITY_METRIC_CHANNEL, ['
|
|
150
|
-
(0, _handle.handle)(_constants3.SUPPORTABILITY_METRIC_CHANNEL, ['
|
|
156
|
+
(0, _handle.handle)(_constants3.SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Failed', failedToFix], undefined, _features.FEATURE_NAMES.metrics, this.parent.ee);
|
|
157
|
+
(0, _handle.handle)(_constants3.SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Fixed', incompletes - failedToFix], undefined, _features.FEATURE_NAMES.metrics, this.parent.ee);
|
|
151
158
|
this.takeFullSnapshot();
|
|
152
159
|
});
|
|
153
160
|
/** Only start ignoring data if got a faulty snapshot */
|
|
@@ -190,6 +197,7 @@ class Recorder {
|
|
|
190
197
|
// snapshot event
|
|
191
198
|
if (event.type === _constants.RRWEB_EVENT_TYPES.FullSnapshot) {
|
|
192
199
|
this.currentBufferTarget.hasSnapshot = true;
|
|
200
|
+
this.hasSeenSnapshot = true;
|
|
193
201
|
}
|
|
194
202
|
this.currentBufferTarget.add(event);
|
|
195
203
|
|
|
@@ -8,7 +8,7 @@ var _nreum = require("../../../common/window/nreum");
|
|
|
8
8
|
var _runtime = require("../../../common/constants/runtime");
|
|
9
9
|
class StylesheetEvaluator {
|
|
10
10
|
#evaluated = new WeakSet();
|
|
11
|
-
#
|
|
11
|
+
#brokenSheets = [];
|
|
12
12
|
/**
|
|
13
13
|
* Flipped to true if stylesheets that cannot be natively inlined are detected by the stylesheetEvaluator class
|
|
14
14
|
* Used at harvest time to denote that all subsequent payloads are subject to this and customers should be advised to handle crossorigin decoration
|
|
@@ -22,6 +22,7 @@ class StylesheetEvaluator {
|
|
|
22
22
|
*/
|
|
23
23
|
evaluate() {
|
|
24
24
|
let incompletes = 0;
|
|
25
|
+
this.#brokenSheets = [];
|
|
25
26
|
if (_runtime.isBrowserScope) {
|
|
26
27
|
for (let i = 0; i < Object.keys(document.styleSheets).length; i++) {
|
|
27
28
|
if (!this.#evaluated.has(document.styleSheets[i])) {
|
|
@@ -32,7 +33,7 @@ class StylesheetEvaluator {
|
|
|
32
33
|
} catch (err) {
|
|
33
34
|
if (!document.styleSheets[i].href) return;
|
|
34
35
|
incompletes++;
|
|
35
|
-
this.#
|
|
36
|
+
this.#brokenSheets.push(document.styleSheets[i]);
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
}
|
|
@@ -46,8 +47,8 @@ class StylesheetEvaluator {
|
|
|
46
47
|
* @returns {Promise}
|
|
47
48
|
*/
|
|
48
49
|
async fix() {
|
|
49
|
-
await Promise.all(this.#
|
|
50
|
-
this.#
|
|
50
|
+
await Promise.all(this.#brokenSheets.map(sheet => this.#fetchAndOverride(sheet)));
|
|
51
|
+
this.#brokenSheets = [];
|
|
51
52
|
const failedToFix = this.failedToFix;
|
|
52
53
|
this.failedToFix = 0;
|
|
53
54
|
return failedToFix;
|
|
@@ -63,7 +63,8 @@ class EventBuffer {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
* Adds an event object to the buffer while tallying size
|
|
66
|
+
* Adds an event object to the buffer while tallying size. Only adds the event if it is valid
|
|
67
|
+
* and would not make the event buffer exceed the maxPayloadSize.
|
|
67
68
|
* @param {Object} event the event object to add to the buffer
|
|
68
69
|
* @returns {EventBuffer} returns the event buffer for chaining
|
|
69
70
|
*/
|
|
@@ -15,6 +15,7 @@ var _instrument7 = require("../features/spa/instrument");
|
|
|
15
15
|
var _instrument8 = require("../features/session_replay/instrument");
|
|
16
16
|
var _instrument9 = require("../features/generic_events/instrument");
|
|
17
17
|
var _instrument10 = require("../features/logging/instrument");
|
|
18
|
+
var _instrument11 = require("../features/soft_navigations/instrument");
|
|
18
19
|
/**
|
|
19
20
|
* An agent class with all feature modules available. Features may be disabled and enabled via runtime configuration.
|
|
20
21
|
* The BrowserAgent class is the most convenient and reliable option for most use cases.
|
|
@@ -23,7 +24,7 @@ class BrowserAgent extends _agent.Agent {
|
|
|
23
24
|
constructor(args) {
|
|
24
25
|
super({
|
|
25
26
|
...args,
|
|
26
|
-
features: [_instrument5.Instrument, _instrument.Instrument, _instrument2.Instrument, _instrument6.Instrument, _instrument3.Instrument, _instrument4.Instrument, _instrument7.Instrument, _instrument8.Instrument, _instrument9.Instrument, _instrument10.Instrument],
|
|
27
|
+
features: [_instrument5.Instrument, _instrument.Instrument, _instrument2.Instrument, _instrument6.Instrument, _instrument3.Instrument, _instrument4.Instrument, _instrument7.Instrument, _instrument11.Instrument, _instrument8.Instrument, _instrument9.Instrument, _instrument10.Instrument],
|
|
27
28
|
loaderType: 'browser-agent'
|
|
28
29
|
});
|
|
29
30
|
}
|
|
@@ -109,10 +109,8 @@ const model = () => {
|
|
|
109
109
|
// serialize fonts for collection without public asset url, this is currently broken in RRWeb -- https://github.com/rrweb-io/rrweb/issues/1304. When fixed, revisit with test cases
|
|
110
110
|
inline_images: false,
|
|
111
111
|
// serialize images for collection without public asset url -- right now this is only useful for testing as it easily generates payloads too large to be harvested
|
|
112
|
-
inline_stylesheet: true,
|
|
113
|
-
// serialize css for collection without public asset url
|
|
114
112
|
fix_stylesheets: true,
|
|
115
|
-
// fetch missing stylesheet resources for inlining
|
|
113
|
+
// fetch missing stylesheet resources for inlining
|
|
116
114
|
// recording config settings
|
|
117
115
|
mask_all_inputs: true,
|
|
118
116
|
// this has a getter/setter to facilitate validation of the selectors
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { originTime } from '../constants/runtime';
|
|
2
2
|
import { getRuntime } from '../config/runtime';
|
|
3
|
-
const rfc2616Regex = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), ([0-3][0-9]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ([0-9]{4}) ([01][0-9]|2[0-3])(:[0-5][0-9]){2} GMT$/;
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Class used to adjust the timestamp of harvested data to New Relic server time. This
|
|
@@ -52,26 +51,21 @@ export class TimeKeeper {
|
|
|
52
51
|
* @param rumRequest {XMLHttpRequest} The xhr for the rum request
|
|
53
52
|
* @param startTime {number} The start time of the RUM request
|
|
54
53
|
* @param endTime {number} The end time of the RUM request
|
|
54
|
+
* @param nrServerTime {number} the unix number value of the NR server time in MS, returned in the RUM request body
|
|
55
55
|
*/
|
|
56
|
-
processRumRequest(rumRequest, startTime, endTime) {
|
|
56
|
+
processRumRequest(rumRequest, startTime, endTime, nrServerTime) {
|
|
57
57
|
this.processStoredDiff(); // Check session entity for stored time diff
|
|
58
58
|
if (this.#ready) return; // Server time calculated from session entity
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
if (!responseDateHeader) {
|
|
62
|
-
throw new Error('Missing date header on rum response.');
|
|
63
|
-
}
|
|
64
|
-
if (!rfc2616Regex.test(responseDateHeader)) {
|
|
65
|
-
throw new Error('Date header invalid format.');
|
|
66
|
-
}
|
|
60
|
+
if (!nrServerTime) throw new Error('nrServerTime not found');
|
|
67
61
|
const medianRumOffset = (endTime - startTime) / 2;
|
|
68
62
|
const serverOffset = startTime + medianRumOffset;
|
|
69
63
|
|
|
70
64
|
// Corrected page origin time
|
|
71
|
-
this.#correctedOriginTime = Math.floor(
|
|
65
|
+
this.#correctedOriginTime = Math.floor(nrServerTime - serverOffset);
|
|
72
66
|
this.#localTimeDiff = originTime - this.#correctedOriginTime;
|
|
73
67
|
if (isNaN(this.#correctedOriginTime)) {
|
|
74
|
-
throw new Error('
|
|
68
|
+
throw new Error('Failed to correct browser time to server time');
|
|
75
69
|
}
|
|
76
70
|
this.#session?.write({
|
|
77
71
|
serverTimeDiff: this.#localTimeDiff
|
|
@@ -12,9 +12,17 @@ if (isBrowserScope) {
|
|
|
12
12
|
}) => {
|
|
13
13
|
const attrs = {
|
|
14
14
|
metricId: id,
|
|
15
|
-
eventTarget: attribution.
|
|
16
|
-
|
|
17
|
-
eventTime: attribution.
|
|
15
|
+
eventTarget: attribution.interactionTarget,
|
|
16
|
+
// event* attrs deprecated in v4, kept for NR backwards compatibility
|
|
17
|
+
eventTime: attribution.interactionTime,
|
|
18
|
+
// event* attrs deprecated in v4, kept for NR backwards compatibility
|
|
19
|
+
interactionTarget: attribution.interactionTarget,
|
|
20
|
+
interactionTime: attribution.interactionTime,
|
|
21
|
+
interactionType: attribution.interactionType,
|
|
22
|
+
inputDelay: attribution.inputDelay,
|
|
23
|
+
nextPaintTime: attribution.nextPaintTime,
|
|
24
|
+
processingDuration: attribution.processingDuration,
|
|
25
|
+
presentationDelay: attribution.presentationDelay,
|
|
18
26
|
loadState: attribution.loadState
|
|
19
27
|
};
|
|
20
28
|
interactionToNextPaint.update({
|
|
@@ -20,7 +20,9 @@ if (isBrowserScope) {
|
|
|
20
20
|
element: attribution.element,
|
|
21
21
|
timeToFirstByte: attribution.timeToFirstByte,
|
|
22
22
|
resourceLoadDelay: attribution.resourceLoadDelay,
|
|
23
|
-
|
|
23
|
+
resourceLoadDuration: attribution.resourceLoadDuration,
|
|
24
|
+
resourceLoadTime: attribution.resourceLoadDuration,
|
|
25
|
+
// kept for NR backwards compatibility, deprecated in v3->v4
|
|
24
26
|
elementRenderDelay: attribution.elementRenderDelay
|
|
25
27
|
};
|
|
26
28
|
if (attribution.url) attrs.elUrl = cleanURL(attribution.url);
|
|
@@ -134,20 +134,20 @@ export class Aggregate extends AggregateBase {
|
|
|
134
134
|
this.ee.abort();
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
|
-
try {
|
|
138
|
-
this.timeKeeper.processRumRequest(xhr, rumStartTime, rumEndTime);
|
|
139
|
-
if (!this.timeKeeper.ready) throw new Error('TimeKeeper not ready');
|
|
140
|
-
agentRuntime.timeKeeper = this.timeKeeper;
|
|
141
|
-
} catch (error) {
|
|
142
|
-
this.ee.abort();
|
|
143
|
-
warn(17, error);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
137
|
try {
|
|
147
138
|
const {
|
|
148
139
|
app,
|
|
149
140
|
...flags
|
|
150
141
|
} = JSON.parse(responseText);
|
|
142
|
+
try {
|
|
143
|
+
this.timeKeeper.processRumRequest(xhr, rumStartTime, rumEndTime, app.nrServerTime);
|
|
144
|
+
if (!this.timeKeeper.ready) throw new Error('TimeKeeper not ready');
|
|
145
|
+
agentRuntime.timeKeeper = this.timeKeeper;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
this.ee.abort();
|
|
148
|
+
warn(17, error);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
151
|
agentRuntime.appMetadata = app;
|
|
152
152
|
activateFeatures(flags, this.agentIdentifier);
|
|
153
153
|
this.drain();
|
|
@@ -103,7 +103,6 @@ export class Aggregate extends AggregateBase {
|
|
|
103
103
|
block_selector,
|
|
104
104
|
mask_text_selector,
|
|
105
105
|
mask_all_inputs,
|
|
106
|
-
inline_stylesheet,
|
|
107
106
|
inline_images,
|
|
108
107
|
collect_fonts
|
|
109
108
|
} = getConfigurationValue(this.agentIdentifier, 'session_replay');
|
|
@@ -130,7 +129,6 @@ export class Aggregate extends AggregateBase {
|
|
|
130
129
|
/** Detect if the default configs have been altered and report a SM. This is useful to evaluate what the reasonable defaults are across a customer base over time */
|
|
131
130
|
if (!autoStart) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/AutoStart/Modified'], undefined, FEATURE_NAMES.metrics, this.ee);
|
|
132
131
|
if (collect_fonts === true) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/CollectFonts/Modified'], undefined, FEATURE_NAMES.metrics, this.ee);
|
|
133
|
-
if (inline_stylesheet !== true) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/InlineStylesheet/Modified'], undefined, FEATURE_NAMES.metrics, this.ee);
|
|
134
132
|
if (inline_images === true) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/InlineImages/Modifed'], undefined, FEATURE_NAMES.metrics, this.ee);
|
|
135
133
|
if (mask_all_inputs !== true) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/MaskAllInputs/Modified'], undefined, FEATURE_NAMES.metrics, this.ee);
|
|
136
134
|
if (block_selector !== '[data-nr-block]') handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/BlockSelector/Modified'], undefined, FEATURE_NAMES.metrics, this.ee);
|
|
@@ -248,7 +246,7 @@ export class Aggregate extends AggregateBase {
|
|
|
248
246
|
prepareHarvest({
|
|
249
247
|
opts
|
|
250
248
|
} = {}) {
|
|
251
|
-
if (!this.recorder || !this.timeKeeper?.ready) return;
|
|
249
|
+
if (!this.recorder || !this.timeKeeper?.ready || !this.recorder.hasSeenSnapshot) return;
|
|
252
250
|
const recorderEvents = this.recorder.getEvents();
|
|
253
251
|
// get the event type and use that to trigger another harvest if needed
|
|
254
252
|
if (!recorderEvents.events.length || this.mode !== MODE.FULL || this.blocked) return;
|
|
@@ -27,14 +27,14 @@ export class Recorder {
|
|
|
27
27
|
this.recording = false;
|
|
28
28
|
/** The pointer to the current bucket holding rrweb events */
|
|
29
29
|
this.currentBufferTarget = this.#events;
|
|
30
|
+
/** Only set to true once a snapshot node has been processed. Used to block preload harvests from sending before we know we have a snapshot */
|
|
31
|
+
this.hasSeenSnapshot = false;
|
|
30
32
|
/** 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 */
|
|
31
33
|
this.lastMeta = false;
|
|
32
34
|
/** The parent class that instantiated the recorder */
|
|
33
35
|
this.parent = parent;
|
|
34
|
-
/** Config to inform to inline stylesheet contents (true default) */
|
|
35
|
-
this.shouldInlineStylesheets = getConfigurationValue(this.parent.agentIdentifier, 'session_replay.inline_stylesheet');
|
|
36
36
|
/** A flag that can be set to false by failing conversions to stop the fetching process */
|
|
37
|
-
this.shouldFix =
|
|
37
|
+
this.shouldFix = getConfigurationValue(this.parent.agentIdentifier, 'session_replay.fix_stylesheets');
|
|
38
38
|
/** The method to stop recording. This defaults to a noop, but is overwritten once the recording library is imported and initialized */
|
|
39
39
|
this.stopRecording = () => {/* no-op until set by rrweb initializer */};
|
|
40
40
|
}
|
|
@@ -76,13 +76,16 @@ export class Recorder {
|
|
|
76
76
|
mask_input_options,
|
|
77
77
|
mask_text_selector,
|
|
78
78
|
mask_all_inputs,
|
|
79
|
-
inline_stylesheet,
|
|
80
79
|
inline_images,
|
|
81
80
|
collect_fonts
|
|
82
81
|
} = getConfigurationValue(this.parent.agentIdentifier, 'session_replay');
|
|
83
82
|
const customMasker = (text, element) => {
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
try {
|
|
84
|
+
if (typeof element?.type === 'string' && element.type.toLowerCase() !== 'password' && (element?.dataset?.nrUnmask !== undefined || element?.classList?.contains('nr-unmask'))) return text;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
// likely an element was passed to this handler that was invalid and was missing attributes or methods
|
|
87
|
+
}
|
|
88
|
+
return '*'.repeat(text?.length || 0);
|
|
86
89
|
};
|
|
87
90
|
// set up rrweb configurations for maximum privacy --
|
|
88
91
|
// https://newrelic.atlassian.net/wiki/spaces/O11Y/pages/2792293280/2023+02+28+Browser+-+Session+Replay#Configuration-options
|
|
@@ -97,7 +100,7 @@ export class Recorder {
|
|
|
97
100
|
maskTextFn: customMasker,
|
|
98
101
|
maskAllInputs: mask_all_inputs,
|
|
99
102
|
maskInputFn: customMasker,
|
|
100
|
-
inlineStylesheet:
|
|
103
|
+
inlineStylesheet: true,
|
|
101
104
|
inlineImages: inline_images,
|
|
102
105
|
collectFonts: collect_fonts,
|
|
103
106
|
checkoutEveryNms: CHECKOUT_MS[this.parent.mode],
|
|
@@ -124,13 +127,17 @@ export class Recorder {
|
|
|
124
127
|
* @param {*} isCheckout - Flag indicating if the payload was triggered as a checkout
|
|
125
128
|
*/
|
|
126
129
|
audit(event, isCheckout) {
|
|
127
|
-
/** only run the audit if inline_stylesheets is configured as on (default behavior) */
|
|
128
|
-
if (this.shouldInlineStylesheets === false || !this.shouldFix) {
|
|
129
|
-
this.currentBufferTarget.inlinedAllStylesheets = false;
|
|
130
|
-
return this.store(event, isCheckout);
|
|
131
|
-
}
|
|
132
130
|
/** An count of stylesheet objects that were blocked from accessing contents via JS */
|
|
133
131
|
const incompletes = stylesheetEvaluator.evaluate();
|
|
132
|
+
const missingInlineSMTag = 'SessionReplay/Payload/Missing-Inline-Css/';
|
|
133
|
+
/** only run the full fixing behavior (more costly) if fix_stylesheets is configured as on (default behavior) */
|
|
134
|
+
if (!this.shouldFix) {
|
|
135
|
+
if (incompletes > 0) {
|
|
136
|
+
this.currentBufferTarget.inlinedAllStylesheets = false;
|
|
137
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Skipped', incompletes], undefined, FEATURE_NAMES.metrics, this.parent.ee);
|
|
138
|
+
}
|
|
139
|
+
return this.store(event, isCheckout);
|
|
140
|
+
}
|
|
134
141
|
/** Only stop ignoring data if already ignoring and a new valid snapshap is taking place (0 incompletes and we get a meta node for the snap) */
|
|
135
142
|
if (!incompletes && this.#fixing && event.type === RRWEB_EVENT_TYPES.Meta) this.#fixing = false;
|
|
136
143
|
if (incompletes > 0) {
|
|
@@ -140,8 +147,8 @@ export class Recorder {
|
|
|
140
147
|
this.currentBufferTarget.inlinedAllStylesheets = false;
|
|
141
148
|
this.shouldFix = false;
|
|
142
149
|
}
|
|
143
|
-
handle(SUPPORTABILITY_METRIC_CHANNEL, ['
|
|
144
|
-
handle(SUPPORTABILITY_METRIC_CHANNEL, ['
|
|
150
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Failed', failedToFix], undefined, FEATURE_NAMES.metrics, this.parent.ee);
|
|
151
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Fixed', incompletes - failedToFix], undefined, FEATURE_NAMES.metrics, this.parent.ee);
|
|
145
152
|
this.takeFullSnapshot();
|
|
146
153
|
});
|
|
147
154
|
/** Only start ignoring data if got a faulty snapshot */
|
|
@@ -184,6 +191,7 @@ export class Recorder {
|
|
|
184
191
|
// snapshot event
|
|
185
192
|
if (event.type === RRWEB_EVENT_TYPES.FullSnapshot) {
|
|
186
193
|
this.currentBufferTarget.hasSnapshot = true;
|
|
194
|
+
this.hasSeenSnapshot = true;
|
|
187
195
|
}
|
|
188
196
|
this.currentBufferTarget.add(event);
|
|
189
197
|
|
|
@@ -2,7 +2,7 @@ import { gosNREUMOriginals } from '../../../common/window/nreum';
|
|
|
2
2
|
import { isBrowserScope } from '../../../common/constants/runtime';
|
|
3
3
|
class StylesheetEvaluator {
|
|
4
4
|
#evaluated = new WeakSet();
|
|
5
|
-
#
|
|
5
|
+
#brokenSheets = [];
|
|
6
6
|
/**
|
|
7
7
|
* Flipped to true if stylesheets that cannot be natively inlined are detected by the stylesheetEvaluator class
|
|
8
8
|
* Used at harvest time to denote that all subsequent payloads are subject to this and customers should be advised to handle crossorigin decoration
|
|
@@ -16,6 +16,7 @@ class StylesheetEvaluator {
|
|
|
16
16
|
*/
|
|
17
17
|
evaluate() {
|
|
18
18
|
let incompletes = 0;
|
|
19
|
+
this.#brokenSheets = [];
|
|
19
20
|
if (isBrowserScope) {
|
|
20
21
|
for (let i = 0; i < Object.keys(document.styleSheets).length; i++) {
|
|
21
22
|
if (!this.#evaluated.has(document.styleSheets[i])) {
|
|
@@ -26,7 +27,7 @@ class StylesheetEvaluator {
|
|
|
26
27
|
} catch (err) {
|
|
27
28
|
if (!document.styleSheets[i].href) return;
|
|
28
29
|
incompletes++;
|
|
29
|
-
this.#
|
|
30
|
+
this.#brokenSheets.push(document.styleSheets[i]);
|
|
30
31
|
}
|
|
31
32
|
}
|
|
32
33
|
}
|
|
@@ -40,8 +41,8 @@ class StylesheetEvaluator {
|
|
|
40
41
|
* @returns {Promise}
|
|
41
42
|
*/
|
|
42
43
|
async fix() {
|
|
43
|
-
await Promise.all(this.#
|
|
44
|
-
this.#
|
|
44
|
+
await Promise.all(this.#brokenSheets.map(sheet => this.#fetchAndOverride(sheet)));
|
|
45
|
+
this.#brokenSheets = [];
|
|
45
46
|
const failedToFix = this.failedToFix;
|
|
46
47
|
this.failedToFix = 0;
|
|
47
48
|
return failedToFix;
|
|
@@ -58,7 +58,8 @@ export class EventBuffer {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
* Adds an event object to the buffer while tallying size
|
|
61
|
+
* Adds an event object to the buffer while tallying size. Only adds the event if it is valid
|
|
62
|
+
* and would not make the event buffer exceed the maxPayloadSize.
|
|
62
63
|
* @param {Object} event the event object to add to the buffer
|
|
63
64
|
* @returns {EventBuffer} returns the event buffer for chaining
|
|
64
65
|
*/
|
|
@@ -9,6 +9,7 @@ import { Instrument as InstrumentSpa } from '../features/spa/instrument';
|
|
|
9
9
|
import { Instrument as InstrumentSessionReplay } from '../features/session_replay/instrument';
|
|
10
10
|
import { Instrument as InstrumentGenericEvents } from '../features/generic_events/instrument';
|
|
11
11
|
import { Instrument as InstrumentLogs } from '../features/logging/instrument';
|
|
12
|
+
import { Instrument as InstrumentSoftNav } from '../features/soft_navigations/instrument';
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* An agent class with all feature modules available. Features may be disabled and enabled via runtime configuration.
|
|
@@ -18,7 +19,7 @@ export class BrowserAgent extends Agent {
|
|
|
18
19
|
constructor(args) {
|
|
19
20
|
super({
|
|
20
21
|
...args,
|
|
21
|
-
features: [InstrumentXhr, InstrumentPageViewEvent, InstrumentPageViewTiming, InstrumentSessionTrace, InstrumentMetrics, InstrumentErrors, InstrumentSpa, InstrumentSessionReplay, InstrumentGenericEvents, InstrumentLogs],
|
|
22
|
+
features: [InstrumentXhr, InstrumentPageViewEvent, InstrumentPageViewTiming, InstrumentSessionTrace, InstrumentMetrics, InstrumentErrors, InstrumentSpa, InstrumentSoftNav, InstrumentSessionReplay, InstrumentGenericEvents, InstrumentLogs],
|
|
22
23
|
loaderType: 'browser-agent'
|
|
23
24
|
});
|
|
24
25
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/common/config/init.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../../../src/common/config/init.js"],"names":[],"mappings":"AAiHA,+CAIC;AAED,0DAKC;AAED,+DAYC"}
|
|
@@ -13,8 +13,9 @@ export class TimeKeeper {
|
|
|
13
13
|
* @param rumRequest {XMLHttpRequest} The xhr for the rum request
|
|
14
14
|
* @param startTime {number} The start time of the RUM request
|
|
15
15
|
* @param endTime {number} The end time of the RUM request
|
|
16
|
+
* @param nrServerTime {number} the unix number value of the NR server time in MS, returned in the RUM request body
|
|
16
17
|
*/
|
|
17
|
-
processRumRequest(rumRequest: XMLHttpRequest, startTime: number, endTime: number): void;
|
|
18
|
+
processRumRequest(rumRequest: XMLHttpRequest, startTime: number, endTime: number, nrServerTime: number): void;
|
|
18
19
|
/**
|
|
19
20
|
* Converts a page origin relative time to an absolute timestamp
|
|
20
21
|
* using the user's local clock.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"time-keeper.d.ts","sourceRoot":"","sources":["../../../../src/common/timing/time-keeper.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"time-keeper.d.ts","sourceRoot":"","sources":["../../../../src/common/timing/time-keeper.js"],"names":[],"mappings":"AAGA;;;;GAIG;AACH;IA2BE,kCAGC;IAED,qBAEC;IAED,kCAEC;IAED,4BAEC;IAED;;;;;;OAMG;IACH,8BALsB,cAAc,aACf,MAAM,WACR,MAAM,gBACD,MAAM,QAqB7B;IAED;;;;;OAKG;IACH,uCAHwB,MAAM,GACjB,MAAM,CAIlB;IAED;;;;;OAKG;IACH,0CAFa,MAAM,CAIlB;IAED;;;;OAIG;IACH,oCAHqB,MAAM,GACf,MAAM,CAIjB;IAED,+FAA+F;IAC/F,0BASC;;CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stringify.d.ts","sourceRoot":"","sources":["../../../../src/common/util/stringify.js"],"names":[],"mappings":"AAyBA;;;;;GAKG;AACH,+BAHW,GAAC,GACC,MAAM,
|
|
1
|
+
{"version":3,"file":"stringify.d.ts","sourceRoot":"","sources":["../../../../src/common/util/stringify.js"],"names":[],"mappings":"AAyBA;;;;;GAKG;AACH,+BAHW,GAAC,GACC,MAAM,CAclB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/page_view_event/aggregate/index.js"],"names":[],"mappings":"AAkBA;IACE,2BAA2C;IAC3C,mDA0BC;IAvBC,wBAAwB;IACxB,8BAA8B;IAC9B,8BAA8B;IAC9B,uBAAsD;IAsBxD,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/page_view_event/aggregate/index.js"],"names":[],"mappings":"AAkBA;IACE,2BAA2C;IAC3C,mDA0BC;IAvBC,wBAAwB;IACxB,8BAA8B;IAC9B,8BAA8B;IAC9B,uBAAsD;IAsBxD,gBAmGC;CACF;8BA1I6B,4BAA4B;2BAK/B,oCAAoC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/aggregate/index.js"],"names":[],"mappings":"AA+BA;IACE,2BAAiC;IAIjC,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/aggregate/index.js"],"names":[],"mappings":"AA+BA;IACE,2BAAiC;IAIjC,8DAsGC;IAzGD,aAAe;IAKb,8GAA8G;IAC9G,wBAAgH;IAChH,iFAAiF;IACjF,qBAAwB;IAGxB,2CAA2C;IAC3C,sDAAwB;IACxB,6CAA6C;IAC7C,gDAAmB;IAEnB,0BAA0B;IAC1B,kBAAqB;IACrB,6CAA6C;IAC7C,gBAA2B;IAE3B,cAA8B;IAC9B,kBAA+C;IAoC/C,4BAKQ;IA4CV,0BAMC;IAED,qBAWC;IAED;;;;;;;OAOG;IACH,gDAHW,OAAO,GACL,IAAI,CA8DhB;IAED,2BASC;IAED;;;;;;;;;;;;oBAiDC;IAED,sCAIC;IAED;;;;;;;;;;MA2EC;IAED,qCAOC;IAED;;;;OAIG;IACH,mCAKC;IAED,yDAAyD;IACzD,+CAUC;IAED,yCAGC;CACF;8BAhZ6B,4BAA4B;iCALzB,2CAA2C"}
|
|
@@ -4,12 +4,12 @@ export class Recorder {
|
|
|
4
4
|
recording: boolean;
|
|
5
5
|
/** The pointer to the current bucket holding rrweb events */
|
|
6
6
|
currentBufferTarget: RecorderEvents;
|
|
7
|
+
/** Only set to true once a snapshot node has been processed. Used to block preload harvests from sending before we know we have a snapshot */
|
|
8
|
+
hasSeenSnapshot: boolean;
|
|
7
9
|
/** 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 */
|
|
8
10
|
lastMeta: boolean;
|
|
9
11
|
/** The parent class that instantiated the recorder */
|
|
10
12
|
parent: any;
|
|
11
|
-
/** Config to inform to inline stylesheet contents (true default) */
|
|
12
|
-
shouldInlineStylesheets: any;
|
|
13
13
|
/** A flag that can be set to false by failing conversions to stop the fetching process */
|
|
14
14
|
shouldFix: any;
|
|
15
15
|
/** The method to stop recording. This defaults to a noop, but is overwritten once the recording library is imported and initialized */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/shared/recorder.js"],"names":[],"mappings":"AAaA;IAUE,yBAkBC;IAdC,iEAAiE;IACjE,mBAAsB;IACtB,6DAA6D;IAC7D,oCAAuC;IACvC,kIAAkI;IAClI,kBAAqB;IACrB,sDAAsD;IACtD,YAAoB;IACpB,
|
|
1
|
+
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/shared/recorder.js"],"names":[],"mappings":"AAaA;IAUE,yBAkBC;IAdC,iEAAiE;IACjE,mBAAsB;IACtB,6DAA6D;IAC7D,oCAAuC;IACvC,+IAA+I;IAC/I,yBAA4B;IAC5B,kIAAkI;IAClI,kBAAqB;IACrB,sDAAsD;IACtD,YAAoB;IACpB,0FAA0F;IAC1F,eAAqG;IACrG,uIAAuI;IACvI,0BAAyE;IAG3E;;;;;;;;;MAmBC;IAED,mFAAmF;IACnF,oBAKC;IAED,qDAAqD;IACrD,uBA2CC;IAED;;;;;OAKG;IACH,aAHW,GAAC,cACD,GAAC,QAgCX;IAED,0HAA0H;IAC1H,yCAoDC;IA1CG,8BAAoB;IA4CxB,0HAA0H;IAC1H,yBAOC;IAED,wBAEC;IAED,gCAAgC;IAChC,uCAGC;IAED;;;SAGK;IACL,oCAGC;;CACF;+BA3O8B,mBAAmB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stylesheet-evaluator.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/shared/stylesheet-evaluator.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"stylesheet-evaluator.d.ts","sourceRoot":"","sources":["../../../../../src/features/session_replay/shared/stylesheet-evaluator.js"],"names":[],"mappings":"AA0FA,sDAA4D;AAvF5D;IAGE;;;QAGI;IACJ,oCAAkC;IAClC,oBAAe;IAEf;;;OAGG;IACH,mBAoBC;IAED;;;OAGG;IACH,oBAMC;;CAuCF"}
|
|
@@ -32,7 +32,8 @@ export class EventBuffer {
|
|
|
32
32
|
*/
|
|
33
33
|
get hasData(): boolean;
|
|
34
34
|
/**
|
|
35
|
-
* Adds an event object to the buffer while tallying size
|
|
35
|
+
* Adds an event object to the buffer while tallying size. Only adds the event if it is valid
|
|
36
|
+
* and would not make the event buffer exceed the maxPayloadSize.
|
|
36
37
|
* @param {Object} event the event object to add to the buffer
|
|
37
38
|
* @returns {EventBuffer} returns the event buffer for chaining
|
|
38
39
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"event-buffer.d.ts","sourceRoot":"","sources":["../../../../src/features/utils/event-buffer.js"],"names":[],"mappings":"AAGA;;;;;GAKG;AAEH;;GAEG;AACH;IAQE;;;OAGG;IACH,6BAFW,MAAM,YAAC,EAIjB;IADC,uBAAoC;IAGtC;;OAEG;IACH,uBAEC;IAED;;OAEG;IACH,oBAEC;IAED;;OAEG;IACH,wBAGC;IAED;;;OAGG;IACH,uBAEC;IAED
|
|
1
|
+
{"version":3,"file":"event-buffer.d.ts","sourceRoot":"","sources":["../../../../src/features/utils/event-buffer.js"],"names":[],"mappings":"AAGA;;;;;GAKG;AAEH;;GAEG;AACH;IAQE;;;OAGG;IACH,6BAFW,MAAM,YAAC,EAIjB;IADC,uBAAoC;IAGtC;;OAEG;IACH,uBAEC;IAED;;OAEG;IACH,oBAEC;IAED;;OAEG;IACH,wBAGC;IAED;;;OAGG;IACH,uBAEC;IAED;;;;;OAKG;IACH,WAHW,MAAM,GACJ,WAAW,CAQvB;IAED;;;OAGG;IACH,SAFa,WAAW,CAMvB;IAED;;;;OAIG;IACH,QAFa,WAAW,CAMvB;IAED;;;;OAIG;IACH,UAFa,WAAW,CAMvB;IAED;;;;;OAKG;IACH,kCAHW,OAAO,GACL,WAAW,CAOvB;IAED;;;;OAIG;IACH,eAHW,MAAM,GACJ,OAAO,CAInB;;CACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-agent.d.ts","sourceRoot":"","sources":["../../../src/loaders/browser-agent.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"browser-agent.d.ts","sourceRoot":"","sources":["../../../src/loaders/browser-agent.js"],"names":[],"mappings":"AAcA;;;GAGG;AACH;IACE,uBAkBC;CACF;sBAtCqB,SAAS"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@newrelic/browser-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.267.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"author": "New Relic Browser Agent Team <browser-agent@newrelic.com>",
|
|
6
6
|
"description": "New Relic Browser Agent",
|
|
@@ -59,6 +59,9 @@
|
|
|
59
59
|
"features/session_replay": [
|
|
60
60
|
"dist/types/features/session_replay/instrument/index.d.ts"
|
|
61
61
|
],
|
|
62
|
+
"features/soft_navigations": [
|
|
63
|
+
"dist/types/features/soft_navigations/instrument/index.d.ts"
|
|
64
|
+
],
|
|
62
65
|
"features/spa": [
|
|
63
66
|
"dist/types/features/spa/instrument/index.d.ts"
|
|
64
67
|
]
|
|
@@ -193,7 +196,7 @@
|
|
|
193
196
|
"dependencies": {
|
|
194
197
|
"fflate": "0.7.4",
|
|
195
198
|
"rrweb": "2.0.0-alpha.12",
|
|
196
|
-
"web-vitals": "
|
|
199
|
+
"web-vitals": "4.2.3"
|
|
197
200
|
},
|
|
198
201
|
"devDependencies": {
|
|
199
202
|
"@babel/cli": "^7.23.4",
|
|
@@ -69,8 +69,7 @@ const model = () => {
|
|
|
69
69
|
error_sampling_rate: 100, // float from 0 - 100
|
|
70
70
|
collect_fonts: false, // serialize fonts for collection without public asset url, this is currently broken in RRWeb -- https://github.com/rrweb-io/rrweb/issues/1304. When fixed, revisit with test cases
|
|
71
71
|
inline_images: false, // serialize images for collection without public asset url -- right now this is only useful for testing as it easily generates payloads too large to be harvested
|
|
72
|
-
|
|
73
|
-
fix_stylesheets: true, // fetch missing stylesheet resources for inlining, only works if 'inline_stylesheet' is also true
|
|
72
|
+
fix_stylesheets: true, // fetch missing stylesheet resources for inlining
|
|
74
73
|
// recording config settings
|
|
75
74
|
mask_all_inputs: true,
|
|
76
75
|
// this has a getter/setter to facilitate validation of the selectors
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { originTime } from '../constants/runtime'
|
|
2
2
|
import { getRuntime } from '../config/runtime'
|
|
3
3
|
|
|
4
|
-
const rfc2616Regex = /^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), ([0-3][0-9]) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ([0-9]{4}) ([01][0-9]|2[0-3])(:[0-5][0-9]){2} GMT$/
|
|
5
|
-
|
|
6
4
|
/**
|
|
7
5
|
* Class used to adjust the timestamp of harvested data to New Relic server time. This
|
|
8
6
|
* is done by tracking the performance timings of the RUM call and applying a calculation
|
|
@@ -57,28 +55,23 @@ export class TimeKeeper {
|
|
|
57
55
|
* @param rumRequest {XMLHttpRequest} The xhr for the rum request
|
|
58
56
|
* @param startTime {number} The start time of the RUM request
|
|
59
57
|
* @param endTime {number} The end time of the RUM request
|
|
58
|
+
* @param nrServerTime {number} the unix number value of the NR server time in MS, returned in the RUM request body
|
|
60
59
|
*/
|
|
61
|
-
processRumRequest (rumRequest, startTime, endTime) {
|
|
60
|
+
processRumRequest (rumRequest, startTime, endTime, nrServerTime) {
|
|
62
61
|
this.processStoredDiff() // Check session entity for stored time diff
|
|
63
62
|
if (this.#ready) return // Server time calculated from session entity
|
|
64
63
|
|
|
65
|
-
|
|
66
|
-
if (!responseDateHeader) {
|
|
67
|
-
throw new Error('Missing date header on rum response.')
|
|
68
|
-
}
|
|
69
|
-
if (!rfc2616Regex.test(responseDateHeader)) {
|
|
70
|
-
throw new Error('Date header invalid format.')
|
|
71
|
-
}
|
|
64
|
+
if (!nrServerTime) throw new Error('nrServerTime not found')
|
|
72
65
|
|
|
73
66
|
const medianRumOffset = (endTime - startTime) / 2
|
|
74
67
|
const serverOffset = startTime + medianRumOffset
|
|
75
68
|
|
|
76
69
|
// Corrected page origin time
|
|
77
|
-
this.#correctedOriginTime = Math.floor(
|
|
70
|
+
this.#correctedOriginTime = Math.floor(nrServerTime - serverOffset)
|
|
78
71
|
this.#localTimeDiff = originTime - this.#correctedOriginTime
|
|
79
72
|
|
|
80
73
|
if (isNaN(this.#correctedOriginTime)) {
|
|
81
|
-
throw new Error('
|
|
74
|
+
throw new Error('Failed to correct browser time to server time')
|
|
82
75
|
}
|
|
83
76
|
|
|
84
77
|
this.#session?.write({ serverTimeDiff: this.#localTimeDiff })
|
|
@@ -10,9 +10,15 @@ if (isBrowserScope) {
|
|
|
10
10
|
onINP(({ value, attribution, id }) => {
|
|
11
11
|
const attrs = {
|
|
12
12
|
metricId: id,
|
|
13
|
-
eventTarget: attribution.
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
eventTarget: attribution.interactionTarget, // event* attrs deprecated in v4, kept for NR backwards compatibility
|
|
14
|
+
eventTime: attribution.interactionTime, // event* attrs deprecated in v4, kept for NR backwards compatibility
|
|
15
|
+
interactionTarget: attribution.interactionTarget,
|
|
16
|
+
interactionTime: attribution.interactionTime,
|
|
17
|
+
interactionType: attribution.interactionType,
|
|
18
|
+
inputDelay: attribution.inputDelay,
|
|
19
|
+
nextPaintTime: attribution.nextPaintTime,
|
|
20
|
+
processingDuration: attribution.processingDuration,
|
|
21
|
+
presentationDelay: attribution.presentationDelay,
|
|
16
22
|
loadState: attribution.loadState
|
|
17
23
|
}
|
|
18
24
|
interactionToNextPaint.update({ value, attrs })
|
|
@@ -20,7 +20,8 @@ if (isBrowserScope) {
|
|
|
20
20
|
element: attribution.element,
|
|
21
21
|
timeToFirstByte: attribution.timeToFirstByte,
|
|
22
22
|
resourceLoadDelay: attribution.resourceLoadDelay,
|
|
23
|
-
|
|
23
|
+
resourceLoadDuration: attribution.resourceLoadDuration,
|
|
24
|
+
resourceLoadTime: attribution.resourceLoadDuration, // kept for NR backwards compatibility, deprecated in v3->v4
|
|
24
25
|
elementRenderDelay: attribution.elementRenderDelay
|
|
25
26
|
}
|
|
26
27
|
if (attribution.url) attrs.elUrl = cleanURL(attribution.url)
|
|
@@ -125,19 +125,17 @@ export class Aggregate extends AggregateBase {
|
|
|
125
125
|
return
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
try {
|
|
129
|
-
this.timeKeeper.processRumRequest(xhr, rumStartTime, rumEndTime)
|
|
130
|
-
if (!this.timeKeeper.ready) throw new Error('TimeKeeper not ready')
|
|
131
|
-
|
|
132
|
-
agentRuntime.timeKeeper = this.timeKeeper
|
|
133
|
-
} catch (error) {
|
|
134
|
-
this.ee.abort()
|
|
135
|
-
warn(17, error)
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
|
|
139
128
|
try {
|
|
140
129
|
const { app, ...flags } = JSON.parse(responseText)
|
|
130
|
+
try {
|
|
131
|
+
this.timeKeeper.processRumRequest(xhr, rumStartTime, rumEndTime, app.nrServerTime)
|
|
132
|
+
if (!this.timeKeeper.ready) throw new Error('TimeKeeper not ready')
|
|
133
|
+
agentRuntime.timeKeeper = this.timeKeeper
|
|
134
|
+
} catch (error) {
|
|
135
|
+
this.ee.abort()
|
|
136
|
+
warn(17, error)
|
|
137
|
+
return
|
|
138
|
+
}
|
|
141
139
|
agentRuntime.appMetadata = app
|
|
142
140
|
activateFeatures(flags, this.agentIdentifier)
|
|
143
141
|
this.drain()
|
|
@@ -104,7 +104,7 @@ export class Aggregate extends AggregateBase {
|
|
|
104
104
|
this.handleError(e)
|
|
105
105
|
}, this.featureName, this.ee)
|
|
106
106
|
|
|
107
|
-
const { error_sampling_rate, sampling_rate, autoStart, block_selector, mask_text_selector, mask_all_inputs,
|
|
107
|
+
const { error_sampling_rate, sampling_rate, autoStart, block_selector, mask_text_selector, mask_all_inputs, inline_images, collect_fonts } = getConfigurationValue(this.agentIdentifier, 'session_replay')
|
|
108
108
|
|
|
109
109
|
this.waitForFlags(['srs', 'sr']).then(([srMode, entitled]) => {
|
|
110
110
|
this.entitled = !!entitled
|
|
@@ -129,7 +129,6 @@ export class Aggregate extends AggregateBase {
|
|
|
129
129
|
/** Detect if the default configs have been altered and report a SM. This is useful to evaluate what the reasonable defaults are across a customer base over time */
|
|
130
130
|
if (!autoStart) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/AutoStart/Modified'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
131
131
|
if (collect_fonts === true) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/CollectFonts/Modified'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
132
|
-
if (inline_stylesheet !== true) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/InlineStylesheet/Modified'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
133
132
|
if (inline_images === true) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/InlineImages/Modifed'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
134
133
|
if (mask_all_inputs !== true) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/MaskAllInputs/Modified'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
135
134
|
if (block_selector !== '[data-nr-block]') handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/BlockSelector/Modified'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
@@ -242,7 +241,7 @@ export class Aggregate extends AggregateBase {
|
|
|
242
241
|
}
|
|
243
242
|
|
|
244
243
|
prepareHarvest ({ opts } = {}) {
|
|
245
|
-
if (!this.recorder || !this.timeKeeper?.ready) return
|
|
244
|
+
if (!this.recorder || !this.timeKeeper?.ready || !this.recorder.hasSeenSnapshot) return
|
|
246
245
|
const recorderEvents = this.recorder.getEvents()
|
|
247
246
|
// get the event type and use that to trigger another harvest if needed
|
|
248
247
|
if (!recorderEvents.events.length || (this.mode !== MODE.FULL) || this.blocked) return
|
|
@@ -29,14 +29,14 @@ export class Recorder {
|
|
|
29
29
|
this.recording = false
|
|
30
30
|
/** The pointer to the current bucket holding rrweb events */
|
|
31
31
|
this.currentBufferTarget = this.#events
|
|
32
|
+
/** Only set to true once a snapshot node has been processed. Used to block preload harvests from sending before we know we have a snapshot */
|
|
33
|
+
this.hasSeenSnapshot = false
|
|
32
34
|
/** 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 */
|
|
33
35
|
this.lastMeta = false
|
|
34
36
|
/** The parent class that instantiated the recorder */
|
|
35
37
|
this.parent = parent
|
|
36
|
-
/** Config to inform to inline stylesheet contents (true default) */
|
|
37
|
-
this.shouldInlineStylesheets = getConfigurationValue(this.parent.agentIdentifier, 'session_replay.inline_stylesheet')
|
|
38
38
|
/** A flag that can be set to false by failing conversions to stop the fetching process */
|
|
39
|
-
this.shouldFix =
|
|
39
|
+
this.shouldFix = getConfigurationValue(this.parent.agentIdentifier, 'session_replay.fix_stylesheets')
|
|
40
40
|
/** The method to stop recording. This defaults to a noop, but is overwritten once the recording library is imported and initialized */
|
|
41
41
|
this.stopRecording = () => { /* no-op until set by rrweb initializer */ }
|
|
42
42
|
}
|
|
@@ -73,10 +73,14 @@ export class Recorder {
|
|
|
73
73
|
/** Begin recording using configured recording lib */
|
|
74
74
|
startRecording () {
|
|
75
75
|
this.recording = true
|
|
76
|
-
const { block_class, ignore_class, mask_text_class, block_selector, mask_input_options, mask_text_selector, mask_all_inputs,
|
|
76
|
+
const { block_class, ignore_class, mask_text_class, block_selector, mask_input_options, mask_text_selector, mask_all_inputs, inline_images, collect_fonts } = getConfigurationValue(this.parent.agentIdentifier, 'session_replay')
|
|
77
77
|
const customMasker = (text, element) => {
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
try {
|
|
79
|
+
if (typeof element?.type === 'string' && element.type.toLowerCase() !== 'password' && (element?.dataset?.nrUnmask !== undefined || element?.classList?.contains('nr-unmask'))) return text
|
|
80
|
+
} catch (err) {
|
|
81
|
+
// likely an element was passed to this handler that was invalid and was missing attributes or methods
|
|
82
|
+
}
|
|
83
|
+
return '*'.repeat(text?.length || 0)
|
|
80
84
|
}
|
|
81
85
|
// set up rrweb configurations for maximum privacy --
|
|
82
86
|
// https://newrelic.atlassian.net/wiki/spaces/O11Y/pages/2792293280/2023+02+28+Browser+-+Session+Replay#Configuration-options
|
|
@@ -91,7 +95,7 @@ export class Recorder {
|
|
|
91
95
|
maskTextFn: customMasker,
|
|
92
96
|
maskAllInputs: mask_all_inputs,
|
|
93
97
|
maskInputFn: customMasker,
|
|
94
|
-
inlineStylesheet:
|
|
98
|
+
inlineStylesheet: true,
|
|
95
99
|
inlineImages: inline_images,
|
|
96
100
|
collectFonts: collect_fonts,
|
|
97
101
|
checkoutEveryNms: CHECKOUT_MS[this.parent.mode],
|
|
@@ -119,13 +123,17 @@ export class Recorder {
|
|
|
119
123
|
* @param {*} isCheckout - Flag indicating if the payload was triggered as a checkout
|
|
120
124
|
*/
|
|
121
125
|
audit (event, isCheckout) {
|
|
122
|
-
/** only run the audit if inline_stylesheets is configured as on (default behavior) */
|
|
123
|
-
if (this.shouldInlineStylesheets === false || !this.shouldFix) {
|
|
124
|
-
this.currentBufferTarget.inlinedAllStylesheets = false
|
|
125
|
-
return this.store(event, isCheckout)
|
|
126
|
-
}
|
|
127
126
|
/** An count of stylesheet objects that were blocked from accessing contents via JS */
|
|
128
127
|
const incompletes = stylesheetEvaluator.evaluate()
|
|
128
|
+
const missingInlineSMTag = 'SessionReplay/Payload/Missing-Inline-Css/'
|
|
129
|
+
/** only run the full fixing behavior (more costly) if fix_stylesheets is configured as on (default behavior) */
|
|
130
|
+
if (!this.shouldFix) {
|
|
131
|
+
if (incompletes > 0) {
|
|
132
|
+
this.currentBufferTarget.inlinedAllStylesheets = false
|
|
133
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Skipped', incompletes], undefined, FEATURE_NAMES.metrics, this.parent.ee)
|
|
134
|
+
}
|
|
135
|
+
return this.store(event, isCheckout)
|
|
136
|
+
}
|
|
129
137
|
/** Only stop ignoring data if already ignoring and a new valid snapshap is taking place (0 incompletes and we get a meta node for the snap) */
|
|
130
138
|
if (!incompletes && this.#fixing && event.type === RRWEB_EVENT_TYPES.Meta) this.#fixing = false
|
|
131
139
|
if (incompletes > 0) {
|
|
@@ -135,8 +143,8 @@ export class Recorder {
|
|
|
135
143
|
this.currentBufferTarget.inlinedAllStylesheets = false
|
|
136
144
|
this.shouldFix = false
|
|
137
145
|
}
|
|
138
|
-
handle(SUPPORTABILITY_METRIC_CHANNEL, ['
|
|
139
|
-
handle(SUPPORTABILITY_METRIC_CHANNEL, ['
|
|
146
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Failed', failedToFix], undefined, FEATURE_NAMES.metrics, this.parent.ee)
|
|
147
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Fixed', incompletes - failedToFix], undefined, FEATURE_NAMES.metrics, this.parent.ee)
|
|
140
148
|
this.takeFullSnapshot()
|
|
141
149
|
})
|
|
142
150
|
/** Only start ignoring data if got a faulty snapshot */
|
|
@@ -184,6 +192,7 @@ export class Recorder {
|
|
|
184
192
|
// snapshot event
|
|
185
193
|
if (event.type === RRWEB_EVENT_TYPES.FullSnapshot) {
|
|
186
194
|
this.currentBufferTarget.hasSnapshot = true
|
|
195
|
+
this.hasSeenSnapshot = true
|
|
187
196
|
}
|
|
188
197
|
this.currentBufferTarget.add(event)
|
|
189
198
|
|
|
@@ -3,7 +3,7 @@ import { isBrowserScope } from '../../../common/constants/runtime'
|
|
|
3
3
|
|
|
4
4
|
class StylesheetEvaluator {
|
|
5
5
|
#evaluated = new WeakSet()
|
|
6
|
-
#
|
|
6
|
+
#brokenSheets = []
|
|
7
7
|
/**
|
|
8
8
|
* Flipped to true if stylesheets that cannot be natively inlined are detected by the stylesheetEvaluator class
|
|
9
9
|
* Used at harvest time to denote that all subsequent payloads are subject to this and customers should be advised to handle crossorigin decoration
|
|
@@ -17,6 +17,7 @@ class StylesheetEvaluator {
|
|
|
17
17
|
*/
|
|
18
18
|
evaluate () {
|
|
19
19
|
let incompletes = 0
|
|
20
|
+
this.#brokenSheets = []
|
|
20
21
|
if (isBrowserScope) {
|
|
21
22
|
for (let i = 0; i < Object.keys(document.styleSheets).length; i++) {
|
|
22
23
|
if (!this.#evaluated.has(document.styleSheets[i])) {
|
|
@@ -27,7 +28,7 @@ class StylesheetEvaluator {
|
|
|
27
28
|
} catch (err) {
|
|
28
29
|
if (!document.styleSheets[i].href) return
|
|
29
30
|
incompletes++
|
|
30
|
-
this.#
|
|
31
|
+
this.#brokenSheets.push(document.styleSheets[i])
|
|
31
32
|
}
|
|
32
33
|
}
|
|
33
34
|
}
|
|
@@ -41,8 +42,8 @@ class StylesheetEvaluator {
|
|
|
41
42
|
* @returns {Promise}
|
|
42
43
|
*/
|
|
43
44
|
async fix () {
|
|
44
|
-
await Promise.all(this.#
|
|
45
|
-
this.#
|
|
45
|
+
await Promise.all(this.#brokenSheets.map(sheet => this.#fetchAndOverride(sheet)))
|
|
46
|
+
this.#brokenSheets = []
|
|
46
47
|
const failedToFix = this.failedToFix
|
|
47
48
|
this.failedToFix = 0
|
|
48
49
|
return failedToFix
|
|
@@ -58,7 +58,8 @@ export class EventBuffer {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
|
-
* Adds an event object to the buffer while tallying size
|
|
61
|
+
* Adds an event object to the buffer while tallying size. Only adds the event if it is valid
|
|
62
|
+
* and would not make the event buffer exceed the maxPayloadSize.
|
|
62
63
|
* @param {Object} event the event object to add to the buffer
|
|
63
64
|
* @returns {EventBuffer} returns the event buffer for chaining
|
|
64
65
|
*/
|
|
@@ -10,6 +10,7 @@ import { Instrument as InstrumentSpa } from '../features/spa/instrument'
|
|
|
10
10
|
import { Instrument as InstrumentSessionReplay } from '../features/session_replay/instrument'
|
|
11
11
|
import { Instrument as InstrumentGenericEvents } from '../features/generic_events/instrument'
|
|
12
12
|
import { Instrument as InstrumentLogs } from '../features/logging/instrument'
|
|
13
|
+
import { Instrument as InstrumentSoftNav } from '../features/soft_navigations/instrument'
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* An agent class with all feature modules available. Features may be disabled and enabled via runtime configuration.
|
|
@@ -27,6 +28,7 @@ export class BrowserAgent extends Agent {
|
|
|
27
28
|
InstrumentMetrics,
|
|
28
29
|
InstrumentErrors,
|
|
29
30
|
InstrumentSpa,
|
|
31
|
+
InstrumentSoftNav,
|
|
30
32
|
InstrumentSessionReplay,
|
|
31
33
|
InstrumentGenericEvents,
|
|
32
34
|
InstrumentLogs
|