@newrelic/browser-agent 1.258.2 → 1.259.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 (117) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/cjs/cdn/polyfills.js +3 -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/drain/drain.js +1 -1
  6. package/dist/cjs/common/harvest/harvest-scheduler.js +4 -2
  7. package/dist/cjs/features/ajax/constants.js +3 -2
  8. package/dist/cjs/features/jserrors/aggregate/index.js +2 -1
  9. package/dist/cjs/features/metrics/aggregate/index.js +0 -8
  10. package/dist/cjs/features/page_view_event/aggregate/index.js +1 -1
  11. package/dist/cjs/features/session_replay/aggregate/index.js +31 -36
  12. package/dist/cjs/features/session_replay/constants.js +5 -2
  13. package/dist/cjs/features/session_replay/instrument/index.js +53 -13
  14. package/dist/cjs/features/session_replay/shared/utils.js +3 -5
  15. package/dist/cjs/features/session_trace/aggregate/index.js +181 -527
  16. package/dist/cjs/features/session_trace/aggregate/trace/node.js +19 -0
  17. package/dist/cjs/features/session_trace/aggregate/trace/storage.js +289 -0
  18. package/dist/cjs/features/session_trace/constants.js +3 -2
  19. package/dist/cjs/features/session_trace/instrument/index.js +7 -3
  20. package/dist/cjs/features/utils/aggregate-base.js +1 -0
  21. package/dist/cjs/features/utils/feature-gates.js +17 -0
  22. package/dist/cjs/features/utils/instrument-base.js +2 -1
  23. package/dist/cjs/loaders/agent-base.js +4 -0
  24. package/dist/cjs/loaders/api/api-methods.js +1 -1
  25. package/dist/cjs/loaders/api/api.js +2 -2
  26. package/dist/cjs/loaders/configure/configure.js +1 -0
  27. package/dist/esm/cdn/polyfills.js +3 -1
  28. package/dist/esm/common/constants/env.cdn.js +1 -1
  29. package/dist/esm/common/constants/env.npm.js +1 -1
  30. package/dist/esm/common/drain/drain.js +1 -1
  31. package/dist/esm/common/harvest/harvest-scheduler.js +4 -2
  32. package/dist/esm/features/ajax/constants.js +2 -1
  33. package/dist/esm/features/jserrors/aggregate/index.js +2 -1
  34. package/dist/esm/features/metrics/aggregate/index.js +0 -8
  35. package/dist/esm/features/page_view_event/aggregate/index.js +1 -1
  36. package/dist/esm/features/session_replay/aggregate/index.js +32 -37
  37. package/dist/esm/features/session_replay/constants.js +4 -1
  38. package/dist/esm/features/session_replay/instrument/index.js +54 -14
  39. package/dist/esm/features/session_replay/shared/utils.js +4 -6
  40. package/dist/esm/features/session_trace/aggregate/index.js +182 -527
  41. package/dist/esm/features/session_trace/aggregate/trace/node.js +12 -0
  42. package/dist/esm/features/session_trace/aggregate/trace/storage.js +282 -0
  43. package/dist/esm/features/session_trace/constants.js +2 -1
  44. package/dist/esm/features/session_trace/instrument/index.js +7 -3
  45. package/dist/esm/features/utils/aggregate-base.js +1 -0
  46. package/dist/esm/features/utils/feature-gates.js +11 -0
  47. package/dist/esm/features/utils/instrument-base.js +3 -2
  48. package/dist/esm/loaders/agent-base.js +4 -0
  49. package/dist/esm/loaders/api/api-methods.js +1 -1
  50. package/dist/esm/loaders/api/api.js +2 -2
  51. package/dist/esm/loaders/configure/configure.js +1 -0
  52. package/dist/types/common/harvest/harvest-scheduler.d.ts +1 -0
  53. package/dist/types/common/harvest/harvest-scheduler.d.ts.map +1 -1
  54. package/dist/types/features/ajax/constants.d.ts +1 -0
  55. package/dist/types/features/ajax/constants.d.ts.map +1 -1
  56. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  57. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  58. package/dist/types/features/session_replay/aggregate/index.d.ts +1 -1
  59. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  60. package/dist/types/features/session_replay/constants.d.ts +3 -0
  61. package/dist/types/features/session_replay/instrument/index.d.ts +0 -1
  62. package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
  63. package/dist/types/features/session_replay/shared/utils.d.ts +1 -1
  64. package/dist/types/features/session_replay/shared/utils.d.ts.map +1 -1
  65. package/dist/types/features/session_trace/aggregate/index.d.ts +39 -52
  66. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  67. package/dist/types/features/session_trace/aggregate/trace/node.d.ts +12 -0
  68. package/dist/types/features/session_trace/aggregate/trace/node.d.ts.map +1 -0
  69. package/dist/types/features/session_trace/aggregate/trace/storage.d.ts +43 -0
  70. package/dist/types/features/session_trace/aggregate/trace/storage.d.ts.map +1 -0
  71. package/dist/types/features/session_trace/constants.d.ts +1 -0
  72. package/dist/types/features/session_trace/constants.d.ts.map +1 -1
  73. package/dist/types/features/session_trace/instrument/index.d.ts.map +1 -1
  74. package/dist/types/features/utils/aggregate-base.d.ts +1 -0
  75. package/dist/types/features/utils/aggregate-base.d.ts.map +1 -1
  76. package/dist/types/features/utils/feature-gates.d.ts +2 -0
  77. package/dist/types/features/utils/feature-gates.d.ts.map +1 -0
  78. package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
  79. package/dist/types/loaders/agent-base.d.ts +1 -0
  80. package/dist/types/loaders/agent-base.d.ts.map +1 -1
  81. package/dist/types/loaders/configure/configure.d.ts.map +1 -1
  82. package/package.json +1 -1
  83. package/src/cdn/polyfills.js +2 -0
  84. package/src/common/drain/drain.js +1 -1
  85. package/src/common/harvest/harvest-scheduler.js +4 -2
  86. package/src/features/ajax/constants.js +2 -0
  87. package/src/features/jserrors/aggregate/index.js +2 -1
  88. package/src/features/metrics/aggregate/index.js +0 -8
  89. package/src/features/page_view_event/aggregate/index.js +1 -1
  90. package/src/features/session_replay/aggregate/index.js +30 -39
  91. package/src/features/session_replay/constants.js +4 -0
  92. package/src/features/session_replay/instrument/index.js +48 -8
  93. package/src/features/session_replay/shared/__mocks__/utils.js +0 -1
  94. package/src/features/session_replay/shared/utils.js +4 -7
  95. package/src/features/session_trace/aggregate/index.js +157 -493
  96. package/src/features/session_trace/aggregate/trace/node.js +12 -0
  97. package/src/features/session_trace/aggregate/trace/storage.js +287 -0
  98. package/src/features/session_trace/constants.js +1 -0
  99. package/src/features/session_trace/instrument/index.js +7 -2
  100. package/src/features/utils/__mocks__/feature-gates.js +1 -0
  101. package/src/features/utils/aggregate-base.js +1 -0
  102. package/src/features/utils/feature-gates.js +11 -0
  103. package/src/features/utils/instrument-base.js +3 -2
  104. package/src/loaders/agent-base.js +4 -0
  105. package/src/loaders/api/api-methods.js +1 -1
  106. package/src/loaders/api/api.js +2 -2
  107. package/src/loaders/configure/configure.js +1 -0
  108. package/dist/cjs/features/session_replay/shared/replay-mode.js +0 -28
  109. package/dist/cjs/features/utils/handler-cache.js +0 -70
  110. package/dist/esm/features/session_replay/shared/replay-mode.js +0 -23
  111. package/dist/esm/features/utils/handler-cache.js +0 -63
  112. package/dist/types/features/session_replay/shared/replay-mode.d.ts +0 -9
  113. package/dist/types/features/session_replay/shared/replay-mode.d.ts.map +0 -1
  114. package/dist/types/features/utils/handler-cache.d.ts +0 -23
  115. package/dist/types/features/utils/handler-cache.d.ts.map +0 -1
  116. package/src/features/session_replay/shared/replay-mode.js +0 -23
  117. package/src/features/utils/handler-cache.js +0 -65
@@ -1,539 +1,203 @@
1
- /*
2
- * Copyright 2020 New Relic Corporation. All rights reserved.
3
- * SPDX-License-Identifier: Apache-2.0
4
- */
5
1
  import { registerHandler } from '../../../common/event-emitter/register-handler'
6
2
  import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler'
7
- import { parseUrl } from '../../../common/url/parse-url'
8
- import { getConfigurationValue, getRuntime } from '../../../common/config/config'
3
+ import { getConfigurationValue, getInfo, getRuntime } from '../../../common/config/config'
9
4
  import { FEATURE_NAME } from '../constants'
10
- import { HandlerCache } from '../../utils/handler-cache'
11
- import { getSessionReplayMode } from '../../session_replay/shared/replay-mode'
12
5
  import { AggregateBase } from '../../utils/aggregate-base'
6
+ import { TraceStorage } from './trace/storage'
7
+ import { obj as encodeObj } from '../../../common/url/encode'
8
+ import { deregisterDrain } from '../../../common/drain/drain'
9
+ import { globalScope } from '../../../common/constants/runtime'
13
10
  import { MODE, SESSION_EVENTS } from '../../../common/session/constants'
14
- import { now } from '../../../common/timing/now'
15
- import { originTime } from '../../../common/constants/runtime'
16
11
 
17
- const ignoredEvents = {
18
- // we find that certain events make the data too noisy to be useful
19
- global: { mouseup: true, mousedown: true },
20
- // certain events are present both in the window and in PVT metrics. PVT metrics are prefered so the window events should be ignored
21
- window: { load: true, pagehide: true },
22
- // when ajax instrumentation is disabled, all XMLHttpRequest events will return with origin = xhrOriginMissing and should be ignored
23
- xhrOriginMissing: { ignoreAll: true }
24
- }
25
- const toAggregate = {
26
- typing: [1000, 2000],
27
- scrolling: [100, 1000],
28
- mousing: [1000, 2000],
29
- touching: [1000, 2000]
30
- }
31
- const MAX_TRACE_DURATION = 10 * 60 * 1000 // 10 minutes
32
- const REQ_THRESHOLD_TO_SEND = 30
33
12
  const ERROR_MODE_SECONDS_WINDOW = 30 * 1000 // sliding window of nodes to track when simply monitoring (but not harvesting) in error mode
34
-
13
+ /** Reserved room for query param attrs */
14
+ const QUERY_PARAM_PADDING = 5000
35
15
  export class Aggregate extends AggregateBase {
36
16
  static featureName = FEATURE_NAME
37
- #scheduler
38
17
 
39
- constructor (agentIdentifier, aggregator, argsObj) {
18
+ constructor (agentIdentifier, aggregator) {
40
19
  super(agentIdentifier, aggregator, FEATURE_NAME)
41
20
  this.agentRuntime = getRuntime(agentIdentifier)
42
- this.resourceObserver = argsObj?.resourceObserver // undefined if observer couldn't be created
43
- this.ptid = ''
44
- this.trace = {}
45
- this.nodeCount = 0
46
- this.sentTrace = null
47
- this.prevStoredEvents = new Set()
48
- this.harvestTimeSeconds = getConfigurationValue(agentIdentifier, 'session_trace.harvestTimeSeconds') || 10
49
- this.maxNodesPerHarvest = getConfigurationValue(agentIdentifier, 'session_trace.maxNodesPerHarvest') || 1000
50
- /**
51
- * Standalone (mode) refers to the legacy version of ST before the idea of 'session' or the Replay feature existed.
52
- * It has some different behavior vs when used in tandem with replay. */
53
- this.isStandalone = false
54
- const operationalGate = new HandlerCache() // acts as a controller-intermediary that can enable or disable this feature's collection dynamically
55
- const sessionEntity = this.agentRuntime.session
56
- this.operationalGate = operationalGate
57
-
58
- /* --- The following section deals with user sessions concept & contains non-trivial control flow. --- */
59
- const controlTraceOp = (traceMode) => {
60
- switch (traceMode) {
61
- case MODE.ERROR:
62
- this.startTracing(operationalGate, true)
63
- break
64
- case MODE.FULL:
65
- case true:
66
- this.startTracing(operationalGate)
67
- break
68
- case MODE.OFF:
69
- case false:
70
- default: // this feature becomes "off" (does nothing & nothing is sent)
71
- operationalGate.decide(false)
72
- break
73
- }
74
- }
75
-
76
- let seenAnError = false
77
- let mostRecentModeKnown
78
-
79
- this.ee.on(SESSION_EVENTS.UPDATE, (eventType, sessionState) => {
80
- // this will only have an effect if ST is NOT already in full mode
81
- if (sessionState.sessionReplayMode === MODE.FULL) switchToFull()
82
- })
83
-
84
- /**
85
- * The goal of switchToFull is to take external input to trigger a change from off or error to full.
86
- * It will have no effect if already running in full mode.
87
- * "external" input in this case means errors thrown on the page or session replay itself being triggered to run in full mode by the API, which updates the session entity.
88
- */
89
- const switchToFull = () => {
90
- if (this.agentRuntime?.session?.state?.sessionReplayMode !== MODE.FULL) return
91
-
92
- if (mostRecentModeKnown !== MODE.FULL) {
93
- const prevMode = mostRecentModeKnown
94
- mostRecentModeKnown = MODE.FULL
95
- sessionEntity.write({ sessionTraceMode: mostRecentModeKnown })
96
- this.isStandalone = false
97
-
98
- if (prevMode === MODE.ERROR && this.#scheduler) {
99
- this.trimSTNs(ERROR_MODE_SECONDS_WINDOW) // up until now, Trace would've been just buffering nodes up to max, which needs to be trimmed to last X seconds
100
- this.#scheduler.runHarvest({ needResponse: true })
101
- } else {
102
- controlTraceOp(MODE.FULL)
103
- }
104
- }
105
- }
106
-
107
- if (!sessionEntity) {
108
- // Since session manager isn't around, do the old Trace behavior of waiting for RUM response to decide feature activation.
109
- this.isStandalone = true
110
- this.waitForFlags((['stn'])).then(([on]) => controlTraceOp(on), this.featureName, this.ee)
111
- } else {
112
- registerHandler('trace-jserror', () => {
113
- seenAnError = true
114
- switchToFull()
115
- }, this.featureName, this.ee)
116
-
117
- const stopTracePerm = () => {
118
- if (sessionEntity.state.sessionTraceMode !== MODE.OFF) sessionEntity.write({ sessionTraceMode: MODE.OFF })
119
- operationalGate.permanentlyDecide(false)
120
- if (mostRecentModeKnown === MODE.FULL) this.#scheduler?.runHarvest() // allow queued nodes (past opGate) to final harvest, unless they were buffered in other modes
121
- this.#scheduler?.stopTimer(true) // the 'true' arg here will forcibly block any future call to runHarvest, so the last runHarvest above must be prior
122
- this.#scheduler = null
123
- }
21
+ this.agentInfo = getInfo(agentIdentifier)
124
22
 
125
- // CAUTION: everything inside this promise runs post-load; event subscribers must be pre-load aka synchronous with constructor
126
- this.waitForFlags(['stn', 'sr']).then(async ([traceOn, replayOn]) => {
127
- if (!replayOn) {
128
- // When sr = 0 from BCS, also do the old Trace behavior:
129
- this.isStandalone = true
130
- controlTraceOp(traceOn)
131
- } else {
132
- this.ee.on('REPLAY_ABORTED', () => stopTracePerm())
133
- /* Assuming on page visible that the trace mode is updated from shared session,
134
- - if trace is turned off from the other page, it should be likewise here.
135
- - if trace switches to Full mode, harvest should start (prev: Error) if not already running (prev: Full). */
136
- this.ee.on(SESSION_EVENTS.RESUME, () => {
137
- const updatedTraceMode = sessionEntity.state.sessionTraceMode
138
- if (updatedTraceMode === MODE.OFF) stopTracePerm()
139
- else if (updatedTraceMode === MODE.FULL && this.#scheduler && !this.#scheduler.started) this.#scheduler.runHarvest({ needResponse: true })
140
- mostRecentModeKnown = updatedTraceMode
141
- })
142
- this.ee.on(SESSION_EVENTS.PAUSE, () => { mostRecentModeKnown = sessionEntity.state.sessionTraceMode })
143
-
144
- if (!sessionEntity.isNew) { // inherit the same mode as existing session's Trace
145
- if (sessionEntity.state.sessionReplayMode === MODE.OFF) this.isStandalone = true
146
- controlTraceOp(mostRecentModeKnown = sessionEntity.state.sessionTraceMode)
147
- } else { // for new sessions, see the truth table associated with NEWRELIC-8662 wrt the new Trace behavior under session management
148
- const replayMode = await getSessionReplayMode(agentIdentifier)
149
- if (replayMode === MODE.OFF) this.isStandalone = true // without SR, Traces are still subject to old harvest limits
150
-
151
- let startingMode
152
- if (traceOn) { // CASE: both trace (entitlement+sampling) & replay (entitlement) flags are true from RUM
153
- startingMode = MODE.FULL // always full capture regardless of replay sampling decisions
154
- } else { // CASE: trace flag is off, BUT it must still run if replay is on (possibly)
155
- // At this point, it's possible that 1 or more exception was thrown, in which case just start in full if Replay originally started in ERROR mode.
156
- if (replayMode === MODE.ERROR && seenAnError) startingMode = MODE.FULL
157
- else startingMode = replayMode
158
- }
159
- sessionEntity.write({ sessionTraceMode: (mostRecentModeKnown = startingMode) })
160
- controlTraceOp(startingMode)
161
- }
162
- }
23
+ /** A buffer to hold on to harvested traces in the case that a retry must be made later */
24
+ this.sentTrace = null
25
+ this.harvestTimeSeconds = getConfigurationValue(agentIdentifier, 'session_trace.harvestTimeSeconds') || 30
26
+ /** Tied to the entitlement flag response from BCS. Will short circuit operations of the agg if false */
27
+ this.entitled = undefined
28
+ /** A flag used to decide if the 30 node threshold should be ignored on the first harvest to ensure sending on the first payload */
29
+ this.everHarvested = false
30
+ /** If the harvest module is harvesting */
31
+ this.harvesting = false
32
+ /** TraceStorage is the mechanism that holds, normalizes and aggregates ST nodes. It will be accessed and purged when harvests occur */
33
+ this.traceStorage = new TraceStorage(this)
34
+ /** This agg needs information about sampling (sts) and entitlements (st) to make the appropriate decisions on running */
35
+ this.waitForFlags(['sts', 'st'])
36
+ .then(([stMode, stEntitled]) => this.initialize(stMode, stEntitled))
37
+ }
38
+
39
+ /** Sets up event listeners, and initializes this module to run in the correct "mode". Can be triggered from a few places, but makes an effort to only set up listeners once */
40
+ initialize (stMode, stEntitled, ignoreSession) {
41
+ this.entitled ??= stEntitled
42
+ if (this.blocked || !this.entitled) return deregisterDrain(this.agentIdentifier, this.featureName)
43
+
44
+ if (!this.initialized) {
45
+ // 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.
46
+ this.ee.on(SESSION_EVENTS.RESET, () => {
47
+ this.abort()
163
48
  })
164
- }
165
- /* --- EoS --- */
166
-
167
- // register the handlers immediately... but let the handlerCache decide if the data should actually get stored...
168
- registerHandler('bst', (...args) => operationalGate.settle(() => this.storeEvent(...args)), this.featureName, this.ee)
169
- registerHandler('bstResource', (...args) => operationalGate.settle(() => this.storeResources(...args)), this.featureName, this.ee)
170
- registerHandler('bstHist', (...args) => operationalGate.settle(() => this.storeHist(...args)), this.featureName, this.ee)
171
- registerHandler('bstXhrAgg', (...args) => operationalGate.settle(() => this.storeXhrAgg(...args)), this.featureName, this.ee)
172
- registerHandler('bstApi', (...args) => operationalGate.settle(() => this.storeSTN(...args)), this.featureName, this.ee)
173
- registerHandler('trace-jserror', (...args) => operationalGate.settle(() => this.storeErrorAgg(...args)), this.featureName, this.ee)
174
- registerHandler('pvtAdded', (...args) => operationalGate.settle(() => this.processPVT(...args)), this.featureName, this.ee)
175
- this.drain()
176
- }
177
-
178
- startTracing (startupBuffer, dontStartHarvestYet = false) {
179
- if (typeof PerformanceNavigationTiming !== 'undefined') {
180
- this.storeTiming(window.performance.getEntriesByType('navigation')[0])
181
- } else {
182
- this.storeTiming(window.performance.timing)
183
- }
184
-
185
- this.#scheduler = new HarvestScheduler('resources', {
186
- onFinished: this.#onHarvestFinished.bind(this),
187
- retryDelay: this.harvestTimeSeconds
188
- }, this)
189
- this.#scheduler.harvest.on('resources', this.#prepareHarvest.bind(this))
190
- if (dontStartHarvestYet === false) this.#scheduler.runHarvest({ needResponse: true }) // sends first stn harvest immediately
191
- startupBuffer.decide(true) // signal to ALLOW & process data in EE's buffer into internal nodes queued for next harvest
192
- }
193
-
194
- #onHarvestFinished (result) {
195
- if (result.sent && result.responseText && !this.ptid) { // continue interval harvest only if ptid was returned by server on the first
196
- this.agentRuntime.ptid = this.ptid = result.responseText
197
- this.#scheduler.startTimer(this.harvestTimeSeconds)
198
- }
199
-
200
- if (result.sent && result.retry && this.sentTrace) { // merge previous trace back into buffer to retry for next harvest
201
- Object.entries(this.sentTrace).forEach(([name, listOfSTNodes]) => {
202
- if (this.nodeCount >= this.maxNodesPerHarvest) return
203
-
204
- this.nodeCount += listOfSTNodes.length
205
- this.trace[name] = this.trace[name] ? listOfSTNodes.concat(this.trace[name]) : listOfSTNodes
49
+ // The SessionEntity can have updates (locally or across tabs for SR mode changes), (across tabs for ST mode changes).
50
+ // Those updates should be sync'd here to ensure this page also honors the mode after initialization
51
+ this.ee.on(SESSION_EVENTS.UPDATE, (eventType, sessionState) => {
52
+ // this will only have an effect if ST is NOT already in full mode
53
+ if (this.mode !== MODE.FULL && (sessionState.sessionReplayMode === MODE.FULL || sessionState.sessionTraceMode === MODE.FULL)) this.switchToFull()
206
54
  })
207
- this.sentTrace = null
208
55
  }
209
- }
210
56
 
211
- #prepareHarvest (options) {
212
- this.prevStoredEvents.clear() // release references to past events for GC
213
- if (this.isStandalone) {
214
- if (this.ptid && now() >= MAX_TRACE_DURATION) {
215
- // Perform a final harvest once we hit or exceed the max session trace time
216
- options.isFinalHarvest = true
217
- this.operationalGate.permanentlyDecide(false)
218
- this.#scheduler.stopTimer(true)
219
- } else if (this.ptid && this.nodeCount <= REQ_THRESHOLD_TO_SEND && !options.isFinalHarvest) {
220
- // Only harvest when more than some threshold of nodes are pending, after the very first harvest, with the exception of the last outgoing harvest.
221
- return
222
- }
223
- } else {
224
- // -- *cli May '26 - Update: Not rate limiting backgrounded pages either for now.
225
- // if (this.ptid && document.visibilityState === 'hidden' && this.nodeCount <= REQ_THRESHOLD_TO_SEND) return
226
-
227
- const currentMode = this.agentRuntime.session.state.sessionTraceMode
228
- /* There could still be nodes previously collected even after Trace (w/ session mgmt) is turned off. Hence, continue to send the last batch.
229
- * The intermediary controller SHOULD be already switched off so that no nodes are further queued. */
230
- if (currentMode === MODE.OFF && Object.keys(this.trace).length === 0) return
231
- if (currentMode === MODE.ERROR) return // Trace in this mode should never be harvesting, even on unload
232
- }
57
+ /** ST/SR sampling flow in BCS - https://drive.google.com/file/d/19hwt2oft-8Hh4RrjpLqEXfpP_9wYBLcq/view?usp=sharing */
58
+ /** ST will run in the mode provided by BCS if the session IS NEW. If not... it will use the state of the session entity to determine what mode to run in */
59
+ if (!this.agentRuntime.session.isNew && !ignoreSession) this.mode = this.agentRuntime.session.state.sessionTraceMode
60
+ else this.mode = stMode
233
61
 
234
- return this.takeSTNs(options.retry)
235
- }
236
-
237
- // PageViewTiming (FEATURE) events and metrics, such as 'load', 'lcp', etc. pipes into ST here.
238
- processPVT (name, value, attrs) {
239
- this.storeTiming({ [name]: value })
240
- if (hasFID(name, attrs)) this.storeEvent({ type: 'fid', target: 'document' }, 'document', value, value + attrs.fid)
241
-
242
- function hasFID (name, attrs) {
243
- return name === 'fi' && !!attrs && typeof attrs.fid === 'number'
244
- }
245
- }
246
-
247
- // This processes the aforementioned PVT and the first navigation entry of the page.
248
- storeTiming (timingEntry) {
249
- if (!timingEntry) return
250
-
251
- // loop iterates through prototype also (for FF)
252
- for (let key in timingEntry) {
253
- let val = timingEntry[key]
254
-
255
- // ignore size and status type nodes that do not map to timestamp metrics
256
- const lck = key.toLowerCase()
257
- if (lck.indexOf('size') >= 0 || lck.indexOf('status') >= 0) continue
258
-
259
- // ignore inherited methods, meaningless 0 values, and bogus timestamps
260
- // that are in the future (Microsoft Edge seems to sometimes produce these)
261
- if (!(typeof val === 'number' && val >= 0)) continue
262
-
263
- val = Math.round(val)
264
- this.storeSTN({
265
- n: key,
266
- s: val,
267
- e: val,
268
- o: 'document',
269
- t: 'timing'
270
- })
271
- }
272
- }
62
+ this.initialized = true
63
+ /** If the mode is off, we do not want to hold up draining for other features, so we deregister the feature for now.
64
+ * If it drains later (due to a mode change), data and handlers will instantly drain instead of waiting for the registry. */
65
+ if (this.mode === MODE.OFF) return deregisterDrain(this.agentIdentifier, this.featureName)
273
66
 
274
- // Tracks the events and their listener's duration on objects wrapped by wrap-events.
275
- storeEvent (currentEvent, target, start, end) {
276
- if (this.shouldIgnoreEvent(currentEvent, target)) return
277
- if (this.prevStoredEvents.has(currentEvent)) return // prevent multiple listeners of an event from creating duplicate trace nodes per occurrence. Cleared every harvest. near-zero chance for re-duplication after clearing per harvest since the timestamps of the event are considered for uniqueness.
278
- this.prevStoredEvents.add(currentEvent)
67
+ this.timeKeeper ??= this.agentRuntime.timeKeeper
279
68
 
280
- const evt = {
281
- n: this.evtName(currentEvent.type),
282
- s: start,
283
- e: end,
284
- t: 'event'
285
- }
286
-
287
- try {
288
- // webcomponents-lite.js can trigger an exception on currentEvent.target getter because
289
- // it does not check currentEvent.currentTarget before calling getRootNode() on it
290
- evt.o = this.evtOrigin(currentEvent.target, target)
291
- } catch (e) {
292
- evt.o = this.evtOrigin(null, target)
293
- }
294
- this.storeSTN(evt)
295
- }
296
-
297
- shouldIgnoreEvent (event, target) {
298
- const origin = this.evtOrigin(event.target, target)
299
- if (event.type in ignoredEvents.global) return true
300
- if (!!ignoredEvents[origin] && ignoredEvents[origin].ignoreAll) return true
301
- return !!(!!ignoredEvents[origin] && event.type in ignoredEvents[origin])
302
- }
303
-
304
- evtName (type) {
305
- switch (type) {
306
- case 'keydown':
307
- case 'keyup':
308
- case 'keypress':
309
- return 'typing'
310
- case 'mousemove':
311
- case 'mouseenter':
312
- case 'mouseleave':
313
- case 'mouseover':
314
- case 'mouseout':
315
- return 'mousing'
316
- case 'scroll':
317
- return 'scrolling'
318
- case 'touchstart':
319
- case 'touchmove':
320
- case 'touchend':
321
- case 'touchcancel':
322
- case 'touchenter':
323
- case 'touchleave':
324
- return 'touching'
325
- default:
326
- return type
327
- }
328
- }
329
-
330
- evtOrigin (t, target) {
331
- let origin = 'unknown'
332
-
333
- if (t && t instanceof XMLHttpRequest) {
334
- const params = this.ee.context(t).params
335
- if (!params || !params.status || !params.method || !params.host || !params.pathname) return 'xhrOriginMissing'
336
- origin = params.status + ' ' + params.method + ': ' + params.host + params.pathname
337
- } else if (t && typeof (t.tagName) === 'string') {
338
- origin = t.tagName.toLowerCase()
339
- if (t.id) origin += '#' + t.id
340
- if (t.className) {
341
- for (let i = 0; i < t.classList.length; i++) origin += '.' + t.classList[i]
342
- }
343
- }
344
-
345
- if (origin === 'unknown') {
346
- if (typeof target === 'string') origin = target
347
- else if (target === document) origin = 'document'
348
- else if (target === window) origin = 'window'
349
- else if (target instanceof FileReader) origin = 'FileReader'
350
- }
351
-
352
- return origin
353
- }
354
-
355
- // Tracks when the window history API specified by wrap-history is used.
356
- storeHist (path, old, time) {
357
- const node = {
358
- n: 'history.pushState',
359
- s: time,
360
- e: time,
361
- o: path,
362
- t: old
363
- }
364
- this.storeSTN(node)
365
- }
366
-
367
- #laststart = 0
368
- // Processes all the PerformanceResourceTiming entries captured (by observer).
369
- storeResources (resources) {
370
- if (!resources || resources.length === 0) return
371
-
372
- resources.forEach((currentResource) => {
373
- if ((currentResource.fetchStart | 0) <= this.#laststart) return // don't recollect already-seen resources
374
-
375
- const parsed = parseUrl(currentResource.name)
376
- const res = {
377
- n: currentResource.initiatorType,
378
- s: currentResource.fetchStart | 0,
379
- e: currentResource.responseEnd | 0,
380
- o: parsed.protocol + '://' + parsed.hostname + ':' + parsed.port + parsed.pathname, // resource.name is actually a URL so it's the source
381
- t: currentResource.entryType
382
- }
383
- this.storeSTN(res)
384
- })
69
+ this.scheduler = new HarvestScheduler('browser/blobs', {
70
+ onFinished: this.onHarvestFinished.bind(this),
71
+ retryDelay: this.harvestTimeSeconds,
72
+ getPayload: this.prepareHarvest.bind(this),
73
+ raw: true
74
+ }, this)
385
75
 
386
- this.#laststart = resources[resources.length - 1].fetchStart | 0
387
- }
76
+ /** The handlers set up by the Inst file */
77
+ registerHandler('bst', (...args) => this.traceStorage.storeEvent(...args), this.featureName, this.ee)
78
+ registerHandler('bstResource', (...args) => this.traceStorage.storeResources(...args), this.featureName, this.ee)
79
+ registerHandler('bstHist', (...args) => this.traceStorage.storeHist(...args), this.featureName, this.ee)
80
+ registerHandler('bstXhrAgg', (...args) => this.traceStorage.storeXhrAgg(...args), this.featureName, this.ee)
81
+ registerHandler('bstApi', (...args) => this.traceStorage.storeSTN(...args), this.featureName, this.ee)
82
+ registerHandler('trace-jserror', (...args) => this.traceStorage.storeErrorAgg(...args), this.featureName, this.ee)
83
+ registerHandler('pvtAdded', (...args) => this.traceStorage.processPVT(...args), this.featureName, this.ee)
388
84
 
389
- // JavascriptError (FEATURE) events pipes into ST here.
390
- storeErrorAgg (type, name, params, metrics) {
391
- if (type !== 'err') return // internal errors are purposefully ignored
392
- const node = {
393
- n: 'error',
394
- s: metrics.time,
395
- e: metrics.time,
396
- o: params.message,
397
- t: params.stackHash
85
+ if (typeof PerformanceNavigationTiming !== 'undefined') {
86
+ this.traceStorage.storeTiming(globalScope.performance?.getEntriesByType?.('navigation')[0])
87
+ } else {
88
+ this.traceStorage.storeTiming(globalScope.performance?.timing)
398
89
  }
399
- this.storeSTN(node)
400
- }
401
90
 
402
- // Ajax (FEATURE) events--XML & fetches--pipes into ST here.
403
- storeXhrAgg (type, name, params, metrics) {
404
- if (type !== 'xhr') return
405
- const node = {
406
- n: 'Ajax',
407
- s: metrics.time,
408
- e: metrics.time + metrics.duration,
409
- o: params.status + ' ' + params.method + ': ' + params.host + params.pathname,
410
- t: 'ajax'
91
+ /** Only start actually harvesting if running in full mode at init time */
92
+ if (this.mode === MODE.FULL) this.startHarvesting()
93
+ else {
94
+ /** A separate handler for noticing errors, and switching to "full" mode if running in "error" mode */
95
+ registerHandler('trace-jserror', () => {
96
+ if (this.mode === MODE.ERROR) this.switchToFull()
97
+ }, this.featureName, this.ee)
411
98
  }
412
- this.storeSTN(node)
99
+ this.agentRuntime.session.write({ sessionTraceMode: this.mode })
100
+ this.drain()
413
101
  }
414
102
 
415
- // Central function called by all the other store__ & addToTrace API to append a trace node.
416
- storeSTN (stn) {
417
- if (this.nodeCount >= this.maxNodesPerHarvest) { // limit the amount of pending data awaiting next harvest
418
- if (this.isStandalone || this.agentRuntime.session.state.sessionTraceMode !== MODE.ERROR) return
419
- const openedSpace = this.trimSTNs(ERROR_MODE_SECONDS_WINDOW) // but maybe we could make some space by discarding irrelevant nodes if we're in sessioned Error mode
420
- if (openedSpace === 0) return
421
- }
422
-
423
- if (this.isStandalone && now() >= MAX_TRACE_DURATION) {
424
- return
425
- }
426
-
427
- if (this.trace[stn.n]) this.trace[stn.n].push(stn)
428
- else this.trace[stn.n] = [stn]
429
-
430
- this.nodeCount++
103
+ /** This module does not auto harvest by default -- it needs to be kicked off. Once this method is called, it will then harvest on an interval */
104
+ startHarvesting () {
105
+ if (this.scheduler.started || this.blocked) return
106
+ this.scheduler.runHarvest()
107
+ this.scheduler.startTimer(this.harvestTimeSeconds)
431
108
  }
432
109
 
433
- /**
434
- * Trim the collection of nodes awaiting harvest such that those seen outside a certain span of time are discarded.
435
- * @param {number} lookbackDuration Past length of time until now for which we care about nodes, in milliseconds
436
- * @returns {number} However many nodes were discarded after trimming.
437
- */
438
- trimSTNs (lookbackDuration) {
439
- let prunedNodes = 0
440
- const cutoffHighResTime = Math.max(now() - lookbackDuration, 0)
441
- Object.keys(this.trace).forEach(nameCategory => {
442
- const nodeList = this.trace[nameCategory]
443
- /* Notice nodes are appending under their name's list as they end and are stored. This means each list is already (roughly) sorted in chronological order by end time.
444
- * This isn't exact since nodes go through some processing & EE handlers chain, but it's close enough as we still capture nodes whose duration overlaps the lookback window.
445
- * ASSUMPTION: all 'end' timings stored are relative to timeOrigin (DOMHighResTimeStamp) and not Unix epoch based. */
446
- let cutoffIdx = nodeList.findIndex(node => cutoffHighResTime <= node.e)
447
-
448
- if (cutoffIdx === 0) return
449
- else if (cutoffIdx < 0) { // whole list falls outside lookback window and is irrelevant
450
- cutoffIdx = nodeList.length
451
- delete this.trace[nameCategory]
452
- } else nodeList.splice(0, cutoffIdx) // chop off everything outside our window i.e. before the last <lookbackDuration> timeframe
110
+ /** Called by the harvest scheduler at harvest time to retrieve the payload. This will only actually return a payload if running in full mode */
111
+ prepareHarvest (options = {}) {
112
+ this.traceStorage.prevStoredEvents.clear() // release references to past events for GC
113
+ if (!this.timeKeeper?.ready) return // this should likely never happen, but just to be safe, we should never harvest if we cant correct time
114
+ if (this.mode === MODE.OFF && this.traceStorage.nodeCount === 0) return
115
+ if (this.mode === MODE.ERROR) return // Trace in this mode should never be harvesting, even on unload
453
116
 
454
- this.nodeCount -= cutoffIdx
455
- prunedNodes += cutoffIdx
456
- })
457
- return prunedNodes
458
- }
459
-
460
- // Used by session trace's harvester to create the payload body.
461
- takeSTNs (retry) {
462
- if (!this.resourceObserver) { // if PO isn't supported, this checks resourcetiming buffer every harvest.
463
- this.storeResources(window.performance.getEntriesByType('resource'))
117
+ /** Get the ST nodes from the traceStorage buffer. This also returns helpful metadata about the payload. */
118
+ const { stns, earliestTimeStamp, latestTimeStamp } = this.traceStorage.takeSTNs()
119
+ if (options.retry) {
120
+ this.sentTrace = stns
464
121
  }
465
122
 
466
- let earliestTimeStamp = Infinity
467
- const stns = Object.entries(this.trace).flatMap(([name, listOfSTNodes]) => { // basically take the "this.trace" map-obj and concat all the list-type values
468
- const oldestNodeTS = listOfSTNodes.reduce((acc, next) => (!acc || next.s < acc) ? next.s : acc, undefined)
469
- if (oldestNodeTS < earliestTimeStamp) earliestTimeStamp = oldestNodeTS
123
+ const firstSessionHarvest = !this.agentRuntime.session.state.traceHarvestStarted
124
+ if (firstSessionHarvest) this.agentRuntime.session.write({ traceHarvestStarted: true })
470
125
 
471
- if (!(name in toAggregate)) return listOfSTNodes
472
- // Special processing for event nodes dealing with user inputs:
473
- const reindexByOriginFn = this.smearEvtsByOrigin(name)
474
- const partitionListByOriginMap = listOfSTNodes.sort((a, b) => a.s - b.s).reduce(reindexByOriginFn, {})
475
- return Object.values(partitionListByOriginMap).flat() // join the partitions back into 1-D, now ordered by origin then start time
476
- }, this)
477
- if (stns.length === 0) return {}
126
+ const hasReplay = this.agentRuntime.session?.state.sessionReplayMode === 1
127
+ const endUserId = this.agentInfo?.jsAttributes?.['enduser.id']
478
128
 
479
- if (retry) {
480
- this.sentTrace = this.trace
481
- }
482
- this.trace = {}
483
- this.nodeCount = 0
484
-
485
- let firstHarvestOfSession
486
- if (this.agentRuntime.session) {
487
- const isFirstPayload = !this.agentRuntime.session.state.traceHarvestStarted
488
- firstHarvestOfSession = { fsh: Number(isFirstPayload) } // converted to '0' | '1'
489
- if (isFirstPayload) this.agentRuntime.session.write({ traceHarvestStarted: true })
490
- }
129
+ this.everHarvested = true
491
130
 
131
+ /** The blob consumer expects the following and will reject if not supplied:
132
+ * browser_monitoring_key
133
+ * type
134
+ * app_id
135
+ * protocol_version
136
+ * attributes
137
+ *
138
+ * For data that does not fit the schema of the above, it should be url-encoded and placed into `attributes`
139
+ */
140
+ const agentMetadata = this.agentRuntime.appMetadata?.agents?.[0] || {}
492
141
  return {
493
142
  qs: {
494
- st: originTime,
495
- /** hr === "hasReplay" in NR1, standalone is always checked and processed before harvesting
496
- * so a race condition between ST and SR states should not be a concern if implemented here */
497
- hr: Number(!this.isStandalone),
498
- /** fts === "firstTimestamp" in NR1, indicates what the earliest NODE timestamp was
499
- * so that blob parsing doesn't need to happen to support UI/API functions */
500
- fts: originTime + earliestTimeStamp,
501
- /** n === "nodeCount" in NR1, a count of nodes in the ST payload, so that blob parsing doesn't need to happen to support UI/API functions */
502
- n: stns.length, // node count
503
- ...firstHarvestOfSession
143
+ browser_monitoring_key: this.agentInfo.licenseKey,
144
+ type: 'BrowserSessionChunk',
145
+ app_id: this.agentInfo.applicationID,
146
+ protocol_version: '0',
147
+ timestamp: this.timeKeeper.convertRelativeTimestamp(earliestTimeStamp),
148
+ attributes: encodeObj({
149
+ ...(agentMetadata.entityGuid && { entityGuid: agentMetadata.entityGuid }),
150
+ harvestId: `${this.agentRuntime.session?.state.value}_${this.agentRuntime.ptid}_${this.agentRuntime.harvestCount}`,
151
+ // this section of attributes must be controllable and stay below the query param padding limit -- see QUERY_PARAM_PADDING
152
+ // if not, data could be lost to truncation at time of sending, potentially breaking parsing / API behavior in NR1
153
+ // trace payload metadata
154
+ 'trace.firstTimestamp': this.timeKeeper.convertRelativeTimestamp(earliestTimeStamp),
155
+ 'trace.lastTimestamp': this.timeKeeper.convertRelativeTimestamp(latestTimeStamp),
156
+ 'trace.nodes': stns.length,
157
+ 'trace.originTimestamp': this.timeKeeper.correctedOriginTime,
158
+ // other payload metadata
159
+ agentVersion: this.agentRuntime.version,
160
+ ...(firstSessionHarvest && { firstSessionHarvest }),
161
+ ...(hasReplay && { hasReplay }),
162
+ ptid: `${this.agentRuntime.ptid}`,
163
+ session: `${this.agentRuntime.session?.state.value}`,
164
+ // customer-defined data should go last so that if it exceeds the query param padding limit it will be truncated instead of important attrs
165
+ ...(endUserId && { 'enduser.id': endUserId })
166
+ // The Query Param is being arbitrarily limited in length here. It is also applied when estimating the size of the payload in getPayloadSize()
167
+ }, QUERY_PARAM_PADDING).substring(1) // remove the leading '&'
504
168
  },
505
- body: { res: stns }
169
+ body: stns
506
170
  }
507
171
  }
508
172
 
509
- smearEvtsByOrigin (name) {
510
- const maxGap = toAggregate[name][0]
511
- const maxLen = toAggregate[name][1]
512
- const lastO = {}
513
-
514
- return (byOrigin, evtNode) => {
515
- let lastArr = byOrigin[evtNode.o]
516
- if (!lastArr) lastArr = byOrigin[evtNode.o] = []
517
-
518
- const last = lastO[evtNode.o]
519
-
520
- if (name === 'scrolling' && !trivial(evtNode)) {
521
- lastO[evtNode.o] = null
522
- evtNode.n = 'scroll'
523
- lastArr.push(evtNode)
524
- } else if (last && (evtNode.s - last.s) < maxLen && last.e > (evtNode.s - maxGap)) {
525
- last.e = evtNode.e
526
- } else {
527
- lastO[evtNode.o] = evtNode
528
- lastArr.push(evtNode)
529
- }
530
-
531
- return byOrigin
173
+ /** When the harvest scheduler finishes, this callback is executed. It's main purpose is to determine if the payload needs to be retried
174
+ * and if so, it will take all data from the temporary buffer and place it back into the traceStorage module
175
+ */
176
+ onHarvestFinished (result) {
177
+ if (result.sent && result.retry && this.sentTrace) { // merge previous trace back into buffer to retry for next harvest
178
+ Object.entries(this.sentTrace).forEach(([name, listOfSTNodes]) => { this.traceStorage.restoreNode(name, listOfSTNodes) })
179
+ this.sentTrace = null
532
180
  }
181
+ }
533
182
 
534
- function trivial (node) {
535
- const limit = 4
536
- return !!(node && typeof node.e === 'number' && typeof node.s === 'number' && (node.e - node.s) < limit)
183
+ /** Switch from "off" or "error" to full mode (if entitled) */
184
+ switchToFull () {
185
+ if (this.mode === MODE.FULL || !this.entitled || this.blocked) return
186
+ const prevMode = this.mode
187
+ this.mode = MODE.FULL
188
+ this.agentRuntime.session.write({ sessionTraceMode: this.mode })
189
+ if (prevMode === MODE.OFF || !this.initialized) return this.initialize(this.mode, this.entitled)
190
+ if (this.initialized) {
191
+ this.traceStorage.trimSTNs(ERROR_MODE_SECONDS_WINDOW) // up until now, Trace would've been just buffering nodes up to max, which needs to be trimmed to last X seconds
537
192
  }
193
+ this.startHarvesting()
194
+ }
195
+
196
+ /** Stop running for the remainder of the page lifecycle */
197
+ abort () {
198
+ this.blocked = true
199
+ this.mode = MODE.OFF
200
+ this.agentRuntime.session.write({ sessionTraceMode: this.mode })
201
+ this.scheduler.stopTimer()
538
202
  }
539
203
  }