@newrelic/browser-agent 1.242.0 → 1.243.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 +1465 -0
- package/dist/cjs/common/constants/env.cdn.js +1 -1
- package/dist/cjs/common/constants/env.npm.js +1 -1
- package/dist/cjs/common/session/session-entity.js +20 -2
- package/dist/cjs/common/wrap/wrap-function.js +1 -1
- package/dist/cjs/features/ajax/aggregate/index.js +1 -1
- package/dist/cjs/features/session_replay/aggregate/index.js +84 -54
- package/dist/cjs/features/utils/feature-base.js +1 -2
- package/dist/cjs/loaders/api/api.js +2 -2
- package/dist/cjs/loaders/api/apiAsync.js +0 -37
- package/dist/cjs/loaders/configure/configure.js +1 -1
- package/dist/cjs/loaders/configure/public-path.js +6 -3
- package/dist/esm/common/constants/env.cdn.js +1 -1
- package/dist/esm/common/constants/env.npm.js +1 -1
- package/dist/esm/common/session/session-entity.js +18 -1
- package/dist/esm/common/wrap/wrap-function.js +1 -1
- package/dist/esm/features/ajax/aggregate/index.js +1 -1
- package/dist/esm/features/session_replay/aggregate/index.js +83 -54
- package/dist/esm/features/utils/feature-base.js +1 -2
- package/dist/esm/loaders/api/api.js +2 -2
- package/dist/esm/loaders/api/apiAsync.js +1 -36
- package/dist/esm/loaders/configure/configure.js +1 -1
- package/dist/esm/loaders/configure/public-path.js +6 -3
- package/dist/types/common/session/session-entity.d.ts +5 -0
- package/dist/types/common/session/session-entity.d.ts.map +1 -1
- package/dist/types/features/session_replay/aggregate/index.d.ts +11 -14
- package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
- package/dist/types/features/utils/feature-base.d.ts.map +1 -1
- package/dist/types/loaders/api/api.d.ts.map +1 -1
- package/dist/types/loaders/api/apiAsync.d.ts.map +1 -1
- package/dist/types/loaders/configure/public-path.d.ts +1 -1
- package/dist/types/loaders/configure/public-path.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/common/session/session-entity.js +20 -1
- package/src/common/wrap/wrap-function.js +1 -1
- package/src/features/ajax/aggregate/index.js +2 -2
- package/src/features/session_replay/aggregate/index.js +81 -37
- package/src/features/utils/feature-base.js +1 -2
- package/src/loaders/api/api.js +1 -2
- package/src/loaders/api/apiAsync.js +1 -39
- package/src/loaders/configure/configure.js +1 -1
- package/src/loaders/configure/public-path.js +6 -3
- package/src/common/aggregate/aggregator.test.js +0 -107
- package/src/common/config/state/configurable.test.js +0 -73
- package/src/common/config/state/info.test.js +0 -31
- package/src/common/config/state/init.test.js +0 -68
- package/src/common/config/state/loader-config.test.js +0 -21
- package/src/common/config/state/runtime.test.js +0 -21
- package/src/common/constants/env.cdn.test.js +0 -7
- package/src/common/constants/env.npm.test.js +0 -7
- package/src/common/constants/env.test.js +0 -7
- package/src/common/constants/runtime.test.js +0 -176
- package/src/common/deny-list/deny-list.test.js +0 -104
- package/src/common/dom/query-selector.test.js +0 -24
- package/src/common/drain/drain.test.js +0 -74
- package/src/common/event-emitter/contextual-ee.component-test.js +0 -293
- package/src/common/event-emitter/handle.test.js +0 -56
- package/src/common/event-emitter/register-handler.test.js +0 -61
- package/src/common/harvest/harvest-scheduler.test.js +0 -492
- package/src/common/harvest/harvest.test.js +0 -813
- package/src/common/ids/id.test.js +0 -92
- package/src/common/ids/unique-id.test.js +0 -58
- package/src/common/session/session-entity.component-test.js +0 -346
- package/src/common/storage/local-storage.test.js +0 -17
- package/src/common/timer/interaction-timer.component-test.js +0 -212
- package/src/common/timer/timer.test.js +0 -99
- package/src/common/timing/nav-timing.test.js +0 -161
- package/src/common/url/canonicalize-url.test.js +0 -45
- package/src/common/url/clean-url.test.js +0 -25
- package/src/common/url/encode.test.js +0 -81
- package/src/common/url/location.test.js +0 -15
- package/src/common/url/parse-url.test.js +0 -110
- package/src/common/url/protocol.test.js +0 -17
- package/src/common/util/console.test.js +0 -34
- package/src/common/util/data-size.test.js +0 -56
- package/src/common/util/feature-flags.test.js +0 -94
- package/src/common/util/get-or-set.test.js +0 -58
- package/src/common/util/invoke.test.js +0 -65
- package/src/common/util/map-own.test.js +0 -52
- package/src/common/util/obfuscate.component-test.js +0 -173
- package/src/common/util/stringify.test.js +0 -49
- package/src/common/util/submit-data.test.js +0 -183
- package/src/common/util/traverse.test.js +0 -50
- package/src/common/vitals/cumulative-layout-shift.test.js +0 -71
- package/src/common/vitals/first-contentful-paint.test.js +0 -124
- package/src/common/vitals/first-input-delay.test.js +0 -88
- package/src/common/vitals/first-paint.test.js +0 -127
- package/src/common/vitals/interaction-to-next-paint.test.js +0 -74
- package/src/common/vitals/largest-contentful-paint.test.js +0 -94
- package/src/common/vitals/long-task.test.js +0 -122
- package/src/common/vitals/time-to-first-byte.test.js +0 -147
- package/src/common/vitals/vital-metric.test.js +0 -171
- package/src/common/wrap/wrap-promise.component-test.js +0 -110
- package/src/features/ajax/instrument/distributed-tracing.test.js +0 -375
- package/src/features/jserrors/aggregate/canonical-function-name.test.js +0 -13
- package/src/features/jserrors/aggregate/compute-stack-trace.test.js +0 -414
- package/src/features/jserrors/aggregate/format-stack-trace.test.js +0 -39
- package/src/features/jserrors/aggregate/string-hash-code.test.js +0 -12
- package/src/features/metrics/aggregate/framework-detection.test.js +0 -332
- package/src/features/page_view_timing/aggregate/index.component-test.js +0 -86
- package/src/features/session_replay/aggregate/index.component-test.js +0 -317
- package/src/features/spa/aggregate/interaction-node.test.js +0 -17
- package/src/features/utils/agent-session.test.js +0 -194
- package/src/features/utils/aggregate-base.test.js +0 -123
- package/src/features/utils/feature-base.test.js +0 -45
- package/src/features/utils/handler-cache.test.js +0 -72
- package/src/features/utils/instrument-base.test.js +0 -216
- package/src/features/utils/lazy-feature-loader.test.js +0 -37
- package/src/loaders/api/api.component-test.js +0 -45
- package/src/loaders/api/api.test.js +0 -85
- package/src/loaders/api/apiAsync.test.js +0 -17
|
@@ -15,20 +15,47 @@ import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler'
|
|
|
15
15
|
import { FEATURE_NAME } from '../constants'
|
|
16
16
|
import { stringify } from '../../../common/util/stringify'
|
|
17
17
|
import { getConfigurationValue, getInfo, getRuntime } from '../../../common/config/config'
|
|
18
|
-
import { SESSION_EVENTS, MODE } from '../../../common/session/session-entity'
|
|
18
|
+
import { SESSION_EVENTS, MODE, SESSION_EVENT_TYPES } from '../../../common/session/session-entity'
|
|
19
19
|
import { AggregateBase } from '../../utils/aggregate-base'
|
|
20
20
|
import { sharedChannel } from '../../../common/constants/shared-channel'
|
|
21
21
|
import { obj as encodeObj } from '../../../common/url/encode'
|
|
22
22
|
import { warn } from '../../../common/util/console'
|
|
23
23
|
import { globalScope } from '../../../common/constants/runtime'
|
|
24
24
|
import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
|
|
25
|
-
import { FEATURE_NAMES } from '../../../loaders/features/features'
|
|
26
25
|
|
|
27
26
|
// would be better to get this dynamically in some way
|
|
28
27
|
export const RRWEB_VERSION = '2.0.0-alpha.8'
|
|
29
28
|
|
|
30
29
|
export const AVG_COMPRESSION = 0.12
|
|
31
30
|
|
|
31
|
+
export const RRWEB_EVENT_TYPES = {
|
|
32
|
+
DomContentLoaded: 0,
|
|
33
|
+
Load: 1,
|
|
34
|
+
FullSnapshot: 2,
|
|
35
|
+
IncrementalSnapshot: 3,
|
|
36
|
+
Meta: 4,
|
|
37
|
+
Custom: 5
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const ABORT_REASONS = {
|
|
41
|
+
RESET: {
|
|
42
|
+
message: 'Session was reset',
|
|
43
|
+
sm: 'Reset'
|
|
44
|
+
},
|
|
45
|
+
IMPORT: {
|
|
46
|
+
message: 'Recorder failed to import',
|
|
47
|
+
sm: 'Import'
|
|
48
|
+
},
|
|
49
|
+
TOO_MANY: {
|
|
50
|
+
message: '429: Too Many Requests',
|
|
51
|
+
sm: 'Too-Many'
|
|
52
|
+
},
|
|
53
|
+
TOO_BIG: {
|
|
54
|
+
message: 'Payload was too large',
|
|
55
|
+
sm: 'Too-Big'
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
32
59
|
let recorder, gzipper, u8
|
|
33
60
|
|
|
34
61
|
/** Vortex caps payload sizes at 1MB */
|
|
@@ -59,8 +86,6 @@ export class Aggregate extends AggregateBase {
|
|
|
59
86
|
/** can shut off efforts to compress the data */
|
|
60
87
|
this.shouldCompress = true
|
|
61
88
|
|
|
62
|
-
/** Payload metadata -- Should indicate that the payload being sent is the first of a session */
|
|
63
|
-
this.isFirstChunk = false
|
|
64
89
|
/** Payload metadata -- Should indicate that the payload being sent has a full DOM snapshot. This can happen
|
|
65
90
|
* -- When the recording library begins recording, it starts by taking a DOM snapshot
|
|
66
91
|
* -- When visibility changes from "hidden" -> "visible", it must capture a full snapshot for the replay to work correctly across tabs
|
|
@@ -74,7 +99,7 @@ export class Aggregate extends AggregateBase {
|
|
|
74
99
|
/** Payload metadata -- Should indicate when a replay blob started recording. Resets each time a harvest occurs.
|
|
75
100
|
* cycle timestamps are used as fallbacks if event timestamps cannot be used
|
|
76
101
|
*/
|
|
77
|
-
this.
|
|
102
|
+
this.cycleTimestamp = undefined
|
|
78
103
|
|
|
79
104
|
/** A value which increments with every new mutation node reported. Resets after a harvest is sent */
|
|
80
105
|
this.payloadBytesEstimation = 0
|
|
@@ -93,17 +118,26 @@ export class Aggregate extends AggregateBase {
|
|
|
93
118
|
if (shouldSetup) {
|
|
94
119
|
// The SessionEntity class can emit a message indicating the session was cleared and reset (expiry, inactivity). This feature must abort and never resume if that occurs.
|
|
95
120
|
this.ee.on(SESSION_EVENTS.RESET, () => {
|
|
96
|
-
this.abort(
|
|
121
|
+
this.abort(ABORT_REASONS.RESET)
|
|
97
122
|
})
|
|
98
123
|
|
|
99
124
|
// The SessionEntity class can emit a message indicating the session was paused (visibility change). This feature must stop recording if that occurs.
|
|
100
125
|
this.ee.on(SESSION_EVENTS.PAUSE, () => { this.stopRecording() })
|
|
101
126
|
// The SessionEntity class can emit a message indicating the session was resumed (visibility change). This feature must start running again (if already running) if that occurs.
|
|
102
127
|
this.ee.on(SESSION_EVENTS.RESUME, () => {
|
|
128
|
+
// if the mode changed on a different tab, it needs to update this instance to match
|
|
129
|
+
const { session } = getRuntime(this.agentIdentifier)
|
|
130
|
+
this.mode = session.state.sessionReplay
|
|
103
131
|
if (!this.initialized || this.mode === MODE.OFF) return
|
|
104
132
|
this.startRecording()
|
|
105
133
|
})
|
|
106
134
|
|
|
135
|
+
this.ee.on(SESSION_EVENTS.UPDATE, (type, data) => {
|
|
136
|
+
if (!this.initialized || this.blocked || type !== SESSION_EVENT_TYPES.CROSS_TAB) return
|
|
137
|
+
if (this.mode !== MODE.OFF && data.sessionReplay === MODE.OFF) this.abort('Session Entity was set to OFF on another tab')
|
|
138
|
+
this.mode = data.sessionReplay
|
|
139
|
+
})
|
|
140
|
+
|
|
107
141
|
// Bespoke logic for new endpoint. This will change as downstream dependencies become solidified.
|
|
108
142
|
this.scheduler = new HarvestScheduler('browser/blobs', {
|
|
109
143
|
onFinished: this.onHarvestFinished.bind(this),
|
|
@@ -118,12 +152,13 @@ export class Aggregate extends AggregateBase {
|
|
|
118
152
|
this.hasError = true
|
|
119
153
|
this.errorNoticed = true
|
|
120
154
|
// run once
|
|
121
|
-
if (this.mode === MODE.ERROR) {
|
|
155
|
+
if (this.mode === MODE.ERROR && globalScope?.document.visibilityState === 'visible') {
|
|
122
156
|
this.mode = MODE.FULL
|
|
123
157
|
// if the error was noticed AFTER the recorder was already imported....
|
|
124
158
|
if (recorder && this.initialized) {
|
|
125
159
|
this.stopRecording()
|
|
126
160
|
this.startRecording()
|
|
161
|
+
|
|
127
162
|
this.scheduler.startTimer(this.harvestTimeSeconds)
|
|
128
163
|
|
|
129
164
|
this.syncWithSessionManager({ sessionReplay: this.mode })
|
|
@@ -178,7 +213,7 @@ export class Aggregate extends AggregateBase {
|
|
|
178
213
|
// Do not change the webpackChunkName or it will break the webpack nrba-chunking plugin
|
|
179
214
|
recorder = (await import(/* webpackChunkName: "recorder" */'rrweb')).record
|
|
180
215
|
} catch (err) {
|
|
181
|
-
return this.abort(
|
|
216
|
+
return this.abort(ABORT_REASONS.IMPORT)
|
|
182
217
|
}
|
|
183
218
|
|
|
184
219
|
// FULL mode records AND reports from the beginning, while ERROR mode only records (but does not report).
|
|
@@ -200,8 +235,6 @@ export class Aggregate extends AggregateBase {
|
|
|
200
235
|
}
|
|
201
236
|
this.startRecording()
|
|
202
237
|
|
|
203
|
-
this.isFirstChunk = !!session.isNew
|
|
204
|
-
|
|
205
238
|
this.syncWithSessionManager({ sessionReplay: this.mode })
|
|
206
239
|
}
|
|
207
240
|
|
|
@@ -216,6 +249,8 @@ export class Aggregate extends AggregateBase {
|
|
|
216
249
|
this.scheduler.opts.gzip = false
|
|
217
250
|
}
|
|
218
251
|
// TODO -- Gracefully handle the buffer for retries.
|
|
252
|
+
const { session } = getRuntime(this.agentIdentifier)
|
|
253
|
+
if (!session.state.sessionReplaySentFirstChunk) this.syncWithSessionManager({ sessionReplaySentFirstChunk: true })
|
|
219
254
|
this.clearBuffer()
|
|
220
255
|
return [payload]
|
|
221
256
|
}
|
|
@@ -223,8 +258,28 @@ export class Aggregate extends AggregateBase {
|
|
|
223
258
|
getHarvestContents () {
|
|
224
259
|
const agentRuntime = getRuntime(this.agentIdentifier)
|
|
225
260
|
const info = getInfo(this.agentIdentifier)
|
|
226
|
-
|
|
227
|
-
|
|
261
|
+
|
|
262
|
+
// do not let the last node be a meta node, since this NEEDS to precede a snapshot
|
|
263
|
+
// we will manually inject it later if we find a payload that is missing a meta node
|
|
264
|
+
const payloadEndsWithMeta = this.events[this.events.length - 1]?.type === RRWEB_EVENT_TYPES.Meta
|
|
265
|
+
if (payloadEndsWithMeta) {
|
|
266
|
+
this.lastMeta = this.events[this.events.length - 1]
|
|
267
|
+
this.events = this.events.slice(0, this.events.length - 1)
|
|
268
|
+
this.hasMeta = !!this.events.find(x => x.type === RRWEB_EVENT_TYPES.Meta)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// do not let the first node be a full snapshot node, since this NEEDS to be preceded by a meta node
|
|
272
|
+
// we will manually inject it if this happens
|
|
273
|
+
const payloadStartsWithFullSnapshot = this.events[0]?.type === RRWEB_EVENT_TYPES.FullSnapshot
|
|
274
|
+
if (payloadStartsWithFullSnapshot) {
|
|
275
|
+
this.hasMeta = true
|
|
276
|
+
this.events.unshift(this.lastMeta)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const firstEventTimestamp = this.events[0]?.timestamp
|
|
280
|
+
const lastEventTimestamp = this.events[this.events.length - 1]?.timestamp
|
|
281
|
+
const firstTimestamp = firstEventTimestamp || this.cycleTimestamp
|
|
282
|
+
const lastTimestamp = lastEventTimestamp || getRuntime(this.agentIdentifier).offset + globalScope.performance.now()
|
|
228
283
|
return {
|
|
229
284
|
qs: {
|
|
230
285
|
browser_monitoring_key: info.licenseKey,
|
|
@@ -236,12 +291,13 @@ export class Aggregate extends AggregateBase {
|
|
|
236
291
|
'replay.firstTimestamp': firstTimestamp,
|
|
237
292
|
'replay.lastTimestamp': lastTimestamp,
|
|
238
293
|
'replay.durationMs': lastTimestamp - firstTimestamp,
|
|
294
|
+
'replay.nodes': this.events.length,
|
|
239
295
|
agentVersion: agentRuntime.version,
|
|
240
296
|
session: agentRuntime.session.state.value,
|
|
241
297
|
hasMeta: this.hasMeta,
|
|
242
298
|
hasSnapshot: this.hasSnapshot,
|
|
243
299
|
hasError: this.hasError,
|
|
244
|
-
isFirstChunk:
|
|
300
|
+
isFirstChunk: agentRuntime.session.state.sessionReplaySentFirstChunk === false,
|
|
245
301
|
decompressedBytes: this.payloadBytesEstimation,
|
|
246
302
|
'nr.rrweb.version': RRWEB_VERSION
|
|
247
303
|
}, MAX_PAYLOAD_SIZE - this.payloadBytesEstimation).substring(1) // remove the leading '&'
|
|
@@ -253,7 +309,7 @@ export class Aggregate extends AggregateBase {
|
|
|
253
309
|
onHarvestFinished (result) {
|
|
254
310
|
// The mutual decision for now is to stop recording and clear buffers if ingest is experiencing 429 rate limiting
|
|
255
311
|
if (result.status === 429) {
|
|
256
|
-
this.abort(
|
|
312
|
+
this.abort(ABORT_REASONS.TOO_MANY)
|
|
257
313
|
}
|
|
258
314
|
|
|
259
315
|
if (this.blocked) this.scheduler.stopTimer(true)
|
|
@@ -262,7 +318,6 @@ export class Aggregate extends AggregateBase {
|
|
|
262
318
|
/** Clears the buffer (this.events), and resets all payload metadata properties */
|
|
263
319
|
clearBuffer () {
|
|
264
320
|
this.events = []
|
|
265
|
-
this.isFirstChunk = false
|
|
266
321
|
this.hasSnapshot = false
|
|
267
322
|
this.hasMeta = false
|
|
268
323
|
this.hasError = false
|
|
@@ -274,7 +329,7 @@ export class Aggregate extends AggregateBase {
|
|
|
274
329
|
startRecording () {
|
|
275
330
|
if (!recorder) {
|
|
276
331
|
warn('Recording library was never imported')
|
|
277
|
-
return this.abort(
|
|
332
|
+
return this.abort(ABORT_REASONS.IMPORT)
|
|
278
333
|
}
|
|
279
334
|
this.clearTimestamps()
|
|
280
335
|
// set the fallbacks as early as possible
|
|
@@ -303,7 +358,7 @@ export class Aggregate extends AggregateBase {
|
|
|
303
358
|
|
|
304
359
|
/** Store a payload in the buffer (this.events). This should be the callback to the recording lib noticing a mutation */
|
|
305
360
|
store (event, isCheckout) {
|
|
306
|
-
this.setTimestamps(
|
|
361
|
+
this.setTimestamps()
|
|
307
362
|
if (this.blocked) return
|
|
308
363
|
const eventBytes = stringify(event).length
|
|
309
364
|
/** The estimated size of the payload after compression */
|
|
@@ -311,8 +366,7 @@ export class Aggregate extends AggregateBase {
|
|
|
311
366
|
// Vortex will block payloads at a certain size, we might as well not send.
|
|
312
367
|
if (payloadSize > MAX_PAYLOAD_SIZE) {
|
|
313
368
|
this.clearBuffer()
|
|
314
|
-
this.
|
|
315
|
-
return this.abort('Payload too big')
|
|
369
|
+
return this.abort(ABORT_REASONS.TOO_BIG)
|
|
316
370
|
}
|
|
317
371
|
// Checkout events are flags by the recording lib that indicate a fullsnapshot was taken every n ms. These are important
|
|
318
372
|
// to help reconstruct the replay later and must be included. While waiting and buffering for errors to come through,
|
|
@@ -323,19 +377,13 @@ export class Aggregate extends AggregateBase {
|
|
|
323
377
|
}
|
|
324
378
|
|
|
325
379
|
// meta event
|
|
326
|
-
if (event.type ===
|
|
380
|
+
if (event.type === RRWEB_EVENT_TYPES.Meta) {
|
|
327
381
|
this.hasMeta = true
|
|
328
382
|
this.lastMeta = event
|
|
329
383
|
}
|
|
330
384
|
// snapshot event
|
|
331
|
-
if (event.type ===
|
|
385
|
+
if (event.type === RRWEB_EVENT_TYPES.FullSnapshot) {
|
|
332
386
|
this.hasSnapshot = true
|
|
333
|
-
// small chance that the meta event got separated from its matching snapshot across payload harvests
|
|
334
|
-
// it needs to precede the snapshot, so shove it in first.
|
|
335
|
-
if (!this.hasMeta) {
|
|
336
|
-
this.events.push(this.lastMeta)
|
|
337
|
-
this.hasMeta = true
|
|
338
|
-
}
|
|
339
387
|
}
|
|
340
388
|
|
|
341
389
|
this.events.push(event)
|
|
@@ -355,18 +403,13 @@ export class Aggregate extends AggregateBase {
|
|
|
355
403
|
recorder.takeFullSnapshot()
|
|
356
404
|
}
|
|
357
405
|
|
|
358
|
-
setTimestamps (
|
|
406
|
+
setTimestamps () {
|
|
359
407
|
// fallbacks if timestamps cannot be derived from rrweb events
|
|
360
|
-
this.
|
|
361
|
-
if (!this.timestamp.cycle.first) this.timestamp.cycle.first = this.timestamp.cycle.last
|
|
362
|
-
// timestamps based on rrweb events
|
|
363
|
-
if (!event || !event.timestamp) return
|
|
364
|
-
if (!this.timestamp.event.first) this.timestamp.event.first = event.timestamp
|
|
365
|
-
this.timestamp.event.last = event.timestamp
|
|
408
|
+
if (!this.cycleTimestamp) this.cycleTimestamp = getRuntime(this.agentIdentifier).offset + globalScope.performance.now()
|
|
366
409
|
}
|
|
367
410
|
|
|
368
411
|
clearTimestamps () {
|
|
369
|
-
this.
|
|
412
|
+
this.cycleTimestamp = undefined
|
|
370
413
|
}
|
|
371
414
|
|
|
372
415
|
/** Estimate the payload size */
|
|
@@ -376,8 +419,9 @@ export class Aggregate extends AggregateBase {
|
|
|
376
419
|
}
|
|
377
420
|
|
|
378
421
|
/** Abort the feature, once aborted it will not resume */
|
|
379
|
-
abort (reason) {
|
|
380
|
-
warn(`SR aborted -- ${reason}`)
|
|
422
|
+
abort (reason = {}) {
|
|
423
|
+
warn(`SR aborted -- ${reason.message}`)
|
|
424
|
+
this.ee.emit(SUPPORTABILITY_METRIC_CHANNEL, [`SessionReplay/Abort/${ABORT_REASONS[reason.sm]}`])
|
|
381
425
|
this.blocked = true
|
|
382
426
|
this.mode = MODE.OFF
|
|
383
427
|
this.stopRecording()
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { getRuntime } from '../../common/config/config'
|
|
2
1
|
import { ee } from '../../common/event-emitter/contextual-ee'
|
|
3
2
|
|
|
4
3
|
export class FeatureBase {
|
|
@@ -8,7 +7,7 @@ export class FeatureBase {
|
|
|
8
7
|
/** @type {Aggregator} */
|
|
9
8
|
this.aggregator = aggregator
|
|
10
9
|
/** @type {ContextualEE} */
|
|
11
|
-
this.ee = ee.get(agentIdentifier
|
|
10
|
+
this.ee = ee.get(agentIdentifier)
|
|
12
11
|
/** @type {string} */
|
|
13
12
|
this.featureName = featureName
|
|
14
13
|
/**
|
package/src/loaders/api/api.js
CHANGED
|
@@ -19,7 +19,7 @@ export const CUSTOM_ATTR_GROUP = 'CUSTOM/' // the subgroup items should be store
|
|
|
19
19
|
export function setTopLevelCallers () {
|
|
20
20
|
const nr = gosCDN()
|
|
21
21
|
const funcs = [
|
|
22
|
-
'setErrorHandler', 'finished', 'addToTrace', '
|
|
22
|
+
'setErrorHandler', 'finished', 'addToTrace', 'addRelease',
|
|
23
23
|
'addPageAction', 'setCurrentRouteName', 'setPageViewName', 'setCustomAttribute',
|
|
24
24
|
'interaction', 'noticeError', 'setUserId', 'setApplicationVersion', 'start'
|
|
25
25
|
]
|
|
@@ -48,7 +48,6 @@ export function setAPI (agentIdentifier, forceDrain) {
|
|
|
48
48
|
'setErrorHandler',
|
|
49
49
|
'finished',
|
|
50
50
|
'addToTrace',
|
|
51
|
-
'inlineHit',
|
|
52
51
|
'addRelease'
|
|
53
52
|
]
|
|
54
53
|
|
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
import { FEATURE_NAMES } from '../features/features'
|
|
2
|
-
import {
|
|
2
|
+
import { getRuntime } from '../../common/config/config'
|
|
3
3
|
import { ee } from '../../common/event-emitter/contextual-ee'
|
|
4
4
|
import { handle } from '../../common/event-emitter/handle'
|
|
5
5
|
import { registerHandler } from '../../common/event-emitter/register-handler'
|
|
6
6
|
import { single } from '../../common/util/invoke'
|
|
7
|
-
import * as submitData from '../../common/util/submit-data'
|
|
8
|
-
import { isBrowserScope } from '../../common/constants/runtime'
|
|
9
7
|
import { CUSTOM_METRIC_CHANNEL } from '../../features/metrics/constants'
|
|
10
8
|
|
|
11
9
|
export function setAPI (agentIdentifier) {
|
|
12
10
|
var instanceEE = ee.get(agentIdentifier)
|
|
13
|
-
var cycle = 0
|
|
14
11
|
|
|
15
12
|
var api = {
|
|
16
13
|
finished: single(finished),
|
|
17
14
|
setErrorHandler,
|
|
18
15
|
addToTrace,
|
|
19
|
-
inlineHit,
|
|
20
16
|
addRelease
|
|
21
17
|
}
|
|
22
18
|
|
|
@@ -47,40 +43,6 @@ export function setAPI (agentIdentifier) {
|
|
|
47
43
|
handle('bstApi', [report], undefined, FEATURE_NAMES.sessionTrace, instanceEE)
|
|
48
44
|
}
|
|
49
45
|
|
|
50
|
-
// NREUM.inlineHit(requestName, queueTime, appTime, totalBackEndTime, domTime, frontEndTime)
|
|
51
|
-
//
|
|
52
|
-
// requestName - the 'web page' name or service name
|
|
53
|
-
// queueTime - the amount of time spent in the app tier queue
|
|
54
|
-
// appTime - the amount of time spent in the application code
|
|
55
|
-
// totalBackEndTime - the total roundtrip time of the remote service call
|
|
56
|
-
// domTime - the time spent processing the result of the service call (or user defined)
|
|
57
|
-
// frontEndTime - the time spent rendering the result of the service call (or user defined)
|
|
58
|
-
function inlineHit (t, requestName, queueTime, appTime, totalBackEndTime, domTime, frontEndTime) {
|
|
59
|
-
if (!isBrowserScope) return
|
|
60
|
-
|
|
61
|
-
requestName = window.encodeURIComponent(requestName)
|
|
62
|
-
cycle += 1
|
|
63
|
-
|
|
64
|
-
const agentInfo = getInfo(agentIdentifier)
|
|
65
|
-
if (!agentInfo.beacon) return
|
|
66
|
-
|
|
67
|
-
const agentInit = getConfiguration(agentIdentifier)
|
|
68
|
-
const scheme = agentInit.ssl === false ? 'http' : 'https'
|
|
69
|
-
const beacon = agentInit.proxy.beacon || agentInfo.beacon
|
|
70
|
-
let url = `${scheme}://${beacon}/1/${agentInfo.licenseKey}`
|
|
71
|
-
|
|
72
|
-
url += '?a=' + agentInfo.applicationID + '&'
|
|
73
|
-
url += 't=' + requestName + '&'
|
|
74
|
-
url += 'qt=' + ~~queueTime + '&'
|
|
75
|
-
url += 'ap=' + ~~appTime + '&'
|
|
76
|
-
url += 'be=' + ~~totalBackEndTime + '&'
|
|
77
|
-
url += 'dc=' + ~~domTime + '&'
|
|
78
|
-
url += 'fe=' + ~~frontEndTime + '&'
|
|
79
|
-
url += 'c=' + cycle
|
|
80
|
-
|
|
81
|
-
submitData.xhr({ url })
|
|
82
|
-
}
|
|
83
|
-
|
|
84
46
|
function setErrorHandler (t, handler) {
|
|
85
47
|
getRuntime(agentIdentifier).onerror = handler
|
|
86
48
|
}
|
|
@@ -33,7 +33,7 @@ export function configure (agentIdentifier, opts = {}, loaderType, forceDrain) {
|
|
|
33
33
|
if (!alreadySetOnce) {
|
|
34
34
|
alreadySetOnce = true
|
|
35
35
|
if (updatedInit.proxy.assets) {
|
|
36
|
-
redefinePublicPath(updatedInit.proxy.assets
|
|
36
|
+
redefinePublicPath(updatedInit.proxy.assets)
|
|
37
37
|
internalTrafficList.push(updatedInit.proxy.assets)
|
|
38
38
|
}
|
|
39
39
|
if (updatedInit.proxy.beacon) internalTrafficList.push(updatedInit.proxy.beacon)
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Set the default CDN or remote for fetching the assets; NPM shouldn't change this var.
|
|
2
2
|
|
|
3
|
-
export const redefinePublicPath = (
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
export const redefinePublicPath = (urlString) => {
|
|
4
|
+
const isOrigin = urlString.startsWith('http')
|
|
5
|
+
// Input is not expected to end in a slash, but webpack concats as-is, so one is inserted.
|
|
6
|
+
urlString += '/'
|
|
7
|
+
// If there's no existing HTTP scheme, the secure protocol is prepended by default.
|
|
8
|
+
__webpack_public_path__ = isOrigin ? urlString : 'https://' + urlString // eslint-disable-line
|
|
6
9
|
}
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import { Aggregator } from './aggregator.js'
|
|
2
|
-
|
|
3
|
-
let aggregator
|
|
4
|
-
beforeEach(() => {
|
|
5
|
-
aggregator = new Aggregator({ agentIdentifier: 'blah' })
|
|
6
|
-
})
|
|
7
|
-
|
|
8
|
-
test('storing and getting buckets', () => {
|
|
9
|
-
let bucket = aggregator.store('abc', '123')
|
|
10
|
-
const expectedBucketPattern = { params: {}, metrics: { count: 1 } }
|
|
11
|
-
expect(bucket).toEqual(expectedBucketPattern)
|
|
12
|
-
expect(aggregator.get('abc', '123')).toEqual(expectedBucketPattern)
|
|
13
|
-
|
|
14
|
-
aggregator.store('abc', '456')
|
|
15
|
-
// check we can get all buckets under the same type
|
|
16
|
-
expect(aggregator.get('abc')).toEqual({ 123: expectedBucketPattern, 456: expectedBucketPattern })
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
describe('storing the same bucket again', () => {
|
|
20
|
-
test('increments the count', () => {
|
|
21
|
-
aggregator.store('abc', '123')
|
|
22
|
-
aggregator.store('abc', '123')
|
|
23
|
-
expect(aggregator.get('abc', '123')).toEqual({ params: {}, metrics: { count: 2 } })
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
test('does not overwrite params', () => {
|
|
27
|
-
aggregator.store('def', '123', { someParam: true })
|
|
28
|
-
aggregator.store('def', '123', { anotherParam: true })
|
|
29
|
-
expect(aggregator.get('def', '123')).toEqual({ params: { someParam: true }, metrics: { count: 2 } })
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
test('does not overwrite custom params either', () => {
|
|
33
|
-
aggregator.store('ghi', '123', undefined, undefined, { someCustomParam: true })
|
|
34
|
-
aggregator.store('ghi', '123', undefined, undefined, { someCustomParam: false })
|
|
35
|
-
expect(aggregator.get('ghi', '123')).toEqual({ params: {}, custom: { someCustomParam: true }, metrics: { count: 2 } })
|
|
36
|
-
})
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
describe('metrics are properly updated', () => {
|
|
40
|
-
test('when using store fn', () => {
|
|
41
|
-
const expectedBucketPattern = {
|
|
42
|
-
params: {},
|
|
43
|
-
metrics: {
|
|
44
|
-
count: 2,
|
|
45
|
-
met1: { t: 3, min: 1, max: 2, sos: 5, c: 2 },
|
|
46
|
-
met2: { t: 7, min: 3, max: 4, sos: 25, c: 2 }
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
aggregator.store('abc', '123', undefined, { met1: 1, met2: 3 })
|
|
50
|
-
aggregator.store('abc', '123', undefined, { met1: 2, met2: 4 })
|
|
51
|
-
expect(aggregator.get('abc', '123')).toEqual(expectedBucketPattern)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
test('when using storeMetric fn', () => {
|
|
55
|
-
const expectedBucketPattern = {
|
|
56
|
-
params: {},
|
|
57
|
-
stats: { t: 6, min: 1, max: 3, sos: 14, c: 3 }
|
|
58
|
-
}
|
|
59
|
-
aggregator.storeMetric('abc', 'metric', undefined, 2)
|
|
60
|
-
aggregator.storeMetric('abc', 'metric', undefined, 1)
|
|
61
|
-
aggregator.storeMetric('abc', 'metric', undefined, 3)
|
|
62
|
-
expect(aggregator.get('abc', 'metric')).toEqual(expectedBucketPattern)
|
|
63
|
-
})
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
test('take fn gets and deletes correctly', () => {
|
|
67
|
-
aggregator.store('type1', 'a')
|
|
68
|
-
aggregator.store('type1', 'b')
|
|
69
|
-
aggregator.store('type2', 'a')
|
|
70
|
-
aggregator.store('type3', 'a')
|
|
71
|
-
|
|
72
|
-
expect(aggregator.take(['type0'])).toBeNull() // when type dne in aggregator
|
|
73
|
-
let obj = aggregator.take(['type1', 'type2'])
|
|
74
|
-
expect(obj.type1.length).toEqual(2)
|
|
75
|
-
expect(obj.type2.length).toEqual(1)
|
|
76
|
-
expect(aggregator.get('type1', 'a')).toBeUndefined() // should be gone now
|
|
77
|
-
expect(aggregator.get('type3', 'a')).toEqual(expect.any(Object))
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
test('merge fn combines metrics correctly', () => {
|
|
81
|
-
aggregator.store('abc', '123', undefined, { met1: 1, met2: 3 })
|
|
82
|
-
let bucket = aggregator.store('abc', '123', undefined, { met1: 2 })
|
|
83
|
-
const expectedMetrics = {
|
|
84
|
-
count: 2,
|
|
85
|
-
met1: { t: 3, min: 1, max: 2, sos: 5, c: 2 },
|
|
86
|
-
met2: { t: 3 }
|
|
87
|
-
}
|
|
88
|
-
expect(bucket.metrics).toEqual(expectedMetrics)
|
|
89
|
-
|
|
90
|
-
aggregator.merge('abc', '456', bucket.metrics)
|
|
91
|
-
expect(aggregator.get('abc', '456').metrics).toEqual(expectedMetrics)
|
|
92
|
-
|
|
93
|
-
aggregator.merge('abc', '123', {
|
|
94
|
-
count: 4,
|
|
95
|
-
met1: { t: 4, min: 0, max: 4, sos: 16, c: 2 },
|
|
96
|
-
met2: { t: 5 },
|
|
97
|
-
met3: { t: 6 },
|
|
98
|
-
met4: { t: 7, min: 3, max: 4, sos: 25, c: 2 }
|
|
99
|
-
})
|
|
100
|
-
expect(aggregator.get('abc', '123').metrics).toEqual({
|
|
101
|
-
count: 6,
|
|
102
|
-
met1: { t: 7, min: 0, max: 4, sos: 21, c: 4 },
|
|
103
|
-
met2: { t: 8, min: 3, max: 5, sos: 34, c: 2 },
|
|
104
|
-
met3: { t: 6 },
|
|
105
|
-
met4: { t: 7, min: 3, max: 4, sos: 25, c: 2 }
|
|
106
|
-
})
|
|
107
|
-
})
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { getModeledObject } from './configurable'
|
|
2
|
-
|
|
3
|
-
test('if either params are not objects, function fails', () => {
|
|
4
|
-
let result = getModeledObject(123, {})
|
|
5
|
-
expect(result).toEqual(undefined)
|
|
6
|
-
|
|
7
|
-
result = getModeledObject({}, () => 'whatever')
|
|
8
|
-
expect(result).toEqual(undefined)
|
|
9
|
-
})
|
|
10
|
-
|
|
11
|
-
test('models are not mutated by function', () => {
|
|
12
|
-
const template = { dontchangethis: true }
|
|
13
|
-
getModeledObject({ dontchangethis: false, addthis: true }, template)
|
|
14
|
-
expect(template).toEqual({ dontchangethis: true })
|
|
15
|
-
})
|
|
16
|
-
|
|
17
|
-
const model = {
|
|
18
|
-
eat: true,
|
|
19
|
-
sleep: true,
|
|
20
|
-
rave: true,
|
|
21
|
-
repeat: {
|
|
22
|
-
eat: true,
|
|
23
|
-
sleep: true,
|
|
24
|
-
rave: true,
|
|
25
|
-
repeat: false
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
test('empty object does not change model output', () => {
|
|
30
|
-
expect(getModeledObject({}, model)).toEqual(model)
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
test('existing keys get their values changed', () => {
|
|
34
|
-
let obj = {
|
|
35
|
-
sleep: false,
|
|
36
|
-
repeat: {
|
|
37
|
-
eat: false
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
expect(getModeledObject(obj, model)).toEqual({
|
|
41
|
-
eat: true,
|
|
42
|
-
sleep: false,
|
|
43
|
-
rave: true,
|
|
44
|
-
repeat: {
|
|
45
|
-
eat: false, // should recurse
|
|
46
|
-
sleep: true,
|
|
47
|
-
rave: true,
|
|
48
|
-
repeat: false
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
test('existing keys can change types too, to/from object', () => {
|
|
54
|
-
// in other words, if replacement is not an object, it has no effect
|
|
55
|
-
let obj = {
|
|
56
|
-
rave: {},
|
|
57
|
-
repeat: 'just a sad lonely string now'
|
|
58
|
-
}
|
|
59
|
-
expect(getModeledObject(obj, model)).toEqual({
|
|
60
|
-
eat: true,
|
|
61
|
-
sleep: true,
|
|
62
|
-
rave: {},
|
|
63
|
-
repeat: 'just a sad lonely string now'
|
|
64
|
-
})
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
test('keys not in model are ignored', () => {
|
|
68
|
-
expect(getModeledObject({ game: 'allnight' }, model)).toEqual(model)
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
test('but if model is empty, then keys are not ignored', () => {
|
|
72
|
-
expect(getModeledObject({ game: 'allnight' }, {})).toEqual({ game: 'allnight' })
|
|
73
|
-
})
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
let isValid, getInfo, setInfo
|
|
2
|
-
beforeEach(async () => {
|
|
3
|
-
jest.resetModules()
|
|
4
|
-
;({ isValid, getInfo, setInfo } = await import('./info.js'))
|
|
5
|
-
})
|
|
6
|
-
|
|
7
|
-
test('set/getInfo should throw on an invalid agent id', () => {
|
|
8
|
-
// currently only checks if it's a truthy value
|
|
9
|
-
expect(() => setInfo(undefined, {})).toThrow('require an agent id')
|
|
10
|
-
expect(() => getInfo(undefined)).toThrow('require an agent id')
|
|
11
|
-
expect(() => setInfo('', {})).toThrow('require an agent id')
|
|
12
|
-
expect(() => getInfo('')).toThrow('require an agent id')
|
|
13
|
-
})
|
|
14
|
-
|
|
15
|
-
test('set/getInfo works correctly', () => {
|
|
16
|
-
expect(() => setInfo(123, { beacon: 'im here' })).not.toThrow() // notice setInfo accepts numbers
|
|
17
|
-
let cachedObj = getInfo('123')
|
|
18
|
-
expect(Object.keys(cachedObj).length).toBeGreaterThan(1)
|
|
19
|
-
expect(cachedObj.beacon).toEqual('im here')
|
|
20
|
-
expect(cachedObj.licenseKey).toEqual(undefined) // this should mirror default in info.js
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
test('isValid is correct', () => {
|
|
24
|
-
setInfo('ab', { errorBeacon: 'some value' })
|
|
25
|
-
setInfo('cd', { errorBeacon: 'some value', licenseKey: 'cereal box' })
|
|
26
|
-
expect(isValid('ab')).toBeFalsy()
|
|
27
|
-
expect(isValid('cd')).toBeFalsy()
|
|
28
|
-
|
|
29
|
-
setInfo('ef', { errorBeacon: 'some value', licenseKey: 'cereal box', applicationID: '25' })
|
|
30
|
-
expect(isValid('ef')).toBeTruthy()
|
|
31
|
-
})
|