Skip to content

Commit f165b3e

Browse files
feat: remove newrelic meta attribute (#1550)
1 parent b763d03 commit f165b3e

File tree

13 files changed

+78
-184
lines changed

13 files changed

+78
-184
lines changed

src/features/session_replay/aggregate/index.js

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { MODE, SESSION_EVENTS, SESSION_EVENT_TYPES } from '../../../common/sessi
1818
import { stringify } from '../../../common/util/stringify'
1919
import { stylesheetEvaluator } from '../shared/stylesheet-evaluator'
2020
import { now } from '../../../common/timing/now'
21-
import { buildNRMetaNode } from '../shared/utils'
2221
import { MAX_PAYLOAD_SIZE } from '../../../common/constants/agent-constants'
2322
import { cleanURL } from '../../../common/url/clean-url'
2423
import { canEnableSessionTracking } from '../../utils/feature-gates'
@@ -94,8 +93,7 @@ export class Aggregate extends AggregateBase {
9493
}
9594
return
9695
}
97-
this.drain()
98-
this.initializeRecording(srMode)
96+
this.initializeRecording(srMode).then(() => { this.drain() })
9997
}).then(() => {
10098
if (this.mode === MODE.OFF) {
10199
this.recorder?.stopRecording() // stop any conservative preload recording launched by instrument
@@ -121,7 +119,7 @@ export class Aggregate extends AggregateBase {
121119
}
122120

123121
handleError (e) {
124-
if (this.recorder) this.recorder.currentBufferTarget.hasError = true
122+
if (this.recorder) this.recorder.events.hasError = true
125123
// run once
126124
if (this.mode === MODE.ERROR && globalScope?.document.visibilityState === 'visible') {
127125
this.switchToFull()
@@ -171,10 +169,10 @@ export class Aggregate extends AggregateBase {
171169

172170
if (!this.recorder) {
173171
try {
174-
// Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
172+
// Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
175173
const { Recorder } = (await import(/* webpackChunkName: "recorder" */'../shared/recorder'))
176174
this.recorder = new Recorder(this)
177-
this.recorder.currentBufferTarget.hasError = this.errorNoticed
175+
this.recorder.events.hasError = this.errorNoticed
178176
} catch (err) {
179177
return this.abort(ABORT_REASONS.IMPORT)
180178
}
@@ -189,11 +187,6 @@ export class Aggregate extends AggregateBase {
189187
// ERROR mode will do this until an error is thrown, and then switch into FULL mode.
190188
// The makeHarvestPayload should ensure that no payload is returned if we're not in FULL mode...
191189

192-
// If theres preloaded events and we are in full mode, just harvest immediately to clear up space and for consistency
193-
if (this.mode === MODE.FULL && this.recorder?.getEvents().type === 'preloaded') {
194-
this.prepUtils().then(() => this.agentRef.runtime.harvester.triggerHarvestFor(this))
195-
}
196-
197190
await this.prepUtils()
198191

199192
if (!this.agentRef.runtime.isRecording) this.recorder.startRecording()
@@ -231,24 +224,10 @@ export class Aggregate extends AggregateBase {
231224

232225
let len = 0
233226
if (!!this.gzipper && !!this.u8) {
234-
payload.body = this.gzipper(this.u8(`[${payload.body.map(({ __serialized, ...e }) => {
235-
if (e.__newrelic && __serialized) return __serialized
236-
const output = { ...e }
237-
if (!output.__newrelic) {
238-
output.__newrelic = buildNRMetaNode(e.timestamp, this.timeKeeper)
239-
output.timestamp = this.timeKeeper.correctAbsoluteTimestamp(e.timestamp)
240-
}
241-
return stringify(output)
242-
}).join(',')}]`))
227+
payload.body = this.gzipper(this.u8(`[${payload.body.map(({ __serialized }) => (__serialized)).join(',')}]`))
243228
len = payload.body.length
244229
} else {
245-
payload.body = payload.body.map(({ __serialized, ...node }) => {
246-
if (node.__newrelic) return node
247-
const output = { ...node }
248-
output.__newrelic = buildNRMetaNode(node.timestamp, this.timeKeeper)
249-
output.timestamp = this.timeKeeper.correctAbsoluteTimestamp(node.timestamp)
250-
return output
251-
})
230+
for (let idx in payload.body) delete payload.body[idx].__serialized
252231
len = stringify(payload.body).length
253232
}
254233

@@ -269,12 +248,6 @@ export class Aggregate extends AggregateBase {
269248
return [payloadOutput]
270249
}
271250

272-
getCorrectedTimestamp (node) {
273-
if (!node?.timestamp) return
274-
if (node.__newrelic) return node.timestamp
275-
return this.timeKeeper.correctAbsoluteTimestamp(node.timestamp)
276-
}
277-
278251
/**
279252
* returns the timestamps for the earliest and latest nodes in the provided array, even if out of order
280253
* @param {Object[]} [nodes] - the nodes to evaluate
@@ -318,8 +291,8 @@ export class Aggregate extends AggregateBase {
318291

319292
const { firstEvent, lastEvent } = this.getFirstAndLastNodes(events)
320293
// from rrweb node || from when the harvest cycle started
321-
const firstTimestamp = this.getCorrectedTimestamp(firstEvent) || Math.floor(this.timeKeeper.correctAbsoluteTimestamp(recorderEvents.cycleTimestamp))
322-
const lastTimestamp = this.getCorrectedTimestamp(lastEvent) || Math.floor(this.timeKeeper.correctRelativeTimestamp(relativeNow))
294+
const firstTimestamp = firstEvent?.timestamp || Math.floor(this.timeKeeper.correctAbsoluteTimestamp(recorderEvents.cycleTimestamp))
295+
const lastTimestamp = lastEvent?.timestamp || Math.floor(this.timeKeeper.correctRelativeTimestamp(relativeNow))
323296

324297
const agentMetadata = agentRuntime.appMetadata?.agents?.[0] || {}
325298

src/features/session_replay/instrument/index.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { handle } from '../../../common/event-emitter/handle'
1010
import { DEFAULT_KEY, MODE, PREFIX } from '../../../common/session/constants'
1111
import { InstrumentBase } from '../../utils/instrument-base'
1212
import { hasReplayPrerequisite, isPreloadAllowed } from '../shared/utils'
13-
import { FEATURE_NAME, SR_EVENT_EMITTER_TYPES, TRIGGERS } from '../constants'
13+
import { FEATURE_NAME, SR_EVENT_EMITTER_TYPES } from '../constants'
1414
import { setupRecordReplayAPI } from '../../../loaders/api/recordReplay'
1515
import { setupPauseReplayAPI } from '../../../loaders/api/pauseReplay'
1616

@@ -69,7 +69,7 @@ export class Instrument extends InstrumentBase {
6969
/**
7070
* This func is use for early pre-load recording prior to replay feature (agg) being loaded onto the page. It should only setup once, including if already called and in-progress.
7171
*/
72-
async #preloadStartRecording (trigger) {
72+
async #preloadStartRecording () {
7373
if (this.#alreadyStarted) return
7474
this.#alreadyStarted = true
7575

@@ -78,7 +78,7 @@ export class Instrument extends InstrumentBase {
7878

7979
// If startReplay() has been used by this point, we must record in full mode regardless of session preload:
8080
// 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({ mode: this.#mode, agentIdentifier: this.agentIdentifier, trigger, ee: this.ee, agentRef: this.#agentRef })
81+
this.recorder ??= new Recorder({ ...this, mode: this.#mode, agentRef: this.#agentRef, timeKeeper: this.#agentRef.runtime.timeKeeper }) // if TK exists due to deferred state, pass it
8282
this.recorder.startRecording()
8383
this.abortHandler = this.recorder.stopRecording
8484
} catch (err) {
@@ -95,7 +95,7 @@ export class Instrument extends InstrumentBase {
9595
if (this.featAggregate.mode !== MODE.FULL) this.featAggregate.initializeRecording(MODE.FULL, true)
9696
} else { // pre-load
9797
this.#mode = MODE.FULL
98-
this.#preloadStartRecording(TRIGGERS.API)
98+
this.#preloadStartRecording()
9999
// There's a race here wherein either:
100100
// a. Recorder has not been initialized, and we've set the enforced mode, so we're good, or;
101101
// b. Record has been initialized, possibly with the "wrong" mode, so we have to correct that + restart.

src/features/session_replay/shared/recorder-events.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ export class RecorderEvents {
2626
this.inlinedAllStylesheets = shouldInlineStylesheets
2727
}
2828

29-
add (event) {
30-
this.#events.add(event)
29+
add (event, evaluatedSize) {
30+
this.#events.add(event, evaluatedSize)
3131
}
3232

3333
get events () {

src/features/session_replay/shared/recorder.js

Lines changed: 39 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,15 @@ import { stylesheetEvaluator } from './stylesheet-evaluator'
1111
import { handle } from '../../../common/event-emitter/handle'
1212
import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
1313
import { FEATURE_NAMES } from '../../../loaders/features/features'
14-
import { buildNRMetaNode, customMasker } from './utils'
14+
import { customMasker } from './utils'
1515
import { IDEAL_PAYLOAD_SIZE } from '../../../common/constants/agent-constants'
16-
import { AggregateBase } from '../../utils/aggregate-base'
1716
import { warn } from '../../../common/util/console'
1817
import { single } from '../../../common/util/invoke'
18+
import { registerHandler } from '../../../common/event-emitter/register-handler'
19+
20+
const RRWEB_DATA_CHANNEL = 'rrweb-data'
1921

2022
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
2723
/** flag that if true, blocks events from being "stored". Only set to true when a full snapshot has incomplete nodes (only stylesheets ATM) */
2824
#fixing = false
2925

@@ -34,47 +30,38 @@ export class Recorder {
3430
this.parent = parent
3531
/** A flag that can be set to false by failing conversions to stop the fetching process */
3632
this.shouldFix = this.parent.agentRef.init.session_replay.fix_stylesheets
37-
/** Event Buffers */
38-
this.#events = new RecorderEvents(this.shouldFix)
39-
this.#backloggedEvents = new RecorderEvents(this.shouldFix)
40-
this.#preloaded = [new RecorderEvents(this.shouldFix)]
41-
/** The pointer to the current bucket holding rrweb events */
42-
this.currentBufferTarget = this.#events
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 */
4439
this.hasSeenSnapshot = false
4540
/** 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 */
4641
this.lastMeta = false
4742
/** The method to stop recording. This defaults to a noop, but is overwritten once the recording library is imported and initialized */
4843
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)
4946
}
5047

5148
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-
}
6049
return {
61-
events: [...this.#backloggedEvents.events, ...this.#events.events].filter(x => x),
50+
events: [...this.backloggedEvents.events, ...this.events.events].filter(x => x),
6251
type: 'standard',
63-
cycleTimestamp: Math.min(this.#backloggedEvents.cycleTimestamp, this.#events.cycleTimestamp),
64-
payloadBytesEstimation: this.#backloggedEvents.payloadBytesEstimation + this.#events.payloadBytesEstimation,
65-
hasError: this.#backloggedEvents.hasError || this.#events.hasError,
66-
hasMeta: this.#backloggedEvents.hasMeta || this.#events.hasMeta,
67-
hasSnapshot: this.#backloggedEvents.hasSnapshot || this.#events.hasSnapshot,
68-
inlinedAllStylesheets: (!!this.#backloggedEvents.events.length && this.#backloggedEvents.inlinedAllStylesheets) || this.#events.inlinedAllStylesheets
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
6958
}
7059
}
7160

72-
/** Clears the buffer (this.#events), and resets all payload metadata properties */
61+
/** Clears the buffer (this.events), and resets all payload metadata properties */
7362
clearBuffer () {
74-
if (this.#preloaded[0]?.events.length) this.#preloaded.shift()
75-
else if (this.parent.mode === MODE.ERROR) this.#backloggedEvents = this.#events
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)
7865
}
7966

8067
/** Begin recording using configured recording lib */
@@ -87,7 +74,7 @@ export class Recorder {
8774
let stop
8875
try {
8976
stop = recorder({
90-
emit: this.audit.bind(this),
77+
emit: (event, isCheckout) => { handle(RRWEB_DATA_CHANNEL, [event, isCheckout], undefined, this.parent.featureName, this.parent.ee) },
9178
blockClass: block_class,
9279
ignoreClass: ignore_class,
9380
maskTextClass: mask_text_class,
@@ -126,7 +113,7 @@ export class Recorder {
126113
/** only run the full fixing behavior (more costly) if fix_stylesheets is configured as on (default behavior) */
127114
if (!this.shouldFix) {
128115
if (incompletes > 0) {
129-
this.currentBufferTarget.inlinedAllStylesheets = false
116+
this.events.inlinedAllStylesheets = false
130117
this.#warnCSSOnce()
131118
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Skipped', incompletes], undefined, FEATURE_NAMES.metrics, this.parent.ee)
132119
}
@@ -138,7 +125,7 @@ export class Recorder {
138125
/** wait for the evaluator to download/replace the incompletes' src code and then take a new snap */
139126
stylesheetEvaluator.fix().then((failedToFix) => {
140127
if (failedToFix > 0) {
141-
this.currentBufferTarget.inlinedAllStylesheets = false
128+
this.events.inlinedAllStylesheets = false
142129
this.shouldFix = false
143130
}
144131
handle(SUPPORTABILITY_METRIC_CHANNEL, [missingInlineSMTag + 'Failed', failedToFix], undefined, FEATURE_NAMES.metrics, this.parent.ee)
@@ -152,19 +139,12 @@ export class Recorder {
152139
if (!this.#fixing) this.store(event, isCheckout)
153140
}
154141

155-
/** Store a payload in the buffer (this.#events). This should be the callback to the recording lib noticing a mutation */
142+
/** Store a payload in the buffer (this.events). This should be the callback to the recording lib noticing a mutation */
156143
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
161145

162-
if (this.parent.blocked) return
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)
168148
event.__serialized = stringify(event)
169149
const eventBytes = event.__serialized.length
170150
/** The estimated size of the payload after compression */
@@ -179,26 +159,18 @@ export class Recorder {
179159
}
180160

181161
// meta event
182-
if (event.type === RRWEB_EVENT_TYPES.Meta) {
183-
this.currentBufferTarget.hasMeta = true
184-
}
162+
this.events.hasMeta ||= event.type === RRWEB_EVENT_TYPES.Meta
185163
// snapshot event
186-
if (event.type === RRWEB_EVENT_TYPES.FullSnapshot) {
187-
this.currentBufferTarget.hasSnapshot = true
188-
this.hasSeenSnapshot = true
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)
191168

192169
// We are making an effort to try to keep payloads manageable for unloading. If they reach the unload limit before their interval,
193170
// it will send immediately. This often happens on the first snapshot, which can be significantly larger than the other payloads.
194-
if (((event.type === RRWEB_EVENT_TYPES.FullSnapshot && this.currentBufferTarget.hasMeta) || payloadSize > IDEAL_PAYLOAD_SIZE) && this.parent.mode === MODE.FULL) {
195-
// if we've made it to the ideal size of ~64kb before the interval timer, we should send early.
196-
if (this.parent instanceof AggregateBase) {
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)
202174
}
203175
}
204176

@@ -213,13 +185,13 @@ export class Recorder {
213185
}
214186

215187
clearTimestamps () {
216-
this.currentBufferTarget.cycleTimestamp = undefined
188+
this.events.cycleTimestamp = undefined
217189
}
218190

219191
/** Estimate the payload size */
220192
getPayloadSize (newBytes = 0) {
221193
// the query param padding constant gives us some padding for the other metadata to be safely injected
222-
return this.estimateCompression(this.currentBufferTarget.payloadBytesEstimation + newBytes) + QUERY_PARAM_PADDING
194+
return this.estimateCompression(this.events.payloadBytesEstimation + newBytes) + QUERY_PARAM_PADDING
223195
}
224196

225197
/** Extensive research has yielded about an 88% compression factor on these payloads.

src/features/session_replay/shared/utils.js

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
*/
55
import { gosNREUMOriginals } from '../../../common/window/nreum'
66
import { canEnableSessionTracking } from '../../utils/feature-gates'
7-
import { originTime } from '../../../common/constants/runtime'
87

98
export function hasReplayPrerequisite (agentInit) {
109
return !!gosNREUMOriginals().o.MO && // Session Replay cannot work without Mutation Observer
@@ -16,18 +15,6 @@ export function isPreloadAllowed (agentInit) {
1615
return agentInit?.session_replay.preload === true && hasReplayPrerequisite(agentInit)
1716
}
1817

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-
3118
export function customMasker (text, element) {
3219
try {
3320
if (typeof element?.type === 'string') {

0 commit comments

Comments
 (0)