@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/cjs/common/constants/env.cdn.js +1 -1
  3. package/dist/cjs/common/constants/env.npm.js +1 -1
  4. package/dist/cjs/common/harvest/harvester.js +2 -1
  5. package/dist/cjs/features/session_replay/aggregate/index.js +10 -41
  6. package/dist/cjs/features/session_replay/instrument/index.js +4 -4
  7. package/dist/cjs/features/session_replay/shared/recorder-events.js +2 -2
  8. package/dist/cjs/features/session_replay/shared/recorder.js +41 -61
  9. package/dist/cjs/features/session_replay/shared/utils.js +0 -13
  10. package/dist/cjs/features/utils/aggregate-base.js +6 -5
  11. package/dist/cjs/features/utils/event-buffer.js +3 -2
  12. package/dist/esm/common/constants/env.cdn.js +1 -1
  13. package/dist/esm/common/constants/env.npm.js +1 -1
  14. package/dist/esm/common/harvest/harvester.js +2 -2
  15. package/dist/esm/features/session_replay/aggregate/index.js +10 -41
  16. package/dist/esm/features/session_replay/instrument/index.js +4 -4
  17. package/dist/esm/features/session_replay/shared/recorder-events.js +2 -2
  18. package/dist/esm/features/session_replay/shared/recorder.js +42 -62
  19. package/dist/esm/features/session_replay/shared/utils.js +0 -12
  20. package/dist/esm/features/utils/aggregate-base.js +6 -5
  21. package/dist/esm/features/utils/event-buffer.js +3 -2
  22. package/dist/types/common/harvest/harvester.d.ts +15 -0
  23. package/dist/types/common/harvest/harvester.d.ts.map +1 -1
  24. package/dist/types/features/session_replay/aggregate/index.d.ts +0 -1
  25. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  26. package/dist/types/features/session_replay/shared/recorder-events.d.ts +1 -1
  27. package/dist/types/features/session_replay/shared/recorder-events.d.ts.map +1 -1
  28. package/dist/types/features/session_replay/shared/recorder.d.ts +10 -8
  29. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  30. package/dist/types/features/session_replay/shared/utils.d.ts +0 -8
  31. package/dist/types/features/session_replay/shared/utils.d.ts.map +1 -1
  32. package/dist/types/features/utils/aggregate-base.d.ts +2 -0
  33. package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
  34. package/dist/types/features/utils/event-buffer.d.ts +2 -1
  35. package/dist/types/features/utils/event-buffer.d.ts.map +1 -1
  36. package/package.json +2 -2
  37. package/src/common/harvest/harvester.js +2 -2
  38. package/src/features/session_replay/aggregate/index.js +8 -35
  39. package/src/features/session_replay/instrument/index.js +1 -1
  40. package/src/features/session_replay/shared/recorder-events.js +2 -2
  41. package/src/features/session_replay/shared/recorder.js +39 -67
  42. package/src/features/session_replay/shared/utils.js +0 -13
  43. package/src/features/utils/aggregate-base.js +6 -4
  44. 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 { buildNRMetaNode, customMasker } from './utils'
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
- /** 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 */
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.#backloggedEvents.events, ...this.#events.events].filter(x => x),
50
+ events: [...this.backloggedEvents.events, ...this.events.events].filter(x => x),
62
51
  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
69
58
  }
70
59
  }
71
60
 
72
- /** Clears the buffer (this.#events), and resets all payload metadata properties */
61
+ /** Clears the buffer (this.events), and resets all payload metadata properties */
73
62
  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)
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.audit.bind(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.currentBufferTarget.inlinedAllStylesheets = false
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.currentBufferTarget.inlinedAllStylesheets = false
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.#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 */
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
- 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)
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
- if (event.type === RRWEB_EVENT_TYPES.Meta) {
183
- this.currentBufferTarget.hasMeta = true
184
- }
162
+ this.events.hasMeta ||= event.type === RRWEB_EVENT_TYPES.Meta
185
163
  // 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)
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 (((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)
202
174
  }
203
175
  }
204
176
 
@@ -213,13 +185,13 @@ export class Recorder {
213
185
  }
214
186
 
215
187
  clearTimestamps () {
216
- this.currentBufferTarget.cycleTimestamp = undefined
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.currentBufferTarget.payloadBytesEstimation + newBytes) + QUERY_PARAM_PADDING
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 // this is set to false in derived classes that need to block early harvests, like ajax under certain conditions
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
- const harvestFailed = result.sent && result.retry
173
- if (harvestFailed) this.events.reloadSave(this.harvestOpts, result.targetApp?.entityGuid)
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