@newrelic/browser-agent 1.252.0 → 1.253.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 (217) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +6 -6
  3. package/dist/cjs/cdn/experimental.js +6 -2
  4. package/dist/cjs/cdn/spa.js +5 -3
  5. package/dist/cjs/common/aggregate/aggregator.js +1 -8
  6. package/dist/cjs/common/config/state/init.js +7 -0
  7. package/dist/cjs/common/constants/env.cdn.js +1 -1
  8. package/dist/cjs/common/constants/env.npm.js +1 -1
  9. package/dist/cjs/common/context/observation-context-manager.js +56 -0
  10. package/dist/cjs/common/event-emitter/contextual-ee.js +12 -9
  11. package/dist/cjs/common/session/constants.js +2 -1
  12. package/dist/cjs/common/session/session-entity.js +3 -1
  13. package/dist/cjs/common/timing/nav-timing.js +8 -3
  14. package/dist/cjs/common/timing/now.js +1 -1
  15. package/dist/cjs/common/util/feature-flags.js +1 -1
  16. package/dist/cjs/common/wrap/index.js +0 -7
  17. package/dist/cjs/common/wrap/wrap-events.js +2 -2
  18. package/dist/cjs/common/wrap/wrap-fetch.js +2 -1
  19. package/dist/cjs/common/wrap/wrap-function.js +5 -7
  20. package/dist/cjs/common/wrap/wrap-promise.js +2 -1
  21. package/dist/cjs/features/ajax/aggregate/index.js +34 -16
  22. package/dist/cjs/features/jserrors/aggregate/index.js +77 -66
  23. package/dist/cjs/features/page_view_event/aggregate/index.js +1 -1
  24. package/dist/cjs/features/page_view_event/aggregate/initialized-features.js +1 -0
  25. package/dist/cjs/features/session_replay/aggregate/index.js +96 -94
  26. package/dist/cjs/features/session_replay/constants.js +5 -1
  27. package/dist/cjs/features/session_replay/instrument/index.js +24 -8
  28. package/dist/cjs/features/session_replay/shared/recorder.js +5 -4
  29. package/dist/cjs/features/session_replay/shared/stylesheet-evaluator.js +8 -7
  30. package/dist/cjs/features/session_replay/shared/utils.js +26 -0
  31. package/dist/cjs/features/soft_navigations/aggregate/ajax-node.js +50 -0
  32. package/dist/cjs/features/soft_navigations/aggregate/bel-node.js +29 -0
  33. package/dist/cjs/features/soft_navigations/aggregate/index.js +263 -0
  34. package/dist/cjs/features/soft_navigations/aggregate/initial-page-load-interaction.js +62 -0
  35. package/dist/cjs/features/soft_navigations/aggregate/interaction.js +146 -0
  36. package/dist/cjs/features/soft_navigations/constants.js +31 -0
  37. package/dist/cjs/features/soft_navigations/index.js +12 -0
  38. package/dist/cjs/features/soft_navigations/instrument/index.js +79 -0
  39. package/dist/cjs/features/spa/aggregate/index.js +4 -4
  40. package/dist/cjs/features/utils/agent-session.js +2 -1
  41. package/dist/cjs/features/utils/instrument-base.js +6 -9
  42. package/dist/cjs/features/utils/lazy-feature-loader.js +2 -0
  43. package/dist/cjs/loaders/agent-base.js +18 -3
  44. package/dist/cjs/loaders/agent.js +15 -18
  45. package/dist/cjs/loaders/api/api-methods.js +9 -0
  46. package/dist/cjs/loaders/api/api.js +17 -18
  47. package/dist/cjs/loaders/configure/configure.js +5 -2
  48. package/dist/cjs/loaders/features/enabled-features.js +1 -1
  49. package/dist/cjs/loaders/features/features.js +3 -1
  50. package/dist/esm/cdn/experimental.js +5 -2
  51. package/dist/esm/cdn/spa.js +3 -1
  52. package/dist/esm/common/aggregate/aggregator.js +1 -8
  53. package/dist/esm/common/config/state/init.js +7 -0
  54. package/dist/esm/common/constants/env.cdn.js +1 -1
  55. package/dist/esm/common/constants/env.npm.js +1 -1
  56. package/dist/esm/common/context/observation-context-manager.js +49 -0
  57. package/dist/esm/common/event-emitter/contextual-ee.js +12 -9
  58. package/dist/esm/common/session/constants.js +1 -0
  59. package/dist/esm/common/session/session-entity.js +3 -1
  60. package/dist/esm/common/timing/nav-timing.js +8 -3
  61. package/dist/esm/common/timing/now.js +1 -1
  62. package/dist/esm/common/util/feature-flags.js +1 -1
  63. package/dist/esm/common/wrap/index.js +1 -2
  64. package/dist/esm/common/wrap/wrap-events.js +3 -3
  65. package/dist/esm/common/wrap/wrap-fetch.js +3 -2
  66. package/dist/esm/common/wrap/wrap-function.js +4 -5
  67. package/dist/esm/common/wrap/wrap-promise.js +3 -2
  68. package/dist/esm/features/ajax/aggregate/index.js +36 -18
  69. package/dist/esm/features/jserrors/aggregate/index.js +77 -66
  70. package/dist/esm/features/page_view_event/aggregate/index.js +1 -1
  71. package/dist/esm/features/page_view_event/aggregate/initialized-features.js +1 -0
  72. package/dist/esm/features/session_replay/aggregate/index.js +97 -95
  73. package/dist/esm/features/session_replay/constants.js +4 -0
  74. package/dist/esm/features/session_replay/instrument/index.js +25 -9
  75. package/dist/esm/features/session_replay/shared/recorder.js +5 -4
  76. package/dist/esm/features/session_replay/shared/stylesheet-evaluator.js +8 -7
  77. package/dist/esm/features/session_replay/shared/utils.js +17 -0
  78. package/dist/esm/features/soft_navigations/aggregate/ajax-node.js +43 -0
  79. package/dist/esm/features/soft_navigations/aggregate/bel-node.js +22 -0
  80. package/dist/esm/features/soft_navigations/aggregate/index.js +256 -0
  81. package/dist/esm/features/soft_navigations/aggregate/initial-page-load-interaction.js +55 -0
  82. package/dist/esm/features/soft_navigations/aggregate/interaction.js +140 -0
  83. package/dist/esm/features/soft_navigations/constants.js +25 -0
  84. package/dist/esm/features/soft_navigations/index.js +1 -0
  85. package/dist/esm/features/soft_navigations/instrument/index.js +73 -0
  86. package/dist/esm/features/spa/aggregate/index.js +4 -4
  87. package/dist/esm/features/utils/agent-session.js +2 -1
  88. package/dist/esm/features/utils/instrument-base.js +7 -10
  89. package/dist/esm/features/utils/lazy-feature-loader.js +2 -0
  90. package/dist/esm/loaders/agent-base.js +18 -3
  91. package/dist/esm/loaders/agent.js +15 -18
  92. package/dist/esm/loaders/api/api-methods.js +3 -0
  93. package/dist/esm/loaders/api/api.js +17 -17
  94. package/dist/esm/loaders/configure/configure.js +5 -2
  95. package/dist/esm/loaders/features/enabled-features.js +1 -1
  96. package/dist/esm/loaders/features/features.js +3 -1
  97. package/dist/types/common/aggregate/aggregator.d.ts.map +1 -1
  98. package/dist/types/common/config/state/init.d.ts.map +1 -1
  99. package/dist/types/common/context/event-context.d.ts.map +1 -0
  100. package/dist/types/common/context/observation-context-manager.d.ts +28 -0
  101. package/dist/types/common/context/observation-context-manager.d.ts.map +1 -0
  102. package/dist/types/common/event-emitter/contextual-ee.d.ts +2 -2
  103. package/dist/types/common/event-emitter/contextual-ee.d.ts.map +1 -1
  104. package/dist/types/common/session/constants.d.ts +1 -0
  105. package/dist/types/common/session/constants.d.ts.map +1 -1
  106. package/dist/types/common/session/session-entity.d.ts +0 -1
  107. package/dist/types/common/session/session-entity.d.ts.map +1 -1
  108. package/dist/types/common/timing/nav-timing.d.ts.map +1 -1
  109. package/dist/types/common/wrap/index.d.ts +1 -2
  110. package/dist/types/common/wrap/index.d.ts.map +1 -1
  111. package/dist/types/common/wrap/wrap-fetch.d.ts.map +1 -1
  112. package/dist/types/common/wrap/wrap-function.d.ts +0 -1
  113. package/dist/types/common/wrap/wrap-function.d.ts.map +1 -1
  114. package/dist/types/common/wrap/wrap-promise.d.ts.map +1 -1
  115. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  116. package/dist/types/features/jserrors/aggregate/index.d.ts +4 -3
  117. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  118. package/dist/types/features/page_view_event/aggregate/initialized-features.d.ts.map +1 -1
  119. package/dist/types/features/session_replay/aggregate/index.d.ts +1 -1
  120. package/dist/types/features/session_replay/aggregate/index.d.ts.map +1 -1
  121. package/dist/types/features/session_replay/constants.d.ts +4 -0
  122. package/dist/types/features/session_replay/constants.d.ts.map +1 -1
  123. package/dist/types/features/session_replay/instrument/index.d.ts.map +1 -1
  124. package/dist/types/features/session_replay/shared/recorder.d.ts.map +1 -1
  125. package/dist/types/features/session_replay/shared/stylesheet-evaluator.d.ts.map +1 -1
  126. package/dist/types/features/session_replay/shared/utils.d.ts +4 -0
  127. package/dist/types/features/session_replay/shared/utils.d.ts.map +1 -0
  128. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts +19 -0
  129. package/dist/types/features/soft_navigations/aggregate/ajax-node.d.ts.map +1 -0
  130. package/dist/types/features/soft_navigations/aggregate/bel-node.d.ts +16 -0
  131. package/dist/types/features/soft_navigations/aggregate/bel-node.d.ts.map +1 -0
  132. package/dist/types/features/soft_navigations/aggregate/index.d.ts +36 -0
  133. package/dist/types/features/soft_navigations/aggregate/index.d.ts.map +1 -0
  134. package/dist/types/features/soft_navigations/aggregate/initial-page-load-interaction.d.ts +12 -0
  135. package/dist/types/features/soft_navigations/aggregate/initial-page-load-interaction.d.ts.map +1 -0
  136. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts +50 -0
  137. package/dist/types/features/soft_navigations/aggregate/interaction.d.ts.map +1 -0
  138. package/dist/types/features/soft_navigations/constants.d.ts +20 -0
  139. package/dist/types/features/soft_navigations/constants.d.ts.map +1 -0
  140. package/dist/types/features/soft_navigations/index.d.ts +2 -0
  141. package/dist/types/features/soft_navigations/index.d.ts.map +1 -0
  142. package/dist/types/features/soft_navigations/instrument/index.d.ts +7 -0
  143. package/dist/types/features/soft_navigations/instrument/index.d.ts.map +1 -0
  144. package/dist/types/features/spa/aggregate/index.d.ts.map +1 -1
  145. package/dist/types/features/utils/agent-session.d.ts.map +1 -1
  146. package/dist/types/features/utils/instrument-base.d.ts +1 -7
  147. package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
  148. package/dist/types/features/utils/lazy-feature-loader.d.ts.map +1 -1
  149. package/dist/types/loaders/agent-base.d.ts +5 -1
  150. package/dist/types/loaders/agent-base.d.ts.map +1 -1
  151. package/dist/types/loaders/agent.d.ts +2 -2
  152. package/dist/types/loaders/agent.d.ts.map +1 -1
  153. package/dist/types/loaders/api/api-methods.d.ts +3 -0
  154. package/dist/types/loaders/api/api-methods.d.ts.map +1 -0
  155. package/dist/types/loaders/api/api.d.ts +3 -6
  156. package/dist/types/loaders/api/api.d.ts.map +1 -1
  157. package/dist/types/loaders/configure/configure.d.ts.map +1 -1
  158. package/dist/types/loaders/features/features.d.ts +1 -0
  159. package/dist/types/loaders/features/features.d.ts.map +1 -1
  160. package/dist/types/loaders/micro-agent.d.ts +0 -1
  161. package/dist/types/loaders/micro-agent.d.ts.map +1 -1
  162. package/package.json +1 -1
  163. package/src/cdn/experimental.js +4 -2
  164. package/src/cdn/spa.js +3 -1
  165. package/src/common/aggregate/aggregator.js +2 -11
  166. package/src/common/config/state/init.js +3 -1
  167. package/src/common/context/observation-context-manager.js +55 -0
  168. package/src/common/event-emitter/contextual-ee.js +20 -10
  169. package/src/common/session/constants.js +1 -0
  170. package/src/common/session/session-entity.js +3 -1
  171. package/src/common/timing/nav-timing.js +7 -3
  172. package/src/common/timing/now.js +1 -1
  173. package/src/common/util/feature-flags.js +1 -1
  174. package/src/common/wrap/index.js +1 -2
  175. package/src/common/wrap/wrap-events.js +3 -3
  176. package/src/common/wrap/wrap-fetch.js +3 -2
  177. package/src/common/wrap/wrap-function.js +4 -6
  178. package/src/common/wrap/wrap-promise.js +3 -2
  179. package/src/features/ajax/aggregate/index.js +36 -18
  180. package/src/features/jserrors/aggregate/index.js +70 -73
  181. package/src/features/page_view_event/aggregate/index.js +1 -1
  182. package/src/features/page_view_event/aggregate/initialized-features.js +1 -0
  183. package/src/features/session_replay/aggregate/index.js +92 -95
  184. package/src/features/session_replay/constants.js +5 -0
  185. package/src/features/session_replay/instrument/index.js +24 -9
  186. package/src/features/session_replay/shared/recorder.js +5 -4
  187. package/src/features/session_replay/shared/stylesheet-evaluator.js +8 -7
  188. package/src/features/session_replay/shared/utils.js +19 -0
  189. package/src/features/soft_navigations/aggregate/ajax-node.js +57 -0
  190. package/src/features/soft_navigations/aggregate/bel-node.js +26 -0
  191. package/src/features/soft_navigations/aggregate/index.js +254 -0
  192. package/src/features/soft_navigations/aggregate/initial-page-load-interaction.js +53 -0
  193. package/src/features/soft_navigations/aggregate/interaction.js +159 -0
  194. package/src/features/soft_navigations/constants.js +29 -0
  195. package/src/features/soft_navigations/index.js +1 -0
  196. package/src/features/soft_navigations/instrument/index.js +67 -0
  197. package/src/features/spa/aggregate/index.js +5 -4
  198. package/src/features/utils/agent-session.js +2 -1
  199. package/src/features/utils/instrument-base.js +7 -10
  200. package/src/features/utils/lazy-feature-loader.js +2 -0
  201. package/src/loaders/agent-base.js +18 -3
  202. package/src/loaders/agent.js +18 -17
  203. package/src/loaders/api/api-methods.js +12 -0
  204. package/src/loaders/api/api.js +17 -28
  205. package/src/loaders/configure/configure.js +4 -1
  206. package/src/loaders/features/enabled-features.js +1 -1
  207. package/src/loaders/features/features.js +3 -1
  208. package/dist/cjs/common/wrap/wrap-raf.js +0 -55
  209. package/dist/esm/common/wrap/wrap-raf.js +0 -48
  210. package/dist/types/common/event-emitter/event-context.d.ts.map +0 -1
  211. package/dist/types/common/wrap/wrap-raf.d.ts +0 -16
  212. package/dist/types/common/wrap/wrap-raf.d.ts.map +0 -1
  213. package/src/common/wrap/wrap-raf.js +0 -52
  214. /package/dist/cjs/common/{event-emitter → context}/event-context.js +0 -0
  215. /package/dist/esm/common/{event-emitter → context}/event-context.js +0 -0
  216. /package/dist/types/common/{event-emitter → context}/event-context.d.ts +0 -0
  217. /package/src/common/{event-emitter → context}/event-context.js +0 -0
@@ -0,0 +1,254 @@
1
+ import { getConfigurationValue } from '../../../common/config/config'
2
+ import { handle } from '../../../common/event-emitter/handle'
3
+ import { registerHandler } from '../../../common/event-emitter/register-handler'
4
+ import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler'
5
+ import { single } from '../../../common/util/invoke'
6
+ import { timeToFirstByte } from '../../../common/vitals/time-to-first-byte'
7
+ import { FEATURE_NAMES } from '../../../loaders/features/features'
8
+ import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
9
+ import { AggregateBase } from '../../utils/aggregate-base'
10
+ import { API_TRIGGER_NAME, FEATURE_NAME, INTERACTION_STATUS } from '../constants'
11
+ import { AjaxNode } from './ajax-node'
12
+ import { InitialPageLoadInteraction } from './initial-page-load-interaction'
13
+ import { Interaction } from './interaction'
14
+
15
+ export class Aggregate extends AggregateBase {
16
+ static featureName = FEATURE_NAME
17
+ constructor (agentIdentifier, aggregator, { domObserver }) {
18
+ super(agentIdentifier, aggregator, FEATURE_NAME)
19
+
20
+ const harvestTimeSeconds = getConfigurationValue(agentIdentifier, 'soft_navigations.harvestTimeSeconds') || 10
21
+ this.interactionsToHarvest = []
22
+ this.interactionsAwaitingRetry = []
23
+ this.domObserver = domObserver
24
+
25
+ this.scheduler = new HarvestScheduler('events', {
26
+ onFinished: this.onHarvestFinished.bind(this),
27
+ retryDelay: harvestTimeSeconds,
28
+ onUnload: () => this.interactionInProgress?.done() // return any held ajax or jserr events so they can be sent with EoL harvest
29
+ }, { agentIdentifier, ee: this.ee })
30
+ this.scheduler.harvest.on('events', this.onHarvestStarted.bind(this))
31
+
32
+ this.initialPageLoadInteraction = new InitialPageLoadInteraction(agentIdentifier)
33
+ timeToFirstByte.subscribe(({ entries }) => {
34
+ const loadEventTime = entries[0].loadEventEnd
35
+ this.initialPageLoadInteraction.forceSave = true
36
+ this.initialPageLoadInteraction.done(loadEventTime)
37
+ this.interactionsToHarvest.push(this.initialPageLoadInteraction)
38
+ this.initialPageLoadInteraction = null
39
+ // Report metric on the initial page load time
40
+ handle(SUPPORTABILITY_METRIC_CHANNEL, ['SoftNav/Interaction/InitialPageLoad/Duration/Ms', Math.round(loadEventTime)], undefined, FEATURE_NAMES.metrics, this.ee)
41
+ })
42
+
43
+ this.latestRouteSetByApi = null
44
+ this.interactionInProgress = null // aside from the "page load" interaction, there can only ever be 1 ongoing at a time
45
+
46
+ this.blocked = false
47
+ this.waitForFlags(['spa']).then(([spaOn]) => {
48
+ if (spaOn) this.scheduler.startTimer(harvestTimeSeconds, 0)
49
+ else this.blocked = true // if rum response determines that customer lacks entitlements for spa endpoint, this feature shouldn't harvest
50
+ })
51
+
52
+ // By default, a complete UI driven interaction requires event -> URL change -> DOM mod in that exact order.
53
+ registerHandler('newUIEvent', (event) => this.startUIInteraction(event.type, event.timeStamp, event.target), this.featureName, this.ee)
54
+ registerHandler('newURL', (timestamp, url) => this.interactionInProgress?.updateHistory(timestamp, url), this.featureName, this.ee)
55
+ registerHandler('newDom', timestamp => {
56
+ this.interactionInProgress?.updateDom(timestamp)
57
+ if (this.interactionInProgress?.seenHistoryAndDomChange()) this.interactionInProgress.done()
58
+ }, this.featureName, this.ee)
59
+
60
+ this.#registerApiHandlers()
61
+
62
+ registerHandler('ajax', this.#handleAjaxEvent.bind(this), this.featureName, this.ee)
63
+ registerHandler('jserror', this.#handleJserror.bind(this), this.featureName, this.ee)
64
+
65
+ this.drain()
66
+ }
67
+
68
+ onHarvestStarted (options) {
69
+ if (this.interactionsToHarvest.length === 0 || this.blocked) return
70
+
71
+ // The payload depacker takes the first ixn of a payload (if there are multiple ixns) and positively offset the subsequent ixns timestamps by that amount.
72
+ // In order to accurately portray the real start & end times of the 2nd & onward ixns, we hence need to negatively offset their start timestamps with that of the 1st ixn.
73
+ let firstIxnStartTime = 0 // the very 1st ixn does not require any offsetting
74
+ const serializedIxnList = []
75
+ for (const interaction of this.interactionsToHarvest) {
76
+ serializedIxnList.push(interaction.serialize(firstIxnStartTime))
77
+ if (!firstIxnStartTime) firstIxnStartTime = Math.floor(interaction.start)
78
+ }
79
+ const payload = `bel.7;${serializedIxnList.join(';')}`
80
+
81
+ if (options.retry) this.interactionsAwaitingRetry.push(...this.interactionsToHarvest)
82
+ this.interactionsToHarvest = []
83
+
84
+ return { body: { e: payload } }
85
+ }
86
+
87
+ onHarvestFinished (result) {
88
+ if (result.sent && result.retry && this.interactionsAwaitingRetry.length > 0) {
89
+ this.interactionsToHarvest = [...this.interactionsAwaitingRetry, ...this.interactionsToHarvest]
90
+ this.interactionsAwaitingRetry = []
91
+ }
92
+ }
93
+
94
+ startUIInteraction (eventName, startedAt, sourceElem) { // this is throttled by instrumentation so that it isn't excessively called
95
+ if (this.interactionInProgress?.createdByApi) return // api-started interactions cannot be disrupted aka cancelled by UI events (and the vice versa applies as well)
96
+ if (this.interactionInProgress?.done() === false) return
97
+
98
+ this.interactionInProgress = new Interaction(this.agentIdentifier, eventName, startedAt, this.latestRouteSetByApi)
99
+ if (eventName === 'click') {
100
+ const sourceElemText = getActionText(sourceElem)
101
+ if (sourceElemText) this.interactionInProgress.customAttributes.actionText = sourceElemText
102
+ }
103
+ this.interactionInProgress.cancellationTimer = setTimeout(() => {
104
+ this.interactionInProgress.done()
105
+ // Report metric on frequency of cancellation due to timeout for UI ixn
106
+ handle(SUPPORTABILITY_METRIC_CHANNEL, ['SoftNav/Interaction/TimeOut'], undefined, FEATURE_NAMES.metrics, this.ee)
107
+ }, 30000) // UI ixn are disregarded after 30 seconds if it's not completed by then
108
+ this.setClosureHandlers()
109
+ }
110
+
111
+ setClosureHandlers () {
112
+ this.interactionInProgress.on('finished', () => {
113
+ const ref = this.interactionInProgress
114
+ this.interactionsToHarvest.push(this.interactionInProgress)
115
+ this.interactionInProgress = null
116
+ this.domObserver.disconnect() // can stop observing whenever our interaction logic completes a cycle
117
+
118
+ // Report metric on the ixn duration
119
+ handle(SUPPORTABILITY_METRIC_CHANNEL, [
120
+ `SoftNav/Interaction/${ref.newURL !== ref.oldURL ? 'RouteChange' : 'Custom'}/Duration/Ms`,
121
+ Math.round(ref.end - ref.start)
122
+ ], undefined, FEATURE_NAMES.metrics, this.ee)
123
+ })
124
+ this.interactionInProgress.on('cancelled', () => {
125
+ this.interactionInProgress = null
126
+ this.domObserver.disconnect()
127
+ })
128
+ }
129
+
130
+ /**
131
+ * Find the active interaction (current or past) for a given timestamp. Note that historic lookups mostly only go as far back as the last harvest for this feature.
132
+ * Also, the caller should check the status of the interaction returned if found via {@link Interaction.status}, if that's pertinent.
133
+ * TIP: Cancelled (status) interactions are NOT returned!
134
+ * IMPORTANT: Finished interactions are in queue for next harvest! It's highly recommended that consumer logic be synchronous for safe reference.
135
+ * @param {DOMHighResTimeStamp} timestamp
136
+ * @returns An {@link Interaction} or undefined, if no active interaction was found.
137
+ */
138
+ getInteractionFor (timestamp) {
139
+ /* In the sole case wherein there can be two "interactions" overlapping (initialPageLoad + regular route-change),
140
+ the regular interaction should get precedence in being assigned the "active" interaction in regards to our one-at-a-time model.
141
+ */
142
+ if (this.interactionInProgress?.isActiveDuring(timestamp)) return this.interactionInProgress
143
+ let saveIxn
144
+ for (let idx = this.interactionsToHarvest.length - 1; idx >= 0; idx--) { // reverse search for the latest completed interaction for efficiency
145
+ const finishedInteraction = this.interactionsToHarvest[idx]
146
+ if (finishedInteraction.isActiveDuring(timestamp)) {
147
+ if (finishedInteraction.trigger !== 'initialPageLoad') return finishedInteraction
148
+ // It's possible that a complete interaction occurs before page is fully loaded, so we need to consider if a route-change ixn may have overlapped this iPL
149
+ else saveIxn = finishedInteraction
150
+ }
151
+ }
152
+ if (saveIxn) return saveIxn // if an iPL was determined to be active and no route-change was found active for the same time, then iPL is deemed the one
153
+ if (this.initialPageLoadInteraction?.isActiveDuring(timestamp)) return this.initialPageLoadInteraction // lowest precedence and also only if it's still in-progress
154
+ // Time must be when no interaction is happening, so return undefined.
155
+ }
156
+
157
+ /**
158
+ * Handles or redirect ajax event based on the interaction, if any, that it's tied to.
159
+ * @param {Object} event see Ajax feature's storeXhr function for object definition
160
+ */
161
+ #handleAjaxEvent (event) {
162
+ const associatedInteraction = this.getInteractionFor(event.startTime)
163
+ if (!associatedInteraction) { // no interaction was happening when this ajax started, so give it back to Ajax feature for processing
164
+ handle('returnAjax', [event], undefined, FEATURE_NAMES.ajax, this.ee)
165
+ } else {
166
+ if (associatedInteraction.status === INTERACTION_STATUS.FIN) processAjax(this.agentIdentifier, event, associatedInteraction) // tack ajax onto the ixn object awaiting harvest
167
+ else { // same thing as above, just at a later time -- if the interaction in progress is cancelled, just send the event back to ajax feat unmodified
168
+ associatedInteraction.on('finished', () => processAjax(this.agentIdentifier, event, associatedInteraction))
169
+ associatedInteraction.on('cancelled', () => handle('returnAjax', [event], undefined, FEATURE_NAMES.ajax, this.ee))
170
+ }
171
+ }
172
+
173
+ function processAjax (agentId, event, parentInteraction) {
174
+ const newNode = new AjaxNode(agentId, event)
175
+ parentInteraction.addChild(newNode)
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Decorate the passed-in params obj with properties relating to any associated interaction at the time of the timestamp.
181
+ * @param {Object} params reference to the local var instance in Jserrors feature's storeError
182
+ * @param {DOMHighResTimeStamp} timestamp time the jserror occurred
183
+ */
184
+ #handleJserror (params, timestamp) {
185
+ const associatedInteraction = this.getInteractionFor(timestamp)
186
+ if (!associatedInteraction) return // do not need to decorate this jserror params
187
+
188
+ // Whether the interaction is in-progress or already finished, the id will let jserror buffer it under its index, until it gets the next step instruction.
189
+ params.browserInteractionId = associatedInteraction.id
190
+ if (associatedInteraction.status === INTERACTION_STATUS.FIN) {
191
+ // This information cannot be relayed back via handle() that flushes buffered errs because this is being called by a jserror's handle() per se and before the err is buffered.
192
+ params._softNavFinished = true // instead, signal that this err can be processed right away without needing to be buffered aka wait for an in-progress ixn
193
+ params._softNavAttributes = associatedInteraction.customAttributes
194
+ } else {
195
+ // These callbacks may be added multiple times for an ixn, but just a single run will deal with all jserrors associated with the interaction.
196
+ // As such, be cautious not to use the params object since that's tied to one specific jserror and won't affect the rest of them.
197
+ associatedInteraction.on('finished', single(() =>
198
+ handle('softNavFlush', [associatedInteraction.id, true, associatedInteraction.customAttributes], undefined, FEATURE_NAMES.jserrors, this.ee)))
199
+ associatedInteraction.on('cancelled', single(() =>
200
+ handle('softNavFlush', [associatedInteraction.id, false, undefined], undefined, FEATURE_NAMES.jserrors, this.ee))) // don't take custom attrs from cancelled ixns
201
+ }
202
+ }
203
+
204
+ #registerApiHandlers () {
205
+ const INTERACTION_API = 'api-ixn-'
206
+ const thisClass = this
207
+
208
+ registerHandler(INTERACTION_API + 'get', function (time, { waitForEnd } = {}) {
209
+ // In here, 'this' refers to the EventContext specific to per InteractionHandle instance spawned by each .interaction() api call.
210
+ // Each api call aka IH instance would therefore retain a reference to either the in-progress interaction *at the time of the call* OR a new api-started interaction.
211
+ this.associatedInteraction = thisClass.getInteractionFor(time)
212
+ if (!this.associatedInteraction) {
213
+ // This new api-driven interaction will be the target of any subsequent .interaction() call, until it is closed by EITHER .end() OR the regular seenHistoryAndDomChange process.
214
+ this.associatedInteraction = thisClass.interactionInProgress = new Interaction(thisClass.agentIdentifier, API_TRIGGER_NAME, time, thisClass.latestRouteSetByApi)
215
+ thisClass.setClosureHandlers()
216
+ }
217
+ if (waitForEnd === true) this.associatedInteraction.keepOpenUntilEndApi = true
218
+ }, thisClass.featureName, thisClass.ee)
219
+ registerHandler(INTERACTION_API + 'end', function (timeNow) { this.associatedInteraction.done(timeNow) }, thisClass.featureName, thisClass.ee)
220
+ registerHandler(INTERACTION_API + 'save', function () { this.associatedInteraction.forceSave = true }, thisClass.featureName, thisClass.ee)
221
+ registerHandler(INTERACTION_API + 'ignore', function () { this.associatedInteraction.forceIgnore = true }, thisClass.featureName, thisClass.ee)
222
+
223
+ registerHandler(INTERACTION_API + 'getContext', function (time, callback) {
224
+ if (typeof callback !== 'function') return
225
+ setTimeout(() => callback(this.associatedInteraction.customDataByApi), 0)
226
+ }, thisClass.featureName, thisClass.ee)
227
+ registerHandler(INTERACTION_API + 'onEnd', function (time, callback) {
228
+ if (typeof callback !== 'function') return
229
+ this.associatedInteraction.onDone.push(callback)
230
+ }, thisClass.featureName, thisClass.ee)
231
+
232
+ registerHandler(INTERACTION_API + 'actionText', function (time, newActionText) {
233
+ if (newActionText) this.associatedInteraction.customAttributes.actionText = newActionText
234
+ }, thisClass.featureName, thisClass.ee)
235
+ registerHandler(INTERACTION_API + 'setName', function (time, name, trigger) {
236
+ if (name) this.associatedInteraction.customName = name
237
+ if (trigger) this.associatedInteraction.trigger = trigger
238
+ }, thisClass.featureName, thisClass.ee)
239
+ registerHandler(INTERACTION_API + 'setAttribute', function (time, key, value) { this.associatedInteraction.customAttributes[key] = value }, thisClass.featureName, thisClass.ee)
240
+
241
+ registerHandler(INTERACTION_API + 'routeName', function (time, newRouteName) { // notice that this fn tampers with the ixn IP, not with the linked ixn
242
+ thisClass.latestRouteSetByApi = newRouteName
243
+ if (thisClass.interactionInProgress) thisClass.interactionInProgress.newRoute = newRouteName
244
+ }, thisClass.featureName, thisClass.ee)
245
+ }
246
+ }
247
+
248
+ function getActionText (elem) {
249
+ const tagName = elem.tagName.toLowerCase()
250
+ const elementsOfInterest = ['a', 'button', 'input']
251
+ if (elementsOfInterest.includes(tagName)) {
252
+ return elem.title || elem.value || elem.innerText
253
+ }
254
+ }
@@ -0,0 +1,53 @@
1
+ import { navTimingValues } from '../../../common/timing/nav-timing'
2
+ import { Interaction } from './interaction'
3
+ import { numeric } from '../../../common/serialize/bel-serializer'
4
+ import { firstPaint } from '../../../common/vitals/first-paint'
5
+ import { firstContentfulPaint } from '../../../common/vitals/first-contentful-paint'
6
+ import { getInfo } from '../../../common/config/config'
7
+
8
+ export class InitialPageLoadInteraction extends Interaction {
9
+ constructor (agentIdentifier) {
10
+ super(agentIdentifier, 'initialPageLoad', 0, null)
11
+ const agentInfo = getInfo(agentIdentifier)
12
+ this.queueTime = agentInfo.queueTime
13
+ this.appTime = agentInfo.applicationTime
14
+ }
15
+
16
+ get firstPaint () { return firstPaint.current.value }
17
+ get firstContentfulPaint () { return firstContentfulPaint.current.value }
18
+
19
+ /**
20
+ * Build the navTiming node. This assumes the navTimingValues array in nav-timing.js has already been filled with values via the PageViewEvent feature having
21
+ * executed the addPT function first and foremost.
22
+ */
23
+ get navTiming () {
24
+ if (!navTimingValues.length) return
25
+ /*
26
+ 1. we initialize the seperator to ',' (seperates the nodeType id from the first value)
27
+ 2. we initialize the navTiming node to 'b' (the nodeType id)
28
+ 3. if the value is present, we add the seperator followed by the value;
29
+ otherwise:
30
+ - we add null seperator ('!') to the navTimingNode
31
+ - we set the seperator to an empty string since we already wrote it above
32
+ the reason for writing the null seperator instead of setting the seperator
33
+ is to ensure we still write it if the null is the last navTiming value.
34
+ */
35
+ let seperator = ','
36
+ let navTimingNode = 'b'
37
+ let prev = 0
38
+
39
+ // Get all navTiming values except offset aka timeOrigin since we just consider that (this.start) 0.
40
+ // These are the latter 20 of the 21 timings appended by addPT:
41
+ navTimingValues.slice(1, 21).forEach(v => {
42
+ if (v !== undefined) {
43
+ navTimingNode += seperator + numeric(v - prev)
44
+ seperator = ','
45
+ prev = v
46
+ } else {
47
+ navTimingNode += seperator + '!'
48
+ seperator = ''
49
+ }
50
+ })
51
+ return navTimingNode
52
+ }
53
+ }
@@ -0,0 +1,159 @@
1
+ import { getInfo } from '../../../common/config/config'
2
+ import { globalScope, initialLocation } from '../../../common/constants/runtime'
3
+ import { generateUuid } from '../../../common/ids/unique-id'
4
+ import { addCustomAttributes, getAddStringContext, nullable, numeric } from '../../../common/serialize/bel-serializer'
5
+ import { cleanURL } from '../../../common/url/clean-url'
6
+ import { NODE_TYPE, INTERACTION_STATUS, INTERACTION_TYPE, API_TRIGGER_NAME } from '../constants'
7
+ import { BelNode } from './bel-node'
8
+
9
+ /**
10
+ * link https://github.com/newrelic/nr-querypack/blob/main/schemas/bel/7.qpschema
11
+ **/
12
+ export class Interaction extends BelNode {
13
+ id = generateUuid() // unique id that is serialized and used to link interactions with errors
14
+ initialPageURL = initialLocation
15
+ oldURL = '' + globalScope?.location
16
+ newURL = '' + globalScope?.location
17
+ customName
18
+ customAttributes = {}
19
+ customDataByApi = {}
20
+ queueTime // only used by initialPageLoad interactions
21
+ appTime // only used by initialPageLoad interactions
22
+ newRoute
23
+ /** Internal state of this interaction: in-progress, finished, or cancelled. */
24
+ status = INTERACTION_STATUS.IP
25
+ domTimestamp = 0
26
+ historyTimestamp = 0
27
+ createdByApi = false
28
+ keepOpenUntilEndApi = false
29
+ onDone = []
30
+ cancellationTimer
31
+
32
+ constructor (agentIdentifier, uiEvent, uiEventTimestamp, currentRouteKnown) {
33
+ super(agentIdentifier)
34
+ this.belType = NODE_TYPE.INTERACTION
35
+ this.trigger = uiEvent
36
+ this.start = uiEventTimestamp
37
+ this.oldRoute = currentRouteKnown
38
+ this.eventSubscription = new Map([
39
+ ['finished', []],
40
+ ['cancelled', []]
41
+ ])
42
+ this.forceSave = this.forceIgnore = false
43
+ if (this.trigger === API_TRIGGER_NAME) this.createdByApi = true
44
+ }
45
+
46
+ updateDom (timestamp) {
47
+ this.domTimestamp = (timestamp || performance.now()) // default timestamp should be precise for accurate isActiveDuring calculations
48
+ }
49
+
50
+ updateHistory (timestamp, newUrl) {
51
+ this.newURL = newUrl || '' + globalScope?.location
52
+ this.historyTimestamp = (timestamp || performance.now())
53
+ }
54
+
55
+ seenHistoryAndDomChange () {
56
+ return this.historyTimestamp > 0 && this.domTimestamp > this.historyTimestamp // URL must change before DOM does
57
+ }
58
+
59
+ on (event, cb) {
60
+ if (!this.eventSubscription.has(event)) throw new Error('Cannot subscribe to non pre-defined events.')
61
+ if (typeof cb !== 'function') throw new Error('Must supply function as callback.')
62
+ this.eventSubscription.get(event).push(cb)
63
+ }
64
+
65
+ done (customEndTime) {
66
+ // User could've mark this interaction--regardless UI or api started--as "don't close until .end() is called on it". Only .end provides a timestamp; the default flows do not.
67
+ if (this.keepOpenUntilEndApi && customEndTime === undefined) return false
68
+ this.onDone.forEach(apiProvidedCb => apiProvidedCb(this.customDataByApi)) // this interaction's .save or .ignore can still be set by these user provided callbacks for example
69
+
70
+ if (this.forceIgnore) this.#cancel() // .ignore() always has precedence over save actions
71
+ else if (this.seenHistoryAndDomChange()) this.#finish(customEndTime) // then this should've already finished while it was the interactionInProgress, with a natural end time
72
+ else if (this.forceSave) this.#finish(customEndTime || performance.now()) // a manually saved ixn (did not fulfill conditions) must have a specified end time, if one wasn't provided
73
+ else this.#cancel()
74
+ return true
75
+ }
76
+
77
+ #finish (customEndTime = 0) {
78
+ if (this.status !== INTERACTION_STATUS.IP) return // disallow this call if the ixn is already done aka not in-progress
79
+ clearTimeout(this.cancellationTimer)
80
+ this.end = Math.max(this.domTimestamp, this.historyTimestamp, customEndTime)
81
+ this.customAttributes = { ...getInfo(this.agentIdentifier).jsAttributes, ...this.customAttributes } // attrs specific to this interaction should have precedence over the general custom attrs
82
+ this.status = INTERACTION_STATUS.FIN
83
+
84
+ // Run all the callbacks awaiting this interaction to finish.
85
+ const callbacks = this.eventSubscription.get('finished')
86
+ callbacks.forEach(fn => fn())
87
+ }
88
+
89
+ #cancel () {
90
+ if (this.status !== INTERACTION_STATUS.IP) return // disallow this call if the ixn is already done aka not in-progress
91
+ clearTimeout(this.cancellationTimer)
92
+ this.status = INTERACTION_STATUS.CAN
93
+
94
+ // Run all the callbacks listening to this interaction's potential cancellation.
95
+ const callbacks = this.eventSubscription.get('cancelled')
96
+ callbacks.forEach(fn => fn())
97
+ }
98
+
99
+ /**
100
+ * Given a timestamp, determine if it falls within this interaction's span, i.e. if this was the active interaction during that time.
101
+ * For in-progress interactions, this only compares the time with the start of span. Cancelled interactions are not considered active at all.
102
+ * @param {DOMHighResTimeStamp} timestamp
103
+ * @returns True or false boolean.
104
+ */
105
+ isActiveDuring (timestamp) {
106
+ if (this.status === INTERACTION_STATUS.IP) return this.start <= timestamp
107
+ return (this.status === INTERACTION_STATUS.FIN && this.start <= timestamp && this.end >= timestamp)
108
+ }
109
+
110
+ // Following are virtual properties overridden by a subclass:
111
+ get firstPaint () {}
112
+ get firstContentfulPaint () {}
113
+ get navTiming () {}
114
+
115
+ serialize (firstStartTimeOfPayload) {
116
+ const addString = getAddStringContext(this.agentIdentifier)
117
+ const nodeList = []
118
+ let ixnType
119
+ if (this.trigger === 'initialPageLoad') ixnType = INTERACTION_TYPE.INITIAL_PAGE_LOAD
120
+ else if (this.newURL !== this.oldURL) ixnType = INTERACTION_TYPE.ROUTE_CHANGE
121
+ else ixnType = INTERACTION_TYPE.UNSPECIFIED
122
+
123
+ // IMPORTANT: The order in which addString is called matters and correlates to the order in which string shows up in the harvest payload. Do not re-order the following code.
124
+ const fields = [
125
+ numeric(this.belType),
126
+ 0, // this will be overwritten below with number of attached nodes
127
+ numeric(Math.floor(this.start - firstStartTimeOfPayload)), // relative to first node
128
+ numeric(Math.floor(this.end - this.start)), // end -- relative to start
129
+ numeric(this.callbackEnd), // cbEnd -- relative to start; not used by BrowserInteraction events
130
+ numeric(this.callbackDuration), // not relative
131
+ addString(this.trigger),
132
+ addString(cleanURL(this.initialPageURL, true)),
133
+ addString(cleanURL(this.oldURL, true)),
134
+ addString(cleanURL(this.newURL, true)),
135
+ addString(this.customName),
136
+ ixnType,
137
+ nullable(this.queueTime, numeric, true) + nullable(this.appTime, numeric, true) +
138
+ nullable(this.oldRoute, addString, true) + nullable(this.newRoute, addString, true) +
139
+ addString(this.id),
140
+ addString(this.nodeId),
141
+ nullable(this.firstPaint, numeric, true) + nullable(this.firstContentfulPaint, numeric)
142
+ ]
143
+ const allAttachedNodes = addCustomAttributes(this.customAttributes || {}, addString) // start with all custom attributes
144
+ if (getInfo(this.agentIdentifier).atts) allAttachedNodes.push('a,' + addString(getInfo(this.agentIdentifier).atts)) // add apm provided attributes
145
+ /* Querypack encoder+decoder quirkiness:
146
+ - If first ixn node of payload is being processed, we use this node's start to offset. (firstStartTime should be 0--or undefined.)
147
+ - Else for subsequent ixn nodes, we use the first ixn node's start to offset. */
148
+ this.children.forEach(node => allAttachedNodes.push(node.serialize(firstStartTimeOfPayload || this.start))) // recursively add the serialized string of every child of this (ixn) bel node
149
+
150
+ fields[1] = numeric(allAttachedNodes.length)
151
+ nodeList.push(fields)
152
+ if (allAttachedNodes.length) nodeList.push(allAttachedNodes.join(';'))
153
+ if (this.navTiming) nodeList.push(this.navTiming)
154
+ else nodeList.push('')
155
+ // nodeList = [<fields array>, <serialized string of all attributes and children>, <serialized nav timing info> || '']
156
+
157
+ return nodeList.join(';')
158
+ }
159
+ }
@@ -0,0 +1,29 @@
1
+ import { FEATURE_NAMES } from '../../loaders/features/features'
2
+
3
+ export const INTERACTION_TRIGGERS = [
4
+ 'click', // e.g. user clicks link or the page back/forward buttons
5
+ 'keydown', // e.g. user presses left and right arrow key to switch between displayed photo gallery
6
+ 'submit' // e.g. user clicks submit butotn or presses enter while editing a form field
7
+ ]
8
+ export const API_TRIGGER_NAME = 'api'
9
+
10
+ export const FEATURE_NAME = FEATURE_NAMES.softNav
11
+
12
+ export const INTERACTION_TYPE = {
13
+ INITIAL_PAGE_LOAD: '',
14
+ ROUTE_CHANGE: 1,
15
+ UNSPECIFIED: 2
16
+ }
17
+
18
+ export const NODE_TYPE = {
19
+ INTERACTION: 1,
20
+ AJAX: 2,
21
+ CUSTOM_END: 3,
22
+ CUSTOM_TRACER: 4
23
+ }
24
+
25
+ export const INTERACTION_STATUS = {
26
+ IP: 'in progress',
27
+ FIN: 'finished',
28
+ CAN: 'cancelled'
29
+ }
@@ -0,0 +1 @@
1
+ export { Instrument as SoftNav } from './instrument/index'
@@ -0,0 +1,67 @@
1
+ import { originals } from '../../../common/config/config'
2
+ import { isBrowserScope } from '../../../common/constants/runtime'
3
+ import { handle } from '../../../common/event-emitter/handle'
4
+ import { windowAddEventListener } from '../../../common/event-listener/event-listener-opts'
5
+ import { now } from '../../../common/timing/now'
6
+ import { debounce } from '../../../common/util/invoke'
7
+ import { wrapEvents, wrapHistory } from '../../../common/wrap'
8
+ import { InstrumentBase } from '../../utils/instrument-base'
9
+ import { FEATURE_NAME, INTERACTION_TRIGGERS } from '../constants'
10
+
11
+ /** The minimal time after a UI event for which no further events will be processed - i.e. a throttling rate to reduce spam.
12
+ * This also give some time for the new interaction to complete without being discarded by a subsequent UI event and wrongly attributed.
13
+ * This value is still subject to change and critique, as it is derived from beyond worst case time to next frame of a page.
14
+ */
15
+ const UI_WAIT_INTERVAL = 1 / 10 * 1000 // assume 10 fps
16
+
17
+ export class Instrument extends InstrumentBase {
18
+ static featureName = FEATURE_NAME
19
+ constructor (agentIdentifier, aggregator, auto = true) {
20
+ super(agentIdentifier, aggregator, FEATURE_NAME, auto)
21
+ if (!isBrowserScope || !originals.MO) return // soft navigations is not supported outside web env or browsers without the mutation observer API
22
+
23
+ const historyEE = wrapHistory(this.ee)
24
+ const eventsEE = wrapEvents(this.ee)
25
+
26
+ const trackURLChange = () => handle('newURL', [now(), '' + window.location], undefined, this.featureName, this.ee)
27
+ historyEE.on('pushState-end', trackURLChange)
28
+ historyEE.on('replaceState-end', trackURLChange)
29
+
30
+ try {
31
+ this.removeOnAbort = new AbortController()
32
+ } catch (e) {}
33
+ const trackURLChangeEvent = (evt) => handle('newURL', [evt.timeStamp, '' + window.location], undefined, this.featureName, this.ee)
34
+ windowAddEventListener('popstate', trackURLChangeEvent, true, this.removeOnAbort?.signal)
35
+
36
+ let oncePerFrame = false // attempt to reduce dom noice since the observer runs very frequently with below options
37
+ const domObserver = new originals.MO((domChanges, observer) => {
38
+ if (oncePerFrame) return
39
+ oncePerFrame = true
40
+ requestAnimationFrame(() => { // waiting for next frame to time when any visuals are supposedly updated
41
+ handle('newDom', [now()], undefined, this.featureName, this.ee)
42
+ oncePerFrame = false
43
+ })
44
+ })
45
+
46
+ const processUserInteraction = debounce((event) => {
47
+ handle('newUIEvent', [event], undefined, this.featureName, this.ee)
48
+ domObserver.observe(document.body, { attributes: true, childList: true, subtree: true, characterData: true })
49
+ }, UI_WAIT_INTERVAL, { leading: true })
50
+
51
+ eventsEE.on('fn-start', ([evt]) => { // set up a new user ixn before the callback for the triggering event executes
52
+ if (INTERACTION_TRIGGERS.includes(evt?.type)) {
53
+ processUserInteraction(evt)
54
+ }
55
+ })
56
+ for (let eventType of INTERACTION_TRIGGERS) document.addEventListener(eventType, () => { /* no-op, this ensures the UI events are monitored by our callback above */ })
57
+
58
+ this.abortHandler = abort
59
+ this.importAggregator({ domObserver })
60
+
61
+ function abort () {
62
+ this.removeOnAbort?.abort()
63
+ domObserver.disconnect()
64
+ this.abortHandler = undefined // weakly allow this abort op to run only once
65
+ }
66
+ }
67
+ }
@@ -706,7 +706,7 @@ export class Aggregate extends AggregateBase {
706
706
 
707
707
  function saveInteraction (interaction) {
708
708
  if (interaction.ignored || (!interaction.save && !interaction.routeChange)) {
709
- baseEE.emit('interactionDiscarded', [interaction])
709
+ baseEE.emit('interactionDone', [interaction, false])
710
710
  return
711
711
  }
712
712
 
@@ -724,12 +724,13 @@ export class Aggregate extends AggregateBase {
724
724
  interaction.root.attrs.firstPaint = firstPaint.current.value
725
725
  interaction.root.attrs.firstContentfulPaint = firstContentfulPaint.current.value
726
726
  }
727
- baseEE.emit('interactionSaved', [interaction])
727
+ baseEE.emit('interactionDone', [interaction, true])
728
728
  state.interactionsToHarvest.push(interaction)
729
729
 
730
- let smCategory = 'RouteChange'
730
+ let smCategory
731
731
  if (interaction.root?.attrs?.trigger === 'initialPageLoad') smCategory = 'InitialPageLoad'
732
- else if (interaction.root?.attrs?.trigger === 'api') smCategory = 'Custom'
732
+ else if (interaction.routeChange) smCategory = 'RouteChange'
733
+ else smCategory = 'Custom'
733
734
  handle(SUPPORTABILITY_METRIC_CHANNEL, [`Spa/Interaction/${smCategory}/Duration/Ms`, Math.max((interaction.root?.end || 0) - (interaction.root?.start || 0), 0)], undefined, FEATURE_NAMES.metrics, baseEE)
734
735
 
735
736
  scheduler.scheduleHarvest(0)
@@ -5,6 +5,7 @@ import { registerHandler } from '../../common/event-emitter/register-handler'
5
5
  import { SessionEntity } from '../../common/session/session-entity'
6
6
  import { LocalStorage } from '../../common/storage/local-storage.js'
7
7
  import { FirstPartyCookies } from '../../common/storage/first-party-cookies'
8
+ import { DEFAULT_KEY } from '../../common/session/constants'
8
9
 
9
10
  let ranOnce = 0
10
11
  export function setupAgentSession (agentIdentifier) {
@@ -20,7 +21,7 @@ export function setupAgentSession (agentIdentifier) {
20
21
 
21
22
  agentRuntime.session = new SessionEntity({
22
23
  agentIdentifier,
23
- key: 'SESSION',
24
+ key: DEFAULT_KEY,
24
25
  storage: storageTypeInst,
25
26
  expiresMs: sessionInit?.expiresMs,
26
27
  inactiveMs: sessionInit?.inactiveMs