@newrelic/browser-agent 1.297.0 → 1.297.1-rc.1
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 +7 -0
- package/README.md +3 -0
- package/dist/cjs/common/constants/agent-constants.js +3 -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/harvest/types.js +0 -1
- package/dist/cjs/common/wrap/wrap-function.js +9 -4
- package/dist/cjs/features/ajax/aggregate/index.js +10 -2
- package/dist/cjs/features/ajax/instrument/index.js +1 -0
- package/dist/cjs/features/jserrors/aggregate/index.js +9 -4
- package/dist/cjs/features/session_replay/aggregate/index.js +18 -21
- package/dist/cjs/features/session_replay/constants.js +5 -1
- package/dist/cjs/features/session_replay/instrument/index.js +36 -50
- package/dist/cjs/features/session_replay/shared/recorder.js +47 -25
- package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +11 -3
- package/dist/cjs/features/soft_navigations/aggregate/index.js +38 -14
- package/dist/cjs/features/soft_navigations/aggregate/interaction.js +34 -20
- package/dist/cjs/features/soft_navigations/constants.js +8 -4
- package/dist/cjs/features/soft_navigations/instrument/index.js +9 -6
- package/dist/cjs/features/utils/instrument-base.js +6 -2
- package/dist/cjs/loaders/api/interaction-types.js +0 -1
- package/dist/cjs/loaders/micro-agent.js +5 -2
- package/dist/esm/common/constants/agent-constants.js +2 -1
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/harvest/types.js +0 -1
- package/dist/esm/common/wrap/wrap-function.js +9 -4
- package/dist/esm/features/ajax/aggregate/index.js +10 -2
- package/dist/esm/features/ajax/instrument/index.js +1 -0
- package/dist/esm/features/jserrors/aggregate/index.js +9 -4
- package/dist/esm/features/session_replay/aggregate/index.js +18 -21
- package/dist/esm/features/session_replay/constants.js +5 -1
- package/dist/esm/features/session_replay/instrument/index.js +36 -50
- package/dist/esm/features/session_replay/shared/recorder.js +48 -26
- package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +11 -3
- package/dist/esm/features/soft_navigations/aggregate/index.js +39 -15
- package/dist/esm/features/soft_navigations/aggregate/interaction.js +35 -21
- package/dist/esm/features/soft_navigations/constants.js +7 -3
- package/dist/esm/features/soft_navigations/instrument/index.js +10 -7
- package/dist/esm/features/utils/instrument-base.js +6 -2
- package/dist/esm/loaders/api/interaction-types.js +0 -1
- package/dist/esm/loaders/micro-agent.js +5 -2
- package/dist/types/common/constants/agent-constants.d.ts +1 -0
- package/dist/types/common/constants/agent-constants.d.ts.map +1 -1
- package/dist/types/common/harvest/types.d.ts.map +1 -1
- package/dist/types/common/wrap/wrap-function.d.ts.map +1 -1
- package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts +1 -1
- package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +9 -2
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/constants.d.ts +4 -0
- package/dist/types/features/session_replay/instrument/index.d.ts +7 -0
- package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder.d.ts +10 -4
- package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +2 -1
- package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/index.d.ts +1 -1
- package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/aggregate/interaction.d.ts +6 -3
- package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/constants.d.ts +4 -0
- package/dist/types/features/soft_navigations/constants.d.ts.map +1 -1
- package/dist/types/features/soft_navigations/instrument/index.d.ts.map +1 -1
- package/dist/types/features/utils/instrument-base.d.ts +2 -1
- package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
- package/dist/types/loaders/api/interaction-types.d.ts.map +1 -1
- package/dist/types/loaders/micro-agent.d.ts +5 -2
- package/dist/types/loaders/micro-agent.d.ts.map +1 -1
- package/package.json +2 -6
- package/src/common/constants/agent-constants.js +1 -0
- package/src/common/harvest/types.js +0 -1
- package/src/common/wrap/wrap-function.js +9 -4
- package/src/features/ajax/aggregate/index.js +10 -2
- package/src/features/ajax/instrument/index.js +1 -0
- package/src/features/jserrors/aggregate/index.js +10 -6
- package/src/features/session_replay/aggregate/index.js +18 -19
- package/src/features/session_replay/constants.js +5 -1
- package/src/features/session_replay/instrument/index.js +39 -40
- package/src/features/session_replay/shared/recorder.js +53 -26
- package/src/features/soft_navigations/aggregate/ajax-node.js +8 -4
- package/src/features/soft_navigations/aggregate/index.js +39 -15
- package/src/features/soft_navigations/aggregate/interaction.js +33 -19
- package/src/features/soft_navigations/constants.js +5 -2
- package/src/features/soft_navigations/instrument/index.js +9 -8
- package/src/features/utils/instrument-base.js +7 -2
- package/src/loaders/api/interaction-types.js +0 -1
- package/src/loaders/micro-agent.js +5 -2
|
@@ -11,6 +11,7 @@ import { ee } from '../event-emitter/contextual-ee'
|
|
|
11
11
|
import { bundleId } from '../ids/bundle-id'
|
|
12
12
|
|
|
13
13
|
export const flag = `nr@original:${bundleId}`
|
|
14
|
+
const LONG_TASK_THRESHOLD = 50
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* A convenience alias of `hasOwnProperty`.
|
|
@@ -95,7 +96,7 @@ export function createWrapperWithEmitter (emitter, always) {
|
|
|
95
96
|
safeEmit(prefix + 'start', [args, originalThis, methodName], ctx, bubble)
|
|
96
97
|
|
|
97
98
|
const fnStartTime = performance.now()
|
|
98
|
-
let fnEndTime
|
|
99
|
+
let fnEndTime
|
|
99
100
|
try {
|
|
100
101
|
result = fn.apply(originalThis, args)
|
|
101
102
|
fnEndTime = performance.now()
|
|
@@ -109,16 +110,20 @@ export function createWrapperWithEmitter (emitter, always) {
|
|
|
109
110
|
} finally {
|
|
110
111
|
const duration = fnEndTime - fnStartTime
|
|
111
112
|
const task = {
|
|
113
|
+
start: fnStartTime,
|
|
114
|
+
end: fnEndTime,
|
|
112
115
|
duration,
|
|
113
|
-
isLongTask: duration >=
|
|
116
|
+
isLongTask: duration >= LONG_TASK_THRESHOLD,
|
|
114
117
|
methodName,
|
|
115
118
|
thrownError
|
|
116
119
|
// could add more properties here later if needed by downstream features
|
|
117
120
|
}
|
|
118
121
|
// standalone long task message
|
|
119
|
-
if (task.isLongTask)
|
|
122
|
+
if (task.isLongTask) {
|
|
123
|
+
safeEmit('long-task', [task, originalThis], ctx, bubble)
|
|
124
|
+
}
|
|
120
125
|
// -end message also includes the task execution info
|
|
121
|
-
safeEmit(prefix + 'end', [args, originalThis, result
|
|
126
|
+
safeEmit(prefix + 'end', [args, originalThis, result], ctx, bubble)
|
|
122
127
|
}
|
|
123
128
|
}
|
|
124
129
|
}
|
|
@@ -11,6 +11,7 @@ import { FEATURE_NAMES } from '../../../loaders/features/features'
|
|
|
11
11
|
import { AggregateBase } from '../../utils/aggregate-base'
|
|
12
12
|
import { parseGQL } from './gql'
|
|
13
13
|
import { nullable, numeric, getAddStringContext, addCustomAttributes } from '../../../common/serialize/bel-serializer'
|
|
14
|
+
import { gosNREUMOriginals } from '../../../common/window/nreum'
|
|
14
15
|
|
|
15
16
|
export class Aggregate extends AggregateBase {
|
|
16
17
|
static featureName = FEATURE_NAME
|
|
@@ -45,6 +46,13 @@ export class Aggregate extends AggregateBase {
|
|
|
45
46
|
classThis.storeXhr(...arguments, this) // this switches the context back to the class instance while passing the NR context as an argument -- see "ctx" in storeXhr
|
|
46
47
|
}, this.featureName, this.ee)
|
|
47
48
|
|
|
49
|
+
this.ee.on('long-task', (task, originator) => {
|
|
50
|
+
if (originator instanceof gosNREUMOriginals().o.XHR) { // any time a long task from XHR callback is observed, update the end time for soft nav use
|
|
51
|
+
const xhrMetadata = this.ee.context(originator)
|
|
52
|
+
xhrMetadata.latestLongtaskEnd = task.end
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
48
56
|
this.waitForFlags(([])).then(() => this.drain())
|
|
49
57
|
}
|
|
50
58
|
|
|
@@ -113,8 +121,8 @@ export class Aggregate extends AggregateBase {
|
|
|
113
121
|
if (event.gql) this.reportSupportabilityMetric('Ajax/Events/GraphQL/Bytes-Added', stringify(event.gql).length)
|
|
114
122
|
|
|
115
123
|
const softNavInUse = Boolean(this.agentRef.features?.[FEATURE_NAMES.softNav])
|
|
116
|
-
if (softNavInUse) { // For newer soft nav (when running), pass the event to it for evaluation -- either part of an interaction or is given back
|
|
117
|
-
handle('ajax', [event], undefined, FEATURE_NAMES.softNav, this.ee)
|
|
124
|
+
if (softNavInUse) { // For newer soft nav (when running), pass the event w/ info to it for evaluation -- either part of an interaction or is given back
|
|
125
|
+
handle('ajax', [event, ctx], undefined, FEATURE_NAMES.softNav, this.ee)
|
|
118
126
|
} else if (ctx.spaNode) { // For old spa (when running), if the ajax happened inside an interaction, hold it until the interaction finishes
|
|
119
127
|
const interactionId = ctx.spaNode.interaction.id
|
|
120
128
|
this.underSpaEvents[interactionId] ??= []
|
|
@@ -96,6 +96,7 @@ function subscribeToEvents (agentRef, ee, handler, dt) {
|
|
|
96
96
|
ctx.loadCaptureCalled = false
|
|
97
97
|
ctx.params = this.params || {}
|
|
98
98
|
ctx.metrics = this.metrics || {}
|
|
99
|
+
ctx.latestLongtaskEnd = 0
|
|
99
100
|
|
|
100
101
|
xhr.addEventListener('load', function (event) {
|
|
101
102
|
captureXhrData(ctx, xhr)
|
|
@@ -43,8 +43,8 @@ export class Aggregate extends AggregateBase {
|
|
|
43
43
|
|
|
44
44
|
register('err', (...args) => this.storeError(...args), this.featureName, this.ee)
|
|
45
45
|
register('ierr', (...args) => this.storeError(...args), this.featureName, this.ee)
|
|
46
|
-
register('softNavFlush', (interactionId, wasFinished, softNavAttrs) =>
|
|
47
|
-
this.onSoftNavNotification(interactionId, wasFinished, softNavAttrs), this.featureName, this.ee) // when an ixn is done or cancelled
|
|
46
|
+
register('softNavFlush', (interactionId, wasFinished, softNavAttrs, interactionEndTime) =>
|
|
47
|
+
this.onSoftNavNotification(interactionId, wasFinished, softNavAttrs, interactionEndTime), this.featureName, this.ee) // when an ixn is done or cancelled
|
|
48
48
|
|
|
49
49
|
this.harvestOpts.aggregatorTypes = ['err', 'ierr', 'xhr'] // the types in EventAggregator this feature cares about
|
|
50
50
|
|
|
@@ -292,12 +292,16 @@ export class Aggregate extends AggregateBase {
|
|
|
292
292
|
delete this.bufferedErrorsUnderSpa[interaction.id]
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
onSoftNavNotification (interactionId, wasFinished, softNavAttrs) {
|
|
295
|
+
onSoftNavNotification (interactionId, wasFinished, softNavAttrs, interactionEndTime) {
|
|
296
296
|
if (this.blocked) return
|
|
297
297
|
|
|
298
|
-
this.bufferedErrorsUnderSpa[interactionId]?.forEach(jsErrorEvent =>
|
|
299
|
-
this.#storeJserrorForHarvest(jsErrorEvent,
|
|
300
|
-
|
|
298
|
+
this.bufferedErrorsUnderSpa[interactionId]?.forEach(jsErrorEvent => { // this should not modify the re-used softNavAttrs contents
|
|
299
|
+
if (!wasFinished) return this.#storeJserrorForHarvest(jsErrorEvent, false, softNavAttrs)
|
|
300
|
+
|
|
301
|
+
const startTime = jsErrorEvent[3].time // in storeError fn, the newMetrics obj contains the time passed to & used by SN to seek the ixn
|
|
302
|
+
if (startTime > interactionEndTime) return this.#storeJserrorForHarvest(jsErrorEvent, false, softNavAttrs) // disassociate any error that ultimately falls outside the final ixn span
|
|
303
|
+
return this.#storeJserrorForHarvest(jsErrorEvent, true, softNavAttrs)
|
|
304
|
+
})
|
|
301
305
|
delete this.bufferedErrorsUnderSpa[interactionId] // wipe the list of jserrors so they aren't duplicated by another call to the same id
|
|
302
306
|
}
|
|
303
307
|
}
|
|
@@ -43,8 +43,10 @@ export class Aggregate extends AggregateBase {
|
|
|
43
43
|
/** set at BCS response, stored in runtime */
|
|
44
44
|
this.timeKeeper = undefined
|
|
45
45
|
|
|
46
|
-
this.
|
|
47
|
-
this.
|
|
46
|
+
this.instrumentClass = args
|
|
47
|
+
// point this var here just in case it already exists and can be used by APIs (session pause, resume, etc.) before handling rum flags
|
|
48
|
+
this.recorder = this.instrumentClass?.recorder
|
|
49
|
+
|
|
48
50
|
this.harvestOpts.raw = true
|
|
49
51
|
|
|
50
52
|
this.isSessionTrackingEnabled = canEnableSessionTracking(agentRef.init) && !!agentRef.runtime.session
|
|
@@ -64,7 +66,7 @@ export class Aggregate extends AggregateBase {
|
|
|
64
66
|
// if the mode changed on a different tab, it needs to update this instance to match
|
|
65
67
|
this.mode = agentRef.runtime.session.state.sessionReplayMode
|
|
66
68
|
if (!this.initialized || this.mode === MODE.OFF) return
|
|
67
|
-
this.recorder?.startRecording()
|
|
69
|
+
this.recorder?.startRecording(TRIGGERS.RESUME, this.mode)
|
|
68
70
|
})
|
|
69
71
|
|
|
70
72
|
this.ee.on(SESSION_EVENTS.UPDATE, (type, data) => {
|
|
@@ -131,7 +133,7 @@ export class Aggregate extends AggregateBase {
|
|
|
131
133
|
this.mode = MODE.FULL
|
|
132
134
|
// if the error was noticed AFTER the recorder was already imported....
|
|
133
135
|
if (this.recorder && this.initialized) {
|
|
134
|
-
if (!this.agentRef.runtime.isRecording) this.recorder.startRecording()
|
|
136
|
+
if (!this.agentRef.runtime.isRecording) this.recorder.startRecording(TRIGGERS.SWITCH_TO_FULL, this.mode) // off --> full
|
|
135
137
|
this.syncWithSessionManager({ sessionReplayMode: this.mode })
|
|
136
138
|
} else {
|
|
137
139
|
this.initializeRecording(MODE.FULL, true)
|
|
@@ -142,9 +144,10 @@ export class Aggregate extends AggregateBase {
|
|
|
142
144
|
* Evaluate entitlements and sampling before starting feature mechanics, importing and configuring recording library, and setting storage state
|
|
143
145
|
* @param {boolean} srMode - the true/false state of the "sr" flag (aka. entitlements) from RUM response
|
|
144
146
|
* @param {boolean} ignoreSession - whether to force the method to ignore the session state and use just the sample flags
|
|
147
|
+
* @param {TRIGGERS} [trigger=TRIGGERS.INITIALIZE] - the trigger that initiated the recording. Usually TRIGGERS.INITIALIZE, but could be TRIGGERS.API since in certain cases that trigger calls this method
|
|
145
148
|
* @returns {void}
|
|
146
149
|
*/
|
|
147
|
-
async initializeRecording (srMode, ignoreSession) {
|
|
150
|
+
async initializeRecording (srMode, ignoreSession, trigger = TRIGGERS.INITIALIZE) {
|
|
148
151
|
this.initialized = true
|
|
149
152
|
if (!this.entitled) return
|
|
150
153
|
|
|
@@ -156,7 +159,8 @@ export class Aggregate extends AggregateBase {
|
|
|
156
159
|
// session replays can continue if already in progress
|
|
157
160
|
const { session, timeKeeper } = this.agentRef.runtime
|
|
158
161
|
this.timeKeeper = timeKeeper
|
|
159
|
-
|
|
162
|
+
|
|
163
|
+
if (this.recorder?.trigger === TRIGGERS.API && this.agentRef.runtime.isRecording) {
|
|
160
164
|
this.mode = MODE.FULL
|
|
161
165
|
} else if (!session.isNew && !ignoreSession) { // inherit the mode of the existing session
|
|
162
166
|
this.mode = session.state.sessionReplayMode
|
|
@@ -167,21 +171,16 @@ export class Aggregate extends AggregateBase {
|
|
|
167
171
|
// If off, then don't record (early return)
|
|
168
172
|
if (this.mode === MODE.OFF) return
|
|
169
173
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
} catch (err) {
|
|
177
|
-
return this.abort(ABORT_REASONS.IMPORT)
|
|
178
|
-
}
|
|
179
|
-
} else {
|
|
180
|
-
this.recorder.parent = this
|
|
174
|
+
try {
|
|
175
|
+
/** will return a recorder instance if already imported, otherwise, will fetch the recorder and initialize it */
|
|
176
|
+
this.recorder ??= await this.instrumentClass.importRecorder()
|
|
177
|
+
} catch (err) {
|
|
178
|
+
/** if the recorder fails to import, abort the feature */
|
|
179
|
+
return this.abort(ABORT_REASONS.IMPORT, err)
|
|
181
180
|
}
|
|
182
181
|
|
|
183
182
|
// If an error was noticed before the mode could be set (like in the early lifecycle of the page), immediately set to FULL mode
|
|
184
|
-
if (this.mode === MODE.ERROR && this.errorNoticed) this.mode = MODE.FULL
|
|
183
|
+
if (this.mode === MODE.ERROR && this.instrumentClass.errorNoticed) { this.mode = MODE.FULL }
|
|
185
184
|
|
|
186
185
|
// FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
|
|
187
186
|
// ERROR mode will do this until an error is thrown, and then switch into FULL mode.
|
|
@@ -189,7 +188,7 @@ export class Aggregate extends AggregateBase {
|
|
|
189
188
|
|
|
190
189
|
await this.prepUtils()
|
|
191
190
|
|
|
192
|
-
if (!this.agentRef.runtime.isRecording) this.recorder.startRecording()
|
|
191
|
+
if (!this.agentRef.runtime.isRecording) this.recorder.startRecording(trigger, this.mode)
|
|
193
192
|
|
|
194
193
|
this.syncWithSessionManager({ sessionReplayMode: this.mode })
|
|
195
194
|
}
|
|
@@ -16,9 +16,11 @@ import { setupPauseReplayAPI } from '../../../loaders/api/pauseReplay'
|
|
|
16
16
|
|
|
17
17
|
export class Instrument extends InstrumentBase {
|
|
18
18
|
static featureName = FEATURE_NAME
|
|
19
|
+
/** @type {Promise|undefined} A promise that resolves when the recorder module is imported and added to the class. Undefined if the recorder has never been staged to import with `importRecorder`. */
|
|
20
|
+
#stagedImport
|
|
21
|
+
/** The RRWEB recorder instance, if imported */
|
|
22
|
+
recorder
|
|
19
23
|
|
|
20
|
-
#mode
|
|
21
|
-
#agentRef
|
|
22
24
|
constructor (agentRef) {
|
|
23
25
|
super(agentRef, FEATURE_NAME)
|
|
24
26
|
|
|
@@ -27,7 +29,6 @@ export class Instrument extends InstrumentBase {
|
|
|
27
29
|
setupPauseReplayAPI(agentRef)
|
|
28
30
|
|
|
29
31
|
let session
|
|
30
|
-
this.#agentRef = agentRef
|
|
31
32
|
try {
|
|
32
33
|
session = JSON.parse(localStorage.getItem(`${PREFIX}_${DEFAULT_KEY}`))
|
|
33
34
|
} catch (err) { }
|
|
@@ -37,15 +38,17 @@ export class Instrument extends InstrumentBase {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
if (this.#canPreloadRecorder(session)) {
|
|
40
|
-
this
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
this.importAggregator(this.#agentRef, () => import(/* webpackChunkName: "session_replay-aggregate" */ '../aggregate'))
|
|
41
|
+
this.importRecorder().then(recorder => {
|
|
42
|
+
recorder.startRecording(TRIGGERS.PRELOAD, session?.sessionReplayMode)
|
|
43
|
+
}) // could handle specific fail-state behaviors with a .catch block here
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
this.importAggregator(this.agentRef, () => import(/* webpackChunkName: "session_replay-aggregate" */ '../aggregate'), this)
|
|
47
|
+
|
|
46
48
|
/** If the recorder is running, we can pass error events on to the agg to help it switch to full mode later */
|
|
47
49
|
this.ee.on('err', (e) => {
|
|
48
|
-
if (this
|
|
50
|
+
if (this.blocked) return
|
|
51
|
+
if (this.agentRef.runtime.isRecording) {
|
|
49
52
|
this.errorNoticed = true
|
|
50
53
|
handle(SR_EVENT_EMITTER_TYPES.ERROR_DURING_REPLAY, [e], undefined, this.featureName, this.ee)
|
|
51
54
|
}
|
|
@@ -57,54 +60,50 @@ export class Instrument extends InstrumentBase {
|
|
|
57
60
|
if (!session) { // this might be a new session if entity initializes: conservatively start recording if first-time config allows
|
|
58
61
|
// Note: users with SR enabled, as well as these other configs enabled by-default, will be penalized by the recorder overhead EVEN IF they don't actually have or get
|
|
59
62
|
// entitlement or sampling decision, or otherwise intentionally opted-in for the feature.
|
|
60
|
-
return isPreloadAllowed(this
|
|
63
|
+
return isPreloadAllowed(this.agentRef.init)
|
|
61
64
|
} else if (session.sessionReplayMode === MODE.FULL || session.sessionReplayMode === MODE.ERROR) {
|
|
62
65
|
return true // existing sessions get to continue recording, regardless of this page's configs or if it has expired (conservatively)
|
|
63
66
|
} else { // SR mode was OFF but may potentially be turned on if session resets and configs allows the new session to have replay...
|
|
64
|
-
return isPreloadAllowed(this
|
|
67
|
+
return isPreloadAllowed(this.agentRef.init)
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
#alreadyStarted = false
|
|
69
71
|
/**
|
|
70
|
-
*
|
|
72
|
+
* Returns a promise that imports the recorder module. Only lets the recorder module be imported and instantiated once. Rejects if failed to import/instantiate.
|
|
73
|
+
* @returns {Promise}
|
|
71
74
|
*/
|
|
72
|
-
|
|
73
|
-
if
|
|
74
|
-
this
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
importRecorder () {
|
|
76
|
+
/** if we already have a recorder fully set up, just return it */
|
|
77
|
+
if (this.recorder) return Promise.resolve(this.recorder)
|
|
78
|
+
/** conditional -- if we have never started importing, stage the import and store it in state */
|
|
79
|
+
this.#stagedImport ??= import(/* webpackChunkName: "recorder" */'../shared/recorder')
|
|
80
|
+
.then(({ Recorder }) => {
|
|
81
|
+
this.recorder = new Recorder(this)
|
|
82
|
+
/** return the recorder for promise chaining */
|
|
83
|
+
return this.recorder
|
|
84
|
+
})
|
|
85
|
+
.catch(err => {
|
|
86
|
+
this.ee.emit('internal-error', [err])
|
|
87
|
+
this.blocked = true
|
|
88
|
+
/** return the err for promise chaining */
|
|
89
|
+
throw err
|
|
90
|
+
})
|
|
78
91
|
|
|
79
|
-
|
|
80
|
-
// Note: recorder starts here with w/e the mode is at this time, but this may be changed later (see #apiStartOrRestartReplay else-case)
|
|
81
|
-
this.recorder ??= new Recorder({ ...this, mode: this.#mode, agentRef: this.#agentRef, trigger, timeKeeper: this.#agentRef.runtime.timeKeeper }) // if TK exists due to deferred state, pass it
|
|
82
|
-
this.recorder.startRecording()
|
|
83
|
-
this.abortHandler = this.recorder.stopRecording
|
|
84
|
-
} catch (err) {
|
|
85
|
-
this.parent.ee.emit('internal-error', [err])
|
|
86
|
-
}
|
|
87
|
-
this.importAggregator(this.#agentRef, () => import(/* webpackChunkName: "session_replay-aggregate" */ '../aggregate'), { recorder: this.recorder, errorNoticed: this.errorNoticed })
|
|
92
|
+
return this.#stagedImport
|
|
88
93
|
}
|
|
89
94
|
|
|
90
95
|
/**
|
|
91
96
|
* Called whenever startReplay API is used. That could occur any time, pre or post load.
|
|
92
97
|
*/
|
|
93
98
|
#apiStartOrRestartReplay () {
|
|
99
|
+
if (this.blocked) return
|
|
94
100
|
if (this.featAggregate) { // post-load; there's possibly already an ongoing recording
|
|
95
|
-
if (this.featAggregate.mode !== MODE.FULL) this.featAggregate.initializeRecording(MODE.FULL, true)
|
|
96
|
-
} else {
|
|
97
|
-
this
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// b. Record has been initialized, possibly with the "wrong" mode, so we have to correct that + restart.
|
|
102
|
-
if (this.recorder && this.recorder.parent.mode !== MODE.FULL) {
|
|
103
|
-
this.recorder.parent.mode = MODE.FULL
|
|
104
|
-
this.recorder.stopRecording()
|
|
105
|
-
this.recorder.startRecording()
|
|
106
|
-
this.abortHandler = this.recorder.stopRecording
|
|
107
|
-
}
|
|
101
|
+
if (this.featAggregate.mode !== MODE.FULL) this.featAggregate.initializeRecording(MODE.FULL, true, TRIGGERS.API)
|
|
102
|
+
} else {
|
|
103
|
+
this.importRecorder()
|
|
104
|
+
.then(() => {
|
|
105
|
+
this.recorder.startRecording(TRIGGERS.API, MODE.FULL)
|
|
106
|
+
}) // could handle specific fail-state behaviors with a .catch block here
|
|
108
107
|
}
|
|
109
108
|
}
|
|
110
109
|
}
|
|
@@ -12,7 +12,7 @@ import { handle } from '../../../common/event-emitter/handle'
|
|
|
12
12
|
import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
|
|
13
13
|
import { FEATURE_NAMES } from '../../../loaders/features/features'
|
|
14
14
|
import { customMasker } from './utils'
|
|
15
|
-
import { IDEAL_PAYLOAD_SIZE } from '../../../common/constants/agent-constants'
|
|
15
|
+
import { IDEAL_PAYLOAD_SIZE, SESSION_ERROR } from '../../../common/constants/agent-constants'
|
|
16
16
|
import { warn } from '../../../common/util/console'
|
|
17
17
|
import { single } from '../../../common/util/invoke'
|
|
18
18
|
import { registerHandler } from '../../../common/event-emitter/register-handler'
|
|
@@ -25,11 +25,21 @@ export class Recorder {
|
|
|
25
25
|
|
|
26
26
|
#warnCSSOnce = single(() => warn(47)) // notifies user of potential replayer issue if fix_stylesheets is off
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
#canRecord = true
|
|
29
|
+
|
|
30
|
+
triggerHistory = [] // useful for debugging
|
|
31
|
+
|
|
32
|
+
constructor (srInstrument) {
|
|
33
|
+
/** The parent classes that share the recorder */
|
|
34
|
+
this.srInstrument = srInstrument
|
|
35
|
+
// --- shortcuts
|
|
36
|
+
this.ee = srInstrument.ee
|
|
37
|
+
this.srFeatureName = srInstrument.featureName
|
|
38
|
+
this.agentRef = srInstrument.agentRef
|
|
39
|
+
|
|
40
|
+
this.isErrorMode = false
|
|
31
41
|
/** A flag that can be set to false by failing conversions to stop the fetching process */
|
|
32
|
-
this.shouldFix = this.
|
|
42
|
+
this.shouldFix = this.agentRef.init.session_replay.fix_stylesheets
|
|
33
43
|
|
|
34
44
|
/** Each page mutation or event will be stored (raw) in this array. This array will be cleared on each harvest */
|
|
35
45
|
this.events = new RecorderEvents(this.shouldFix)
|
|
@@ -40,9 +50,18 @@ export class Recorder {
|
|
|
40
50
|
/** 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 */
|
|
41
51
|
this.lastMeta = false
|
|
42
52
|
/** The method to stop recording. This defaults to a noop, but is overwritten once the recording library is imported and initialized */
|
|
43
|
-
this.stopRecording = () => { this.
|
|
53
|
+
this.stopRecording = () => { this.agentRef.runtime.isRecording = false }
|
|
44
54
|
|
|
45
|
-
registerHandler(
|
|
55
|
+
registerHandler(SESSION_ERROR, () => {
|
|
56
|
+
this.#canRecord = false
|
|
57
|
+
this.stopRecording()
|
|
58
|
+
}, this.srFeatureName, this.ee)
|
|
59
|
+
|
|
60
|
+
registerHandler(RRWEB_DATA_CHANNEL, (event, isCheckout) => { this.audit(event, isCheckout) }, this.srFeatureName, this.ee)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get trigger () {
|
|
64
|
+
return this.triggerHistory[this.triggerHistory.length - 1]
|
|
46
65
|
}
|
|
47
66
|
|
|
48
67
|
getEvents () {
|
|
@@ -60,21 +79,29 @@ export class Recorder {
|
|
|
60
79
|
|
|
61
80
|
/** Clears the buffer (this.events), and resets all payload metadata properties */
|
|
62
81
|
clearBuffer () {
|
|
63
|
-
this.backloggedEvents = (this.
|
|
82
|
+
this.backloggedEvents = (this.isErrorMode) ? this.events : new RecorderEvents(this.shouldFix)
|
|
64
83
|
this.events = new RecorderEvents(this.shouldFix)
|
|
65
84
|
}
|
|
66
85
|
|
|
67
86
|
/** Begin recording using configured recording lib */
|
|
68
|
-
startRecording () {
|
|
69
|
-
this
|
|
70
|
-
|
|
87
|
+
startRecording (trigger, mode) {
|
|
88
|
+
if (!this.#canRecord) return
|
|
89
|
+
this.triggerHistory.push(trigger) // keep track of all triggers, useful for lifecycle debugging. "this.trigger" returns the latest entry
|
|
90
|
+
|
|
91
|
+
this.isErrorMode = mode === MODE.ERROR
|
|
92
|
+
|
|
93
|
+
/** if the recorder is already recording... lets stop it before starting a new one */
|
|
94
|
+
this.stopRecording()
|
|
95
|
+
|
|
96
|
+
this.agentRef.runtime.isRecording = true
|
|
97
|
+
const { block_class, ignore_class, mask_text_class, block_selector, mask_input_options, mask_text_selector, mask_all_inputs, inline_images, collect_fonts } = this.agentRef.init.session_replay
|
|
71
98
|
|
|
72
99
|
// set up rrweb configurations for maximum privacy --
|
|
73
100
|
// https://newrelic.atlassian.net/wiki/spaces/O11Y/pages/2792293280/2023+02+28+Browser+-+Session+Replay#Configuration-options
|
|
74
101
|
let stop
|
|
75
102
|
try {
|
|
76
103
|
stop = recorder({
|
|
77
|
-
emit: (event, isCheckout) => { handle(RRWEB_DATA_CHANNEL, [event, isCheckout], undefined, this.
|
|
104
|
+
emit: (event, isCheckout) => { handle(RRWEB_DATA_CHANNEL, [event, isCheckout], undefined, this.srFeatureName, this.ee) },
|
|
78
105
|
blockClass: block_class,
|
|
79
106
|
ignoreClass: ignore_class,
|
|
80
107
|
maskTextClass: mask_text_class,
|
|
@@ -87,15 +114,15 @@ export class Recorder {
|
|
|
87
114
|
inlineStylesheet: true,
|
|
88
115
|
inlineImages: inline_images,
|
|
89
116
|
collectFonts: collect_fonts,
|
|
90
|
-
checkoutEveryNms: CHECKOUT_MS[
|
|
117
|
+
checkoutEveryNms: CHECKOUT_MS[mode],
|
|
91
118
|
recordAfter: 'DOMContentLoaded'
|
|
92
119
|
})
|
|
93
120
|
} catch (err) {
|
|
94
|
-
this.
|
|
121
|
+
this.ee.emit('internal-error', [err])
|
|
95
122
|
}
|
|
96
123
|
|
|
97
124
|
this.stopRecording = () => {
|
|
98
|
-
this.
|
|
125
|
+
this.agentRef.runtime.isRecording = false
|
|
99
126
|
stop?.()
|
|
100
127
|
}
|
|
101
128
|
}
|
|
@@ -108,14 +135,14 @@ export class Recorder {
|
|
|
108
135
|
*/
|
|
109
136
|
audit (event, isCheckout) {
|
|
110
137
|
/** An count of stylesheet objects that were blocked from accessing contents via JS */
|
|
111
|
-
const incompletes = this.
|
|
138
|
+
const incompletes = this.agentRef.init.session_replay.fix_stylesheets ? stylesheetEvaluator.evaluate() : 0
|
|
112
139
|
const missingInlineSMTag = 'SessionReplay/Payload/Missing-Inline-Css/'
|
|
113
140
|
/** only run the full fixing behavior (more costly) if fix_stylesheets is configured as on (default behavior) */
|
|
114
141
|
if (!this.shouldFix) {
|
|
115
142
|
if (incompletes > 0) {
|
|
116
143
|
this.events.inlinedAllStylesheets = false
|
|
117
144
|
this.#warnCSSOnce()
|
|
118
|
-
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Skipped', incompletes], undefined, FEATURE_NAMES.metrics, this.
|
|
145
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Skipped', incompletes], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
119
146
|
}
|
|
120
147
|
return this.store(event, isCheckout)
|
|
121
148
|
}
|
|
@@ -128,8 +155,8 @@ export class Recorder {
|
|
|
128
155
|
this.events.inlinedAllStylesheets = false
|
|
129
156
|
this.shouldFix = false
|
|
130
157
|
}
|
|
131
|
-
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Failed', failedToFix], undefined, FEATURE_NAMES.metrics, this.
|
|
132
|
-
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Fixed', incompletes - failedToFix], undefined, FEATURE_NAMES.metrics, this.
|
|
158
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Failed', failedToFix], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
159
|
+
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Fixed', incompletes - failedToFix], undefined, FEATURE_NAMES.metrics, this.ee)
|
|
133
160
|
this.takeFullSnapshot()
|
|
134
161
|
})
|
|
135
162
|
/** Only start ignoring data if got a faulty snapshot */
|
|
@@ -141,10 +168,10 @@ export class Recorder {
|
|
|
141
168
|
|
|
142
169
|
/** Store a payload in the buffer (this.events). This should be the callback to the recording lib noticing a mutation */
|
|
143
170
|
store (event, isCheckout) {
|
|
144
|
-
if (!event || this.
|
|
171
|
+
if (!event || this.srInstrument.featAggregate?.blocked) return
|
|
145
172
|
|
|
146
173
|
/** because we've waited until draining to process the buffered rrweb events, we can guarantee the timekeeper exists */
|
|
147
|
-
event.timestamp = this.
|
|
174
|
+
event.timestamp = this.agentRef.runtime.timeKeeper.correctAbsoluteTimestamp(event.timestamp)
|
|
148
175
|
event.__serialized = stringify(event)
|
|
149
176
|
const eventBytes = event.__serialized.length
|
|
150
177
|
/** The estimated size of the payload after compression */
|
|
@@ -153,7 +180,7 @@ export class Recorder {
|
|
|
153
180
|
// to help reconstruct the replay later and must be included. While waiting and buffering for errors to come through,
|
|
154
181
|
// each time we see a new checkout, we can drop the old data.
|
|
155
182
|
// we need to check for meta because rrweb will flag it as checkout twice, once for meta, then once for snapshot
|
|
156
|
-
if (this.
|
|
183
|
+
if (this.isErrorMode && isCheckout && event.type === RRWEB_EVENT_TYPES.Meta) {
|
|
157
184
|
// we are still waiting for an error to throw, so keep wiping the buffer over time
|
|
158
185
|
this.clearBuffer()
|
|
159
186
|
}
|
|
@@ -168,16 +195,16 @@ export class Recorder {
|
|
|
168
195
|
|
|
169
196
|
// We are making an effort to try to keep payloads manageable for unloading. If they reach the unload limit before their interval,
|
|
170
197
|
// it will send immediately. This often happens on the first snapshot, which can be significantly larger than the other payloads.
|
|
171
|
-
if (((this.events.hasSnapshot && this.events.hasMeta) || payloadSize > IDEAL_PAYLOAD_SIZE) && this.
|
|
198
|
+
if (((this.events.hasSnapshot && this.events.hasMeta) || payloadSize > IDEAL_PAYLOAD_SIZE) && !this.isErrorMode) {
|
|
172
199
|
// if we've made it to the ideal size of ~16kb before the interval timer, we should send early.
|
|
173
|
-
this.
|
|
200
|
+
this.agentRef.runtime.harvester.triggerHarvestFor(this.srInstrument.featAggregate)
|
|
174
201
|
}
|
|
175
202
|
}
|
|
176
203
|
|
|
177
204
|
/** force the recording lib to take a full DOM snapshot. This needs to occur in certain cases, like visibility changes */
|
|
178
205
|
takeFullSnapshot () {
|
|
179
206
|
try {
|
|
180
|
-
if (!this.
|
|
207
|
+
if (!this.agentRef.runtime.isRecording) return
|
|
181
208
|
recorder.takeFullSnapshot()
|
|
182
209
|
} catch (err) {
|
|
183
210
|
// 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
|
|
@@ -199,7 +226,7 @@ export class Recorder {
|
|
|
199
226
|
* https://staging.onenr.io/037jbJWxbjy
|
|
200
227
|
* */
|
|
201
228
|
estimateCompression (data) {
|
|
202
|
-
if (!!this.
|
|
229
|
+
if (!!this.srInstrument.featAggregate?.gzipper && !!this.srInstrument.featAggregate?.u8) return data * AVG_COMPRESSION
|
|
203
230
|
return data
|
|
204
231
|
}
|
|
205
232
|
}
|
|
@@ -7,7 +7,7 @@ import { NODE_TYPE } from '../constants'
|
|
|
7
7
|
import { BelNode } from './bel-node'
|
|
8
8
|
|
|
9
9
|
export class AjaxNode extends BelNode {
|
|
10
|
-
constructor (ajaxEvent) {
|
|
10
|
+
constructor (ajaxEvent, ajaxContext) {
|
|
11
11
|
super()
|
|
12
12
|
this.belType = NODE_TYPE.AJAX
|
|
13
13
|
this.method = ajaxEvent.method
|
|
@@ -22,8 +22,12 @@ export class AjaxNode extends BelNode {
|
|
|
22
22
|
this.spanTimestamp = ajaxEvent.spanTimestamp
|
|
23
23
|
this.gql = ajaxEvent.gql
|
|
24
24
|
|
|
25
|
-
this.start = ajaxEvent.startTime
|
|
25
|
+
this.start = ajaxEvent.startTime
|
|
26
26
|
this.end = ajaxEvent.endTime
|
|
27
|
+
if (ajaxContext?.latestLongtaskEnd) {
|
|
28
|
+
this.callbackEnd = Math.max(ajaxContext.latestLongtaskEnd, this.end) // typically lt end if non-zero, but added clamping to end just in case
|
|
29
|
+
this.callbackDuration = this.callbackEnd - this.end // callbackDuration is the time from ajax loaded to last long task observed from it
|
|
30
|
+
} else this.callbackEnd = this.end // if no long task was observed, callbackEnd is the same as end
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
serialize (parentStartTimestamp, agentRef) {
|
|
@@ -36,8 +40,8 @@ export class AjaxNode extends BelNode {
|
|
|
36
40
|
0, // this will be overwritten below with number of attached nodes
|
|
37
41
|
numeric(this.start - parentStartTimestamp), // start relative to parent start (if part of first node in payload) or first parent start
|
|
38
42
|
numeric(this.end - this.start), // end is relative to start
|
|
39
|
-
numeric(this.callbackEnd),
|
|
40
|
-
numeric(this.callbackDuration),
|
|
43
|
+
numeric(this.callbackEnd - this.end), // callbackEnd is relative to end
|
|
44
|
+
numeric(this.callbackDuration), // not relative
|
|
41
45
|
addString(this.method),
|
|
42
46
|
numeric(this.status),
|
|
43
47
|
addString(this.domain),
|