@newrelic/browser-agent 1.251.0 → 1.252.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 +25 -0
- package/dist/cjs/common/config/state/init.js +2 -2
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/drain/drain.js +3 -1
- package/dist/cjs/common/event-emitter/contextual-ee.js +7 -1
- package/dist/cjs/common/harvest/harvest-scheduler.js +2 -1
- package/dist/cjs/common/harvest/harvest.js +3 -2
- package/dist/cjs/features/ajax/instrument/index.js +2 -0
- package/dist/cjs/features/jserrors/instrument/index.js +5 -0
- package/dist/cjs/features/metrics/aggregate/index.js +1 -1
- package/dist/cjs/features/page_view_event/aggregate/index.js +1 -1
- package/dist/cjs/features/session_replay/aggregate/index.js +53 -17
- package/dist/cjs/features/session_replay/shared/recorder.js +9 -4
- package/dist/cjs/features/session_replay/shared/stylesheet-evaluator.js +33 -28
- package/dist/cjs/features/utils/instrument-base.js +1 -1
- package/dist/cjs/loaders/api/api.js +4 -1
- package/dist/esm/common/config/state/init.js +2 -2
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/drain/drain.js +3 -1
- package/dist/esm/common/event-emitter/contextual-ee.js +7 -1
- package/dist/esm/common/harvest/harvest-scheduler.js +2 -1
- package/dist/esm/common/harvest/harvest.js +3 -2
- package/dist/esm/features/ajax/instrument/index.js +2 -0
- package/dist/esm/features/jserrors/instrument/index.js +5 -0
- package/dist/esm/features/metrics/aggregate/index.js +1 -1
- package/dist/esm/features/page_view_event/aggregate/index.js +1 -1
- package/dist/esm/features/session_replay/aggregate/index.js +53 -17
- package/dist/esm/features/session_replay/shared/recorder.js +9 -4
- package/dist/esm/features/session_replay/shared/stylesheet-evaluator.js +33 -28
- package/dist/esm/features/utils/instrument-base.js +1 -1
- package/dist/esm/loaders/api/api.js +4 -1
- package/dist/types/common/drain/drain.d.ts +2 -1
- package/dist/types/common/drain/drain.d.ts.map +1 -1
- package/dist/types/common/event-emitter/contextual-ee.d.ts.map +1 -1
- package/dist/types/common/harvest/harvest.d.ts.map +1 -1
- package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
- package/dist/types/features/jserrors/instrument/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +8 -3
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/stylesheet-evaluator.d.ts +1 -1
- package/dist/types/features/session_replay/shared/stylesheet-evaluator.d.ts.map +1 -1
- package/dist/types/loaders/api/api.d.ts.map +1 -1
- package/package.json +4 -1
- package/src/common/config/state/init.js +2 -2
- package/src/common/drain/drain.js +3 -2
- package/src/common/event-emitter/contextual-ee.js +7 -1
- package/src/common/harvest/harvest-scheduler.js +1 -1
- package/src/common/harvest/harvest.js +3 -2
- package/src/features/ajax/instrument/index.js +2 -0
- package/src/features/jserrors/instrument/index.js +6 -0
- package/src/features/metrics/aggregate/index.js +1 -1
- package/src/features/page_view_event/aggregate/index.js +1 -1
- package/src/features/session_replay/aggregate/index.js +47 -19
- package/src/features/session_replay/shared/recorder.js +9 -4
- package/src/features/session_replay/shared/stylesheet-evaluator.js +26 -21
- package/src/features/utils/instrument-base.js +1 -1
- package/src/loaders/api/api.js +4 -1
|
@@ -28,8 +28,6 @@ import { MODE, SESSION_EVENTS, SESSION_EVENT_TYPES } from '../../../common/sessi
|
|
|
28
28
|
import { stringify } from '../../../common/util/stringify'
|
|
29
29
|
import { stylesheetEvaluator } from '../shared/stylesheet-evaluator'
|
|
30
30
|
|
|
31
|
-
let gzipper, u8
|
|
32
|
-
|
|
33
31
|
export class Aggregate extends AggregateBase {
|
|
34
32
|
static featureName = FEATURE_NAME
|
|
35
33
|
// pass the recorder into the aggregator
|
|
@@ -41,8 +39,10 @@ export class Aggregate extends AggregateBase {
|
|
|
41
39
|
this.initialized = false
|
|
42
40
|
/** Set once the feature has been "aborted" to prevent other side-effects from continuing */
|
|
43
41
|
this.blocked = false
|
|
44
|
-
/**
|
|
45
|
-
this.
|
|
42
|
+
/** populated with the gzipper lib async */
|
|
43
|
+
this.gzipper = undefined
|
|
44
|
+
/** populated with the u8 string lib async */
|
|
45
|
+
this.u8 = undefined
|
|
46
46
|
/** the mode to start in. Defaults to off */
|
|
47
47
|
const { session } = getRuntime(this.agentIdentifier)
|
|
48
48
|
this.mode = session.state.sessionReplayMode || MODE.OFF
|
|
@@ -53,6 +53,8 @@ export class Aggregate extends AggregateBase {
|
|
|
53
53
|
this.recorder = args?.recorder
|
|
54
54
|
if (this.recorder) this.recorder.parent = this
|
|
55
55
|
|
|
56
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/Enabled'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
57
|
+
|
|
56
58
|
const shouldSetup = (
|
|
57
59
|
getConfigurationValue(agentIdentifier, 'privacy.cookies_enabled') === true &&
|
|
58
60
|
getConfigurationValue(agentIdentifier, 'session_trace.enabled') === true
|
|
@@ -91,6 +93,12 @@ export class Aggregate extends AggregateBase {
|
|
|
91
93
|
raw: true
|
|
92
94
|
}, this)
|
|
93
95
|
|
|
96
|
+
if (this.recorder?.getEvents().type === 'preloaded') {
|
|
97
|
+
this.prepUtils().then(() => {
|
|
98
|
+
this.scheduler.runHarvest()
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
94
102
|
registerHandler('recordReplay', () => {
|
|
95
103
|
// if it has aborted or BCS returned bad entitlements, do not allow
|
|
96
104
|
if (this.blocked || !this.entitled) return
|
|
@@ -116,15 +124,31 @@ export class Aggregate extends AggregateBase {
|
|
|
116
124
|
}
|
|
117
125
|
}, this.featureName, this.ee)
|
|
118
126
|
|
|
127
|
+
const { error_sampling_rate, sampling_rate, autoStart, block_selector, mask_text_selector, mask_all_inputs, inline_stylesheet, inline_images, collect_fonts } = getConfigurationValue(this.agentIdentifier, 'session_replay')
|
|
128
|
+
|
|
119
129
|
this.waitForFlags(['sr']).then(([flagOn]) => {
|
|
120
130
|
this.entitled = flagOn
|
|
121
|
-
if (!this.entitled && this.recorder?.recording)
|
|
131
|
+
if (!this.entitled && this.recorder?.recording) {
|
|
132
|
+
this.recorder.abort(ABORT_REASONS.ENTITLEMENTS)
|
|
133
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, ['SessionReplay/EnabledNotEntitled/Detected'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
134
|
+
}
|
|
122
135
|
this.initializeRecording(
|
|
123
|
-
(Math.random() * 100) <
|
|
124
|
-
(Math.random() * 100) <
|
|
136
|
+
(Math.random() * 100) < error_sampling_rate,
|
|
137
|
+
(Math.random() * 100) < sampling_rate
|
|
125
138
|
)
|
|
126
139
|
}).then(() => sharedChannel.onReplayReady(this.mode)) // notify watchers that replay started with the mode
|
|
127
140
|
|
|
141
|
+
/** 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 */
|
|
142
|
+
if (!autoStart) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/AutoStart/Modified'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
143
|
+
if (collect_fonts === true) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/CollectFonts/Modified'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
144
|
+
if (inline_stylesheet !== true) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/InlineStylesheet/Modified'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
145
|
+
if (inline_images === true) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/InlineImages/Modifed'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
146
|
+
if (mask_all_inputs !== true) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/MaskAllInputs/Modified'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
147
|
+
if (block_selector !== '[data-nr-block]') handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/BlockSelector/Modified'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
148
|
+
if (mask_text_selector !== '*') handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/MaskTextSelector/Modified'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
149
|
+
|
|
150
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/SamplingRate/Value', sampling_rate], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
151
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/ErrorSamplingRate/Value', error_sampling_rate], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
128
152
|
this.drain()
|
|
129
153
|
}
|
|
130
154
|
}
|
|
@@ -197,21 +221,25 @@ export class Aggregate extends AggregateBase {
|
|
|
197
221
|
this.scheduler.startTimer(this.harvestTimeSeconds)
|
|
198
222
|
}
|
|
199
223
|
|
|
224
|
+
await this.prepUtils()
|
|
225
|
+
|
|
226
|
+
if (!this.recorder.recording) this.recorder.startRecording()
|
|
227
|
+
|
|
228
|
+
this.syncWithSessionManager({ sessionReplayMode: this.mode })
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async prepUtils () {
|
|
200
232
|
try {
|
|
201
233
|
// Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
|
|
202
234
|
const { gzipSync, strToU8 } = await import(/* webpackChunkName: "compressor" */'fflate')
|
|
203
|
-
gzipper = gzipSync
|
|
204
|
-
u8 = strToU8
|
|
235
|
+
this.gzipper = gzipSync
|
|
236
|
+
this.u8 = strToU8
|
|
205
237
|
} catch (err) {
|
|
206
238
|
// compressor failed to load, but we can still record without compression as a last ditch effort
|
|
207
|
-
this.shouldCompress = false
|
|
208
239
|
}
|
|
209
|
-
if (!this.recorder.recording) this.recorder.startRecording()
|
|
210
|
-
|
|
211
|
-
this.syncWithSessionManager({ sessionReplayMode: this.mode })
|
|
212
240
|
}
|
|
213
241
|
|
|
214
|
-
prepareHarvest () {
|
|
242
|
+
prepareHarvest ({ opts } = {}) {
|
|
215
243
|
if (!this.recorder) return
|
|
216
244
|
const recorderEvents = this.recorder.getEvents()
|
|
217
245
|
// get the event type and use that to trigger another harvest if needed
|
|
@@ -224,8 +252,8 @@ export class Aggregate extends AggregateBase {
|
|
|
224
252
|
}
|
|
225
253
|
|
|
226
254
|
let len = 0
|
|
227
|
-
if (this.
|
|
228
|
-
payload.body = gzipper(u8(`[${payload.body.map(e => e.__serialized).join(',')}]`))
|
|
255
|
+
if (!!this.gzipper && !!this.u8) {
|
|
256
|
+
payload.body = this.gzipper(this.u8(`[${payload.body.map(e => e.__serialized).join(',')}]`))
|
|
229
257
|
len = payload.body.length
|
|
230
258
|
this.scheduler.opts.gzip = true
|
|
231
259
|
} else {
|
|
@@ -242,7 +270,7 @@ export class Aggregate extends AggregateBase {
|
|
|
242
270
|
const { session } = getRuntime(this.agentIdentifier)
|
|
243
271
|
if (!session.state.sessionReplaySentFirstChunk) this.syncWithSessionManager({ sessionReplaySentFirstChunk: true })
|
|
244
272
|
this.recorder.clearBuffer()
|
|
245
|
-
if (recorderEvents.type === 'preloaded') this.scheduler.runHarvest()
|
|
273
|
+
if (recorderEvents.type === 'preloaded') this.scheduler.runHarvest(opts)
|
|
246
274
|
return [payload]
|
|
247
275
|
}
|
|
248
276
|
|
|
@@ -276,7 +304,7 @@ export class Aggregate extends AggregateBase {
|
|
|
276
304
|
|
|
277
305
|
const firstEventTimestamp = events[0]?.timestamp // from rrweb node
|
|
278
306
|
const lastEventTimestamp = events[events.length - 1]?.timestamp // from rrweb node
|
|
279
|
-
const firstTimestamp = firstEventTimestamp || recorderEvents.cycleTimestamp
|
|
307
|
+
const firstTimestamp = firstEventTimestamp || recorderEvents.cycleTimestamp // from rrweb node || from when the harvest cycle started
|
|
280
308
|
const lastTimestamp = lastEventTimestamp || agentOffset + relativeNow
|
|
281
309
|
|
|
282
310
|
return {
|
|
@@ -288,7 +316,7 @@ export class Aggregate extends AggregateBase {
|
|
|
288
316
|
attributes: encodeObj({
|
|
289
317
|
// this section of attributes must be controllable and stay below the query param padding limit -- see QUERY_PARAM_PADDING
|
|
290
318
|
// if not, data could be lost to truncation at time of sending, potentially breaking parsing / API behavior in NR1
|
|
291
|
-
...(this.
|
|
319
|
+
...(!!this.gzipper && !!this.u8 && { content_encoding: 'gzip' }),
|
|
292
320
|
'replay.firstTimestamp': firstTimestamp,
|
|
293
321
|
'replay.firstTimestampOffset': firstTimestamp - agentOffset,
|
|
294
322
|
'replay.lastTimestamp': lastTimestamp,
|
|
@@ -102,13 +102,13 @@ export class Recorder {
|
|
|
102
102
|
/** 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) */
|
|
103
103
|
if (!incompletes && this.#fixing && event.type === RRWEB_EVENT_TYPES.Meta) this.#fixing = false
|
|
104
104
|
if (incompletes) {
|
|
105
|
-
handle(SUPPORTABILITY_METRIC_CHANNEL, ['SessionReplay/Payload/Missing-Inline-Css', incompletes], undefined, FEATURE_NAMES.metrics, this.parent.ee)
|
|
106
105
|
/** wait for the evaluator to download/replace the incompletes' src code and then take a new snap */
|
|
107
106
|
stylesheetEvaluator.fix().then((failedToFix) => {
|
|
108
107
|
if (failedToFix) {
|
|
109
108
|
this.currentBufferTarget.inlinedAllStylesheets = false
|
|
110
109
|
this.shouldFix = false
|
|
111
|
-
|
|
110
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, ['SessionReplay/Payload/Missing-Inline-Css/Failed', failedToFix], undefined, FEATURE_NAMES.metrics, this.parent.ee)
|
|
111
|
+
} else handle(SUPPORTABILITY_METRIC_CHANNEL, ['SessionReplay/Payload/Missing-Inline-Css/Fixed', incompletes - failedToFix], undefined, FEATURE_NAMES.metrics, this.parent.ee)
|
|
112
112
|
this.takeFullSnapshot()
|
|
113
113
|
})
|
|
114
114
|
/** Only start ignoring data if got a faulty snapshot */
|
|
@@ -166,7 +166,12 @@ export class Recorder {
|
|
|
166
166
|
|
|
167
167
|
/** force the recording lib to take a full DOM snapshot. This needs to occur in certain cases, like visibility changes */
|
|
168
168
|
takeFullSnapshot () {
|
|
169
|
-
|
|
169
|
+
try {
|
|
170
|
+
if (!this.recording) return
|
|
171
|
+
recorder.takeFullSnapshot()
|
|
172
|
+
} catch (err) {
|
|
173
|
+
// in the off chance we think we are recording, but rrweb does not, rrweb's lib will throw an error. This catch is just a precaution
|
|
174
|
+
}
|
|
170
175
|
}
|
|
171
176
|
|
|
172
177
|
clearTimestamps () {
|
|
@@ -184,7 +189,7 @@ export class Recorder {
|
|
|
184
189
|
* https://staging.onenr.io/037jbJWxbjy
|
|
185
190
|
* */
|
|
186
191
|
estimateCompression (data) {
|
|
187
|
-
if (this.
|
|
192
|
+
if (!!this.parent.gzipper && !!this.parent.u8) return data * AVG_COMPRESSION
|
|
188
193
|
return data
|
|
189
194
|
}
|
|
190
195
|
}
|
|
@@ -9,7 +9,7 @@ class StylesheetEvaluator {
|
|
|
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
|
|
10
10
|
* */
|
|
11
11
|
invalidStylesheetsDetected = false
|
|
12
|
-
failedToFix =
|
|
12
|
+
failedToFix = 0
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* this works by checking (only ever once) each cssRules obj in the style sheets array. The try/catch will catch an error if the cssRules obj blocks access, triggering the module to try to "fix" the asset`. Returns the count of incomplete assets discovered.
|
|
@@ -44,7 +44,7 @@ class StylesheetEvaluator {
|
|
|
44
44
|
await Promise.all(this.#fetchProms)
|
|
45
45
|
this.#fetchProms = []
|
|
46
46
|
const failedToFix = this.failedToFix
|
|
47
|
-
this.failedToFix =
|
|
47
|
+
this.failedToFix = 0
|
|
48
48
|
return failedToFix
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -55,28 +55,33 @@ class StylesheetEvaluator {
|
|
|
55
55
|
* @returns {Promise}
|
|
56
56
|
*/
|
|
57
57
|
async #fetchAndOverride (target, href) {
|
|
58
|
-
const stylesheetContents = await originals.FETCH.bind(window)(href)
|
|
59
|
-
if (!stylesheetContents.ok) {
|
|
60
|
-
this.failedToFix = true
|
|
61
|
-
return
|
|
62
|
-
}
|
|
63
|
-
const stylesheetText = await stylesheetContents.text()
|
|
64
58
|
try {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
59
|
+
const stylesheetContents = await originals.FETCH.bind(window)(href)
|
|
60
|
+
if (!stylesheetContents.ok) {
|
|
61
|
+
this.failedToFix++
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
const stylesheetText = await stylesheetContents.text()
|
|
65
|
+
try {
|
|
66
|
+
const cssSheet = new CSSStyleSheet()
|
|
67
|
+
await cssSheet.replace(stylesheetText)
|
|
68
|
+
Object.defineProperty(target, 'cssRules', {
|
|
69
|
+
get () { return cssSheet.cssRules }
|
|
70
|
+
})
|
|
71
|
+
Object.defineProperty(target, 'rules', {
|
|
72
|
+
get () { return cssSheet.rules }
|
|
73
|
+
})
|
|
74
|
+
} catch (err) {
|
|
74
75
|
// cant make new dynamic stylesheets, browser likely doesn't support `.replace()`...
|
|
75
76
|
// this is appended in prep of forking rrweb
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
Object.defineProperty(target, 'cssText', {
|
|
78
|
+
get () { return stylesheetText }
|
|
79
|
+
})
|
|
80
|
+
this.failedToFix++
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
// failed to fetch
|
|
84
|
+
this.failedToFix++
|
|
80
85
|
}
|
|
81
86
|
}
|
|
82
87
|
}
|
|
@@ -108,7 +108,7 @@ export class InstrumentBase extends FeatureBase {
|
|
|
108
108
|
warn(`Downloading and initializing ${this.featureName} failed...`, e)
|
|
109
109
|
this.abortHandler?.() // undo any important alterations made to the page
|
|
110
110
|
// not supported yet but nice to do: "abort" this agent's EE for this feature specifically
|
|
111
|
-
drain(this.agentIdentifier, this.featureName)
|
|
111
|
+
drain(this.agentIdentifier, this.featureName, true)
|
|
112
112
|
loadedSuccessfully(false)
|
|
113
113
|
}
|
|
114
114
|
}
|
package/src/loaders/api/api.js
CHANGED
|
@@ -207,7 +207,10 @@ export function setAPI (agentIdentifier, forceDrain) {
|
|
|
207
207
|
import(/* webpackChunkName: "async-api" */'./apiAsync').then(({ setAPI }) => {
|
|
208
208
|
setAPI(agentIdentifier)
|
|
209
209
|
drain(agentIdentifier, 'api')
|
|
210
|
-
}).catch(() =>
|
|
210
|
+
}).catch(() => {
|
|
211
|
+
warn('Downloading runtime APIs failed...')
|
|
212
|
+
drain(agentIdentifier, 'api', true)
|
|
213
|
+
})
|
|
211
214
|
}
|
|
212
215
|
|
|
213
216
|
return apiInterface
|