@newrelic/browser-agent 1.249.0 → 1.250.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 (74) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/cjs/common/config/state/init.js +1 -1
  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/harvest/harvest-scheduler.js +2 -2
  6. package/dist/cjs/common/harvest/harvest.js +4 -3
  7. package/dist/cjs/common/ids/unique-id.js +1 -1
  8. package/dist/cjs/common/session/constants.js +20 -2
  9. package/dist/cjs/common/session/session-entity.js +8 -26
  10. package/dist/cjs/common/url/encode.js +2 -0
  11. package/dist/cjs/features/metrics/aggregate/index.js +3 -1
  12. package/dist/cjs/features/session_replay/aggregate/index.js +114 -277
  13. package/dist/cjs/features/session_replay/constants.js +57 -2
  14. package/dist/cjs/features/session_replay/instrument/index.js +38 -16
  15. package/dist/cjs/features/session_replay/shared/recorder-events.js +31 -0
  16. package/dist/cjs/features/session_replay/shared/recorder.js +155 -0
  17. package/dist/cjs/features/session_replay/{replay-mode.js → shared/replay-mode.js} +5 -5
  18. package/dist/cjs/features/session_trace/aggregate/index.js +25 -25
  19. package/dist/esm/common/config/state/init.js +1 -1
  20. package/dist/esm/common/constants/env.cdn.js +1 -1
  21. package/dist/esm/common/constants/env.npm.js +1 -1
  22. package/dist/esm/common/harvest/harvest-scheduler.js +1 -1
  23. package/dist/esm/common/harvest/harvest.js +4 -3
  24. package/dist/esm/common/ids/unique-id.js +1 -1
  25. package/dist/esm/common/session/constants.js +16 -1
  26. package/dist/esm/common/session/session-entity.js +2 -16
  27. package/dist/esm/common/url/encode.js +2 -0
  28. package/dist/esm/features/metrics/aggregate/index.js +3 -1
  29. package/dist/esm/features/session_replay/aggregate/index.js +95 -254
  30. package/dist/esm/features/session_replay/constants.js +49 -1
  31. package/dist/esm/features/session_replay/instrument/index.js +24 -1
  32. package/dist/esm/features/session_replay/shared/recorder-events.js +24 -0
  33. package/dist/esm/features/session_replay/shared/recorder.js +148 -0
  34. package/dist/esm/features/session_replay/{replay-mode.js → shared/replay-mode.js} +4 -4
  35. package/dist/esm/features/session_trace/aggregate/index.js +2 -2
  36. package/dist/types/common/harvest/harvest.d.ts +1 -1
  37. package/dist/types/common/harvest/harvest.d.ts.map +1 -1
  38. package/dist/types/common/session/constants.d.ts +15 -0
  39. package/dist/types/common/session/session-entity.d.ts +0 -15
  40. package/dist/types/common/session/session-entity.d.ts.map +1 -1
  41. package/dist/types/common/url/encode.d.ts +1 -1
  42. package/dist/types/common/url/encode.d.ts.map +1 -1
  43. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  44. package/dist/types/features/session_replay/aggregate/index.d.ts +7 -63
  45. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  46. package/dist/types/features/session_replay/constants.d.ts +55 -0
  47. package/dist/types/features/session_replay/constants.d.ts.map +1 -1
  48. package/dist/types/features/session_replay/instrument/index.d.ts +2 -0
  49. package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
  50. package/dist/types/features/session_replay/shared/recorder-events.d.ts +21 -0
  51. package/dist/types/features/session_replay/shared/recorder-events.d.ts.map +1 -0
  52. package/dist/types/features/session_replay/shared/recorder.d.ts +40 -0
  53. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -0
  54. package/dist/types/features/session_replay/shared/replay-mode.d.ts.map +1 -0
  55. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  56. package/package.json +1 -1
  57. package/src/common/config/state/init.js +1 -1
  58. package/src/common/harvest/harvest-scheduler.js +1 -1
  59. package/src/common/harvest/harvest.js +4 -3
  60. package/src/common/ids/unique-id.js +1 -1
  61. package/src/common/session/__mocks__/session-entity.js +0 -6
  62. package/src/common/session/constants.js +18 -0
  63. package/src/common/session/session-entity.js +1 -17
  64. package/src/common/url/encode.js +2 -1
  65. package/src/features/metrics/aggregate/index.js +3 -1
  66. package/src/features/session_replay/aggregate/index.js +88 -246
  67. package/src/features/session_replay/constants.js +45 -0
  68. package/src/features/session_replay/instrument/index.js +18 -1
  69. package/src/features/session_replay/shared/recorder-events.js +25 -0
  70. package/src/features/session_replay/shared/recorder.js +145 -0
  71. package/src/features/session_replay/{replay-mode.js → shared/replay-mode.js} +4 -4
  72. package/src/features/session_trace/aggregate/index.js +2 -2
  73. package/dist/types/features/session_replay/replay-mode.d.ts.map +0 -1
  74. /package/dist/types/features/session_replay/{replay-mode.d.ts → shared/replay-mode.d.ts} +0 -0
@@ -9,6 +9,7 @@
9
9
  * It is not production ready, and is not intended to be imported or implemented in any build of the browser agent until
10
10
  * functionality is validated and a full user experience is curated.
11
11
  */
12
+ import { MODE } from '../../../common/session/constants'
12
13
  import { InstrumentBase } from '../../utils/instrument-base'
13
14
  import { FEATURE_NAME } from '../constants'
14
15
 
@@ -16,6 +17,22 @@ export class Instrument extends InstrumentBase {
16
17
  static featureName = FEATURE_NAME
17
18
  constructor (agentIdentifier, aggregator, auto = true) {
18
19
  super(agentIdentifier, aggregator, FEATURE_NAME, auto)
19
- this.importAggregator()
20
+ try {
21
+ const session = JSON.parse(localStorage.getItem('NRBA_SESSION'))
22
+ if (session.sessionReplayMode !== MODE.OFF) {
23
+ this.#startRecording(session.sessionReplayMode)
24
+ } else {
25
+ this.importAggregator({})
26
+ }
27
+ } catch (err) {
28
+ this.importAggregator({})
29
+ }
30
+ }
31
+
32
+ async #startRecording (mode) {
33
+ const { Recorder } = (await import(/* webpackChunkName: "recorder" */'../shared/recorder'))
34
+ this.recorder = new Recorder({ mode, agentIdentifier: this.agentIdentifier })
35
+ this.recorder.startRecording()
36
+ this.importAggregator({ recorder: this.recorder })
20
37
  }
21
38
  }
@@ -0,0 +1,25 @@
1
+ export class RecorderEvents {
2
+ constructor () {
3
+ /** The buffer to hold recorder event nodes */
4
+ this.events = []
5
+ /** Payload metadata -- Should indicate when a replay blob started recording. Resets each time a harvest occurs.
6
+ * cycle timestamps are used as fallbacks if event timestamps cannot be used
7
+ */
8
+ this.cycleTimestamp = Date.now()
9
+ /** A value which increments with every new mutation node reported. Resets after a harvest is sent */
10
+ this.payloadBytesEstimation = 0
11
+ /** Payload metadata -- Should indicate that the payload being sent has a full DOM snapshot. This can happen
12
+ * -- When the recording library begins recording, it starts by taking a DOM snapshot
13
+ * -- When visibility changes from "hidden" -> "visible", it must capture a full snapshot for the replay to work correctly across tabs
14
+ */
15
+ this.hasSnapshot = false
16
+ /** Payload metadata -- Should indicate that the payload being sent has a meta node. The meta node should always precede a snapshot node. */
17
+ this.hasMeta = false
18
+ /** Payload metadata -- Should indicate that the payload being sent contains an error. Used for query/filter purposes in UI */
19
+ this.hasError = false
20
+ }
21
+
22
+ add (event) {
23
+ this.events.push(event)
24
+ }
25
+ }
@@ -0,0 +1,145 @@
1
+ import { record as recorder } from 'rrweb'
2
+ import { stringify } from '../../../common/util/stringify'
3
+ import { AVG_COMPRESSION, CHECKOUT_MS, IDEAL_PAYLOAD_SIZE, QUERY_PARAM_PADDING, RRWEB_EVENT_TYPES } from '../constants'
4
+ import { getConfigurationValue } from '../../../common/config/config'
5
+ import { RecorderEvents } from './recorder-events'
6
+ import { MODE } from '../../../common/session/constants'
7
+
8
+ export class Recorder {
9
+ /** Each page mutation or event will be stored (raw) in this array. This array will be cleared on each harvest */
10
+ #events = new RecorderEvents()
11
+ /** Backlog used for a 2-part sliding window to guarantee a 15-30s buffer window */
12
+ #backloggedEvents = new RecorderEvents()
13
+ /** array of recorder events -- Will be filled only if forced harvest was triggered and harvester does not exist */
14
+ #preloaded = [new RecorderEvents()]
15
+
16
+ constructor (parent) {
17
+ /** True when actively recording, false when paused or stopped */
18
+ this.recording = false
19
+ this.currentBufferTarget = this.#events
20
+ /** 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 */
21
+ this.lastMeta = false
22
+
23
+ this.parent = parent
24
+
25
+ /** The method to stop recording. This defaults to a noop, but is overwritten once the recording library is imported and initialized */
26
+ this.stopRecording = () => { /* no-op until set by rrweb initializer */ }
27
+ }
28
+
29
+ getEvents () {
30
+ if (this.#preloaded[0]?.events.length) return { ...this.#preloaded[0], type: 'preloaded' }
31
+ return {
32
+ events: [...this.#backloggedEvents.events, ...this.#events.events].filter(x => x),
33
+ type: 'standard',
34
+ cycleTimestamp: Math.min(this.#backloggedEvents.cycleTimestamp, this.#events.cycleTimestamp),
35
+ payloadBytesEstimation: this.#backloggedEvents.payloadBytesEstimation + this.#events.payloadBytesEstimation,
36
+ hasError: this.#backloggedEvents.hasError || this.#events.hasError,
37
+ hasMeta: this.#backloggedEvents.hasMeta || this.#events.hasMeta,
38
+ hasSnapshot: this.#backloggedEvents.hasSnapshot || this.#events.hasSnapshot
39
+ }
40
+ }
41
+
42
+ /** Clears the buffer (this.#events), and resets all payload metadata properties */
43
+ clearBuffer () {
44
+ if (this.#preloaded[0]?.events.length) this.#preloaded.shift()
45
+ else if (this.parent.mode === MODE.ERROR) this.#backloggedEvents = this.#events
46
+ else this.#backloggedEvents = new RecorderEvents()
47
+ this.#events = new RecorderEvents()
48
+ }
49
+
50
+ /** Begin recording using configured recording lib */
51
+ startRecording () {
52
+ this.recording = true
53
+ const { block_class, ignore_class, mask_text_class, block_selector, mask_input_options, mask_text_selector, mask_all_inputs, inline_stylesheet, inline_images, collect_fonts } = getConfigurationValue(this.parent.agentIdentifier, 'session_replay')
54
+ // set up rrweb configurations for maximum privacy --
55
+ // https://newrelic.atlassian.net/wiki/spaces/O11Y/pages/2792293280/2023+02+28+Browser+-+Session+Replay#Configuration-options
56
+ const stop = recorder({
57
+ emit: this.store.bind(this),
58
+ blockClass: block_class,
59
+ ignoreClass: ignore_class,
60
+ maskTextClass: mask_text_class,
61
+ blockSelector: block_selector,
62
+ maskInputOptions: mask_input_options,
63
+ maskTextSelector: mask_text_selector,
64
+ maskAllInputs: mask_all_inputs,
65
+ inlineStylesheet: inline_stylesheet,
66
+ inlineImages: inline_images,
67
+ collectFonts: collect_fonts,
68
+ checkoutEveryNms: CHECKOUT_MS[this.parent.mode]
69
+ })
70
+
71
+ this.stopRecording = () => {
72
+ this.recording = false
73
+ stop()
74
+ }
75
+ }
76
+
77
+ /** Store a payload in the buffer (this.#events). This should be the callback to the recording lib noticing a mutation */
78
+ store (event, isCheckout) {
79
+ event.__serialized = stringify(event)
80
+
81
+ if (!this.parent.scheduler) this.currentBufferTarget = this.#preloaded[this.#preloaded.length - 1]
82
+ else this.currentBufferTarget = this.#events
83
+
84
+ if (this.parent.blocked) return
85
+ const eventBytes = event.__serialized.length
86
+ /** The estimated size of the payload after compression */
87
+ const payloadSize = this.getPayloadSize(eventBytes)
88
+ // Checkout events are flags by the recording lib that indicate a fullsnapshot was taken every n ms. These are important
89
+ // to help reconstruct the replay later and must be included. While waiting and buffering for errors to come through,
90
+ // each time we see a new checkout, we can drop the old data.
91
+ // we need to check for meta because rrweb will flag it as checkout twice, once for meta, then once for snapshot
92
+ if (this.parent.mode === MODE.ERROR && isCheckout && event.type === RRWEB_EVENT_TYPES.Meta) {
93
+ // we are still waiting for an error to throw, so keep wiping the buffer over time
94
+ this.clearBuffer()
95
+ }
96
+
97
+ // meta event
98
+ if (event.type === RRWEB_EVENT_TYPES.Meta) {
99
+ this.currentBufferTarget.hasMeta = true
100
+ }
101
+ // snapshot event
102
+ if (event.type === RRWEB_EVENT_TYPES.FullSnapshot) {
103
+ this.currentBufferTarget.hasSnapshot = true
104
+ }
105
+
106
+ this.currentBufferTarget.add(event)
107
+ this.currentBufferTarget.payloadBytesEstimation += eventBytes
108
+
109
+ // We are making an effort to try to keep payloads manageable for unloading. If they reach the unload limit before their interval,
110
+ // it will send immediately. This often happens on the first snapshot, which can be significantly larger than the other payloads.
111
+ if (payloadSize > IDEAL_PAYLOAD_SIZE && this.parent.mode !== MODE.ERROR) {
112
+ // if we've made it to the ideal size of ~64kb before the interval timer, we should send early.
113
+ if (this.parent.scheduler) {
114
+ this.parent.scheduler.runHarvest()
115
+ } else {
116
+ // we are still in "preload" and it triggered a "stop point". Make a new set, which will get pointed at on next cycle
117
+ this.#preloaded.push(new RecorderEvents())
118
+ }
119
+ }
120
+ }
121
+
122
+ /** force the recording lib to take a full DOM snapshot. This needs to occur in certain cases, like visibility changes */
123
+ takeFullSnapshot () {
124
+ recorder.takeFullSnapshot()
125
+ }
126
+
127
+ clearTimestamps () {
128
+ this.currentBufferTarget.cycleTimestamp = undefined
129
+ }
130
+
131
+ /** Estimate the payload size */
132
+ getPayloadSize (newBytes = 0) {
133
+ // the query param padding constant gives us some padding for the other metadata to be safely injected
134
+ return this.estimateCompression(this.currentBufferTarget.payloadBytesEstimation + newBytes) + QUERY_PARAM_PADDING
135
+ }
136
+
137
+ /** Extensive research has yielded about an 88% compression factor on these payloads.
138
+ * This is an estimation using that factor as to not cause performance issues while evaluating
139
+ * https://staging.onenr.io/037jbJWxbjy
140
+ * */
141
+ estimateCompression (data) {
142
+ if (this.shouldCompress) return data * AVG_COMPRESSION
143
+ return data
144
+ }
145
+ }
@@ -1,7 +1,7 @@
1
- import { getConfigurationValue } from '../../common/config/config'
2
- import { MODE } from '../../common/session/session-entity'
3
- import { gosNREUM } from '../../common/window/nreum'
4
- import { sharedChannel } from '../../common/constants/shared-channel'
1
+ import { getConfigurationValue } from '../../../common/config/config'
2
+ import { gosNREUM } from '../../../common/window/nreum'
3
+ import { sharedChannel } from '../../../common/constants/shared-channel'
4
+ import { MODE } from '../../../common/session/constants'
5
5
 
6
6
  /**
7
7
  * Figure out if the Replay feature is running (what mode it's in).
@@ -9,9 +9,9 @@ import { getConfigurationValue, getRuntime } from '../../../common/config/config
9
9
  import { now } from '../../../common/timing/now'
10
10
  import { FEATURE_NAME } from '../constants'
11
11
  import { HandlerCache } from '../../utils/handler-cache'
12
- import { MODE, SESSION_EVENTS } from '../../../common/session/session-entity'
13
- import { getSessionReplayMode } from '../../session_replay/replay-mode'
12
+ import { getSessionReplayMode } from '../../session_replay/shared/replay-mode'
14
13
  import { AggregateBase } from '../../utils/aggregate-base'
14
+ import { MODE, SESSION_EVENTS } from '../../../common/session/constants'
15
15
 
16
16
  const ignoredEvents = {
17
17
  // we find that certain events make the data too noisy to be useful
@@ -1 +0,0 @@
1
- {"version":3,"file":"replay-mode.d.ts","sourceRoot":"","sources":["../../../../src/features/session_replay/replay-mode.js"],"names":[],"mappings":"AAKA;;;;;;GAMG;AACH,oEAUC"}