@newrelic/browser-agent 0.1.231 → 1.232.1

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 (256) 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/url/canonicalize-url.js +32 -0
  27. package/dist/cjs/common/url/canonicalize-url.test.js +42 -0
  28. package/dist/cjs/common/url/clean-url.js +10 -3
  29. package/dist/cjs/common/util/data-size.js +6 -0
  30. package/dist/cjs/common/util/data-size.test.js +47 -0
  31. package/dist/cjs/common/util/global-scope.js +4 -2
  32. package/dist/cjs/common/util/invoke.js +73 -0
  33. package/dist/cjs/common/util/invoke.test.js +49 -0
  34. package/dist/cjs/common/util/obfuscate.js +0 -4
  35. package/dist/cjs/common/window/page-visibility.js +3 -1
  36. package/dist/cjs/common/wrap/wrap-fetch.js +1 -3
  37. package/dist/cjs/common/wrap/wrap-function.js +1 -3
  38. package/dist/cjs/common/wrap/wrap-timer.js +1 -1
  39. package/dist/cjs/features/ajax/aggregate/index.js +2 -2
  40. package/dist/cjs/features/ajax/instrument/index.js +1 -1
  41. package/dist/cjs/features/jserrors/aggregate/canonical-function-name.js +12 -4
  42. package/dist/cjs/features/jserrors/aggregate/compute-stack-trace.js +93 -10
  43. package/dist/cjs/features/jserrors/aggregate/compute-stack-trace.test.js +164 -38
  44. package/dist/cjs/features/jserrors/aggregate/index.js +25 -46
  45. package/dist/cjs/features/jserrors/instrument/index.js +0 -2
  46. package/dist/cjs/features/metrics/aggregate/index.js +13 -2
  47. package/dist/cjs/features/page_action/aggregate/index.js +2 -2
  48. package/dist/cjs/features/page_view_event/aggregate/index.js +6 -3
  49. package/dist/cjs/features/page_view_timing/aggregate/index.js +6 -6
  50. package/dist/cjs/features/session_trace/aggregate/index.js +3 -5
  51. package/dist/cjs/features/spa/aggregate/index.js +6 -5
  52. package/dist/cjs/features/utils/agent-session.js +73 -0
  53. package/dist/cjs/features/utils/feature-base.js +1 -1
  54. package/dist/cjs/features/utils/instrument-base.js +7 -2
  55. package/dist/cjs/features/utils/lazy-loader.js +1 -1
  56. package/dist/cjs/loaders/agent.js +1 -1
  57. package/dist/cjs/loaders/api/api.js +1 -4
  58. package/dist/cjs/loaders/api/apiAsync.js +3 -2
  59. package/dist/cjs/loaders/configure/configure.js +0 -6
  60. package/dist/esm/common/config/state/configurable.js +26 -20
  61. package/dist/esm/common/config/state/init.js +8 -0
  62. package/dist/esm/common/config/state/runtime.js +24 -26
  63. package/dist/esm/common/constants/env.cdn.js +1 -1
  64. package/dist/esm/common/constants/env.npm.js +1 -1
  65. package/dist/esm/common/context/shared-context.js +2 -1
  66. package/dist/esm/common/event-emitter/contextual-ee.test.js +2 -2
  67. package/dist/esm/common/event-emitter/register-handler.test.js +1 -1
  68. package/dist/esm/common/event-listener/event-listener-opts.js +4 -2
  69. package/dist/esm/common/harvest/harvest-scheduler.js +14 -11
  70. package/dist/esm/common/harvest/harvest.js +3 -1
  71. package/dist/esm/common/session/constants.js +3 -0
  72. package/dist/esm/common/session/session-entity.js +271 -0
  73. package/dist/esm/common/session/session-entity.test.js +434 -0
  74. package/dist/esm/common/storage/first-party-cookies.js +28 -0
  75. package/dist/esm/common/storage/local-memory.js +28 -0
  76. package/dist/esm/common/storage/local-memory.test.js +18 -0
  77. package/dist/esm/common/storage/local-storage.js +26 -0
  78. package/dist/esm/common/storage/local-storage.test.js +12 -0
  79. package/dist/esm/common/timer/interaction-timer.js +71 -0
  80. package/dist/esm/common/timer/interaction-timer.test.js +214 -0
  81. package/dist/esm/common/timer/timer.js +25 -0
  82. package/dist/esm/common/timer/timer.test.js +103 -0
  83. package/dist/esm/common/unload/eol.js +1 -1
  84. package/dist/esm/common/url/canonicalize-url.js +27 -0
  85. package/dist/esm/common/url/canonicalize-url.test.js +38 -0
  86. package/dist/esm/common/url/clean-url.js +10 -3
  87. package/dist/esm/common/util/data-size.js +7 -0
  88. package/dist/esm/common/util/data-size.test.js +45 -0
  89. package/dist/esm/common/util/global-scope.js +1 -0
  90. package/dist/esm/common/util/invoke.js +66 -0
  91. package/dist/esm/common/util/invoke.test.js +47 -0
  92. package/dist/esm/common/util/obfuscate.js +0 -4
  93. package/dist/esm/common/window/page-visibility.js +3 -1
  94. package/dist/esm/common/wrap/wrap-fetch.js +1 -2
  95. package/dist/esm/common/wrap/wrap-function.js +1 -2
  96. package/dist/esm/common/wrap/wrap-timer.js +1 -1
  97. package/dist/esm/features/ajax/aggregate/index.js +2 -2
  98. package/dist/esm/features/ajax/instrument/index.js +1 -1
  99. package/dist/esm/features/jserrors/aggregate/canonical-function-name.js +12 -4
  100. package/dist/esm/features/jserrors/aggregate/compute-stack-trace.js +93 -10
  101. package/dist/esm/features/jserrors/aggregate/compute-stack-trace.test.js +149 -25
  102. package/dist/esm/features/jserrors/aggregate/index.js +26 -46
  103. package/dist/esm/features/jserrors/instrument/index.js +0 -1
  104. package/dist/esm/features/metrics/aggregate/index.js +14 -3
  105. package/dist/esm/features/page_action/aggregate/index.js +2 -2
  106. package/dist/esm/features/page_view_event/aggregate/index.js +6 -3
  107. package/dist/esm/features/page_view_timing/aggregate/index.js +6 -6
  108. package/dist/esm/features/session_trace/aggregate/index.js +3 -4
  109. package/dist/esm/features/spa/aggregate/index.js +6 -5
  110. package/dist/esm/features/utils/agent-session.js +67 -0
  111. package/dist/esm/features/utils/feature-base.js +1 -1
  112. package/dist/esm/features/utils/instrument-base.js +7 -2
  113. package/dist/esm/features/utils/lazy-loader.js +1 -1
  114. package/dist/esm/loaders/agent.js +1 -1
  115. package/dist/esm/loaders/api/api.js +2 -5
  116. package/dist/esm/loaders/api/apiAsync.js +2 -1
  117. package/dist/esm/loaders/configure/configure.js +2 -8
  118. package/dist/types/common/config/state/configurable.d.ts.map +1 -1
  119. package/dist/types/common/config/state/init.d.ts.map +1 -1
  120. package/dist/types/common/config/state/runtime.d.ts.map +1 -1
  121. package/dist/types/common/context/shared-context.d.ts.map +1 -1
  122. package/dist/types/common/event-listener/event-listener-opts.d.ts +2 -2
  123. package/dist/types/common/event-listener/event-listener-opts.d.ts.map +1 -1
  124. package/dist/types/common/harvest/harvest-scheduler.d.ts +1 -0
  125. package/dist/types/common/harvest/harvest-scheduler.d.ts.map +1 -1
  126. package/dist/types/common/harvest/harvest.d.ts.map +1 -1
  127. package/dist/types/common/session/constants.d.ts +4 -0
  128. package/dist/types/common/session/constants.d.ts.map +1 -0
  129. package/dist/types/common/session/session-entity.d.ts +72 -0
  130. package/dist/types/common/session/session-entity.d.ts.map +1 -0
  131. package/dist/types/common/storage/first-party-cookies.d.ts +8 -0
  132. package/dist/types/common/storage/first-party-cookies.d.ts.map +1 -0
  133. package/dist/types/common/storage/local-memory.d.ts +8 -0
  134. package/dist/types/common/storage/local-memory.d.ts.map +1 -0
  135. package/dist/types/common/storage/local-storage.d.ts +6 -0
  136. package/dist/types/common/storage/local-storage.d.ts.map +1 -0
  137. package/dist/types/common/timer/interaction-timer.d.ts +11 -0
  138. package/dist/types/common/timer/interaction-timer.d.ts.map +1 -0
  139. package/dist/types/common/timer/timer.d.ts +12 -0
  140. package/dist/types/common/timer/timer.d.ts.map +1 -0
  141. package/dist/types/common/url/canonicalize-url.d.ts +9 -0
  142. package/dist/types/common/url/canonicalize-url.d.ts.map +1 -0
  143. package/dist/types/common/url/clean-url.d.ts +7 -1
  144. package/dist/types/common/url/clean-url.d.ts.map +1 -1
  145. package/dist/types/common/util/data-size.d.ts +7 -1
  146. package/dist/types/common/util/data-size.d.ts.map +1 -1
  147. package/dist/types/common/util/global-scope.d.ts +1 -0
  148. package/dist/types/common/util/global-scope.d.ts.map +1 -1
  149. package/dist/types/common/util/invoke.d.ts +35 -0
  150. package/dist/types/common/util/invoke.d.ts.map +1 -0
  151. package/dist/types/common/util/obfuscate.d.ts.map +1 -1
  152. package/dist/types/common/window/page-visibility.d.ts +1 -1
  153. package/dist/types/common/window/page-visibility.d.ts.map +1 -1
  154. package/dist/types/common/wrap/wrap-fetch.d.ts.map +1 -1
  155. package/dist/types/common/wrap/wrap-function.d.ts.map +1 -1
  156. package/dist/types/features/ajax/aggregate/index.d.ts +2 -2
  157. package/dist/types/features/ajax/aggregate/index.d.ts.map +1 -1
  158. package/dist/types/features/jserrors/aggregate/canonical-function-name.d.ts +8 -1
  159. package/dist/types/features/jserrors/aggregate/canonical-function-name.d.ts.map +1 -1
  160. package/dist/types/features/jserrors/aggregate/compute-stack-trace.d.ts +48 -19
  161. package/dist/types/features/jserrors/aggregate/compute-stack-trace.d.ts.map +1 -1
  162. package/dist/types/features/jserrors/aggregate/index.d.ts +14 -5
  163. package/dist/types/features/jserrors/aggregate/index.d.ts.map +1 -1
  164. package/dist/types/features/jserrors/instrument/index.d.ts.map +1 -1
  165. package/dist/types/features/metrics/aggregate/index.d.ts +2 -2
  166. package/dist/types/features/metrics/aggregate/index.d.ts.map +1 -1
  167. package/dist/types/features/page_action/aggregate/index.d.ts +3 -3
  168. package/dist/types/features/page_action/aggregate/index.d.ts.map +1 -1
  169. package/dist/types/features/page_view_event/aggregate/index.d.ts +2 -2
  170. package/dist/types/features/page_view_event/aggregate/index.d.ts.map +1 -1
  171. package/dist/types/features/page_view_timing/aggregate/index.d.ts +2 -2
  172. package/dist/types/features/page_view_timing/aggregate/index.d.ts.map +1 -1
  173. package/dist/types/features/session_trace/aggregate/index.d.ts +2 -2
  174. package/dist/types/features/session_trace/aggregate/index.d.ts.map +1 -1
  175. package/dist/types/features/spa/aggregate/index.d.ts +2 -2
  176. package/dist/types/features/spa/aggregate/index.d.ts.map +1 -1
  177. package/dist/types/features/utils/agent-session.d.ts +2 -0
  178. package/dist/types/features/utils/agent-session.d.ts.map +1 -0
  179. package/dist/types/features/utils/instrument-base.d.ts.map +1 -1
  180. package/dist/types/features/utils/lazy-loader.d.ts +2 -2
  181. package/dist/types/features/utils/lazy-loader.d.ts.map +1 -1
  182. package/dist/types/loaders/api/api.d.ts.map +1 -1
  183. package/dist/types/loaders/api/apiAsync.d.ts.map +1 -1
  184. package/dist/types/loaders/configure/configure.d.ts.map +1 -1
  185. package/package.json +9 -8
  186. package/src/common/config/state/configurable.js +26 -19
  187. package/src/common/config/state/init.js +7 -0
  188. package/src/common/config/state/runtime.js +22 -27
  189. package/src/common/context/shared-context.js +2 -1
  190. package/src/common/event-emitter/contextual-ee.test.js +2 -2
  191. package/src/common/event-emitter/register-handler.test.js +1 -1
  192. package/src/common/event-listener/event-listener-opts.js +4 -4
  193. package/src/common/harvest/harvest-scheduler.js +12 -8
  194. package/src/common/harvest/harvest.js +3 -1
  195. package/src/common/session/constants.js +3 -0
  196. package/src/common/session/session-entity.js +271 -0
  197. package/src/common/session/session-entity.test.js +317 -0
  198. package/src/common/storage/first-party-cookies.js +31 -0
  199. package/src/common/storage/local-memory.js +30 -0
  200. package/src/common/storage/local-memory.test.js +19 -0
  201. package/src/common/storage/local-storage.js +28 -0
  202. package/src/common/storage/local-storage.test.js +17 -0
  203. package/src/common/timer/interaction-timer.js +75 -0
  204. package/src/common/timer/interaction-timer.test.js +167 -0
  205. package/src/common/timer/timer.js +31 -0
  206. package/src/common/timer/timer.test.js +100 -0
  207. package/src/common/unload/eol.js +1 -1
  208. package/src/common/url/canonicalize-url.js +28 -0
  209. package/src/common/url/canonicalize-url.test.js +34 -0
  210. package/src/common/url/clean-url.js +10 -3
  211. package/src/common/util/data-size.js +6 -0
  212. package/src/common/util/data-size.test.js +50 -0
  213. package/src/common/util/global-scope.js +2 -0
  214. package/src/common/util/invoke.js +55 -0
  215. package/src/common/util/invoke.test.js +65 -0
  216. package/src/common/util/obfuscate.js +0 -4
  217. package/src/common/window/page-visibility.js +2 -2
  218. package/src/common/wrap/wrap-fetch.js +1 -2
  219. package/src/common/wrap/wrap-function.js +1 -2
  220. package/src/common/wrap/wrap-timer.js +1 -1
  221. package/src/features/ajax/aggregate/index.js +2 -2
  222. package/src/features/ajax/instrument/index.js +1 -1
  223. package/src/features/jserrors/aggregate/canonical-function-name.js +12 -4
  224. package/src/features/jserrors/aggregate/compute-stack-trace.js +85 -11
  225. package/src/features/jserrors/aggregate/compute-stack-trace.test.js +141 -24
  226. package/src/features/jserrors/aggregate/index.js +24 -50
  227. package/src/features/jserrors/instrument/index.js +0 -1
  228. package/src/features/metrics/aggregate/index.js +18 -3
  229. package/src/features/page_action/aggregate/index.js +2 -2
  230. package/src/features/page_view_event/aggregate/index.js +6 -3
  231. package/src/features/page_view_timing/aggregate/index.js +6 -6
  232. package/src/features/session_trace/aggregate/index.js +3 -4
  233. package/src/features/spa/aggregate/index.js +5 -5
  234. package/src/features/utils/agent-session.js +68 -0
  235. package/src/features/utils/feature-base.js +1 -1
  236. package/src/features/utils/instrument-base.js +5 -2
  237. package/src/features/utils/lazy-loader.js +1 -1
  238. package/src/loaders/agent.js +1 -1
  239. package/src/loaders/api/api.js +2 -5
  240. package/src/loaders/api/apiAsync.js +2 -1
  241. package/src/loaders/configure/configure.js +2 -7
  242. package/dist/cjs/common/util/single.js +0 -23
  243. package/dist/cjs/common/window/session-storage.js +0 -87
  244. package/dist/cjs/features/utils/aggregate-base.js +0 -13
  245. package/dist/esm/common/util/single.js +0 -16
  246. package/dist/esm/common/window/session-storage.js +0 -77
  247. package/dist/esm/features/utils/aggregate-base.js +0 -6
  248. package/dist/types/common/util/single.d.ts +0 -2
  249. package/dist/types/common/util/single.d.ts.map +0 -1
  250. package/dist/types/common/window/session-storage.d.ts +0 -18
  251. package/dist/types/common/window/session-storage.d.ts.map +0 -1
  252. package/dist/types/features/utils/aggregate-base.d.ts +0 -4
  253. package/dist/types/features/utils/aggregate-base.d.ts.map +0 -1
  254. package/src/common/util/single.js +0 -18
  255. package/src/common/window/session-storage.js +0 -75
  256. package/src/features/utils/aggregate-base.js +0 -7
@@ -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
+ }
@@ -0,0 +1,17 @@
1
+ import { LocalStorage } from './local-storage'
2
+
3
+ test('Local-memory', () => {
4
+ const LS = new LocalStorage()
5
+
6
+ LS.set('test', 1)
7
+ expect(LS.get('test')).toEqual('1')
8
+
9
+ LS.set('test')
10
+ expect(LS.get('test')).toEqual(undefined)
11
+
12
+ LS.set('test', 2)
13
+ expect(LS.get('test')).toEqual('2')
14
+
15
+ LS.remove('test')
16
+ expect(LS.get('test')).toEqual(undefined)
17
+ })
@@ -0,0 +1,75 @@
1
+ import { Timer } from './timer'
2
+ import { subscribeToVisibilityChange } from '../window/page-visibility'
3
+ import { debounce } from '../util/invoke'
4
+ import { isBrowserScope } from '../util/global-scope'
5
+
6
+ export class InteractionTimer extends Timer {
7
+ constructor (opts, ms) {
8
+ super(opts, ms)
9
+ this.onRefresh = opts.onRefresh
10
+ this.onPause = opts.onPause
11
+
12
+ // used by pause/resume
13
+ this.remainingMs = undefined
14
+
15
+ if (!opts.refreshEvents) opts.refreshEvents = ['click', 'keydown', 'scroll']
16
+
17
+ // the abort controller is used to "reset" the event listeners and prevent them from duplicating when new sessions are created
18
+ try {
19
+ this.abortController = new AbortController()
20
+ } catch (e) {
21
+ // this try-catch can be removed when IE11 is completely unsupported & gone
22
+ }
23
+
24
+ if (isBrowserScope && opts.ee) {
25
+ if (opts.ee) {
26
+ const debouncedRefresh = debounce(this.refresh.bind(this), 500, { leading: true })
27
+ opts.ee.on('fn-end', (evts) => {
28
+ if (opts.refreshEvents.includes(evts?.[0]?.type)) {
29
+ debouncedRefresh()
30
+ }
31
+ })
32
+ }
33
+
34
+ // watch for the vis state changing. If the page is hidden, the local inactivity timer should be paused
35
+ // if the page is brought BACK to visibility and the timer hasnt "naturally" expired, refresh the timer...
36
+ // this is to support the concept that other tabs could be experiencing activity. The thought would be that
37
+ // "backgrounded" tabs would pause, while "closed" tabs that "reopen" will just instantiate a new SessionEntity class if restored
38
+ // which will do a "hard" check of the timestamps.
39
+
40
+ // NOTE -- this does not account for 2 browser windows open side by side, blurring/focusing between them
41
+ // IF DEEMED necessary, more event handling would be needed to account for this.
42
+ subscribeToVisibilityChange((state) => {
43
+ if (state === 'hidden') this.pause()
44
+ // vis change --> visible is treated like a new interaction with the page
45
+ else this.refresh()
46
+ }, false, false, this.abortController?.signal)
47
+ }
48
+ }
49
+
50
+ abort () {
51
+ this.clear()
52
+ this.abortController?.abort()
53
+ }
54
+
55
+ pause () {
56
+ this.onPause?.()
57
+ clearTimeout(this.timer)
58
+ this.remainingMs = this.initialMs - (Date.now() - this.startTimestamp)
59
+ }
60
+
61
+ refresh (cb, ms) {
62
+ this.clear()
63
+ this.timer = this.create(cb, ms)
64
+ this.startTimestamp = Date.now()
65
+ this.remainingMs = undefined
66
+ this.onRefresh?.()
67
+ }
68
+
69
+ // NOT CURRENTLY UTILIZED BY ANYTHING
70
+ // resume () {
71
+ // if (!this.remainingMs || !this.isValid()) return
72
+ // this.timer = this.create(this.cb, this.remainingMs)
73
+ // this.remainingMs = undefined
74
+ // }
75
+ }
@@ -0,0 +1,167 @@
1
+ import { InteractionTimer } from './interaction-timer'
2
+
3
+ jest.useFakeTimers()
4
+
5
+ let now
6
+
7
+ beforeEach(() => {
8
+ now = Date.now()
9
+ jest.setSystemTime(now)
10
+ })
11
+
12
+ afterEach(() => {
13
+ jest.clearAllMocks()
14
+ })
15
+
16
+ describe('constructor', () => {
17
+ test('appropriate properties are set with valid values -- no refresh', () => {
18
+ const timer = new InteractionTimer({ onEnd: jest.fn() }, 100)
19
+ const requiredKeys = ['onEnd', 'initialMs', 'startTimestamp', 'timer']
20
+ expect(requiredKeys.every(rk => !!timer[rk])).toBeTruthy()
21
+ })
22
+
23
+ test('appropriate properties are set with valid values -- with refresh', () => {
24
+ const timer = new InteractionTimer({ onEnd: jest.fn() }, 100)
25
+ const requiredKeys = ['onEnd', 'refresh', 'initialMs', 'startTimestamp', 'timer']
26
+ expect(requiredKeys.every(rk => !!timer[rk])).toBeTruthy()
27
+ })
28
+
29
+ test('required keys are enforced', () => {
30
+ try {
31
+ new InteractionTimer({}, 100)
32
+ } catch (e) {
33
+ expect(e).toEqual(new Error('onEnd handler is required'))
34
+ }
35
+
36
+ try {
37
+ new InteractionTimer({ onEnd: jest.fn() })
38
+ } catch (e) {
39
+ expect(e).toEqual(new Error('ms duration is required'))
40
+ }
41
+ })
42
+
43
+ test('refresh type timers set event listeners', () => {
44
+ // eslint-disable-next-line
45
+ let ee = { on: jest.fn().mockImplementation((evt, cb) => { cb([{ type: 'click' }]) }) }
46
+ let it = new InteractionTimer({ onEnd: jest.fn(), onRefresh: jest.fn(), ee }, 100)
47
+ // scroll, keypress, click
48
+ expect(ee.on).toHaveBeenCalledTimes(1)
49
+ expect(it.onRefresh).toHaveBeenCalledTimes(1)
50
+
51
+ // eslint-disable-next-line
52
+ ee = { on: jest.fn().mockImplementation((evt, cb) => { cb([{ type: 'scroll' }]) }) }
53
+ it = new InteractionTimer({ onEnd: jest.fn(), onRefresh: jest.fn(), ee }, 100)
54
+ // scroll, keypress, click
55
+ expect(ee.on).toHaveBeenCalledTimes(1)
56
+ expect(it.onRefresh).toHaveBeenCalledTimes(1)
57
+
58
+ // eslint-disable-next-line
59
+ ee = { on: jest.fn().mockImplementation((evt, cb) => { cb([{ type: 'keydown' }]) }) }
60
+ it = new InteractionTimer({ onEnd: jest.fn(), onRefresh: jest.fn(), ee }, 100)
61
+ // scroll, keypress, click
62
+ expect(ee.on).toHaveBeenCalledTimes(1)
63
+ expect(it.onRefresh).toHaveBeenCalledTimes(1)
64
+
65
+ const aelSpy = jest.spyOn(document, 'addEventListener')
66
+ // eslint-disable-next-line
67
+ ee = { on: jest.fn().mockImplementation((evt, cb) => { cb([{ type: 'keydown' }]) }) }
68
+ it = new InteractionTimer({ onEnd: jest.fn(), onRefresh: jest.fn(), ee }, 100)
69
+ // visibility change
70
+ expect(aelSpy).toHaveBeenCalledTimes(1)
71
+ })
72
+ })
73
+
74
+ describe('create()', () => {
75
+ test('Create sets a timeout that can execute a cb', () => {
76
+ const timer = new InteractionTimer({ onEnd: jest.fn() }, 100)
77
+ expect(timer.timer).toBeTruthy()
78
+ expect(timer.onEnd).toHaveBeenCalledTimes(0)
79
+ jest.runOnlyPendingTimers()
80
+ expect(timer.onEnd).toHaveBeenCalledTimes(1)
81
+ })
82
+
83
+ test('Create can fallback to use defaults', () => {
84
+ let called = 0
85
+ const timer1 = new InteractionTimer({ onEnd: jest.fn() }, 100)
86
+ timer1.create()
87
+
88
+ const timer2 = new InteractionTimer({ onEnd: jest.fn() }, 100)
89
+ timer2.create(timer2.onEnd)
90
+
91
+ const timer3 = new InteractionTimer({ onEnd: jest.fn() }, 100)
92
+ timer3.create(undefined, 100)
93
+
94
+ jest.runAllTimers(200)
95
+
96
+ expect(timer1.onEnd).toHaveBeenCalledTimes(1)
97
+ expect(timer2.onEnd).toHaveBeenCalledTimes(1)
98
+ expect(timer3.onEnd).toHaveBeenCalledTimes(1)
99
+ })
100
+ })
101
+
102
+ describe('refresh()', () => {
103
+ test('refresh prevents the callback from firing', () => {
104
+ const timer = new InteractionTimer({ onEnd: jest.fn(), onRefresh: jest.fn() }, 100)
105
+ expect(timer.onEnd).toHaveBeenCalledTimes(0)
106
+ jest.advanceTimersByTime(75)
107
+ expect(timer.onEnd).toHaveBeenCalledTimes(0)
108
+ timer.refresh()
109
+ jest.advanceTimersByTime(75)
110
+ expect(timer.onEnd).toHaveBeenCalledTimes(0)
111
+ jest.advanceTimersByTime(100)
112
+ expect(timer.onEnd).toHaveBeenCalledTimes(1)
113
+ })
114
+
115
+ test('refresh executes a callback for consumers', () => {
116
+ const timer = new InteractionTimer({ onEnd: jest.fn(), onRefresh: jest.fn() }, 100)
117
+ timer.refresh()
118
+ expect(timer.onRefresh).toHaveBeenCalledTimes(1)
119
+ })
120
+ })
121
+
122
+ describe('pause()', () => {
123
+ test('pause prevents the callback from firing', () => {
124
+ const timer = new InteractionTimer({ onEnd: jest.fn() }, 100)
125
+ expect(timer.onEnd).toHaveBeenCalledTimes(0)
126
+ timer.pause()
127
+ jest.advanceTimersByTime(150)
128
+ expect(timer.onEnd).toHaveBeenCalledTimes(0)
129
+ })
130
+
131
+ test('pause sets remainingMs timestamp', () => {
132
+ const timer = new InteractionTimer({ onEnd: jest.fn() }, 100)
133
+ expect(timer.remainingMs).toEqual(undefined)
134
+ timer.pause()
135
+ expect(timer.remainingMs).toEqual(timer.initialMs - (now - timer.startTimestamp))
136
+ })
137
+ })
138
+
139
+ describe('clear()', () => {
140
+ test('clear prevents the callback from firing and deletes the pointer', () => {
141
+ const timer = new InteractionTimer({ onEnd: jest.fn() }, 100)
142
+ expect(timer.onEnd).toHaveBeenCalledTimes(0)
143
+ timer.clear()
144
+ jest.advanceTimersByTime(150)
145
+ expect(timer.onEnd).toHaveBeenCalledTimes(0)
146
+ expect(timer.timer).toEqual(null)
147
+ })
148
+ })
149
+
150
+ describe('end()', () => {
151
+ test('end clears the callback and calls the onEnd callback', () => {
152
+ const timer = new InteractionTimer({ onEnd: jest.fn() }, 100)
153
+ expect(timer.onEnd).toHaveBeenCalledTimes(0)
154
+ timer.end()
155
+ expect(timer.onEnd).toHaveBeenCalledTimes(1)
156
+ expect(timer.timer).toEqual(null)
157
+ })
158
+ })
159
+
160
+ describe('isValid', () => {
161
+ test('isValid validates timeStamps', () => {
162
+ const timer = new InteractionTimer({ onEnd: jest.fn() }, 100)
163
+ expect(timer.isValid()).toEqual(true)
164
+ timer.startTimestamp -= 100
165
+ expect(timer.isValid()).toEqual(false)
166
+ })
167
+ })