@newrelic/browser-agent 1.296.0 → 1.297.0-rc.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 +13 -0
- 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/harvester.js +2 -1
- package/dist/cjs/features/session_replay/aggregate/index.js +10 -41
- package/dist/cjs/features/session_replay/instrument/index.js +4 -4
- package/dist/cjs/features/session_replay/shared/recorder-events.js +2 -2
- package/dist/cjs/features/session_replay/shared/recorder.js +41 -61
- package/dist/cjs/features/session_replay/shared/utils.js +0 -13
- package/dist/cjs/features/utils/aggregate-base.js +6 -5
- package/dist/cjs/features/utils/event-buffer.js +3 -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/harvest/harvester.js +2 -2
- package/dist/esm/features/session_replay/aggregate/index.js +10 -41
- package/dist/esm/features/session_replay/instrument/index.js +4 -4
- package/dist/esm/features/session_replay/shared/recorder-events.js +2 -2
- package/dist/esm/features/session_replay/shared/recorder.js +42 -62
- package/dist/esm/features/session_replay/shared/utils.js +0 -12
- package/dist/esm/features/utils/aggregate-base.js +6 -5
- package/dist/esm/features/utils/event-buffer.js +3 -2
- package/dist/types/common/harvest/harvester.d.ts +15 -0
- package/dist/types/common/harvest/harvester.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +0 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder-events.d.ts +1 -1
- package/dist/types/features/session_replay/shared/recorder-events.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder.d.ts +10 -8
- package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/utils.d.ts +0 -8
- package/dist/types/features/session_replay/shared/utils.d.ts.map +1 -1
- package/dist/types/features/utils/aggregate-base.d.ts +2 -0
- package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
- package/dist/types/features/utils/event-buffer.d.ts +2 -1
- package/dist/types/features/utils/event-buffer.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/common/harvest/harvester.js +2 -2
- package/src/features/session_replay/aggregate/index.js +8 -35
- package/src/features/session_replay/instrument/index.js +1 -1
- package/src/features/session_replay/shared/recorder-events.js +2 -2
- package/src/features/session_replay/shared/recorder.js +39 -67
- package/src/features/session_replay/shared/utils.js +0 -13
- package/src/features/utils/aggregate-base.js +6 -4
- package/src/features/utils/event-buffer.js +3 -2
|
@@ -11,19 +11,15 @@ import { stylesheetEvaluator } from './stylesheet-evaluator'
|
|
|
11
11
|
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
|
-
import {
|
|
14
|
+
import { customMasker } from './utils'
|
|
15
15
|
import { IDEAL_PAYLOAD_SIZE } from '../../../common/constants/agent-constants'
|
|
16
|
-
import { AggregateBase } from '../../utils/aggregate-base'
|
|
17
16
|
import { warn } from '../../../common/util/console'
|
|
18
17
|
import { single } from '../../../common/util/invoke'
|
|
18
|
+
import { registerHandler } from '../../../common/event-emitter/register-handler'
|
|
19
|
+
|
|
20
|
+
const RRWEB_DATA_CHANNEL = 'rrweb-data'
|
|
19
21
|
|
|
20
22
|
export class Recorder {
|
|
21
|
-
/** Each page mutation or event will be stored (raw) in this array. This array will be cleared on each harvest */
|
|
22
|
-
#events
|
|
23
|
-
/** Backlog used for a 2-part sliding window to guarantee a 15-30s buffer window */
|
|
24
|
-
#backloggedEvents
|
|
25
|
-
/** array of recorder events -- Will be filled only if forced harvest was triggered and harvester does not exist */
|
|
26
|
-
#preloaded
|
|
27
23
|
/** flag that if true, blocks events from being "stored". Only set to true when a full snapshot has incomplete nodes (only stylesheets ATM) */
|
|
28
24
|
#fixing = false
|
|
29
25
|
|
|
@@ -34,47 +30,38 @@ export class Recorder {
|
|
|
34
30
|
this.parent = parent
|
|
35
31
|
/** A flag that can be set to false by failing conversions to stop the fetching process */
|
|
36
32
|
this.shouldFix = this.parent.agentRef.init.session_replay.fix_stylesheets
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
this
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
/** Only set to true once a snapshot node has been processed. Used to block preload harvests from sending before we know we have a snapshot */
|
|
33
|
+
|
|
34
|
+
/** Each page mutation or event will be stored (raw) in this array. This array will be cleared on each harvest */
|
|
35
|
+
this.events = new RecorderEvents(this.shouldFix)
|
|
36
|
+
/** Backlog used for a 2-part sliding window to guarantee a 15-30s buffer window */
|
|
37
|
+
this.backloggedEvents = new RecorderEvents(this.shouldFix)
|
|
38
|
+
/** Only set to true once a snapshot node has been processed. Used to block harvests from sending before we know we have a snapshot */
|
|
44
39
|
this.hasSeenSnapshot = false
|
|
45
40
|
/** 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 */
|
|
46
41
|
this.lastMeta = false
|
|
47
42
|
/** The method to stop recording. This defaults to a noop, but is overwritten once the recording library is imported and initialized */
|
|
48
43
|
this.stopRecording = () => { this.parent.agentRef.runtime.isRecording = false }
|
|
44
|
+
|
|
45
|
+
registerHandler(RRWEB_DATA_CHANNEL, (event, isCheckout) => { this.audit(event, isCheckout) }, this.parent.featureName, this.parent.ee)
|
|
49
46
|
}
|
|
50
47
|
|
|
51
48
|
getEvents () {
|
|
52
|
-
if (this.#preloaded[0]?.events.length) {
|
|
53
|
-
return {
|
|
54
|
-
...this.#preloaded[0],
|
|
55
|
-
events: this.#preloaded[0].events,
|
|
56
|
-
payloadBytesEstimation: this.#preloaded[0].payloadBytesEstimation,
|
|
57
|
-
type: 'preloaded'
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
49
|
return {
|
|
61
|
-
events: [...this
|
|
50
|
+
events: [...this.backloggedEvents.events, ...this.events.events].filter(x => x),
|
|
62
51
|
type: 'standard',
|
|
63
|
-
cycleTimestamp: Math.min(this
|
|
64
|
-
payloadBytesEstimation: this
|
|
65
|
-
hasError: this
|
|
66
|
-
hasMeta: this
|
|
67
|
-
hasSnapshot: this
|
|
68
|
-
inlinedAllStylesheets: (!!this
|
|
52
|
+
cycleTimestamp: Math.min(this.backloggedEvents.cycleTimestamp, this.events.cycleTimestamp),
|
|
53
|
+
payloadBytesEstimation: this.backloggedEvents.payloadBytesEstimation + this.events.payloadBytesEstimation,
|
|
54
|
+
hasError: this.backloggedEvents.hasError || this.events.hasError,
|
|
55
|
+
hasMeta: this.backloggedEvents.hasMeta || this.events.hasMeta,
|
|
56
|
+
hasSnapshot: this.backloggedEvents.hasSnapshot || this.events.hasSnapshot,
|
|
57
|
+
inlinedAllStylesheets: (!!this.backloggedEvents.events.length && this.backloggedEvents.inlinedAllStylesheets) || this.events.inlinedAllStylesheets
|
|
69
58
|
}
|
|
70
59
|
}
|
|
71
60
|
|
|
72
|
-
/** Clears the buffer (this
|
|
61
|
+
/** Clears the buffer (this.events), and resets all payload metadata properties */
|
|
73
62
|
clearBuffer () {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
else this.#backloggedEvents = new RecorderEvents(this.shouldFix)
|
|
77
|
-
this.#events = new RecorderEvents(this.shouldFix)
|
|
63
|
+
this.backloggedEvents = (this.parent.mode === MODE.ERROR) ? this.events : new RecorderEvents(this.shouldFix)
|
|
64
|
+
this.events = new RecorderEvents(this.shouldFix)
|
|
78
65
|
}
|
|
79
66
|
|
|
80
67
|
/** Begin recording using configured recording lib */
|
|
@@ -87,7 +74,7 @@ export class Recorder {
|
|
|
87
74
|
let stop
|
|
88
75
|
try {
|
|
89
76
|
stop = recorder({
|
|
90
|
-
emit: this.
|
|
77
|
+
emit: (event, isCheckout) => { handle(RRWEB_DATA_CHANNEL, [event, isCheckout], undefined, this.parent.featureName, this.parent.ee) },
|
|
91
78
|
blockClass: block_class,
|
|
92
79
|
ignoreClass: ignore_class,
|
|
93
80
|
maskTextClass: mask_text_class,
|
|
@@ -126,7 +113,7 @@ export class Recorder {
|
|
|
126
113
|
/** only run the full fixing behavior (more costly) if fix_stylesheets is configured as on (default behavior) */
|
|
127
114
|
if (!this.shouldFix) {
|
|
128
115
|
if (incompletes > 0) {
|
|
129
|
-
this.
|
|
116
|
+
this.events.inlinedAllStylesheets = false
|
|
130
117
|
this.#warnCSSOnce()
|
|
131
118
|
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Skipped', incompletes], undefined, FEATURE_NAMES.metrics, this.parent.ee)
|
|
132
119
|
}
|
|
@@ -138,7 +125,7 @@ export class Recorder {
|
|
|
138
125
|
/** wait for the evaluator to download/replace the incompletes' src code and then take a new snap */
|
|
139
126
|
stylesheetEvaluator.fix().then((failedToFix) => {
|
|
140
127
|
if (failedToFix > 0) {
|
|
141
|
-
this.
|
|
128
|
+
this.events.inlinedAllStylesheets = false
|
|
142
129
|
this.shouldFix = false
|
|
143
130
|
}
|
|
144
131
|
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Failed', failedToFix], undefined, FEATURE_NAMES.metrics, this.parent.ee)
|
|
@@ -152,19 +139,12 @@ export class Recorder {
|
|
|
152
139
|
if (!this.#fixing) this.store(event, isCheckout)
|
|
153
140
|
}
|
|
154
141
|
|
|
155
|
-
/** Store a payload in the buffer (this
|
|
142
|
+
/** Store a payload in the buffer (this.events). This should be the callback to the recording lib noticing a mutation */
|
|
156
143
|
store (event, isCheckout) {
|
|
157
|
-
if (!event) return
|
|
158
|
-
|
|
159
|
-
if (!(this.parent instanceof AggregateBase) && this.#preloaded.length) this.currentBufferTarget = this.#preloaded[this.#preloaded.length - 1]
|
|
160
|
-
else this.currentBufferTarget = this.#events
|
|
144
|
+
if (!event || this.parent.blocked) return
|
|
161
145
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
if (this.parent.timeKeeper?.ready && !event.__newrelic) {
|
|
165
|
-
event.__newrelic = buildNRMetaNode(event.timestamp, this.parent.timeKeeper)
|
|
166
|
-
event.timestamp = this.parent.timeKeeper.correctAbsoluteTimestamp(event.timestamp)
|
|
167
|
-
}
|
|
146
|
+
/** because we've waited until draining to process the buffered rrweb events, we can guarantee the timekeeper exists */
|
|
147
|
+
event.timestamp = this.parent.timeKeeper.correctAbsoluteTimestamp(event.timestamp)
|
|
168
148
|
event.__serialized = stringify(event)
|
|
169
149
|
const eventBytes = event.__serialized.length
|
|
170
150
|
/** The estimated size of the payload after compression */
|
|
@@ -179,26 +159,18 @@ export class Recorder {
|
|
|
179
159
|
}
|
|
180
160
|
|
|
181
161
|
// meta event
|
|
182
|
-
|
|
183
|
-
this.currentBufferTarget.hasMeta = true
|
|
184
|
-
}
|
|
162
|
+
this.events.hasMeta ||= event.type === RRWEB_EVENT_TYPES.Meta
|
|
185
163
|
// snapshot event
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
this.currentBufferTarget.add(event)
|
|
164
|
+
this.events.hasSnapshot ||= this.hasSeenSnapshot ||= event.type === RRWEB_EVENT_TYPES.FullSnapshot
|
|
165
|
+
|
|
166
|
+
//* dont let the EventBuffer class double evaluate the event data size, it's a performance burden and we have special reasons to do it outside the event buffer */
|
|
167
|
+
this.events.add(event, eventBytes)
|
|
191
168
|
|
|
192
169
|
// We are making an effort to try to keep payloads manageable for unloading. If they reach the unload limit before their interval,
|
|
193
170
|
// it will send immediately. This often happens on the first snapshot, which can be significantly larger than the other payloads.
|
|
194
|
-
if (((
|
|
195
|
-
// if we've made it to the ideal size of ~
|
|
196
|
-
|
|
197
|
-
this.parent.agentRef.runtime.harvester.triggerHarvestFor(this.parent)
|
|
198
|
-
} else {
|
|
199
|
-
// we are still in "preload" and it triggered a "stop point". Make a new set, which will get pointed at on next cycle
|
|
200
|
-
this.#preloaded.push(new RecorderEvents(this.shouldFix))
|
|
201
|
-
}
|
|
171
|
+
if (((this.events.hasSnapshot && this.events.hasMeta) || payloadSize > IDEAL_PAYLOAD_SIZE) && this.parent.mode === MODE.FULL) {
|
|
172
|
+
// if we've made it to the ideal size of ~16kb before the interval timer, we should send early.
|
|
173
|
+
this.parent.agentRef.runtime.harvester.triggerHarvestFor(this.parent)
|
|
202
174
|
}
|
|
203
175
|
}
|
|
204
176
|
|
|
@@ -213,13 +185,13 @@ export class Recorder {
|
|
|
213
185
|
}
|
|
214
186
|
|
|
215
187
|
clearTimestamps () {
|
|
216
|
-
this.
|
|
188
|
+
this.events.cycleTimestamp = undefined
|
|
217
189
|
}
|
|
218
190
|
|
|
219
191
|
/** Estimate the payload size */
|
|
220
192
|
getPayloadSize (newBytes = 0) {
|
|
221
193
|
// the query param padding constant gives us some padding for the other metadata to be safely injected
|
|
222
|
-
return this.estimateCompression(this.
|
|
194
|
+
return this.estimateCompression(this.events.payloadBytesEstimation + newBytes) + QUERY_PARAM_PADDING
|
|
223
195
|
}
|
|
224
196
|
|
|
225
197
|
/** Extensive research has yielded about an 88% compression factor on these payloads.
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { gosNREUMOriginals } from '../../../common/window/nreum'
|
|
6
6
|
import { canEnableSessionTracking } from '../../utils/feature-gates'
|
|
7
|
-
import { originTime } from '../../../common/constants/runtime'
|
|
8
7
|
|
|
9
8
|
export function hasReplayPrerequisite (agentInit) {
|
|
10
9
|
return !!gosNREUMOriginals().o.MO && // Session Replay cannot work without Mutation Observer
|
|
@@ -16,18 +15,6 @@ export function isPreloadAllowed (agentInit) {
|
|
|
16
15
|
return agentInit?.session_replay.preload === true && hasReplayPrerequisite(agentInit)
|
|
17
16
|
}
|
|
18
17
|
|
|
19
|
-
export function buildNRMetaNode (timestamp, timeKeeper) {
|
|
20
|
-
const correctedTimestamp = timeKeeper.correctAbsoluteTimestamp(timestamp)
|
|
21
|
-
return {
|
|
22
|
-
originalTimestamp: timestamp,
|
|
23
|
-
correctedTimestamp,
|
|
24
|
-
timestampDiff: timestamp - correctedTimestamp,
|
|
25
|
-
originTime,
|
|
26
|
-
correctedOriginTime: timeKeeper.correctedOriginTime,
|
|
27
|
-
originTimeDiff: Math.floor(originTime - timeKeeper.correctedOriginTime)
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
18
|
export function customMasker (text, element) {
|
|
32
19
|
try {
|
|
33
20
|
if (typeof element?.type === 'string') {
|
|
@@ -35,7 +35,9 @@ export class AggregateBase extends FeatureBase {
|
|
|
35
35
|
/** @type {Boolean} indicates if custom attributes are combined in each event payload for size estimation purposes. this is set to true in derived classes that need to evaluate custom attributes separately from the event payload */
|
|
36
36
|
this.customAttributesAreSeparate = false
|
|
37
37
|
/** @type {Boolean} indicates if the feature can harvest early. This is set to false in derived classes that need to block early harvests, like ajax under certain conditions */
|
|
38
|
-
this.canHarvestEarly = true
|
|
38
|
+
this.canHarvestEarly = true
|
|
39
|
+
/** @type {Boolean} indicates if the feature is actively in a retry deferral period */
|
|
40
|
+
this.isRetrying = false
|
|
39
41
|
|
|
40
42
|
this.harvestOpts = {} // features aggregate classes can define custom opts for when their harvest is called
|
|
41
43
|
|
|
@@ -82,7 +84,7 @@ export class AggregateBase extends FeatureBase {
|
|
|
82
84
|
* @returns void
|
|
83
85
|
*/
|
|
84
86
|
decideEarlyHarvest () {
|
|
85
|
-
if (!this.canHarvestEarly) return
|
|
87
|
+
if (!this.canHarvestEarly || this.blocked || this.isRetrying) return
|
|
86
88
|
const estimatedSize = this.events.byteSize() + (this.customAttributesAreSeparate ? this.agentRef.runtime.jsAttributesMetadata.bytes : 0)
|
|
87
89
|
if (estimatedSize > IDEAL_PAYLOAD_SIZE) {
|
|
88
90
|
this.agentRef.runtime.harvester.triggerHarvestFor(this)
|
|
@@ -169,8 +171,8 @@ export class AggregateBase extends FeatureBase {
|
|
|
169
171
|
* @param {boolean=} result.retry - whether the harvest should be retried
|
|
170
172
|
*/
|
|
171
173
|
postHarvestCleanup (result = {}) {
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
+
this.isRetrying = result.sent && result.retry
|
|
175
|
+
if (this.isRetrying) this.events.reloadSave(this.harvestOpts, result.targetApp?.entityGuid)
|
|
174
176
|
this.events.clearSave(this.harvestOpts, result.targetApp?.entityGuid)
|
|
175
177
|
}
|
|
176
178
|
|
|
@@ -44,10 +44,11 @@ export class EventBuffer {
|
|
|
44
44
|
/**
|
|
45
45
|
* Add feature-processed event to our buffer. If this event would cause our total raw size to exceed the set max payload size, it is dropped.
|
|
46
46
|
* @param {any} event - any primitive type or object
|
|
47
|
+
* @param {number} [evaluatedSize] - the evalated size of the event, if already done so before storing in the event buffer
|
|
47
48
|
* @returns {Boolean} true if successfully added; false otherwise
|
|
48
49
|
*/
|
|
49
|
-
add (event) {
|
|
50
|
-
const addSize = stringify(event)?.length || 0 // (estimate) # of bytes a directly stringified event it would take to send
|
|
50
|
+
add (event, evaluatedSize) {
|
|
51
|
+
const addSize = evaluatedSize || stringify(event)?.length || 0 // (estimate) # of bytes a directly stringified event it would take to send
|
|
51
52
|
if (this.#rawBytes + addSize > this.maxPayloadSize) {
|
|
52
53
|
const smTag = inject => `EventBuffer/${inject}/Dropped/Bytes`
|
|
53
54
|
this.featureAgg?.reportSupportabilityMetric(smTag(this.featureAgg.featureName), addSize) // bytes dropped for this feature will aggregate with this metric tag
|