@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.
- package/CHANGELOG.md +13 -0
- package/dist/cjs/common/config/state/init.js +1 -1
- 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/harvest-scheduler.js +2 -2
- package/dist/cjs/common/harvest/harvest.js +4 -3
- package/dist/cjs/common/ids/unique-id.js +1 -1
- package/dist/cjs/common/session/constants.js +20 -2
- package/dist/cjs/common/session/session-entity.js +8 -26
- package/dist/cjs/common/url/encode.js +2 -0
- package/dist/cjs/features/metrics/aggregate/index.js +3 -1
- package/dist/cjs/features/session_replay/aggregate/index.js +114 -277
- package/dist/cjs/features/session_replay/constants.js +57 -2
- package/dist/cjs/features/session_replay/instrument/index.js +38 -16
- package/dist/cjs/features/session_replay/shared/recorder-events.js +31 -0
- package/dist/cjs/features/session_replay/shared/recorder.js +155 -0
- package/dist/cjs/features/session_replay/{replay-mode.js → shared/replay-mode.js} +5 -5
- package/dist/cjs/features/session_trace/aggregate/index.js +25 -25
- package/dist/esm/common/config/state/init.js +1 -1
- 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/harvest-scheduler.js +1 -1
- package/dist/esm/common/harvest/harvest.js +4 -3
- package/dist/esm/common/ids/unique-id.js +1 -1
- package/dist/esm/common/session/constants.js +16 -1
- package/dist/esm/common/session/session-entity.js +2 -16
- package/dist/esm/common/url/encode.js +2 -0
- package/dist/esm/features/metrics/aggregate/index.js +3 -1
- package/dist/esm/features/session_replay/aggregate/index.js +95 -254
- package/dist/esm/features/session_replay/constants.js +49 -1
- package/dist/esm/features/session_replay/instrument/index.js +24 -1
- package/dist/esm/features/session_replay/shared/recorder-events.js +24 -0
- package/dist/esm/features/session_replay/shared/recorder.js +148 -0
- package/dist/esm/features/session_replay/{replay-mode.js → shared/replay-mode.js} +4 -4
- package/dist/esm/features/session_trace/aggregate/index.js +2 -2
- package/dist/types/common/harvest/harvest.d.ts +1 -1
- package/dist/types/common/harvest/harvest.d.ts.map +1 -1
- package/dist/types/common/session/constants.d.ts +15 -0
- package/dist/types/common/session/session-entity.d.ts +0 -15
- package/dist/types/common/session/session-entity.d.ts.map +1 -1
- package/dist/types/common/url/encode.d.ts +1 -1
- package/dist/types/common/url/encode.d.ts.map +1 -1
- package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +7 -63
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/constants.d.ts +55 -0
- package/dist/types/features/session_replay/constants.d.ts.map +1 -1
- package/dist/types/features/session_replay/instrument/index.d.ts +2 -0
- package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
- package/dist/types/features/session_replay/shared/recorder-events.d.ts +21 -0
- package/dist/types/features/session_replay/shared/recorder-events.d.ts.map +1 -0
- package/dist/types/features/session_replay/shared/recorder.d.ts +40 -0
- package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -0
- package/dist/types/features/session_replay/shared/replay-mode.d.ts.map +1 -0
- package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/common/config/state/init.js +1 -1
- package/src/common/harvest/harvest-scheduler.js +1 -1
- package/src/common/harvest/harvest.js +4 -3
- package/src/common/ids/unique-id.js +1 -1
- package/src/common/session/__mocks__/session-entity.js +0 -6
- package/src/common/session/constants.js +18 -0
- package/src/common/session/session-entity.js +1 -17
- package/src/common/url/encode.js +2 -1
- package/src/features/metrics/aggregate/index.js +3 -1
- package/src/features/session_replay/aggregate/index.js +88 -246
- package/src/features/session_replay/constants.js +45 -0
- package/src/features/session_replay/instrument/index.js +18 -1
- package/src/features/session_replay/shared/recorder-events.js +25 -0
- package/src/features/session_replay/shared/recorder.js +145 -0
- package/src/features/session_replay/{replay-mode.js → shared/replay-mode.js} +4 -4
- package/src/features/session_trace/aggregate/index.js +2 -2
- package/dist/types/features/session_replay/replay-mode.d.ts.map +0 -1
- /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
|
-
|
|
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 '
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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 {
|
|
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"}
|
|
File without changes
|