@newrelic/browser-agent 1.251.0 → 1.252.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 (60) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/cjs/common/config/state/init.js +2 -2
  3. package/dist/cjs/common/constants/env.cdn.js +1 -1
  4. package/dist/cjs/common/constants/env.npm.js +1 -1
  5. package/dist/cjs/common/drain/drain.js +3 -1
  6. package/dist/cjs/common/event-emitter/contextual-ee.js +7 -1
  7. package/dist/cjs/common/harvest/harvest-scheduler.js +2 -1
  8. package/dist/cjs/common/harvest/harvest.js +3 -2
  9. package/dist/cjs/features/ajax/instrument/index.js +2 -0
  10. package/dist/cjs/features/jserrors/instrument/index.js +5 -0
  11. package/dist/cjs/features/metrics/aggregate/index.js +1 -1
  12. package/dist/cjs/features/page_view_event/aggregate/index.js +1 -1
  13. package/dist/cjs/features/session_replay/aggregate/index.js +53 -17
  14. package/dist/cjs/features/session_replay/shared/recorder.js +9 -4
  15. package/dist/cjs/features/session_replay/shared/stylesheet-evaluator.js +33 -28
  16. package/dist/cjs/features/utils/instrument-base.js +1 -1
  17. package/dist/cjs/loaders/api/api.js +4 -1
  18. package/dist/esm/common/config/state/init.js +2 -2
  19. package/dist/esm/common/constants/env.cdn.js +1 -1
  20. package/dist/esm/common/constants/env.npm.js +1 -1
  21. package/dist/esm/common/drain/drain.js +3 -1
  22. package/dist/esm/common/event-emitter/contextual-ee.js +7 -1
  23. package/dist/esm/common/harvest/harvest-scheduler.js +2 -1
  24. package/dist/esm/common/harvest/harvest.js +3 -2
  25. package/dist/esm/features/ajax/instrument/index.js +2 -0
  26. package/dist/esm/features/jserrors/instrument/index.js +5 -0
  27. package/dist/esm/features/metrics/aggregate/index.js +1 -1
  28. package/dist/esm/features/page_view_event/aggregate/index.js +1 -1
  29. package/dist/esm/features/session_replay/aggregate/index.js +53 -17
  30. package/dist/esm/features/session_replay/shared/recorder.js +9 -4
  31. package/dist/esm/features/session_replay/shared/stylesheet-evaluator.js +33 -28
  32. package/dist/esm/features/utils/instrument-base.js +1 -1
  33. package/dist/esm/loaders/api/api.js +4 -1
  34. package/dist/types/common/drain/drain.d.ts +2 -1
  35. package/dist/types/common/drain/drain.d.ts.map +1 -1
  36. package/dist/types/common/event-emitter/contextual-ee.d.ts.map +1 -1
  37. package/dist/types/common/harvest/harvest.d.ts.map +1 -1
  38. package/dist/types/features/ajax/instrument/index.d.ts.map +1 -1
  39. package/dist/types/features/jserrors/instrument/index.d.ts.map +1 -1
  40. package/dist/types/features/session_replay/aggregate/index.d.ts +8 -3
  41. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  42. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  43. package/dist/types/features/session_replay/shared/stylesheet-evaluator.d.ts +1 -1
  44. package/dist/types/features/session_replay/shared/stylesheet-evaluator.d.ts.map +1 -1
  45. package/dist/types/loaders/api/api.d.ts.map +1 -1
  46. package/package.json +4 -1
  47. package/src/common/config/state/init.js +2 -2
  48. package/src/common/drain/drain.js +3 -2
  49. package/src/common/event-emitter/contextual-ee.js +7 -1
  50. package/src/common/harvest/harvest-scheduler.js +1 -1
  51. package/src/common/harvest/harvest.js +3 -2
  52. package/src/features/ajax/instrument/index.js +2 -0
  53. package/src/features/jserrors/instrument/index.js +6 -0
  54. package/src/features/metrics/aggregate/index.js +1 -1
  55. package/src/features/page_view_event/aggregate/index.js +1 -1
  56. package/src/features/session_replay/aggregate/index.js +47 -19
  57. package/src/features/session_replay/shared/recorder.js +9 -4
  58. package/src/features/session_replay/shared/stylesheet-evaluator.js +26 -21
  59. package/src/features/utils/instrument-base.js +1 -1
  60. package/src/loaders/api/api.js +4 -1
@@ -28,8 +28,6 @@ import { MODE, SESSION_EVENTS, SESSION_EVENT_TYPES } from '../../../common/sessi
28
28
  import { stringify } from '../../../common/util/stringify'
29
29
  import { stylesheetEvaluator } from '../shared/stylesheet-evaluator'
30
30
 
31
- let gzipper, u8
32
-
33
31
  export class Aggregate extends AggregateBase {
34
32
  static featureName = FEATURE_NAME
35
33
  // pass the recorder into the aggregator
@@ -41,8 +39,10 @@ export class Aggregate extends AggregateBase {
41
39
  this.initialized = false
42
40
  /** Set once the feature has been "aborted" to prevent other side-effects from continuing */
43
41
  this.blocked = false
44
- /** can shut off efforts to compress the data */
45
- this.shouldCompress = true
42
+ /** populated with the gzipper lib async */
43
+ this.gzipper = undefined
44
+ /** populated with the u8 string lib async */
45
+ this.u8 = undefined
46
46
  /** the mode to start in. Defaults to off */
47
47
  const { session } = getRuntime(this.agentIdentifier)
48
48
  this.mode = session.state.sessionReplayMode || MODE.OFF
@@ -53,6 +53,8 @@ export class Aggregate extends AggregateBase {
53
53
  this.recorder = args?.recorder
54
54
  if (this.recorder) this.recorder.parent = this
55
55
 
56
+ handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/Enabled'], undefined, FEATURE_NAMES.metrics, this.ee)
57
+
56
58
  const shouldSetup = (
57
59
  getConfigurationValue(agentIdentifier, 'privacy.cookies_enabled') === true &&
58
60
  getConfigurationValue(agentIdentifier, 'session_trace.enabled') === true
@@ -91,6 +93,12 @@ export class Aggregate extends AggregateBase {
91
93
  raw: true
92
94
  }, this)
93
95
 
96
+ if (this.recorder?.getEvents().type === 'preloaded') {
97
+ this.prepUtils().then(() => {
98
+ this.scheduler.runHarvest()
99
+ })
100
+ }
101
+
94
102
  registerHandler('recordReplay', () => {
95
103
  // if it has aborted or BCS returned bad entitlements, do not allow
96
104
  if (this.blocked || !this.entitled) return
@@ -116,15 +124,31 @@ export class Aggregate extends AggregateBase {
116
124
  }
117
125
  }, this.featureName, this.ee)
118
126
 
127
+ const { error_sampling_rate, sampling_rate, autoStart, block_selector, mask_text_selector, mask_all_inputs, inline_stylesheet, inline_images, collect_fonts } = getConfigurationValue(this.agentIdentifier, 'session_replay')
128
+
119
129
  this.waitForFlags(['sr']).then(([flagOn]) => {
120
130
  this.entitled = flagOn
121
- if (!this.entitled && this.recorder?.recording) this.recorder.abort(ABORT_REASONS.ENTITLEMENTS)
131
+ if (!this.entitled && this.recorder?.recording) {
132
+ this.recorder.abort(ABORT_REASONS.ENTITLEMENTS)
133
+ handle(SUPPORTABILITY_METRIC_CHANNEL, ['SessionReplay/EnabledNotEntitled/Detected'], undefined, FEATURE_NAMES.metrics, this.ee)
134
+ }
122
135
  this.initializeRecording(
123
- (Math.random() * 100) < getConfigurationValue(this.agentIdentifier, 'session_replay.error_sampling_rate'),
124
- (Math.random() * 100) < getConfigurationValue(this.agentIdentifier, 'session_replay.sampling_rate')
136
+ (Math.random() * 100) < error_sampling_rate,
137
+ (Math.random() * 100) < sampling_rate
125
138
  )
126
139
  }).then(() => sharedChannel.onReplayReady(this.mode)) // notify watchers that replay started with the mode
127
140
 
141
+ /** Detect if the default configs have been altered and report a SM. This is useful to evaluate what the reasonable defaults are across a customer base over time */
142
+ if (!autoStart) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/AutoStart/Modified'], undefined, FEATURE_NAMES.metrics, this.ee)
143
+ if (collect_fonts === true) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/CollectFonts/Modified'], undefined, FEATURE_NAMES.metrics, this.ee)
144
+ if (inline_stylesheet !== true) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/InlineStylesheet/Modified'], undefined, FEATURE_NAMES.metrics, this.ee)
145
+ if (inline_images === true) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/InlineImages/Modifed'], undefined, FEATURE_NAMES.metrics, this.ee)
146
+ if (mask_all_inputs !== true) handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/MaskAllInputs/Modified'], undefined, FEATURE_NAMES.metrics, this.ee)
147
+ if (block_selector !== '[data-nr-block]') handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/BlockSelector/Modified'], undefined, FEATURE_NAMES.metrics, this.ee)
148
+ if (mask_text_selector !== '*') handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/MaskTextSelector/Modified'], undefined, FEATURE_NAMES.metrics, this.ee)
149
+
150
+ handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/SamplingRate/Value', sampling_rate], undefined, FEATURE_NAMES.metrics, this.ee)
151
+ handle(SUPPORTABILITY_METRIC_CHANNEL, ['Config/SessionReplay/ErrorSamplingRate/Value', error_sampling_rate], undefined, FEATURE_NAMES.metrics, this.ee)
128
152
  this.drain()
129
153
  }
130
154
  }
@@ -197,21 +221,25 @@ export class Aggregate extends AggregateBase {
197
221
  this.scheduler.startTimer(this.harvestTimeSeconds)
198
222
  }
199
223
 
224
+ await this.prepUtils()
225
+
226
+ if (!this.recorder.recording) this.recorder.startRecording()
227
+
228
+ this.syncWithSessionManager({ sessionReplayMode: this.mode })
229
+ }
230
+
231
+ async prepUtils () {
200
232
  try {
201
233
  // Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
202
234
  const { gzipSync, strToU8 } = await import(/* webpackChunkName: "compressor" */'fflate')
203
- gzipper = gzipSync
204
- u8 = strToU8
235
+ this.gzipper = gzipSync
236
+ this.u8 = strToU8
205
237
  } catch (err) {
206
238
  // compressor failed to load, but we can still record without compression as a last ditch effort
207
- this.shouldCompress = false
208
239
  }
209
- if (!this.recorder.recording) this.recorder.startRecording()
210
-
211
- this.syncWithSessionManager({ sessionReplayMode: this.mode })
212
240
  }
213
241
 
214
- prepareHarvest () {
242
+ prepareHarvest ({ opts } = {}) {
215
243
  if (!this.recorder) return
216
244
  const recorderEvents = this.recorder.getEvents()
217
245
  // get the event type and use that to trigger another harvest if needed
@@ -224,8 +252,8 @@ export class Aggregate extends AggregateBase {
224
252
  }
225
253
 
226
254
  let len = 0
227
- if (this.shouldCompress) {
228
- payload.body = gzipper(u8(`[${payload.body.map(e => e.__serialized).join(',')}]`))
255
+ if (!!this.gzipper && !!this.u8) {
256
+ payload.body = this.gzipper(this.u8(`[${payload.body.map(e => e.__serialized).join(',')}]`))
229
257
  len = payload.body.length
230
258
  this.scheduler.opts.gzip = true
231
259
  } else {
@@ -242,7 +270,7 @@ export class Aggregate extends AggregateBase {
242
270
  const { session } = getRuntime(this.agentIdentifier)
243
271
  if (!session.state.sessionReplaySentFirstChunk) this.syncWithSessionManager({ sessionReplaySentFirstChunk: true })
244
272
  this.recorder.clearBuffer()
245
- if (recorderEvents.type === 'preloaded') this.scheduler.runHarvest()
273
+ if (recorderEvents.type === 'preloaded') this.scheduler.runHarvest(opts)
246
274
  return [payload]
247
275
  }
248
276
 
@@ -276,7 +304,7 @@ export class Aggregate extends AggregateBase {
276
304
 
277
305
  const firstEventTimestamp = events[0]?.timestamp // from rrweb node
278
306
  const lastEventTimestamp = events[events.length - 1]?.timestamp // from rrweb node
279
- const firstTimestamp = firstEventTimestamp || recorderEvents.cycleTimestamp
307
+ const firstTimestamp = firstEventTimestamp || recorderEvents.cycleTimestamp // from rrweb node || from when the harvest cycle started
280
308
  const lastTimestamp = lastEventTimestamp || agentOffset + relativeNow
281
309
 
282
310
  return {
@@ -288,7 +316,7 @@ export class Aggregate extends AggregateBase {
288
316
  attributes: encodeObj({
289
317
  // this section of attributes must be controllable and stay below the query param padding limit -- see QUERY_PARAM_PADDING
290
318
  // if not, data could be lost to truncation at time of sending, potentially breaking parsing / API behavior in NR1
291
- ...(this.shouldCompress && { content_encoding: 'gzip' }),
319
+ ...(!!this.gzipper && !!this.u8 && { content_encoding: 'gzip' }),
292
320
  'replay.firstTimestamp': firstTimestamp,
293
321
  'replay.firstTimestampOffset': firstTimestamp - agentOffset,
294
322
  'replay.lastTimestamp': lastTimestamp,
@@ -102,13 +102,13 @@ export class Recorder {
102
102
  /** Only stop ignoring data if already ignoring and a new valid snapshap is taking place (0 incompletes and we get a meta node for the snap) */
103
103
  if (!incompletes && this.#fixing && event.type === RRWEB_EVENT_TYPES.Meta) this.#fixing = false
104
104
  if (incompletes) {
105
- handle(SUPPORTABILITY_METRIC_CHANNEL, ['SessionReplay/Payload/Missing-Inline-Css', incompletes], undefined, FEATURE_NAMES.metrics, this.parent.ee)
106
105
  /** wait for the evaluator to download/replace the incompletes' src code and then take a new snap */
107
106
  stylesheetEvaluator.fix().then((failedToFix) => {
108
107
  if (failedToFix) {
109
108
  this.currentBufferTarget.inlinedAllStylesheets = false
110
109
  this.shouldFix = false
111
- }
110
+ handle(SUPPORTABILITY_METRIC_CHANNEL, ['SessionReplay/Payload/Missing-Inline-Css/Failed', failedToFix], undefined, FEATURE_NAMES.metrics, this.parent.ee)
111
+ } else handle(SUPPORTABILITY_METRIC_CHANNEL, ['SessionReplay/Payload/Missing-Inline-Css/Fixed', incompletes - failedToFix], undefined, FEATURE_NAMES.metrics, this.parent.ee)
112
112
  this.takeFullSnapshot()
113
113
  })
114
114
  /** Only start ignoring data if got a faulty snapshot */
@@ -166,7 +166,12 @@ export class Recorder {
166
166
 
167
167
  /** force the recording lib to take a full DOM snapshot. This needs to occur in certain cases, like visibility changes */
168
168
  takeFullSnapshot () {
169
- recorder.takeFullSnapshot()
169
+ try {
170
+ if (!this.recording) return
171
+ recorder.takeFullSnapshot()
172
+ } catch (err) {
173
+ // 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
174
+ }
170
175
  }
171
176
 
172
177
  clearTimestamps () {
@@ -184,7 +189,7 @@ export class Recorder {
184
189
  * https://staging.onenr.io/037jbJWxbjy
185
190
  * */
186
191
  estimateCompression (data) {
187
- if (this.shouldCompress) return data * AVG_COMPRESSION
192
+ if (!!this.parent.gzipper && !!this.parent.u8) return data * AVG_COMPRESSION
188
193
  return data
189
194
  }
190
195
  }
@@ -9,7 +9,7 @@ class StylesheetEvaluator {
9
9
  * Used at harvest time to denote that all subsequent payloads are subject to this and customers should be advised to handle crossorigin decoration
10
10
  * */
11
11
  invalidStylesheetsDetected = false
12
- failedToFix = false
12
+ failedToFix = 0
13
13
 
14
14
  /**
15
15
  * this works by checking (only ever once) each cssRules obj in the style sheets array. The try/catch will catch an error if the cssRules obj blocks access, triggering the module to try to "fix" the asset`. Returns the count of incomplete assets discovered.
@@ -44,7 +44,7 @@ class StylesheetEvaluator {
44
44
  await Promise.all(this.#fetchProms)
45
45
  this.#fetchProms = []
46
46
  const failedToFix = this.failedToFix
47
- this.failedToFix = false
47
+ this.failedToFix = 0
48
48
  return failedToFix
49
49
  }
50
50
 
@@ -55,28 +55,33 @@ class StylesheetEvaluator {
55
55
  * @returns {Promise}
56
56
  */
57
57
  async #fetchAndOverride (target, href) {
58
- const stylesheetContents = await originals.FETCH.bind(window)(href)
59
- if (!stylesheetContents.ok) {
60
- this.failedToFix = true
61
- return
62
- }
63
- const stylesheetText = await stylesheetContents.text()
64
58
  try {
65
- const cssSheet = new CSSStyleSheet()
66
- await cssSheet.replace(stylesheetText)
67
- Object.defineProperty(target, 'cssRules', {
68
- get () { return cssSheet.cssRules }
69
- })
70
- Object.defineProperty(target, 'rules', {
71
- get () { return cssSheet.rules }
72
- })
73
- } catch (err) {
59
+ const stylesheetContents = await originals.FETCH.bind(window)(href)
60
+ if (!stylesheetContents.ok) {
61
+ this.failedToFix++
62
+ return
63
+ }
64
+ const stylesheetText = await stylesheetContents.text()
65
+ try {
66
+ const cssSheet = new CSSStyleSheet()
67
+ await cssSheet.replace(stylesheetText)
68
+ Object.defineProperty(target, 'cssRules', {
69
+ get () { return cssSheet.cssRules }
70
+ })
71
+ Object.defineProperty(target, 'rules', {
72
+ get () { return cssSheet.rules }
73
+ })
74
+ } catch (err) {
74
75
  // cant make new dynamic stylesheets, browser likely doesn't support `.replace()`...
75
76
  // this is appended in prep of forking rrweb
76
- Object.defineProperty(target, 'cssText', {
77
- get () { return stylesheetText }
78
- })
79
- this.failedToFix = true
77
+ Object.defineProperty(target, 'cssText', {
78
+ get () { return stylesheetText }
79
+ })
80
+ this.failedToFix++
81
+ }
82
+ } catch (err) {
83
+ // failed to fetch
84
+ this.failedToFix++
80
85
  }
81
86
  }
82
87
  }
@@ -108,7 +108,7 @@ export class InstrumentBase extends FeatureBase {
108
108
  warn(`Downloading and initializing ${this.featureName} failed...`, e)
109
109
  this.abortHandler?.() // undo any important alterations made to the page
110
110
  // not supported yet but nice to do: "abort" this agent's EE for this feature specifically
111
- drain(this.agentIdentifier, this.featureName)
111
+ drain(this.agentIdentifier, this.featureName, true)
112
112
  loadedSuccessfully(false)
113
113
  }
114
114
  }
@@ -207,7 +207,10 @@ export function setAPI (agentIdentifier, forceDrain) {
207
207
  import(/* webpackChunkName: "async-api" */'./apiAsync').then(({ setAPI }) => {
208
208
  setAPI(agentIdentifier)
209
209
  drain(agentIdentifier, 'api')
210
- }).catch(() => warn('Downloading runtime APIs failed...'))
210
+ }).catch(() => {
211
+ warn('Downloading runtime APIs failed...')
212
+ drain(agentIdentifier, 'api', true)
213
+ })
211
214
  }
212
215
 
213
216
  return apiInterface