@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,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
+ })
@@ -0,0 +1,31 @@
1
+
2
+ export class Timer {
3
+ constructor (opts, ms) {
4
+ if (!opts.onEnd) throw new Error('onEnd handler is required')
5
+ if (!ms) throw new Error('ms duration is required')
6
+ this.onEnd = opts.onEnd
7
+ this.initialMs = ms
8
+ this.startTimestamp = Date.now()
9
+
10
+ this.timer = this.create(this.onEnd, ms)
11
+ }
12
+
13
+ create (cb, ms) {
14
+ if (this.timer) this.clear()
15
+ return setTimeout(() => cb ? cb() : this.onEnd(), ms || this.initialMs)
16
+ }
17
+
18
+ clear () {
19
+ clearTimeout(this.timer)
20
+ this.timer = null
21
+ }
22
+
23
+ end () {
24
+ this.clear()
25
+ this.onEnd()
26
+ }
27
+
28
+ isValid () {
29
+ return this.initialMs - (Date.now() - this.startTimestamp) > 0
30
+ }
31
+ }
@@ -0,0 +1,100 @@
1
+ import { Timer } from './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 Timer({ 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 Timer({ onEnd: jest.fn() }, 100)
25
+ const requiredKeys = ['onEnd', 'initialMs', 'startTimestamp', 'timer']
26
+ expect(requiredKeys.every(rk => !!timer[rk])).toBeTruthy()
27
+ })
28
+
29
+ test('required keys are enforced', () => {
30
+ try {
31
+ new Timer({}, 100)
32
+ } catch (e) {
33
+ expect(e).toEqual(new Error('onEnd handler is required'))
34
+ }
35
+
36
+ try {
37
+ new Timer({ onEnd: jest.fn() })
38
+ } catch (e) {
39
+ expect(e).toEqual(new Error('ms duration is required'))
40
+ }
41
+ })
42
+ })
43
+
44
+ describe('create()', () => {
45
+ test('Create sets a timeout that can execute a cb', () => {
46
+ const timer = new Timer({ onEnd: jest.fn() }, 100)
47
+ expect(timer.timer).toBeTruthy()
48
+ expect(timer.onEnd).toHaveBeenCalledTimes(0)
49
+ jest.runOnlyPendingTimers()
50
+ expect(timer.onEnd).toHaveBeenCalledTimes(1)
51
+ })
52
+
53
+ test('Create can fallback to use defaults', () => {
54
+ let called = 0
55
+ const timer1 = new Timer({ onEnd: jest.fn() }, 100)
56
+ timer1.create()
57
+
58
+ const timer2 = new Timer({ onEnd: jest.fn() }, 100)
59
+ timer2.create(timer2.onEnd)
60
+
61
+ const timer3 = new Timer({ onEnd: jest.fn() }, 100)
62
+ timer3.create(undefined, 100)
63
+
64
+ jest.runAllTimers(200)
65
+
66
+ expect(timer1.onEnd).toHaveBeenCalledTimes(1)
67
+ expect(timer2.onEnd).toHaveBeenCalledTimes(1)
68
+ expect(timer3.onEnd).toHaveBeenCalledTimes(1)
69
+ })
70
+ })
71
+
72
+ describe('clear()', () => {
73
+ test('clear prevents the callback from firing and deletes the pointer', () => {
74
+ const timer = new Timer({ onEnd: jest.fn() }, 100)
75
+ expect(timer.onEnd).toHaveBeenCalledTimes(0)
76
+ timer.clear()
77
+ jest.advanceTimersByTime(150)
78
+ expect(timer.onEnd).toHaveBeenCalledTimes(0)
79
+ expect(timer.timer).toEqual(null)
80
+ })
81
+ })
82
+
83
+ describe('end()', () => {
84
+ test('end clears the callback and calls the onEnd callback', () => {
85
+ const timer = new Timer({ onEnd: jest.fn() }, 100)
86
+ expect(timer.onEnd).toHaveBeenCalledTimes(0)
87
+ timer.end()
88
+ expect(timer.onEnd).toHaveBeenCalledTimes(1)
89
+ expect(timer.timer).toEqual(null)
90
+ })
91
+ })
92
+
93
+ describe('isValid', () => {
94
+ test('isValid validates timeStamps', () => {
95
+ const timer = new Timer({ onEnd: jest.fn() }, 100)
96
+ expect(timer.isValid()).toEqual(true)
97
+ timer.startTimestamp -= 100
98
+ expect(timer.isValid()).toEqual(false)
99
+ })
100
+ })
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { ffVersion } from '../browser-version/firefox-version'
6
6
  import { windowAddEventListener } from '../event-listener/event-listener-opts'
7
- import { single } from '../util/single'
7
+ import { single } from '../util/invoke'
8
8
  import { globalScope, isWorkerScope, isBrowserScope } from '../util/global-scope'
9
9
  import { subscribeToVisibilityChange } from '../window/page-visibility'
10
10
 
@@ -5,6 +5,12 @@
5
5
 
6
6
  import { stringify } from './stringify'
7
7
 
8
+ /**
9
+ * Returns the size of the provided data. Designed for measuring XHR responses.
10
+ *
11
+ * @param {*} data - The data to be measured.
12
+ * @returns {(number|undefined)} - The size of the data or undefined if size cannot be determined.
13
+ */
8
14
  export function dataSize (data) {
9
15
  if (typeof data === 'string' && data.length) return data.length
10
16
  if (typeof data !== 'object') return undefined
@@ -0,0 +1,50 @@
1
+ import { dataSize } from './data-size'
2
+
3
+ describe('dataSize', () => {
4
+ test('returns length of string', () => {
5
+ const str = 'Hello, world!'
6
+ expect(dataSize(str)).toBe(str.length)
7
+ })
8
+
9
+ test('returns undefined for non-object, number, or empty string', () => {
10
+ expect(dataSize(Infinity)).toBeUndefined()
11
+ expect(dataSize(12345)).toBeUndefined() // might not actually be by design, but this is how it works today
12
+ expect(dataSize('')).toBeUndefined()
13
+ })
14
+
15
+ test('returns byte length of ArrayBuffer object', () => {
16
+ const buffer = new ArrayBuffer(8)
17
+ expect(dataSize(buffer)).toBe(8)
18
+ })
19
+
20
+ test('returns size of Blob object', () => {
21
+ const blob = new Blob(['Hello, world!'], { type: 'text/plain' })
22
+ expect(dataSize(blob)).toBe(blob.size)
23
+ })
24
+
25
+ test('returns undefined for FormData object', () => {
26
+ const formData = new FormData()
27
+ expect(dataSize(formData)).toBeUndefined()
28
+ })
29
+
30
+ test('returns length of JSON string representation of object', () => {
31
+ const obj = {
32
+ str: 'Hello, world!',
33
+ num: 12345,
34
+ nestedObj: {
35
+ arr: [1, 2, 3]
36
+ }
37
+ }
38
+ const expectedSize = JSON.stringify(obj).length
39
+ expect(dataSize(obj)).toBe(expectedSize)
40
+ })
41
+
42
+ test('returns undefined for object with toJSON method that throws an error', () => {
43
+ const obj = {
44
+ toJSON: () => {
45
+ throw new Error('Error in toJSON')
46
+ }
47
+ }
48
+ expect(dataSize(obj)).toBeUndefined()
49
+ })
50
+ })
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Reduce the invocation of the supplied function so that it is only invoked
3
+ * once within a given timeout.
4
+ *
5
+ * If `wait` is `0`, the function will be invoked during the next tick.
6
+ * If `options.leading` is false or not provided, the function will be invoked
7
+ * N milliseconds after the last invocation of the returned function where
8
+ * N is the `timeout` value.
9
+ * If `options.leading` is true, the function will be invoked immediately upon
10
+ * the first invocation of the returned function and not again for N milliseconds
11
+ * where N is the `timeout` value.
12
+ * @param {function} func Function whose invocation should be limited so it is only invoked
13
+ * once within a given timeout period.
14
+ * @param {number} timeout Time in milliseconds that the function should only be invoked
15
+ * once within.
16
+ * @param {object} options Debounce options
17
+ * @param {boolean} options.leading Forces the function to be invoked on the first
18
+ * invocation of the returned function instead of N milliseconds after the last
19
+ * invocation.
20
+ * @returns {function} A wrapping function that will ensure the provided function
21
+ * is invoked only once within the given timeout.
22
+ */
23
+ export function debounce (func, timeout = 500, options = {}) {
24
+ const leading = options?.leading || false
25
+ let timer
26
+ return (...args) => {
27
+ if (leading && timer === undefined) {
28
+ func.apply(this, args)
29
+ timer = setTimeout(() => timer = clearTimeout(timer), timeout)
30
+ }
31
+
32
+ if (!leading) {
33
+ clearTimeout(timer)
34
+ timer = setTimeout(() => { func.apply(this, args) }, timeout)
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Reduce the invocation of the supplied function so that it is only invoked
41
+ * once.
42
+ * @param {function} func Function whose invocation should be limited so it is only invoked
43
+ * once.
44
+ * @returns {function} A wrapping function that will ensure the provided function
45
+ * is invoked only once.
46
+ */
47
+ export function single (func) {
48
+ let called = false
49
+ return (...args) => {
50
+ if (!called) {
51
+ called = true
52
+ func.apply(this, args)
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,65 @@
1
+ import { debounce, single } from './invoke'
2
+
3
+ jest.useFakeTimers()
4
+
5
+ describe('debounce', () => {
6
+ test('should run the supplied function after 100ms', () => {
7
+ let mockCallback = jest.fn()
8
+
9
+ let debouncedMethod = debounce(mockCallback, 100)
10
+ execFnTimes(debouncedMethod, 100)
11
+
12
+ expect(mockCallback).not.toHaveBeenCalled()
13
+
14
+ jest.advanceTimersByTime(1000)
15
+ expect(mockCallback).toHaveBeenCalledTimes(1)
16
+ })
17
+
18
+ test('should rerun the supplied function when called again after 100ms', () => {
19
+ let mockCallback = jest.fn()
20
+
21
+ let debouncedMethod = debounce(mockCallback, 100)
22
+
23
+ execFnTimes(debouncedMethod, 100)
24
+ jest.advanceTimersByTime(200)
25
+ execFnTimes(debouncedMethod, 100)
26
+ jest.advanceTimersByTime(2000)
27
+
28
+ expect(mockCallback).toHaveBeenCalledTimes(2)
29
+ })
30
+
31
+ test('should run the supplied function on the first event and debounce subsequent events', () => {
32
+ let mockCallback = jest.fn()
33
+
34
+ let debouncedMethod = debounce(mockCallback, 100, { leading: true })
35
+
36
+ execFnTimes(debouncedMethod, 100)
37
+ expect(mockCallback).toHaveBeenCalledTimes(1)
38
+
39
+ jest.advanceTimersByTime(200)
40
+
41
+ expect(mockCallback).toHaveBeenCalledTimes(1)
42
+
43
+ execFnTimes(debouncedMethod, 100)
44
+ jest.advanceTimersByTime(200)
45
+
46
+ expect(mockCallback).toHaveBeenCalledTimes(2)
47
+ })
48
+ })
49
+
50
+ describe('single', () => {
51
+ test('should run the supplied function only once', () => {
52
+ let mockCallback = jest.fn()
53
+
54
+ let singleMethod = single(mockCallback, 100)
55
+ execFnTimes(singleMethod, 100)
56
+
57
+ expect(mockCallback).toHaveBeenCalledTimes(1)
58
+ })
59
+ })
60
+
61
+ function execFnTimes (fn, count) {
62
+ for (let i = 0; i < count; i++) {
63
+ fn()
64
+ }
65
+ }
@@ -8,10 +8,6 @@ var fileProtocolRule = {
8
8
  replacement: 'file://OBFUSCATED'
9
9
  }
10
10
  export class Obfuscator extends SharedContext {
11
- constructor (parent) {
12
- super(parent) // gets any allowed properties from the parent and stores them in `sharedContext`
13
- }
14
-
15
11
  shouldObfuscate () {
16
12
  return getRules(this.sharedContext.agentIdentifier).length > 0
17
13
  }
@@ -10,8 +10,8 @@ import { documentAddEventListener } from '../event-listener/event-listener-opts'
10
10
  * @param {boolean} [toHiddenOnly=false] - only execute the 'cb' when the vis is changing to the hidden state; no arg is passed to 'cb' if used
11
11
  * @returns void
12
12
  */
13
- export function subscribeToVisibilityChange (cb, toHiddenOnly = false) {
14
- documentAddEventListener('visibilitychange', handleVisibilityChange)
13
+ export function subscribeToVisibilityChange (cb, toHiddenOnly = false, capture, abortSignal) {
14
+ documentAddEventListener('visibilitychange', handleVisibilityChange, capture, abortSignal)
15
15
  return
16
16
 
17
17
  function handleVisibilityChange () {
@@ -26,7 +26,7 @@ const TIMER_FNS = [SET_TIMEOUT, 'setImmediate', SET_INTERVAL, CLEAR_TIMEOUT, 'cl
26
26
  * @param {Object} sharedEE - The shared event emitter on which a new scoped event emitter will be based.
27
27
  * @returns {Object} Scoped event emitter with a debug ID of `timer`.
28
28
  */
29
- //eslint-disable-next-line
29
+ // eslint-disable-next-line
30
30
  export function wrapTimer(sharedEE) {
31
31
  const ee = scopedEE(sharedEE)
32
32
 
@@ -9,13 +9,13 @@ import { handle } from '../../../common/event-emitter/handle'
9
9
  import { getConfigurationValue, getInfo } from '../../../common/config/config'
10
10
  import { HarvestScheduler } from '../../../common/harvest/harvest-scheduler'
11
11
  import { setDenyList, shouldCollectEvent } from '../../../common/deny-list/deny-list'
12
- import { AggregateBase } from '../../utils/aggregate-base'
13
12
  import { FEATURE_NAME } from '../constants'
14
13
  import { drain } from '../../../common/drain/drain'
15
14
  import { FEATURE_NAMES } from '../../../loaders/features/features'
16
15
  import { SUPPORTABILITY_METRIC_CHANNEL } from '../../metrics/constants'
16
+ import { FeatureBase } from '../../utils/feature-base'
17
17
 
18
- export class Aggregate extends AggregateBase {
18
+ export class Aggregate extends FeatureBase {
19
19
  static featureName = FEATURE_NAME
20
20
  constructor (agentIdentifier, aggregator) {
21
21
  super(agentIdentifier, aggregator, FEATURE_NAME)
@@ -18,12 +18,12 @@ import { getInfo, getConfigurationValue, getRuntime } from '../../../common/conf
18
18
  import { now } from '../../../common/timing/now'
19
19
  import { globalScope } from '../../../common/util/global-scope'
20
20
 
21
- import { AggregateBase } from '../../utils/aggregate-base'
22
21
  import { FEATURE_NAME } from '../constants'
23
22
  import { drain } from '../../../common/drain/drain'
24
23
  import { FEATURE_NAMES } from '../../../loaders/features/features'
24
+ import { FeatureBase } from '../../utils/feature-base'
25
25
 
26
- export class Aggregate extends AggregateBase {
26
+ export class Aggregate extends FeatureBase {
27
27
  static featureName = FEATURE_NAME
28
28
  constructor (agentIdentifier, aggregator) {
29
29
  super(agentIdentifier, aggregator, FEATURE_NAME)
@@ -222,6 +222,7 @@ export class Aggregate extends AggregateBase {
222
222
 
223
223
  // still send EE events for other features such as above, but stop this one from aggregating internal data
224
224
  if (this.blocked) return
225
+ var att = getInfo(this.agentIdentifier).jsAttributes
225
226
  if (params._interactionId != null) {
226
227
  // hold on to the error until the interaction finishes
227
228
  this.errorCache[params._interactionId] = this.errorCache[params._interactionId] || []
@@ -229,7 +230,6 @@ export class Aggregate extends AggregateBase {
229
230
  } else {
230
231
  // store custom attributes
231
232
  var customParams = {}
232
- var att = getInfo(this.agentIdentifier).jsAttributes
233
233
  mapOwn(att, setCustom)
234
234
  if (customAttributes) {
235
235
  mapOwn(customAttributes, setCustom)