@newrelic/browser-agent 1.255.0 → 1.256.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/cjs/common/constants/env.cdn.js +2 -2
- package/dist/cjs/common/constants/env.npm.js +2 -2
- package/dist/cjs/common/constants/runtime.js +1 -1
- package/dist/cjs/common/harvest/harvest.js +1 -0
- package/dist/cjs/common/session/session-entity.js +2 -1
- package/dist/cjs/common/timer/interaction-timer.js +16 -2
- package/dist/cjs/common/timing/time-keeper.js +1 -2
- package/dist/cjs/features/jserrors/aggregate/index.js +16 -6
- package/dist/cjs/features/jserrors/instrument/index.js +8 -3
- package/dist/cjs/features/session_replay/aggregate/index.js +48 -29
- package/dist/cjs/features/session_replay/constants.js +2 -1
- package/dist/cjs/features/session_replay/instrument/index.js +9 -2
- package/dist/cjs/features/session_replay/shared/recorder-events.js +1 -9
- package/dist/cjs/features/session_replay/shared/recorder.js +22 -50
- package/dist/cjs/features/session_replay/shared/utils.js +12 -0
- package/dist/cjs/features/session_trace/aggregate/index.js +19 -22
- package/dist/cjs/loaders/api/api.js +7 -1
- package/dist/cjs/loaders/configure/configure.js +1 -0
- package/dist/esm/common/constants/env.cdn.js +2 -2
- package/dist/esm/common/constants/env.npm.js +2 -2
- package/dist/esm/common/constants/runtime.js +1 -1
- package/dist/esm/common/harvest/harvest.js +1 -0
- package/dist/esm/common/session/session-entity.js +2 -1
- package/dist/esm/common/timer/interaction-timer.js +16 -2
- package/dist/esm/common/timing/time-keeper.js +1 -3
- package/dist/esm/features/jserrors/aggregate/index.js +16 -6
- package/dist/esm/features/jserrors/instrument/index.js +8 -3
- package/dist/esm/features/session_replay/aggregate/index.js +48 -29
- package/dist/esm/features/session_replay/constants.js +2 -1
- package/dist/esm/features/session_replay/instrument/index.js +9 -2
- package/dist/esm/features/session_replay/shared/recorder-events.js +1 -9
- package/dist/esm/features/session_replay/shared/recorder.js +23 -51
- package/dist/esm/features/session_replay/shared/utils.js +11 -0
- package/dist/esm/features/session_trace/aggregate/index.js +19 -22
- package/dist/esm/loaders/api/api.js +7 -1
- package/dist/esm/loaders/configure/configure.js +1 -0
- package/dist/types/common/constants/runtime.d.ts.map +1 -1
- package/dist/types/common/session/session-entity.d.ts.map +1 -1
- package/dist/types/common/timer/interaction-timer.d.ts +2 -0
- package/dist/types/common/timer/interaction-timer.d.ts.map +1 -1
- package/dist/types/common/timing/time-keeper.d.ts.map +1 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts +2 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/jserrors/instrument/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +5 -2
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/constants.d.ts +1 -0
- package/dist/types/features/session_replay/constants.d.ts.map +1 -1
- package/dist/types/features/session_replay/instrument/index.d.ts +1 -0
- package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder-events.d.ts +0 -8
- package/dist/types/features/session_replay/shared/recorder-events.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder.d.ts +1 -17
- package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/utils.d.ts +8 -0
- package/dist/types/features/session_replay/shared/utils.d.ts.map +1 -1
- package/dist/types/features/session_trace/aggregate/index.d.ts +2 -1
- package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
- package/dist/types/loaders/api/api.d.ts.map +1 -1
- package/dist/types/loaders/configure/configure.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/common/constants/runtime.js +1 -1
- package/src/common/harvest/harvest.js +1 -1
- package/src/common/session/session-entity.js +2 -1
- package/src/common/timer/interaction-timer.js +17 -2
- package/src/common/timing/time-keeper.js +1 -3
- package/src/features/jserrors/aggregate/index.js +15 -6
- package/src/features/jserrors/instrument/index.js +9 -4
- package/src/features/session_replay/aggregate/index.js +43 -25
- package/src/features/session_replay/constants.js +2 -1
- package/src/features/session_replay/instrument/index.js +7 -2
- package/src/features/session_replay/shared/recorder-events.js +1 -6
- package/src/features/session_replay/shared/recorder.js +21 -27
- package/src/features/session_replay/shared/utils.js +12 -0
- package/src/features/session_trace/aggregate/index.js +18 -16
- package/src/loaders/api/api.js +10 -1
- package/src/loaders/configure/configure.js +1 -0
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { globalScope } from '../constants/runtime'
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Class used to adjust the timestamp of harvested data to New Relic server time. This
|
|
5
3
|
* is done by tracking the performance timings of the RUM call and applying a calculation
|
|
@@ -33,7 +31,7 @@ export class TimeKeeper {
|
|
|
33
31
|
#ready = false
|
|
34
32
|
|
|
35
33
|
constructor () {
|
|
36
|
-
this.#originTime =
|
|
34
|
+
this.#originTime = Date.now() - performance.now()
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
get ready () {
|
|
@@ -38,10 +38,13 @@ export class Aggregate extends AggregateBase {
|
|
|
38
38
|
this.bufferedErrorsUnderSpa = {}
|
|
39
39
|
this.currentBody = undefined
|
|
40
40
|
this.errorOnPage = false
|
|
41
|
+
this.replayAborted = false
|
|
41
42
|
|
|
42
43
|
// this will need to change to match whatever ee we use in the instrument
|
|
43
44
|
this.ee.on('interactionDone', (interaction, wasSaved) => this.onInteractionDone(interaction, wasSaved))
|
|
44
45
|
|
|
46
|
+
this.ee.on('REPLAY_ABORTED', () => { this.replayAborted = true })
|
|
47
|
+
|
|
45
48
|
register('err', (...args) => this.storeError(...args), this.featureName, this.ee)
|
|
46
49
|
register('ierr', (...args) => this.storeError(...args), this.featureName, this.ee)
|
|
47
50
|
register('softNavFlush', (interactionId, wasFinished, softNavAttrs) =>
|
|
@@ -78,9 +81,16 @@ export class Aggregate extends AggregateBase {
|
|
|
78
81
|
payload.qs.ri = releaseIds
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
if (body && body.err && body.err.length
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
if (body && body.err && body.err.length) {
|
|
85
|
+
if (this.replayAborted) {
|
|
86
|
+
body.err.forEach((e) => {
|
|
87
|
+
delete e.params?.hasReplay
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
if (!this.errorOnPage) {
|
|
91
|
+
payload.qs.pve = '1'
|
|
92
|
+
this.errorOnPage = true
|
|
93
|
+
}
|
|
84
94
|
}
|
|
85
95
|
return payload
|
|
86
96
|
}
|
|
@@ -133,7 +143,7 @@ export class Aggregate extends AggregateBase {
|
|
|
133
143
|
return canonicalStackString
|
|
134
144
|
}
|
|
135
145
|
|
|
136
|
-
storeError (err, time, internal, customAttributes) {
|
|
146
|
+
storeError (err, time, internal, customAttributes, hasReplay) {
|
|
137
147
|
// are we in an interaction
|
|
138
148
|
time = time || now()
|
|
139
149
|
const agentRuntime = getRuntime(this.agentIdentifier)
|
|
@@ -189,7 +199,7 @@ export class Aggregate extends AggregateBase {
|
|
|
189
199
|
this.pageviewReported[bucketHash] = true
|
|
190
200
|
}
|
|
191
201
|
|
|
192
|
-
if (
|
|
202
|
+
if (hasReplay && !this.replayAborted) params.hasReplay = hasReplay
|
|
193
203
|
params.firstOccurrenceTimestamp = this.observedAt[bucketHash]
|
|
194
204
|
params.timestamp = this.observedAt[bucketHash]
|
|
195
205
|
|
|
@@ -199,7 +209,6 @@ export class Aggregate extends AggregateBase {
|
|
|
199
209
|
// Trace sends the error in its payload, and both trace & replay simply listens for any error to occur.
|
|
200
210
|
const jsErrorEvent = [type, bucketHash, params, newMetrics, customAttributes]
|
|
201
211
|
handle('errorAgg', jsErrorEvent, undefined, FEATURE_NAMES.sessionTrace, this.ee)
|
|
202
|
-
handle('errorAgg', jsErrorEvent, undefined, FEATURE_NAMES.sessionReplay, this.ee)
|
|
203
212
|
// still send EE events for other features such as above, but stop this one from aggregating internal data
|
|
204
213
|
if (this.blocked) return
|
|
205
214
|
|
|
@@ -12,11 +12,13 @@ import { eventListenerOpts } from '../../../common/event-listener/event-listener
|
|
|
12
12
|
import { stringify } from '../../../common/util/stringify'
|
|
13
13
|
import { UncaughtError } from './uncaught-error'
|
|
14
14
|
import { now } from '../../../common/timing/now'
|
|
15
|
+
import { SR_EVENT_EMITTER_TYPES } from '../../session_replay/constants'
|
|
15
16
|
|
|
16
17
|
export class Instrument extends InstrumentBase {
|
|
17
18
|
static featureName = FEATURE_NAME
|
|
18
19
|
|
|
19
20
|
#seenErrors = new Set()
|
|
21
|
+
#replayRunning = false
|
|
20
22
|
|
|
21
23
|
constructor (agentIdentifier, aggregator, auto = true) {
|
|
22
24
|
super(agentIdentifier, aggregator, FEATURE_NAME, auto)
|
|
@@ -36,13 +38,16 @@ export class Instrument extends InstrumentBase {
|
|
|
36
38
|
|
|
37
39
|
this.ee.on('internal-error', (error) => {
|
|
38
40
|
if (!this.abortHandler) return
|
|
39
|
-
handle('ierr', [this.#castError(error), now(), true], undefined, FEATURE_NAMES.jserrors, this.ee)
|
|
41
|
+
handle('ierr', [this.#castError(error), now(), true, {}, this.#replayRunning], undefined, FEATURE_NAMES.jserrors, this.ee)
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
this.ee.on(SR_EVENT_EMITTER_TYPES.REPLAY_RUNNING, (isRunning) => {
|
|
45
|
+
this.#replayRunning = isRunning
|
|
40
46
|
})
|
|
41
47
|
|
|
42
48
|
globalScope.addEventListener('unhandledrejection', (promiseRejectionEvent) => {
|
|
43
49
|
if (!this.abortHandler) return
|
|
44
|
-
|
|
45
|
-
handle('err', [this.#castPromiseRejectionEvent(promiseRejectionEvent), now(), false, { unhandledPromiseRejection: 1 }], undefined, FEATURE_NAMES.jserrors, this.ee)
|
|
50
|
+
handle('err', [this.#castPromiseRejectionEvent(promiseRejectionEvent), now(), false, { unhandledPromiseRejection: 1 }, this.#replayRunning], undefined, FEATURE_NAMES.jserrors, this.ee)
|
|
46
51
|
}, eventListenerOpts(false, this.removeOnAbort?.signal))
|
|
47
52
|
|
|
48
53
|
globalScope.addEventListener('error', (errorEvent) => {
|
|
@@ -57,7 +62,7 @@ export class Instrument extends InstrumentBase {
|
|
|
57
62
|
return
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
handle('err', [this.#castErrorEvent(errorEvent), now()], undefined, FEATURE_NAMES.jserrors, this.ee)
|
|
65
|
+
handle('err', [this.#castErrorEvent(errorEvent), now(), false, {}, this.#replayRunning], undefined, FEATURE_NAMES.jserrors, this.ee)
|
|
61
66
|
}, eventListenerOpts(false, this.removeOnAbort?.signal))
|
|
62
67
|
|
|
63
68
|
this.abortHandler = this.#abort // we also use this as a flag to denote that the feature is active or on and handling errors
|
|
@@ -28,9 +28,12 @@ import { stringify } from '../../../common/util/stringify'
|
|
|
28
28
|
import { stylesheetEvaluator } from '../shared/stylesheet-evaluator'
|
|
29
29
|
import { deregisterDrain } from '../../../common/drain/drain'
|
|
30
30
|
import { now } from '../../../common/timing/now'
|
|
31
|
+
import { buildNRMetaNode } from '../shared/utils'
|
|
31
32
|
|
|
32
33
|
export class Aggregate extends AggregateBase {
|
|
33
34
|
static featureName = FEATURE_NAME
|
|
35
|
+
mode = MODE.OFF
|
|
36
|
+
|
|
34
37
|
// pass the recorder into the aggregator
|
|
35
38
|
constructor (agentIdentifier, aggregator, args) {
|
|
36
39
|
super(agentIdentifier, aggregator, FEATURE_NAME)
|
|
@@ -44,9 +47,6 @@ export class Aggregate extends AggregateBase {
|
|
|
44
47
|
this.gzipper = undefined
|
|
45
48
|
/** populated with the u8 string lib async */
|
|
46
49
|
this.u8 = undefined
|
|
47
|
-
/** the mode to start in. Defaults to off */
|
|
48
|
-
const { session } = getRuntime(this.agentIdentifier)
|
|
49
|
-
this.mode = session.state.sessionReplayMode || MODE.OFF
|
|
50
50
|
|
|
51
51
|
/** set by BCS response */
|
|
52
52
|
this.entitled = false
|
|
@@ -54,13 +54,13 @@ export class Aggregate extends AggregateBase {
|
|
|
54
54
|
this.timeKeeper = undefined
|
|
55
55
|
|
|
56
56
|
this.recorder = args?.recorder
|
|
57
|
-
|
|
57
|
+
this.preloaded = !!this.recorder
|
|
58
|
+
this.errorNoticed = args?.errorNoticed || false
|
|
58
59
|
|
|
59
60
|
handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/Enabled'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
60
61
|
|
|
61
62
|
// 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.
|
|
62
63
|
this.ee.on(SESSION_EVENTS.RESET, () => {
|
|
63
|
-
this.scheduler.runHarvest()
|
|
64
64
|
this.abort(ABORT_REASONS.RESET)
|
|
65
65
|
})
|
|
66
66
|
|
|
@@ -104,17 +104,6 @@ export class Aggregate extends AggregateBase {
|
|
|
104
104
|
this.forceStop(this.mode !== MODE.ERROR)
|
|
105
105
|
}, this.featureName, this.ee)
|
|
106
106
|
|
|
107
|
-
// Wait for an error to be reported. This currently is wrapped around the "Error" feature. This is a feature-feature dependency.
|
|
108
|
-
// This was to ensure that all errors, including those on the page before load and those handled with "noticeError" are accounted for. Needs evalulation
|
|
109
|
-
registerHandler('errorAgg', (e) => {
|
|
110
|
-
this.errorNoticed = true
|
|
111
|
-
if (this.recorder) this.recorder.currentBufferTarget.hasError = true
|
|
112
|
-
// run once
|
|
113
|
-
if (this.mode === MODE.ERROR && globalScope?.document.visibilityState === 'visible') {
|
|
114
|
-
this.switchToFull()
|
|
115
|
-
}
|
|
116
|
-
}, this.featureName, this.ee)
|
|
117
|
-
|
|
118
107
|
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')
|
|
119
108
|
|
|
120
109
|
this.waitForFlags(['sr']).then(([flagOn]) => {
|
|
@@ -150,6 +139,14 @@ export class Aggregate extends AggregateBase {
|
|
|
150
139
|
handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/ErrorSamplingRate/Value', error_sampling_rate], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
151
140
|
}
|
|
152
141
|
|
|
142
|
+
handleError (e) {
|
|
143
|
+
if (this.recorder) this.recorder.currentBufferTarget.hasError = true
|
|
144
|
+
// run once
|
|
145
|
+
if (this.mode === MODE.ERROR && globalScope?.document.visibilityState === 'visible') {
|
|
146
|
+
this.switchToFull()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
153
150
|
switchToFull () {
|
|
154
151
|
this.mode = MODE.FULL
|
|
155
152
|
// if the error was noticed AFTER the recorder was already imported....
|
|
@@ -210,12 +207,13 @@ export class Aggregate extends AggregateBase {
|
|
|
210
207
|
} catch (err) {
|
|
211
208
|
return this.abort(ABORT_REASONS.IMPORT)
|
|
212
209
|
}
|
|
210
|
+
} else {
|
|
211
|
+
this.recorder.parent = this
|
|
213
212
|
}
|
|
214
213
|
|
|
215
214
|
// If an error was noticed before the mode could be set (like in the early lifecycle of the page), immediately set to FULL mode
|
|
216
|
-
if (this.mode === MODE.ERROR && this.errorNoticed)
|
|
217
|
-
|
|
218
|
-
}
|
|
215
|
+
if (this.mode === MODE.ERROR && this.errorNoticed) this.mode = MODE.FULL
|
|
216
|
+
if (!this.preloaded) this.ee.on('err', e => this.handleError(e))
|
|
219
217
|
|
|
220
218
|
// FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
|
|
221
219
|
// ERROR mode will do this until an error is thrown, and then switch into FULL mode.
|
|
@@ -255,16 +253,29 @@ export class Aggregate extends AggregateBase {
|
|
|
255
253
|
return
|
|
256
254
|
}
|
|
257
255
|
|
|
256
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, ['SessionReplay/Harvest/Attempts'], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
257
|
+
|
|
258
258
|
let len = 0
|
|
259
259
|
if (!!this.gzipper && !!this.u8) {
|
|
260
|
-
payload.body = this.gzipper(this.u8(`[${payload.body.map(e => {
|
|
261
|
-
if (e.__serialized) return
|
|
262
|
-
|
|
260
|
+
payload.body = this.gzipper(this.u8(`[${payload.body.map(({ __serialized, ...e }) => {
|
|
261
|
+
if (e.__newrelic && __serialized) return __serialized
|
|
262
|
+
const output = { ...e }
|
|
263
|
+
if (!output.__newrelic) {
|
|
264
|
+
output.__newrelic = buildNRMetaNode(e.timestamp, this.timeKeeper)
|
|
265
|
+
output.timestamp = this.timeKeeper.correctAbsoluteTimestamp(e.timestamp)
|
|
266
|
+
}
|
|
267
|
+
return stringify(output)
|
|
263
268
|
}).join(',')}]`))
|
|
264
269
|
len = payload.body.length
|
|
265
270
|
this.scheduler.opts.gzip = true
|
|
266
271
|
} else {
|
|
267
|
-
payload.body = payload.body.map(({ __serialized, ...node }) =>
|
|
272
|
+
payload.body = payload.body.map(({ __serialized, ...node }) => {
|
|
273
|
+
if (node.__newrelic) return node
|
|
274
|
+
const output = { ...node }
|
|
275
|
+
output.__newrelic = buildNRMetaNode(node.timestamp, this.timeKeeper)
|
|
276
|
+
output.timestamp = this.timeKeeper.correctAbsoluteTimestamp(node.timestamp)
|
|
277
|
+
return output
|
|
278
|
+
})
|
|
268
279
|
len = stringify(payload.body).length
|
|
269
280
|
this.scheduler.opts.gzip = false
|
|
270
281
|
}
|
|
@@ -281,6 +292,12 @@ export class Aggregate extends AggregateBase {
|
|
|
281
292
|
return [payload]
|
|
282
293
|
}
|
|
283
294
|
|
|
295
|
+
getCorrectedTimestamp (node) {
|
|
296
|
+
if (!node.timestamp) return
|
|
297
|
+
if (node.__newrelic) return node.timestamp
|
|
298
|
+
return this.timeKeeper.correctAbsoluteTimestamp(node.timestamp)
|
|
299
|
+
}
|
|
300
|
+
|
|
284
301
|
getHarvestContents (recorderEvents) {
|
|
285
302
|
recorderEvents ??= this.recorder.getEvents()
|
|
286
303
|
let events = recorderEvents.events
|
|
@@ -308,8 +325,8 @@ export class Aggregate extends AggregateBase {
|
|
|
308
325
|
|
|
309
326
|
const relativeNow = now()
|
|
310
327
|
|
|
311
|
-
const firstEventTimestamp = events[0]
|
|
312
|
-
const lastEventTimestamp = events[events.length - 1]
|
|
328
|
+
const firstEventTimestamp = this.getCorrectedTimestamp(events[0]) // from rrweb node
|
|
329
|
+
const lastEventTimestamp = this.getCorrectedTimestamp(events[events.length - 1]) // from rrweb node
|
|
313
330
|
const firstTimestamp = firstEventTimestamp || this.timeKeeper.correctAbsoluteTimestamp(recorderEvents.cycleTimestamp) // from rrweb node || from when the harvest cycle started
|
|
314
331
|
const lastTimestamp = lastEventTimestamp || this.timeKeeper.convertRelativeTimestamp(relativeNow)
|
|
315
332
|
|
|
@@ -341,6 +358,7 @@ export class Aggregate extends AggregateBase {
|
|
|
341
358
|
invalidStylesheetsDetected: stylesheetEvaluator.invalidStylesheetsDetected,
|
|
342
359
|
inlinedAllStylesheets: recorderEvents.inlinedAllStylesheets,
|
|
343
360
|
'rrweb.version': RRWEB_VERSION,
|
|
361
|
+
'payload.type': recorderEvents.type,
|
|
344
362
|
// customer-defined data should go last so that if it exceeds the query param padding limit it will be truncated instead of important attrs
|
|
345
363
|
...(endUserId && { 'enduser.id': endUserId })
|
|
346
364
|
// The Query Param is being arbitrarily limited in length here. It is also applied when estimating the size of the payload in getPayloadSize()
|
|
@@ -24,6 +24,11 @@ export class Instrument extends InstrumentBase {
|
|
|
24
24
|
} catch (err) { }
|
|
25
25
|
|
|
26
26
|
if (this.#canPreloadRecorder(session)) {
|
|
27
|
+
/** If this is preloaded, set up a buffer, if not, later when sampling we will set up a .on for live events */
|
|
28
|
+
this.ee.on('err', (e) => {
|
|
29
|
+
this.errorNoticed = true
|
|
30
|
+
if (this.featAggregate) this.featAggregate.handleError()
|
|
31
|
+
})
|
|
27
32
|
this.#startRecording(session?.sessionReplayMode)
|
|
28
33
|
} else {
|
|
29
34
|
this.importAggregator()
|
|
@@ -45,9 +50,9 @@ export class Instrument extends InstrumentBase {
|
|
|
45
50
|
|
|
46
51
|
async #startRecording (mode) {
|
|
47
52
|
const { Recorder } = (await import(/* webpackChunkName: "recorder" */'../shared/recorder'))
|
|
48
|
-
this.recorder = new Recorder({ mode, agentIdentifier: this.agentIdentifier })
|
|
53
|
+
this.recorder = new Recorder({ mode, agentIdentifier: this.agentIdentifier, ee: this.ee })
|
|
49
54
|
this.recorder.startRecording()
|
|
50
55
|
this.abortHandler = this.recorder.stopRecording
|
|
51
|
-
this.importAggregator({ recorder: this.recorder })
|
|
56
|
+
this.importAggregator({ recorder: this.recorder, errorNoticed: this.errorNoticed })
|
|
52
57
|
}
|
|
53
58
|
}
|
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
export class RecorderEvents {
|
|
2
|
-
constructor (
|
|
2
|
+
constructor () {
|
|
3
3
|
/** The buffer to hold recorder event nodes */
|
|
4
4
|
this.events = []
|
|
5
5
|
/** Payload metadata -- Should indicate when a replay blob started recording. Resets each time a harvest occurs.
|
|
6
6
|
* cycle timestamps are used as fallbacks if event timestamps cannot be used
|
|
7
7
|
*/
|
|
8
8
|
this.cycleTimestamp = Date.now()
|
|
9
|
-
/** Payload metadata -- Whether timestamps can be corrected, defaults as false, can be set to true if timekeeper is present at init time. Used to determine
|
|
10
|
-
* if harvest needs to re-loop through nodes and correct them before sending. Ideal behavior is to correct them as they flow into the recorder
|
|
11
|
-
* to prevent re-looping, but is not always possible since the timekeeper is not set until after page load and the recorder can be preloaded.
|
|
12
|
-
*/
|
|
13
|
-
this.canCorrectTimestamps = !!canCorrectTimestamps
|
|
14
9
|
/** A value which increments with every new mutation node reported. Resets after a harvest is sent */
|
|
15
10
|
this.payloadBytesEstimation = 0
|
|
16
11
|
/** Payload metadata -- Should indicate that the payload being sent has a full DOM snapshot. This can happen
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { record as recorder } from 'rrweb'
|
|
2
2
|
import { stringify } from '../../../common/util/stringify'
|
|
3
|
-
import { AVG_COMPRESSION, CHECKOUT_MS, IDEAL_PAYLOAD_SIZE, QUERY_PARAM_PADDING, RRWEB_EVENT_TYPES } from '../constants'
|
|
3
|
+
import { AVG_COMPRESSION, CHECKOUT_MS, IDEAL_PAYLOAD_SIZE, QUERY_PARAM_PADDING, RRWEB_EVENT_TYPES, SR_EVENT_EMITTER_TYPES } from '../constants'
|
|
4
4
|
import { getConfigurationValue } from '../../../common/config/config'
|
|
5
5
|
import { RecorderEvents } from './recorder-events'
|
|
6
6
|
import { MODE } from '../../../common/session/constants'
|
|
@@ -8,6 +8,7 @@ import { stylesheetEvaluator } from './stylesheet-evaluator'
|
|
|
8
8
|
import { handle } from '../../../common/event-emitter/handle'
|
|
9
9
|
import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
|
|
10
10
|
import { FEATURE_NAMES } from '../../../loaders/features/features'
|
|
11
|
+
import { buildNRMetaNode } from './utils'
|
|
11
12
|
|
|
12
13
|
export class Recorder {
|
|
13
14
|
/** Each page mutation or event will be stored (raw) in this array. This array will be cleared on each harvest */
|
|
@@ -20,9 +21,9 @@ export class Recorder {
|
|
|
20
21
|
#fixing = false
|
|
21
22
|
|
|
22
23
|
constructor (parent) {
|
|
23
|
-
this.#events = new RecorderEvents(
|
|
24
|
-
this.#backloggedEvents = new RecorderEvents(
|
|
25
|
-
this.#preloaded = [new RecorderEvents(
|
|
24
|
+
this.#events = new RecorderEvents()
|
|
25
|
+
this.#backloggedEvents = new RecorderEvents()
|
|
26
|
+
this.#preloaded = [new RecorderEvents()]
|
|
26
27
|
/** True when actively recording, false when paused or stopped */
|
|
27
28
|
this.recording = false
|
|
28
29
|
/** The pointer to the current bucket holding rrweb events */
|
|
@@ -40,14 +41,9 @@ export class Recorder {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
getEvents () {
|
|
43
|
-
if (this.#preloaded[0]?.events.length) {
|
|
44
|
-
const preloadedEvents = this.returnCorrectTimestamps(this.#preloaded[0])
|
|
45
|
-
return { ...this.#preloaded[0], events: preloadedEvents, type: 'preloaded' }
|
|
46
|
-
}
|
|
47
|
-
const backloggedEvents = this.returnCorrectTimestamps(this.#backloggedEvents)
|
|
48
|
-
const events = this.returnCorrectTimestamps(this.#events)
|
|
44
|
+
if (this.#preloaded[0]?.events.length) return { ...this.#preloaded[0], type: 'preloaded' }
|
|
49
45
|
return {
|
|
50
|
-
events: [...backloggedEvents, ...events].filter(x => x),
|
|
46
|
+
events: [...this.#backloggedEvents.events, ...this.#events.events].filter(x => x),
|
|
51
47
|
type: 'standard',
|
|
52
48
|
cycleTimestamp: Math.min(this.#backloggedEvents.cycleTimestamp, this.#events.cycleTimestamp),
|
|
53
49
|
payloadBytesEstimation: this.#backloggedEvents.payloadBytesEstimation + this.#events.payloadBytesEstimation,
|
|
@@ -58,30 +54,22 @@ export class Recorder {
|
|
|
58
54
|
}
|
|
59
55
|
}
|
|
60
56
|
|
|
61
|
-
/**
|
|
62
|
-
* Returns time-corrected events. If the events were correctable from the beginning, this correction will have already been applied.
|
|
63
|
-
* @param {SessionReplayEvent[]} events The array of buffered SR nodes
|
|
64
|
-
* @returns {CorrectedSessionReplayEvent[]}
|
|
65
|
-
*/
|
|
66
|
-
returnCorrectTimestamps (events) {
|
|
67
|
-
if (!this.parent.timeKeeper?.ready) return events.events
|
|
68
|
-
return events.canCorrectTimestamps
|
|
69
|
-
? events.events
|
|
70
|
-
: events.events.map(({ __serialized, timestamp, ...e }) => ({ timestamp: this.parent.timeKeeper.correctAbsoluteTimestamp(timestamp), ...e }))
|
|
71
|
-
}
|
|
72
|
-
|
|
73
57
|
/** Clears the buffer (this.#events), and resets all payload metadata properties */
|
|
74
58
|
clearBuffer () {
|
|
75
59
|
if (this.#preloaded[0]?.events.length) this.#preloaded.shift()
|
|
76
60
|
else if (this.parent.mode === MODE.ERROR) this.#backloggedEvents = this.#events
|
|
77
|
-
else this.#backloggedEvents = new RecorderEvents(
|
|
78
|
-
this.#events = new RecorderEvents(
|
|
61
|
+
else this.#backloggedEvents = new RecorderEvents()
|
|
62
|
+
this.#events = new RecorderEvents()
|
|
79
63
|
}
|
|
80
64
|
|
|
81
65
|
/** Begin recording using configured recording lib */
|
|
82
66
|
startRecording () {
|
|
83
67
|
this.recording = true
|
|
84
68
|
const { block_class, ignore_class, mask_text_class, block_selector, mask_input_options, mask_text_selector, mask_all_inputs, inline_stylesheet, inline_images, collect_fonts } = getConfigurationValue(this.parent.agentIdentifier, 'session_replay')
|
|
69
|
+
const customMasker = (text, element) => {
|
|
70
|
+
if (element?.type?.toLowerCase() !== 'password' && (element?.dataset.nrUnmask !== undefined || element?.classList.contains('nr-unmask'))) return text
|
|
71
|
+
return '*'.repeat(text.length)
|
|
72
|
+
}
|
|
85
73
|
// set up rrweb configurations for maximum privacy --
|
|
86
74
|
// https://newrelic.atlassian.net/wiki/spaces/O11Y/pages/2792293280/2023+02+28+Browser+-+Session+Replay#Configuration-options
|
|
87
75
|
const stop = recorder({
|
|
@@ -92,15 +80,20 @@ export class Recorder {
|
|
|
92
80
|
blockSelector: block_selector,
|
|
93
81
|
maskInputOptions: mask_input_options,
|
|
94
82
|
maskTextSelector: mask_text_selector,
|
|
83
|
+
maskTextFn: customMasker,
|
|
95
84
|
maskAllInputs: mask_all_inputs,
|
|
85
|
+
maskInputFn: customMasker,
|
|
96
86
|
inlineStylesheet: inline_stylesheet,
|
|
97
87
|
inlineImages: inline_images,
|
|
98
88
|
collectFonts: collect_fonts,
|
|
99
89
|
checkoutEveryNms: CHECKOUT_MS[this.parent.mode]
|
|
100
90
|
})
|
|
101
91
|
|
|
92
|
+
this.parent.ee.emit(SR_EVENT_EMITTER_TYPES.REPLAY_RUNNING, [true, this.parent.mode])
|
|
93
|
+
|
|
102
94
|
this.stopRecording = () => {
|
|
103
95
|
this.recording = false
|
|
96
|
+
this.parent.ee.emit(SR_EVENT_EMITTER_TYPES.REPLAY_RUNNING, [false, this.parent.mode])
|
|
104
97
|
stop()
|
|
105
98
|
}
|
|
106
99
|
}
|
|
@@ -148,7 +141,8 @@ export class Recorder {
|
|
|
148
141
|
|
|
149
142
|
if (this.parent.blocked) return
|
|
150
143
|
|
|
151
|
-
if (this.
|
|
144
|
+
if (this.parent.timeKeeper?.ready && !event.__newrelic) {
|
|
145
|
+
event.__newrelic = buildNRMetaNode(event.timestamp, this.parent.timeKeeper)
|
|
152
146
|
event.timestamp = this.parent.timeKeeper.correctAbsoluteTimestamp(event.timestamp)
|
|
153
147
|
}
|
|
154
148
|
event.__serialized = stringify(event)
|
|
@@ -184,7 +178,7 @@ export class Recorder {
|
|
|
184
178
|
this.parent.scheduler.runHarvest()
|
|
185
179
|
} else {
|
|
186
180
|
// we are still in "preload" and it triggered a "stop point". Make a new set, which will get pointed at on next cycle
|
|
187
|
-
this.#preloaded.push(new RecorderEvents(
|
|
181
|
+
this.#preloaded.push(new RecorderEvents())
|
|
188
182
|
}
|
|
189
183
|
}
|
|
190
184
|
}
|
|
@@ -17,3 +17,15 @@ export function canImportReplayAgg (agentId, sessionMgr) {
|
|
|
17
17
|
if (!hasReplayPrerequisite(agentId)) return false
|
|
18
18
|
return !!sessionMgr?.isNew || !!sessionMgr?.state.sessionReplayMode // Session Replay should only try to run if already running from a previous page, or at the beginning of a session
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
export function buildNRMetaNode (timestamp, timeKeeper) {
|
|
22
|
+
const correctedTimestamp = timeKeeper.correctAbsoluteTimestamp(timestamp)
|
|
23
|
+
return {
|
|
24
|
+
originalTimestamp: timestamp,
|
|
25
|
+
correctedTimestamp,
|
|
26
|
+
timestampDiff: timestamp - correctedTimestamp,
|
|
27
|
+
timeKeeperOriginTime: timeKeeper.originTime,
|
|
28
|
+
timeKeeperCorrectedOriginTime: timeKeeper.correctedOriginTime,
|
|
29
|
+
timeKeeperDiff: Math.floor(timeKeeper.originTime - timeKeeper.correctedOriginTime)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -43,10 +43,11 @@ export class Aggregate extends AggregateBase {
|
|
|
43
43
|
if (!this.agentRuntime.xhrWrappable) return
|
|
44
44
|
|
|
45
45
|
this.resourceObserver = argsObj?.resourceObserver // undefined if observer couldn't be created
|
|
46
|
-
this.ptid =
|
|
46
|
+
this.ptid = this.agentRuntime.ptid
|
|
47
47
|
this.trace = {}
|
|
48
48
|
this.nodeCount = 0
|
|
49
49
|
this.sentTrace = null
|
|
50
|
+
this.everSent = false
|
|
50
51
|
this.harvestTimeSeconds = getConfigurationValue(agentIdentifier, 'session_trace.harvestTimeSeconds') || 10
|
|
51
52
|
this.maxNodesPerHarvest = getConfigurationValue(agentIdentifier, 'session_trace.maxNodesPerHarvest') || 1000
|
|
52
53
|
/**
|
|
@@ -99,7 +100,7 @@ export class Aggregate extends AggregateBase {
|
|
|
99
100
|
|
|
100
101
|
if (prevMode === MODE.ERROR && this.#scheduler) {
|
|
101
102
|
this.trimSTNs(ERROR_MODE_SECONDS_WINDOW) // up until now, Trace would've been just buffering nodes up to max, which needs to be trimmed to last X seconds
|
|
102
|
-
this.#scheduler.runHarvest({
|
|
103
|
+
this.#scheduler.runHarvest({})
|
|
103
104
|
} else {
|
|
104
105
|
controlTraceOp(MODE.FULL)
|
|
105
106
|
}
|
|
@@ -119,7 +120,7 @@ export class Aggregate extends AggregateBase {
|
|
|
119
120
|
const stopTracePerm = () => {
|
|
120
121
|
if (sessionEntity.state.sessionTraceMode !== MODE.OFF) sessionEntity.write({ sessionTraceMode: MODE.OFF })
|
|
121
122
|
operationalGate.permanentlyDecide(false)
|
|
122
|
-
if (mostRecentModeKnown === MODE.FULL) this.#scheduler?.runHarvest() // allow queued nodes (past opGate) to final harvest, unless they were buffered in other modes
|
|
123
|
+
if (mostRecentModeKnown === MODE.FULL) this.#scheduler?.runHarvest({}) // allow queued nodes (past opGate) to final harvest, unless they were buffered in other modes
|
|
123
124
|
this.#scheduler?.stopTimer(true) // the 'true' arg here will forcibly block any future call to runHarvest, so the last runHarvest above must be prior
|
|
124
125
|
this.#scheduler = null
|
|
125
126
|
}
|
|
@@ -138,7 +139,7 @@ export class Aggregate extends AggregateBase {
|
|
|
138
139
|
this.ee.on(SESSION_EVENTS.RESUME, () => {
|
|
139
140
|
const updatedTraceMode = sessionEntity.state.sessionTraceMode
|
|
140
141
|
if (updatedTraceMode === MODE.OFF) stopTracePerm()
|
|
141
|
-
else if (updatedTraceMode === MODE.FULL && this.#scheduler && !this.#scheduler.started) this.#scheduler.runHarvest({
|
|
142
|
+
else if (updatedTraceMode === MODE.FULL && this.#scheduler && !this.#scheduler.started) this.#scheduler.runHarvest({})
|
|
142
143
|
mostRecentModeKnown = updatedTraceMode
|
|
143
144
|
})
|
|
144
145
|
this.ee.on(SESSION_EVENTS.PAUSE, () => { mostRecentModeKnown = sessionEntity.state.sessionTraceMode })
|
|
@@ -189,13 +190,12 @@ export class Aggregate extends AggregateBase {
|
|
|
189
190
|
retryDelay: this.harvestTimeSeconds
|
|
190
191
|
}, this)
|
|
191
192
|
this.#scheduler.harvest.on('resources', this.#prepareHarvest.bind(this))
|
|
192
|
-
if (dontStartHarvestYet === false) this.#scheduler.runHarvest({
|
|
193
|
+
if (dontStartHarvestYet === false) this.#scheduler.runHarvest({}) // sends first stn harvest immediately
|
|
193
194
|
startupBuffer.decide(true) // signal to ALLOW & process data in EE's buffer into internal nodes queued for next harvest
|
|
194
195
|
}
|
|
195
196
|
|
|
196
197
|
#onHarvestFinished (result) {
|
|
197
|
-
if (result.sent && result.
|
|
198
|
-
this.agentRuntime.ptid = this.ptid = result.responseText
|
|
198
|
+
if (result.sent && !result.failed && !this.#scheduler.started) { // continue interval harvest only after first call
|
|
199
199
|
this.#scheduler.startTimer(this.harvestTimeSeconds)
|
|
200
200
|
}
|
|
201
201
|
|
|
@@ -212,15 +212,18 @@ export class Aggregate extends AggregateBase {
|
|
|
212
212
|
|
|
213
213
|
#prepareHarvest (options) {
|
|
214
214
|
if (this.isStandalone) {
|
|
215
|
-
if (this.
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
215
|
+
if (this.#scheduler.started) {
|
|
216
|
+
if (now() >= MAX_TRACE_DURATION) {
|
|
217
|
+
// Perform a final harvest once we hit or exceed the max session trace time
|
|
218
|
+
options.isFinalHarvest = true
|
|
219
|
+
this.operationalGate.permanentlyDecide(false)
|
|
220
|
+
this.#scheduler.stopTimer(true)
|
|
221
|
+
} else if (this.nodeCount <= REQ_THRESHOLD_TO_SEND && !options.isFinalHarvest) {
|
|
222
|
+
// Only harvest when more than some threshold of nodes are pending, after the very first harvest, with the exception of the last outgoing harvest.
|
|
223
|
+
return
|
|
224
|
+
}
|
|
223
225
|
}
|
|
226
|
+
// else, we must be on the very first harvest (standalone mode), so go to next square
|
|
224
227
|
} else {
|
|
225
228
|
// -- *cli May '26 - Update: Not rate limiting backgrounded pages either for now.
|
|
226
229
|
// if (this.ptid && document.visibilityState === 'hidden' && this.nodeCount <= REQ_THRESHOLD_TO_SEND) return
|
|
@@ -231,7 +234,6 @@ export class Aggregate extends AggregateBase {
|
|
|
231
234
|
if (currentMode === MODE.OFF && Object.keys(this.trace).length === 0) return
|
|
232
235
|
if (currentMode === MODE.ERROR) return // Trace in this mode should never be harvesting, even on unload
|
|
233
236
|
}
|
|
234
|
-
|
|
235
237
|
return this.takeSTNs(options.retry)
|
|
236
238
|
}
|
|
237
239
|
|
package/src/loaders/api/api.js
CHANGED
|
@@ -15,6 +15,7 @@ import { gosCDN } from '../../common/window/nreum'
|
|
|
15
15
|
import { apiMethods, asyncApiMethods } from './api-methods'
|
|
16
16
|
import { SR_EVENT_EMITTER_TYPES } from '../../features/session_replay/constants'
|
|
17
17
|
import { now } from '../../common/timing/now'
|
|
18
|
+
import { MODE } from '../../common/session/constants'
|
|
18
19
|
|
|
19
20
|
export function setTopLevelCallers () {
|
|
20
21
|
const nr = gosCDN()
|
|
@@ -33,12 +34,20 @@ export function setTopLevelCallers () {
|
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
const replayRunning = {}
|
|
38
|
+
|
|
36
39
|
export function setAPI (agentIdentifier, forceDrain, runSoftNavOverSpa = false) {
|
|
37
40
|
if (!forceDrain) registerDrain(agentIdentifier, 'api')
|
|
38
41
|
const apiInterface = {}
|
|
39
42
|
var instanceEE = ee.get(agentIdentifier)
|
|
40
43
|
var tracerEE = instanceEE.get('tracer')
|
|
41
44
|
|
|
45
|
+
replayRunning[agentIdentifier] = MODE.OFF
|
|
46
|
+
|
|
47
|
+
instanceEE.on(SR_EVENT_EMITTER_TYPES.REPLAY_RUNNING, (isRunning) => {
|
|
48
|
+
replayRunning[agentIdentifier] = isRunning
|
|
49
|
+
})
|
|
50
|
+
|
|
42
51
|
var prefix = 'api-'
|
|
43
52
|
var spaPrefix = prefix + 'ixn-'
|
|
44
53
|
|
|
@@ -184,7 +193,7 @@ export function setAPI (agentIdentifier, forceDrain, runSoftNavOverSpa = false)
|
|
|
184
193
|
apiInterface.noticeError = function (err, customAttributes) {
|
|
185
194
|
if (typeof err === 'string') err = new Error(err)
|
|
186
195
|
handle(SUPPORTABILITY_METRIC_CHANNEL, ['API/noticeError/called'], undefined, FEATURE_NAMES.metrics, instanceEE)
|
|
187
|
-
handle('err', [err, now(), false, customAttributes], undefined, FEATURE_NAMES.jserrors, instanceEE)
|
|
196
|
+
handle('err', [err, now(), false, customAttributes, !!replayRunning[agentIdentifier]], undefined, FEATURE_NAMES.jserrors, instanceEE)
|
|
188
197
|
}
|
|
189
198
|
|
|
190
199
|
// theres no window.load event on non-browser scopes, lazy load immediately
|
|
@@ -52,6 +52,7 @@ export function configure (agent, opts = {}, loaderType, forceDrain) {
|
|
|
52
52
|
...(updatedInit.ajax.deny_list || []),
|
|
53
53
|
...(updatedInit.ajax.block_internal ? internalTrafficList : [])
|
|
54
54
|
]
|
|
55
|
+
runtime.ptid = agent.agentIdentifier
|
|
55
56
|
setRuntime(agent.agentIdentifier, runtime)
|
|
56
57
|
|
|
57
58
|
if (agent.api === undefined) agent.api = setAPI(agent.agentIdentifier, forceDrain, agent.runSoftNavOverSpa)
|