@newrelic/browser-agent 0.1.231 → 1.232.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 (210) hide show
  1. package/README.md +2 -2
  2. package/dist/cjs/common/config/state/configurable.js +27 -21
  3. package/dist/cjs/common/config/state/init.js +8 -0
  4. package/dist/cjs/common/config/state/runtime.js +24 -26
  5. package/dist/cjs/common/constants/env.cdn.js +1 -1
  6. package/dist/cjs/common/constants/env.npm.js +1 -1
  7. package/dist/cjs/common/context/shared-context.js +2 -1
  8. package/dist/cjs/common/event-emitter/contextual-ee.test.js +2 -2
  9. package/dist/cjs/common/event-emitter/register-handler.test.js +1 -1
  10. package/dist/cjs/common/event-listener/event-listener-opts.js +4 -2
  11. package/dist/cjs/common/harvest/harvest-scheduler.js +14 -11
  12. package/dist/cjs/common/harvest/harvest.js +3 -1
  13. package/dist/cjs/common/session/constants.js +12 -0
  14. package/dist/cjs/common/session/session-entity.js +278 -0
  15. package/dist/cjs/common/session/session-entity.test.js +436 -0
  16. package/dist/cjs/common/storage/first-party-cookies.js +35 -0
  17. package/dist/cjs/common/storage/local-memory.js +35 -0
  18. package/dist/cjs/common/storage/local-memory.test.js +20 -0
  19. package/dist/cjs/common/storage/local-storage.js +33 -0
  20. package/dist/cjs/common/storage/local-storage.test.js +14 -0
  21. package/dist/cjs/common/timer/interaction-timer.js +78 -0
  22. package/dist/cjs/common/timer/interaction-timer.test.js +216 -0
  23. package/dist/cjs/common/timer/timer.js +32 -0
  24. package/dist/cjs/common/timer/timer.test.js +105 -0
  25. package/dist/cjs/common/unload/eol.js +2 -2
  26. package/dist/cjs/common/util/data-size.js +6 -0
  27. package/dist/cjs/common/util/data-size.test.js +47 -0
  28. package/dist/cjs/common/util/invoke.js +73 -0
  29. package/dist/cjs/common/util/invoke.test.js +49 -0
  30. package/dist/cjs/common/util/obfuscate.js +0 -4
  31. package/dist/cjs/common/window/page-visibility.js +3 -1
  32. package/dist/cjs/common/wrap/wrap-timer.js +1 -1
  33. package/dist/cjs/features/ajax/aggregate/index.js +2 -2
  34. package/dist/cjs/features/jserrors/aggregate/index.js +3 -3
  35. package/dist/cjs/features/metrics/aggregate/index.js +13 -2
  36. package/dist/cjs/features/page_action/aggregate/index.js +2 -2
  37. package/dist/cjs/features/page_view_event/aggregate/index.js +6 -3
  38. package/dist/cjs/features/page_view_timing/aggregate/index.js +6 -6
  39. package/dist/cjs/features/session_trace/aggregate/index.js +2 -2
  40. package/dist/cjs/features/spa/aggregate/index.js +6 -5
  41. package/dist/cjs/features/utils/agent-session.js +73 -0
  42. package/dist/cjs/features/utils/feature-base.js +1 -1
  43. package/dist/cjs/features/utils/instrument-base.js +7 -2
  44. package/dist/cjs/features/utils/lazy-loader.js +1 -1
  45. package/dist/cjs/loaders/agent.js +1 -1
  46. package/dist/cjs/loaders/api/api.js +1 -4
  47. package/dist/cjs/loaders/api/apiAsync.js +3 -2
  48. package/dist/cjs/loaders/configure/configure.js +0 -6
  49. package/dist/esm/common/config/state/configurable.js +26 -20
  50. package/dist/esm/common/config/state/init.js +8 -0
  51. package/dist/esm/common/config/state/runtime.js +24 -26
  52. package/dist/esm/common/constants/env.cdn.js +1 -1
  53. package/dist/esm/common/constants/env.npm.js +1 -1
  54. package/dist/esm/common/context/shared-context.js +2 -1
  55. package/dist/esm/common/event-emitter/contextual-ee.test.js +2 -2
  56. package/dist/esm/common/event-emitter/register-handler.test.js +1 -1
  57. package/dist/esm/common/event-listener/event-listener-opts.js +4 -2
  58. package/dist/esm/common/harvest/harvest-scheduler.js +14 -11
  59. package/dist/esm/common/harvest/harvest.js +3 -1
  60. package/dist/esm/common/session/constants.js +3 -0
  61. package/dist/esm/common/session/session-entity.js +271 -0
  62. package/dist/esm/common/session/session-entity.test.js +434 -0
  63. package/dist/esm/common/storage/first-party-cookies.js +28 -0
  64. package/dist/esm/common/storage/local-memory.js +28 -0
  65. package/dist/esm/common/storage/local-memory.test.js +18 -0
  66. package/dist/esm/common/storage/local-storage.js +26 -0
  67. package/dist/esm/common/storage/local-storage.test.js +12 -0
  68. package/dist/esm/common/timer/interaction-timer.js +71 -0
  69. package/dist/esm/common/timer/interaction-timer.test.js +214 -0
  70. package/dist/esm/common/timer/timer.js +25 -0
  71. package/dist/esm/common/timer/timer.test.js +103 -0
  72. package/dist/esm/common/unload/eol.js +1 -1
  73. package/dist/esm/common/util/data-size.js +7 -0
  74. package/dist/esm/common/util/data-size.test.js +45 -0
  75. package/dist/esm/common/util/invoke.js +66 -0
  76. package/dist/esm/common/util/invoke.test.js +47 -0
  77. package/dist/esm/common/util/obfuscate.js +0 -4
  78. package/dist/esm/common/window/page-visibility.js +3 -1
  79. package/dist/esm/common/wrap/wrap-timer.js +1 -1
  80. package/dist/esm/features/ajax/aggregate/index.js +2 -2
  81. package/dist/esm/features/jserrors/aggregate/index.js +3 -3
  82. package/dist/esm/features/metrics/aggregate/index.js +14 -3
  83. package/dist/esm/features/page_action/aggregate/index.js +2 -2
  84. package/dist/esm/features/page_view_event/aggregate/index.js +6 -3
  85. package/dist/esm/features/page_view_timing/aggregate/index.js +6 -6
  86. package/dist/esm/features/session_trace/aggregate/index.js +2 -2
  87. package/dist/esm/features/spa/aggregate/index.js +6 -5
  88. package/dist/esm/features/utils/agent-session.js +67 -0
  89. package/dist/esm/features/utils/feature-base.js +1 -1
  90. package/dist/esm/features/utils/instrument-base.js +7 -2
  91. package/dist/esm/features/utils/lazy-loader.js +1 -1
  92. package/dist/esm/loaders/agent.js +1 -1
  93. package/dist/esm/loaders/api/api.js +2 -5
  94. package/dist/esm/loaders/api/apiAsync.js +2 -1
  95. package/dist/esm/loaders/configure/configure.js +2 -8
  96. package/dist/types/common/config/state/configurable.d.ts.map +1 -1
  97. package/dist/types/common/config/state/init.d.ts.map +1 -1
  98. package/dist/types/common/config/state/runtime.d.ts.map +1 -1
  99. package/dist/types/common/context/shared-context.d.ts.map +1 -1
  100. package/dist/types/common/event-listener/event-listener-opts.d.ts +2 -2
  101. package/dist/types/common/event-listener/event-listener-opts.d.ts.map +1 -1
  102. package/dist/types/common/harvest/harvest-scheduler.d.ts +1 -0
  103. package/dist/types/common/harvest/harvest-scheduler.d.ts.map +1 -1
  104. package/dist/types/common/harvest/harvest.d.ts.map +1 -1
  105. package/dist/types/common/session/constants.d.ts +4 -0
  106. package/dist/types/common/session/constants.d.ts.map +1 -0
  107. package/dist/types/common/session/session-entity.d.ts +72 -0
  108. package/dist/types/common/session/session-entity.d.ts.map +1 -0
  109. package/dist/types/common/storage/first-party-cookies.d.ts +8 -0
  110. package/dist/types/common/storage/first-party-cookies.d.ts.map +1 -0
  111. package/dist/types/common/storage/local-memory.d.ts +8 -0
  112. package/dist/types/common/storage/local-memory.d.ts.map +1 -0
  113. package/dist/types/common/storage/local-storage.d.ts +6 -0
  114. package/dist/types/common/storage/local-storage.d.ts.map +1 -0
  115. package/dist/types/common/timer/interaction-timer.d.ts +11 -0
  116. package/dist/types/common/timer/interaction-timer.d.ts.map +1 -0
  117. package/dist/types/common/timer/timer.d.ts +12 -0
  118. package/dist/types/common/timer/timer.d.ts.map +1 -0
  119. package/dist/types/common/util/data-size.d.ts +7 -1
  120. package/dist/types/common/util/data-size.d.ts.map +1 -1
  121. package/dist/types/common/util/invoke.d.ts +35 -0
  122. package/dist/types/common/util/invoke.d.ts.map +1 -0
  123. package/dist/types/common/util/obfuscate.d.ts.map +1 -1
  124. package/dist/types/common/window/page-visibility.d.ts +1 -1
  125. package/dist/types/common/window/page-visibility.d.ts.map +1 -1
  126. package/dist/types/features/ajax/aggregate/index.d.ts +2 -2
  127. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  128. package/dist/types/features/jserrors/aggregate/index.d.ts +2 -2
  129. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  130. package/dist/types/features/metrics/aggregate/index.d.ts +2 -2
  131. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  132. package/dist/types/features/page_action/aggregate/index.d.ts +2 -2
  133. package/dist/types/features/page_action/aggregate/index.d.ts.map +1 -1
  134. package/dist/types/features/page_view_event/aggregate/index.d.ts +2 -2
  135. package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
  136. package/dist/types/features/page_view_timing/aggregate/index.d.ts +2 -2
  137. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  138. package/dist/types/features/session_trace/aggregate/index.d.ts +2 -2
  139. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  140. package/dist/types/features/spa/aggregate/index.d.ts +2 -2
  141. package/dist/types/features/spa/aggregate/index.d.ts.map +1 -1
  142. package/dist/types/features/utils/agent-session.d.ts +2 -0
  143. package/dist/types/features/utils/agent-session.d.ts.map +1 -0
  144. package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
  145. package/dist/types/features/utils/lazy-loader.d.ts +2 -2
  146. package/dist/types/features/utils/lazy-loader.d.ts.map +1 -1
  147. package/dist/types/loaders/api/api.d.ts.map +1 -1
  148. package/dist/types/loaders/api/apiAsync.d.ts.map +1 -1
  149. package/dist/types/loaders/configure/configure.d.ts.map +1 -1
  150. package/package.json +6 -5
  151. package/src/common/config/state/configurable.js +26 -19
  152. package/src/common/config/state/init.js +7 -0
  153. package/src/common/config/state/runtime.js +22 -27
  154. package/src/common/context/shared-context.js +2 -1
  155. package/src/common/event-emitter/contextual-ee.test.js +2 -2
  156. package/src/common/event-emitter/register-handler.test.js +1 -1
  157. package/src/common/event-listener/event-listener-opts.js +4 -4
  158. package/src/common/harvest/harvest-scheduler.js +12 -8
  159. package/src/common/harvest/harvest.js +3 -1
  160. package/src/common/session/constants.js +3 -0
  161. package/src/common/session/session-entity.js +271 -0
  162. package/src/common/session/session-entity.test.js +317 -0
  163. package/src/common/storage/first-party-cookies.js +31 -0
  164. package/src/common/storage/local-memory.js +30 -0
  165. package/src/common/storage/local-memory.test.js +19 -0
  166. package/src/common/storage/local-storage.js +28 -0
  167. package/src/common/storage/local-storage.test.js +17 -0
  168. package/src/common/timer/interaction-timer.js +75 -0
  169. package/src/common/timer/interaction-timer.test.js +167 -0
  170. package/src/common/timer/timer.js +31 -0
  171. package/src/common/timer/timer.test.js +100 -0
  172. package/src/common/unload/eol.js +1 -1
  173. package/src/common/util/data-size.js +6 -0
  174. package/src/common/util/data-size.test.js +50 -0
  175. package/src/common/util/invoke.js +55 -0
  176. package/src/common/util/invoke.test.js +65 -0
  177. package/src/common/util/obfuscate.js +0 -4
  178. package/src/common/window/page-visibility.js +2 -2
  179. package/src/common/wrap/wrap-timer.js +1 -1
  180. package/src/features/ajax/aggregate/index.js +2 -2
  181. package/src/features/jserrors/aggregate/index.js +3 -3
  182. package/src/features/metrics/aggregate/index.js +18 -3
  183. package/src/features/page_action/aggregate/index.js +2 -2
  184. package/src/features/page_view_event/aggregate/index.js +6 -3
  185. package/src/features/page_view_timing/aggregate/index.js +6 -6
  186. package/src/features/session_trace/aggregate/index.js +2 -2
  187. package/src/features/spa/aggregate/index.js +5 -5
  188. package/src/features/utils/agent-session.js +68 -0
  189. package/src/features/utils/feature-base.js +1 -1
  190. package/src/features/utils/instrument-base.js +5 -2
  191. package/src/features/utils/lazy-loader.js +1 -1
  192. package/src/loaders/agent.js +1 -1
  193. package/src/loaders/api/api.js +2 -5
  194. package/src/loaders/api/apiAsync.js +2 -1
  195. package/src/loaders/configure/configure.js +2 -7
  196. package/dist/cjs/common/util/single.js +0 -23
  197. package/dist/cjs/common/window/session-storage.js +0 -87
  198. package/dist/cjs/features/utils/aggregate-base.js +0 -13
  199. package/dist/esm/common/util/single.js +0 -16
  200. package/dist/esm/common/window/session-storage.js +0 -77
  201. package/dist/esm/features/utils/aggregate-base.js +0 -6
  202. package/dist/types/common/util/single.d.ts +0 -2
  203. package/dist/types/common/util/single.d.ts.map +0 -1
  204. package/dist/types/common/window/session-storage.d.ts +0 -18
  205. package/dist/types/common/window/session-storage.d.ts.map +0 -1
  206. package/dist/types/features/utils/aggregate-base.d.ts +0 -4
  207. package/dist/types/features/utils/aggregate-base.d.ts.map +0 -1
  208. package/src/common/util/single.js +0 -18
  209. package/src/common/window/session-storage.js +0 -75
  210. package/src/features/utils/aggregate-base.js +0 -7
@@ -0,0 +1,271 @@
1
+ import { generateRandomHexString } from '../ids/unique-id'
2
+ import { warn } from '../util/console'
3
+ import { stringify } from '../util/stringify'
4
+ import { ee } from '../event-emitter/contextual-ee'
5
+ import { Timer } from '../timer/timer'
6
+ import { isBrowserScope } from '../util/global-scope'
7
+ import { DEFAULT_EXPIRES_MS, DEFAULT_INACTIVE_MS, PREFIX } from './constants'
8
+ import { LocalMemory } from '../storage/local-memory'
9
+ import { InteractionTimer } from '../timer/interaction-timer'
10
+ import { wrapEvents } from '../wrap'
11
+ import { Configurable } from '../config/state/configurable'
12
+ import { handle } from '../event-emitter/handle'
13
+ import { SUPPORTABILITY_METRIC_CHANNEL } from '../../features/metrics/constants'
14
+ import { FEATURE_NAMES } from '../../loaders/features/features'
15
+
16
+ const model = {
17
+ value: '',
18
+ inactiveAt: 0,
19
+ expiresAt: 0,
20
+ updatedAt: Date.now(),
21
+ sessionReplayActive: false,
22
+ sessionTraceActive: false,
23
+ custom: {}
24
+ }
25
+
26
+ export class SessionEntity {
27
+ /**
28
+ * Create a self-managing Session Entity. This entity is scoped to the agent identifier which triggered it, allowing for multiple simultaneous session objects to exist.
29
+ * There is one "namespace" an agent can store data in LS -- NRBA_{key}. If there are two agents on one page, and they both use the same key, they could overwrite each other since they would both use the same namespace in LS by default.
30
+ * The value can be overridden in the constructor, but will default to a unique 16 character hex string
31
+ * expiresMs and inactiveMs are used to "expire" the session, but can be overridden in the constructor. Pass 0 to disable expiration timers.
32
+ */
33
+ constructor (opts) {
34
+ this.setup(opts)
35
+ }
36
+
37
+ setup ({ agentIdentifier, key, value = generateRandomHexString(16), expiresMs = DEFAULT_EXPIRES_MS, inactiveMs = DEFAULT_INACTIVE_MS, storageAPI = new LocalMemory() }) {
38
+ if (!agentIdentifier || !key) throw new Error('Missing Required Fields')
39
+ if (!isBrowserScope) this.storage = new LocalMemory()
40
+ else this.storage = storageAPI
41
+
42
+ this.sync(model)
43
+
44
+ this.agentIdentifier = agentIdentifier
45
+
46
+ // key is intended to act as the k=v pair
47
+ this.key = key
48
+ // value is intended to act as the primary value of the k=v pair
49
+ this.value = value
50
+
51
+ this.expiresMs = expiresMs
52
+ this.inactiveMs = inactiveMs
53
+
54
+ this.ee = ee.get(agentIdentifier)
55
+
56
+ wrapEvents(this.ee)
57
+
58
+ // the first time the session entity class is instantiated, we check the storage API for an existing
59
+ // object. If it exists, the values inside the object are used to inform the timers that run locally.
60
+ // if the initial read is empty, it allows us to set a "fresh" "new" session immediately.
61
+ // the local timers are used after the session is running to "expire" the session, allowing for pausing timers etc.
62
+ // the timestamps stored in the storage API can be checked at initial run, and when the page is restored, otherwise we lean
63
+ // on the local timers to expire the session
64
+ const initialRead = this.read()
65
+
66
+ // the set-up of the timer used to expire the session "naturally" at a certain time
67
+ // this gets ignored if the value is falsy, allowing for session entities that do not expire
68
+ if (expiresMs) {
69
+ this.expiresAt = initialRead?.expiresAt || this.getFutureTimestamp(expiresMs)
70
+ this.expiresTimer = new Timer({
71
+ // When the inactive timer ends, collect a SM and reset the session
72
+ onEnd: () => {
73
+ this.collectSM('expired', this)
74
+ this.collectSM('duration', this)
75
+ this.reset()
76
+ }
77
+ }, this.expiresAt - Date.now())
78
+ } else {
79
+ this.expiresAt = Infinity
80
+ }
81
+
82
+ // the set-up of the timer used to expire the session due to "inactivity" at a certain time
83
+ // this gets ignored if the value is falsy, allowing for session entities that do not expire
84
+ // this gets "refreshed" when "activity" is observed
85
+ if (inactiveMs) {
86
+ this.inactiveAt = initialRead?.inactiveAt || this.getFutureTimestamp(inactiveMs)
87
+ this.inactiveTimer = new InteractionTimer({
88
+ // When the inactive timer ends, collect a SM and reset the session
89
+ onEnd: () => {
90
+ this.collectSM('inactive', this)
91
+ this.collectSM('duration', this)
92
+ this.reset()
93
+ },
94
+ // When the inactive timer refreshes, it will update the storage values with an update timestamp
95
+ onRefresh: this.refresh.bind(this),
96
+ // When the inactive timer pauses, update the storage values with an update timestamp
97
+ onPause: () => this.write(new Configurable(this.read(), model)),
98
+ ee: this.ee,
99
+ refreshEvents: ['click', 'keydown', 'scroll']
100
+ }, this.inactiveAt - Date.now())
101
+ } else {
102
+ this.inactiveAt = Infinity
103
+ }
104
+
105
+ // The fact that the session is "new" or pre-existing is used in some places in the agent. Session Replay and Trace
106
+ // can use this info to inform whether to trust a new sampling decision vs continue a previous tracking effort.
107
+ this.isNew = !Object.keys(initialRead).length
108
+ // if its a "new" session, we write to storage API with the default values. These values may change over the lifespan of the agent run.
109
+ // we can use configurable here to help us know and manage what values are being used. -- see "model" above
110
+ if (this.isNew) this.write(new Configurable(this, model), true)
111
+ else this.sync(initialRead)
112
+
113
+ this.initialized = true
114
+ }
115
+
116
+ // This is the actual key appended to the storage API
117
+ get lookupKey () {
118
+ return `${PREFIX}_${this.key}`
119
+ }
120
+
121
+ sync (data) {
122
+ Object.assign(this, data)
123
+ }
124
+
125
+ /**
126
+ * Fetch the stored values from the storage API tied to this entity
127
+ * @returns {Object}
128
+ */
129
+ read () {
130
+ try {
131
+ const val = this.storage.get(this.lookupKey)
132
+ if (!val) return {}
133
+ // TODO - decompression would need to happen here if we decide to do it
134
+ const obj = typeof val === 'string' ? JSON.parse(val) : val
135
+ if (this.isInvalid(obj)) return {}
136
+ // if the session expires, collect a SM count before resetting
137
+ if (this.isExpired(obj.expiresAt)) {
138
+ this.collectSM('expired', this)
139
+ this.collectSM('duration', obj, true)
140
+ return this.reset()
141
+ }
142
+ // if "inactive" timer is expired at "read" time -- esp. initial read -- reset
143
+ // collect a SM count before resetting
144
+ if (this.isExpired(obj.inactiveAt)) {
145
+ this.collectSM('inactive', this)
146
+ this.collectSM('duration', obj, true)
147
+ return this.reset()
148
+ }
149
+
150
+ return obj
151
+ } catch (e) {
152
+ warn('Failed to read from storage API', e)
153
+ // storage is inaccessible
154
+ return {}
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Store data to the storage API tied to this entity
160
+ * To preseve existing attributes, the output of ...session.read()
161
+ * should be appended to the data argument
162
+ * @param {Object} data
163
+ * @returns {Object}
164
+ */
165
+ write (data) {
166
+ try {
167
+ if (!data || typeof data !== 'object') return
168
+ // everytime we update, we can update a timestamp for sanity
169
+ data.updatedAt = Date.now()
170
+ this.sync(data)
171
+ // TODO - compression would need happen here if we decide to do it
172
+ this.storage.set(this.lookupKey, stringify(data))
173
+ return data
174
+ } catch (e) {
175
+ // storage is inaccessible
176
+ warn('Failed to write to the storage API', e)
177
+ return null
178
+ }
179
+ }
180
+
181
+ reset () {
182
+ // this method should set off a chain of actions across the features by emitting 'new-session'
183
+ // * send off pending payloads
184
+ // * stop recording (stn and sr)...
185
+ // * delete the session and start over
186
+ try {
187
+ if (this.initialized) this.ee.emit('session-reset')
188
+
189
+ this.storage.remove(this.lookupKey)
190
+ this.inactiveTimer?.abort?.()
191
+ this.expiresTimer?.clear?.()
192
+ delete this.custom
193
+ delete this.value
194
+
195
+ this.setup({
196
+ agentIdentifier: this.agentIdentifier,
197
+ key: this.key,
198
+ storageAPI: this.storage,
199
+ expiresMs: this.expiresMs,
200
+ inactiveMs: this.inactiveMs
201
+ })
202
+ return this.read()
203
+ } catch (e) {
204
+ return {}
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Refresh the inactivity timer data
210
+ */
211
+ refresh () {
212
+ // read here & invalidate
213
+ const existingData = this.read()
214
+ this.inactiveAt = this.getFutureTimestamp(this.inactiveMs)
215
+ this.write({ ...existingData, inactiveAt: this.inactiveAt })
216
+ }
217
+
218
+ /**
219
+ * @param {number} timestamp
220
+ * @returns {boolean}
221
+ */
222
+ isExpired (timestamp) {
223
+ return Date.now() > timestamp
224
+ }
225
+
226
+ /**
227
+ * @param {Object} data
228
+ * @returns {boolean}
229
+ */
230
+ isInvalid (data) {
231
+ const requiredKeys = ['value', 'expiresAt', 'inactiveAt']
232
+ return !requiredKeys.every(x => Object.keys(data).includes(x))
233
+ }
234
+
235
+ collectSM (type, data, useUpdatedAt) {
236
+ let value, tag
237
+ if (type === 'duration') {
238
+ const startingTimestamp = data.expiresAt - data.expiresMs
239
+ const endingTimestamp = useUpdatedAt ? data.updatedAt : Date.now()
240
+ value = endingTimestamp - startingTimestamp
241
+ tag = 'Session/Duration/Ms'
242
+ }
243
+ if (type === 'expired') tag = 'Session/Expired/Seen'
244
+ if (type === 'inactive') tag = 'Session/Inactive/Seen'
245
+
246
+ if (tag) handle(SUPPORTABILITY_METRIC_CHANNEL, [tag, value], undefined, FEATURE_NAMES.metrics, this.ee)
247
+ }
248
+
249
+ /**
250
+ * @param {number} futureMs - The number of ms to use to generate a future timestamp
251
+ * @returns {number}
252
+ */
253
+ getFutureTimestamp (futureMs) {
254
+ return Date.now() + futureMs
255
+ }
256
+
257
+ syncCustomAttribute (key, value) {
258
+ if (!isBrowserScope) return
259
+ if (value === null) {
260
+ const curr = this.read()
261
+ if (curr.custom) {
262
+ delete curr.custom[key]
263
+ this.write({ ...curr })
264
+ }
265
+ } else {
266
+ const curr = this.read()
267
+ this.custom = { ...(curr?.custom || {}), [key]: value }
268
+ this.write({ ...curr, custom: this.custom })
269
+ }
270
+ }
271
+ }
@@ -0,0 +1,317 @@
1
+
2
+ import { LocalMemory } from '../storage/local-memory'
3
+ import { LocalStorage } from '../storage/local-storage'
4
+
5
+ import { PREFIX } from './constants'
6
+ import { SessionEntity } from './session-entity'
7
+
8
+ const agentIdentifier = 'test_agent_identifier'
9
+ const key = 'test_key'
10
+ const value = 'test_value'
11
+
12
+ jest.mock('../timer/timer')
13
+ jest.mock('../timer/interaction-timer')
14
+ jest.useFakeTimers()
15
+
16
+ const mockBrowserScope = jest.fn().mockImplementation(() => true)
17
+ jest.mock('../util/global-scope', () => ({
18
+ __esModule: true,
19
+ get isBrowserScope () {
20
+ return mockBrowserScope()
21
+ },
22
+ get globalScope () {
23
+ return global.window
24
+ }
25
+ }))
26
+
27
+ beforeEach(() => {
28
+ jest.restoreAllMocks()
29
+ mockBrowserScope.mockReturnValue(true)
30
+ })
31
+
32
+ describe('constructor', () => {
33
+ test('must use required fields', () => {
34
+ try {
35
+ expect(new SessionEntity({})).toThrow(new Error('Missing Required Fields'))
36
+ } catch (err) {}
37
+ })
38
+
39
+ test('top-level properties are set and exposed', () => {
40
+ const session = new SessionEntity({ agentIdentifier, key })
41
+ expect(session).toMatchObject({
42
+ agentIdentifier: expect.any(String),
43
+ key: expect.any(String),
44
+ value: expect.any(String),
45
+ expiresMs: expect.any(Number),
46
+ expiresAt: expect.any(Number),
47
+ expiresTimer: expect.any(Object),
48
+ inactiveMs: expect.any(Number),
49
+ inactiveAt: expect.any(Number),
50
+ inactiveTimer: expect.any(Object),
51
+ isNew: expect.any(Boolean),
52
+ sessionReplayActive: expect.any(Boolean),
53
+ sessionTraceActive: expect.any(Boolean),
54
+ storage: expect.any(Object)
55
+ })
56
+ })
57
+
58
+ test('can use sane defaults', () => {
59
+ const session = new SessionEntity({ agentIdentifier, key })
60
+ expect(session).toEqual(expect.objectContaining({
61
+ value: expect.any(String),
62
+ expiresAt: expect.any(Number),
63
+ inactiveAt: expect.any(Number),
64
+ sessionReplayActive: expect.any(Boolean),
65
+ sessionTraceActive: expect.any(Boolean)
66
+ }))
67
+ })
68
+
69
+ test('Workers are forced to use local memory', () => {
70
+ mockBrowserScope.mockReturnValueOnce(false)
71
+ const session = new SessionEntity({ agentIdentifier, key, storageAPI: new LocalStorage() })
72
+ expect(session.storage instanceof LocalMemory).toEqual(true)
73
+ })
74
+
75
+ test('expiresAt is the correct future timestamp - new session', () => {
76
+ const now = Date.now()
77
+ jest.setSystemTime(now)
78
+ const session = new SessionEntity({ agentIdentifier, key, expiresMs: 100 })
79
+ expect(session.expiresAt).toEqual(now + 100)
80
+ })
81
+
82
+ test('expiresAt is the correct future timestamp - existing session', () => {
83
+ const now = Date.now()
84
+ jest.setSystemTime(now)
85
+ const existingData = new LocalMemory({ [`${PREFIX}_${key}`]: { value, expiresAt: now + 5000, inactiveAt: Infinity } })
86
+ const session = new SessionEntity({ agentIdentifier, key, expiresMs: 100, storageAPI: existingData })
87
+ expect(session.expiresAt).toEqual(now + 5000)
88
+ })
89
+
90
+ test('expiresAt never expires if 0', () => {
91
+ const session = new SessionEntity({ agentIdentifier, key, expiresMs: 0 })
92
+ expect(session.expiresAt).toEqual(Infinity)
93
+ })
94
+
95
+ test('inactiveAt is the correct future timestamp - new session', () => {
96
+ const now = Date.now()
97
+ jest.setSystemTime(now)
98
+ const session = new SessionEntity({ agentIdentifier, key, inactiveMs: 100 })
99
+ expect(session.inactiveAt).toEqual(now + 100)
100
+ })
101
+
102
+ test('inactiveAt is the correct future timestamp - existing session', () => {
103
+ const now = Date.now()
104
+ jest.setSystemTime(now)
105
+ const existingData = new LocalMemory({ [`${PREFIX}_${key}`]: { value, inactiveAt: now + 5000, expiresAt: Infinity } })
106
+ const session = new SessionEntity({ agentIdentifier, key, inactiveMs: 100, storageAPI: existingData })
107
+ expect(session.inactiveAt).toEqual(now + 5000)
108
+ })
109
+
110
+ test('inactiveAt never expires if 0', () => {
111
+ const session = new SessionEntity({ agentIdentifier, key, inactiveMs: 0 })
112
+ expect(session.inactiveAt).toEqual(Infinity)
113
+ })
114
+
115
+ test('should handle isNew', () => {
116
+ const newSession = new SessionEntity({ agentIdentifier, key, expiresMs: 10 })
117
+ expect(newSession.isNew).toBeTruthy()
118
+
119
+ const storageAPI = new LocalMemory({ [`${PREFIX}_${key}`]: { value, expiresAt: Infinity, inactiveAt: Infinity } })
120
+ const existingSession = new SessionEntity({ agentIdentifier, key, expiresMs: 10, storageAPI })
121
+ expect(existingSession.isNew).toBeFalsy()
122
+ })
123
+
124
+ test('invalid stored values sets new defaults', () => {
125
+ // missing required fields
126
+ const storageAPI = new LocalMemory({ [`${PREFIX}_${key}`]: { invalid_fields: true } })
127
+ const session = new SessionEntity({ agentIdentifier, key, storageAPI })
128
+ expect(session).toEqual(expect.objectContaining({
129
+ value: expect.any(String),
130
+ expiresAt: expect.any(Number),
131
+ inactiveAt: expect.any(Number),
132
+ sessionReplayActive: expect.any(Boolean),
133
+ sessionTraceActive: expect.any(Boolean)
134
+ }))
135
+ })
136
+
137
+ test('expired expiresAt value in storage sets new defaults', () => {
138
+ const now = Date.now()
139
+ jest.setSystemTime(now)
140
+ const storageAPI = new LocalMemory({ [`${PREFIX}_${key}`]: { value, expiresAt: now - 100, inactiveAt: Infinity } })
141
+ const session = new SessionEntity({ agentIdentifier, key, storageAPI })
142
+ expect(session).toEqual(expect.objectContaining({
143
+ value: expect.any(String),
144
+ expiresAt: expect.any(Number),
145
+ inactiveAt: expect.any(Number),
146
+ sessionReplayActive: expect.any(Boolean),
147
+ sessionTraceActive: expect.any(Boolean)
148
+ }))
149
+ })
150
+
151
+ test('expired inactiveAt value in storage sets new defaults', () => {
152
+ const now = Date.now()
153
+ jest.setSystemTime(now)
154
+ const storageAPI = new LocalMemory({ [`${PREFIX}_${key}`]: { value, inactiveAt: now - 100, expiresAt: Infinity } })
155
+ const session = new SessionEntity({ agentIdentifier, key, storageAPI })
156
+ expect(session).toEqual(expect.objectContaining({
157
+ value: expect.any(String),
158
+ expiresAt: expect.any(Number),
159
+ inactiveAt: expect.any(Number),
160
+ sessionReplayActive: expect.any(Boolean),
161
+ sessionTraceActive: expect.any(Boolean)
162
+ }))
163
+ })
164
+ })
165
+
166
+ describe('reset()', () => {
167
+ test('should create new default values when resetting', () => {
168
+ const now = Date.now()
169
+ jest.setSystemTime(now)
170
+ const session = new SessionEntity({ agentIdentifier, key, expiresMs: 10 })
171
+ const sessionVal = session.value
172
+ expect(session.value).toBeTruthy()
173
+ session.reset()
174
+ expect(session.value).toBeTruthy()
175
+ expect(session.value).not.toEqual(sessionVal)
176
+ })
177
+
178
+ test('custom data should be wiped on reset', () => {
179
+ const now = Date.now()
180
+ jest.setSystemTime(now)
181
+ const session = new SessionEntity({ agentIdentifier, key, expiresMs: 10 })
182
+ session.syncCustomAttribute('test', 123)
183
+ expect(session.custom.test).toEqual(123)
184
+ expect(session.read().custom.test).toEqual(123)
185
+
186
+ // simulate a timer expiring
187
+ session.reset()
188
+ expect(session.custom?.test).toEqual(undefined)
189
+ expect(session.read()?.custom?.test).toEqual(undefined)
190
+ })
191
+ })
192
+
193
+ describe('read()', () => {
194
+ test('"new" sessions get data from read()', () => {
195
+ const newSession = new SessionEntity({ agentIdentifier, key, expiresMs: 10 })
196
+ expect(newSession.isNew).toBeTruthy()
197
+
198
+ expect(newSession.read()).toEqual(expect.objectContaining({
199
+ value: expect.any(String),
200
+ expiresAt: expect.any(Number),
201
+ inactiveAt: expect.any(Number),
202
+ sessionReplayActive: expect.any(Boolean),
203
+ sessionTraceActive: expect.any(Boolean)
204
+ }))
205
+ })
206
+
207
+ test('"pre-existing" sessions get data from read()', () => {
208
+ const storageAPI = new LocalMemory({ [`${PREFIX}_${key}`]: { value, expiresAt: Infinity, inactiveAt: Infinity } })
209
+ const session = new SessionEntity({ agentIdentifier, key, storageAPI })
210
+ expect(session.isNew).toBeFalsy()
211
+ expect(session.read()).toEqual(expect.objectContaining({
212
+ value,
213
+ expiresAt: Infinity,
214
+ inactiveAt: Infinity
215
+ }))
216
+ })
217
+ })
218
+
219
+ describe('write()', () => {
220
+ test('write() sets data to top-level wrapper', () => {
221
+ const session = new SessionEntity({ agentIdentifier, key })
222
+ expect(session.value).not.toEqual(value)
223
+ expect(session.expiresAt).not.toEqual(Infinity)
224
+ expect(session.inactiveAt).not.toEqual(Infinity)
225
+ session.write({ value, expiresAt: Infinity, inactiveAt: Infinity })
226
+ expect(session.value).toEqual(value)
227
+ expect(session.expiresAt).toEqual(Infinity)
228
+ expect(session.inactiveAt).toEqual(Infinity)
229
+ })
230
+
231
+ test('write() sets data that read() can access', () => {
232
+ const now = Date.now()
233
+ jest.setSystemTime(now)
234
+ const session = new SessionEntity({ agentIdentifier, key })
235
+ session.write({ value, expiresAt: now + 100, inactiveAt: now + 100 })
236
+ const read = session.read()
237
+ expect(read.value).toEqual(value)
238
+ expect(read.expiresAt).toEqual(now + 100)
239
+ expect(read.inactiveAt).toEqual(now + 100)
240
+ })
241
+
242
+ test('write() does not run with invalid data', () => {
243
+ const session = new SessionEntity({ agentIdentifier, key })
244
+ let out = session.write()
245
+ expect(out).toEqual(undefined)
246
+ out = session.write('string')
247
+ expect(out).toEqual(undefined)
248
+ out = session.write(123)
249
+ expect(out).toEqual(undefined)
250
+ out = session.write(true)
251
+ expect(out).toEqual(undefined)
252
+ out = session.write(false)
253
+ expect(out).toEqual(undefined)
254
+ })
255
+ })
256
+
257
+ describe('refresh()', () => {
258
+ test('refresh sets inactiveAt to future time', () => {
259
+ const now = Date.now()
260
+ jest.setSystemTime(now)
261
+ const session = new SessionEntity({ agentIdentifier, key, inactiveMs: 100 })
262
+ expect(session.inactiveAt).toEqual(now + 100)
263
+ jest.setSystemTime(now + 1000)
264
+ session.refresh()
265
+ expect(session.inactiveAt).toEqual(now + 100 + 1000)
266
+ })
267
+
268
+ test('refresh resets the entity if expiresTimer is invalid', () => {
269
+ const now = Date.now()
270
+ jest.setSystemTime(now)
271
+ const session = new SessionEntity({ agentIdentifier, key, value })
272
+ expect(session.value).toEqual(value)
273
+ session.write({ ...session.read(), expiresAt: now - 1 })
274
+ session.refresh()
275
+ expect(session.value).not.toEqual(value)
276
+ })
277
+
278
+ test('refresh resets the entity if inactiveTimer is invalid', () => {
279
+ const now = Date.now()
280
+ jest.setSystemTime(now)
281
+ const session = new SessionEntity({ agentIdentifier, key, value })
282
+ expect(session.value).toEqual(value)
283
+ session.write({ ...session.read(), inactiveAt: now - 1 })
284
+ session.refresh()
285
+ expect(session.value).not.toEqual(value)
286
+ })
287
+ })
288
+
289
+ describe('syncCustomAttribute()', () => {
290
+ test('Custom data can be managed by session entity', () => {
291
+ const session = new SessionEntity({ agentIdentifier, key })
292
+
293
+ // if custom has never been set, and a "delete" action is triggered, do nothing
294
+ session.syncCustomAttribute('test', null)
295
+ expect(session?.custom?.test).toEqual(undefined)
296
+
297
+ session.syncCustomAttribute('test', 1)
298
+ expect(session?.custom?.test).toEqual(1)
299
+
300
+ session.syncCustomAttribute('test', 'string')
301
+ expect(session?.custom?.test).toEqual('string')
302
+
303
+ session.syncCustomAttribute('test', false)
304
+ expect(session?.custom?.test).toEqual(false)
305
+
306
+ // null specifically deletes the object completely
307
+ session.syncCustomAttribute('test', null)
308
+ expect(session?.custom?.test).toEqual(undefined)
309
+ })
310
+
311
+ test('Only runs in browser scope', () => {
312
+ mockBrowserScope.mockReturnValue(false)
313
+ const session = new SessionEntity({ agentIdentifier, key })
314
+ session.syncCustomAttribute('test', 1)
315
+ expect(session.read().custom?.test).toEqual(undefined)
316
+ })
317
+ })
@@ -0,0 +1,31 @@
1
+ export class FirstPartyCookies {
2
+ constructor (domain) {
3
+ this.domain = domain
4
+ }
5
+
6
+ get (name) {
7
+ try {
8
+ var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'))
9
+ if (match) return match[2]
10
+ } catch (err) {
11
+ return ''
12
+ }
13
+ }
14
+
15
+ set (key, value) {
16
+ try {
17
+ const cookie = `${key}=${value}; Domain=${domain}; Path=/`
18
+ document.cookie = cookie
19
+ } catch (err) {
20
+ return
21
+ }
22
+ }
23
+
24
+ remove (key) {
25
+ try {
26
+ return document.cookie = `${key}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; Domain=${domain}; Path=/`
27
+ } catch (err) {
28
+ return
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,30 @@
1
+ export class LocalMemory {
2
+ constructor (initialState = {}) {
3
+ this.state = initialState
4
+ }
5
+
6
+ get (key) {
7
+ try {
8
+ return this.state[key]
9
+ } catch (err) {
10
+ return ''
11
+ }
12
+ }
13
+
14
+ set (key, value) {
15
+ try {
16
+ if (value === undefined || value === null) return this.remove(key)
17
+ this.state[key] = value
18
+ } catch (err) {
19
+ return
20
+ }
21
+ }
22
+
23
+ remove (key) {
24
+ try {
25
+ delete this.state[key]
26
+ } catch (err) {
27
+ return
28
+ }
29
+ }
30
+ }
@@ -0,0 +1,19 @@
1
+ import { LocalMemory } from './local-memory'
2
+
3
+ test('Local-memory', () => {
4
+ const LM = new LocalMemory({ test: 1 })
5
+ expect(LM.state).toEqual({ test: 1 })
6
+ expect(LM.get('test')).toEqual(1)
7
+
8
+ LM.set('test', 2)
9
+ expect(LM.get('test')).toEqual(2)
10
+
11
+ LM.set('test')
12
+ expect(LM.get('test')).toEqual(undefined)
13
+
14
+ LM.set('test', 2)
15
+ expect(LM.get('test')).toEqual(2)
16
+
17
+ LM.remove('test')
18
+ expect(LM.get('test')).toEqual(undefined)
19
+ })
@@ -0,0 +1,28 @@
1
+ export class LocalStorage {
2
+ get (key) {
3
+ try {
4
+ // localStorage strangely type-casts non-existing data to "null"...
5
+ // Cast it back to undefined if it doesnt exist
6
+ return localStorage.getItem(key) || undefined
7
+ } catch (err) {
8
+ return ''
9
+ }
10
+ }
11
+
12
+ set (key, value) {
13
+ try {
14
+ if (value === undefined || value === null) return this.remove(key)
15
+ return localStorage.setItem(key, value)
16
+ } catch (err) {
17
+ return
18
+ }
19
+ }
20
+
21
+ remove (key) {
22
+ try {
23
+ localStorage.removeItem(key)
24
+ } catch (err) {
25
+ return
26
+ }
27
+ }
28
+ }