@tracelog/lib 0.0.8 → 0.2.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 (228) hide show
  1. package/README.md +58 -24
  2. package/dist/browser/tracelog.js +1934 -3226
  3. package/dist/cjs/api.d.ts +33 -19
  4. package/dist/cjs/api.js +111 -156
  5. package/dist/cjs/app.constants.d.ts +80 -1
  6. package/dist/cjs/app.constants.js +90 -3
  7. package/dist/cjs/app.d.ts +29 -44
  8. package/dist/cjs/app.js +114 -212
  9. package/dist/cjs/app.types.d.ts +2 -7
  10. package/dist/cjs/app.types.js +10 -21
  11. package/dist/cjs/constants/api.constants.js +11 -5
  12. package/dist/cjs/constants/config.constants.d.ts +75 -0
  13. package/dist/cjs/constants/config.constants.js +178 -0
  14. package/dist/cjs/constants/error.constants.d.ts +29 -0
  15. package/dist/cjs/constants/error.constants.js +50 -0
  16. package/dist/cjs/constants/index.d.ts +3 -6
  17. package/dist/cjs/constants/index.js +3 -6
  18. package/dist/cjs/constants/performance.constants.d.ts +28 -0
  19. package/dist/cjs/constants/performance.constants.js +43 -0
  20. package/dist/cjs/handlers/click.handler.d.ts +1 -0
  21. package/dist/cjs/handlers/click.handler.js +30 -49
  22. package/dist/cjs/handlers/error.handler.d.ts +11 -6
  23. package/dist/cjs/handlers/error.handler.js +91 -51
  24. package/dist/cjs/handlers/page-view.handler.js +38 -29
  25. package/dist/cjs/handlers/performance.handler.d.ts +3 -0
  26. package/dist/cjs/handlers/performance.handler.js +76 -37
  27. package/dist/cjs/handlers/scroll.handler.d.ts +15 -0
  28. package/dist/cjs/handlers/scroll.handler.js +105 -31
  29. package/dist/cjs/handlers/session.handler.d.ts +6 -20
  30. package/dist/cjs/handlers/session.handler.js +38 -326
  31. package/dist/cjs/integrations/google-analytics.integration.d.ts +0 -1
  32. package/dist/cjs/integrations/google-analytics.integration.js +27 -98
  33. package/dist/cjs/listeners/input-listener-managers.d.ts +18 -9
  34. package/dist/cjs/listeners/input-listener-managers.js +24 -33
  35. package/dist/cjs/listeners/touch-listener-manager.d.ts +1 -3
  36. package/dist/cjs/listeners/touch-listener-manager.js +1 -23
  37. package/dist/cjs/listeners/visibility-listener-manager.d.ts +1 -4
  38. package/dist/cjs/listeners/visibility-listener-manager.js +6 -42
  39. package/dist/cjs/managers/api.manager.d.ts +13 -3
  40. package/dist/cjs/managers/api.manager.js +35 -5
  41. package/dist/cjs/managers/config.manager.d.ts +53 -3
  42. package/dist/cjs/managers/config.manager.js +131 -62
  43. package/dist/cjs/managers/event.manager.d.ts +57 -36
  44. package/dist/cjs/managers/event.manager.js +266 -417
  45. package/dist/cjs/managers/sender.manager.d.ts +40 -22
  46. package/dist/cjs/managers/sender.manager.js +200 -198
  47. package/dist/cjs/managers/session.manager.d.ts +80 -66
  48. package/dist/cjs/managers/session.manager.js +267 -522
  49. package/dist/cjs/managers/state.manager.d.ts +33 -0
  50. package/dist/cjs/managers/state.manager.js +79 -6
  51. package/dist/cjs/managers/storage.manager.d.ts +26 -2
  52. package/dist/cjs/managers/storage.manager.js +67 -34
  53. package/dist/cjs/managers/tags.manager.d.ts +31 -7
  54. package/dist/cjs/managers/tags.manager.js +123 -241
  55. package/dist/cjs/managers/user.manager.d.ts +14 -5
  56. package/dist/cjs/managers/user.manager.js +17 -9
  57. package/dist/cjs/public-api.d.ts +10 -1
  58. package/dist/cjs/public-api.js +18 -24
  59. package/dist/cjs/test-bridge.d.ts +48 -0
  60. package/dist/cjs/test-bridge.js +110 -0
  61. package/dist/cjs/types/api.types.d.ts +21 -6
  62. package/dist/cjs/types/api.types.js +21 -6
  63. package/dist/cjs/types/config.types.d.ts +22 -84
  64. package/dist/cjs/types/emitter.types.d.ts +11 -0
  65. package/dist/cjs/types/emitter.types.js +8 -0
  66. package/dist/cjs/types/event.types.d.ts +8 -11
  67. package/dist/cjs/types/index.d.ts +3 -1
  68. package/dist/cjs/types/index.js +3 -1
  69. package/dist/cjs/types/queue.types.d.ts +1 -0
  70. package/dist/cjs/types/session.types.d.ts +0 -64
  71. package/dist/cjs/types/state.types.d.ts +1 -0
  72. package/dist/cjs/types/test-bridge.types.d.ts +38 -0
  73. package/dist/cjs/types/validation-error.types.d.ts +7 -0
  74. package/dist/cjs/types/validation-error.types.js +11 -1
  75. package/dist/cjs/types/window.types.d.ts +1 -8
  76. package/dist/cjs/utils/data/uuid.utils.d.ts +1 -1
  77. package/dist/cjs/utils/data/uuid.utils.js +7 -5
  78. package/dist/cjs/utils/emitter.utils.d.ts +8 -0
  79. package/dist/cjs/utils/emitter.utils.js +33 -0
  80. package/dist/cjs/utils/index.d.ts +1 -0
  81. package/dist/cjs/utils/index.js +1 -0
  82. package/dist/cjs/utils/logging/debug-logger.utils.d.ts +10 -51
  83. package/dist/cjs/utils/logging/debug-logger.utils.js +36 -127
  84. package/dist/cjs/utils/network/fetch-with-timeout.utils.d.ts +4 -0
  85. package/dist/cjs/utils/network/fetch-with-timeout.utils.js +25 -0
  86. package/dist/cjs/utils/network/index.d.ts +1 -0
  87. package/dist/cjs/utils/network/index.js +1 -0
  88. package/dist/cjs/utils/network/url.utils.js +2 -42
  89. package/dist/cjs/utils/security/sanitize.utils.d.ts +1 -8
  90. package/dist/cjs/utils/security/sanitize.utils.js +7 -41
  91. package/dist/cjs/utils/validations/config-validations.utils.d.ts +7 -0
  92. package/dist/cjs/utils/validations/config-validations.utils.js +77 -22
  93. package/dist/esm/api.d.ts +33 -19
  94. package/dist/esm/api.js +105 -118
  95. package/dist/esm/app.constants.d.ts +80 -1
  96. package/dist/esm/app.constants.js +89 -1
  97. package/dist/esm/app.d.ts +29 -44
  98. package/dist/esm/app.js +115 -213
  99. package/dist/esm/app.types.d.ts +2 -7
  100. package/dist/esm/app.types.js +1 -7
  101. package/dist/esm/constants/api.constants.js +10 -4
  102. package/dist/esm/constants/config.constants.d.ts +75 -0
  103. package/dist/esm/constants/config.constants.js +174 -0
  104. package/dist/esm/constants/error.constants.d.ts +29 -0
  105. package/dist/esm/constants/error.constants.js +47 -0
  106. package/dist/esm/constants/index.d.ts +3 -6
  107. package/dist/esm/constants/index.js +3 -6
  108. package/dist/esm/constants/performance.constants.d.ts +28 -0
  109. package/dist/esm/constants/performance.constants.js +40 -0
  110. package/dist/esm/handlers/click.handler.d.ts +1 -0
  111. package/dist/esm/handlers/click.handler.js +30 -49
  112. package/dist/esm/handlers/error.handler.d.ts +11 -6
  113. package/dist/esm/handlers/error.handler.js +91 -51
  114. package/dist/esm/handlers/page-view.handler.js +38 -29
  115. package/dist/esm/handlers/performance.handler.d.ts +3 -0
  116. package/dist/esm/handlers/performance.handler.js +71 -32
  117. package/dist/esm/handlers/scroll.handler.d.ts +15 -0
  118. package/dist/esm/handlers/scroll.handler.js +106 -32
  119. package/dist/esm/handlers/session.handler.d.ts +6 -20
  120. package/dist/esm/handlers/session.handler.js +38 -326
  121. package/dist/esm/integrations/google-analytics.integration.d.ts +0 -1
  122. package/dist/esm/integrations/google-analytics.integration.js +27 -98
  123. package/dist/esm/listeners/input-listener-managers.d.ts +18 -9
  124. package/dist/esm/listeners/input-listener-managers.js +23 -32
  125. package/dist/esm/listeners/touch-listener-manager.d.ts +1 -3
  126. package/dist/esm/listeners/touch-listener-manager.js +1 -23
  127. package/dist/esm/listeners/visibility-listener-manager.d.ts +1 -4
  128. package/dist/esm/listeners/visibility-listener-manager.js +6 -42
  129. package/dist/esm/managers/api.manager.d.ts +13 -3
  130. package/dist/esm/managers/api.manager.js +34 -3
  131. package/dist/esm/managers/config.manager.d.ts +53 -3
  132. package/dist/esm/managers/config.manager.js +133 -64
  133. package/dist/esm/managers/event.manager.d.ts +57 -36
  134. package/dist/esm/managers/event.manager.js +268 -419
  135. package/dist/esm/managers/sender.manager.d.ts +40 -22
  136. package/dist/esm/managers/sender.manager.js +201 -199
  137. package/dist/esm/managers/session.manager.d.ts +80 -66
  138. package/dist/esm/managers/session.manager.js +269 -524
  139. package/dist/esm/managers/state.manager.d.ts +33 -0
  140. package/dist/esm/managers/state.manager.js +78 -6
  141. package/dist/esm/managers/storage.manager.d.ts +26 -2
  142. package/dist/esm/managers/storage.manager.js +66 -33
  143. package/dist/esm/managers/tags.manager.d.ts +31 -7
  144. package/dist/esm/managers/tags.manager.js +124 -242
  145. package/dist/esm/managers/user.manager.d.ts +14 -5
  146. package/dist/esm/managers/user.manager.js +17 -9
  147. package/dist/esm/public-api.d.ts +10 -1
  148. package/dist/esm/public-api.js +14 -1
  149. package/dist/esm/test-bridge.d.ts +48 -0
  150. package/dist/esm/test-bridge.js +106 -0
  151. package/dist/esm/types/api.types.d.ts +21 -6
  152. package/dist/esm/types/api.types.js +21 -6
  153. package/dist/esm/types/config.types.d.ts +22 -84
  154. package/dist/esm/types/emitter.types.d.ts +11 -0
  155. package/dist/esm/types/emitter.types.js +5 -0
  156. package/dist/esm/types/event.types.d.ts +8 -11
  157. package/dist/esm/types/index.d.ts +3 -1
  158. package/dist/esm/types/index.js +3 -1
  159. package/dist/esm/types/queue.types.d.ts +1 -0
  160. package/dist/esm/types/session.types.d.ts +0 -64
  161. package/dist/esm/types/state.types.d.ts +1 -0
  162. package/dist/esm/types/test-bridge.types.d.ts +38 -0
  163. package/dist/esm/types/validation-error.types.d.ts +7 -0
  164. package/dist/esm/types/validation-error.types.js +9 -0
  165. package/dist/esm/types/window.types.d.ts +1 -8
  166. package/dist/esm/utils/data/uuid.utils.d.ts +1 -1
  167. package/dist/esm/utils/data/uuid.utils.js +7 -5
  168. package/dist/esm/utils/emitter.utils.d.ts +8 -0
  169. package/dist/esm/utils/emitter.utils.js +29 -0
  170. package/dist/esm/utils/index.d.ts +1 -0
  171. package/dist/esm/utils/index.js +1 -0
  172. package/dist/esm/utils/logging/debug-logger.utils.d.ts +10 -51
  173. package/dist/esm/utils/logging/debug-logger.utils.js +36 -127
  174. package/dist/esm/utils/network/fetch-with-timeout.utils.d.ts +4 -0
  175. package/dist/esm/utils/network/fetch-with-timeout.utils.js +22 -0
  176. package/dist/esm/utils/network/index.d.ts +1 -0
  177. package/dist/esm/utils/network/index.js +1 -0
  178. package/dist/esm/utils/network/url.utils.js +2 -42
  179. package/dist/esm/utils/security/sanitize.utils.d.ts +1 -8
  180. package/dist/esm/utils/security/sanitize.utils.js +6 -39
  181. package/dist/esm/utils/validations/config-validations.utils.d.ts +7 -0
  182. package/dist/esm/utils/validations/config-validations.utils.js +76 -22
  183. package/package.json +23 -16
  184. package/dist/browser/web-vitals-CCnqwnC8.mjs +0 -198
  185. package/dist/cjs/constants/browser.constants.d.ts +0 -3
  186. package/dist/cjs/constants/browser.constants.js +0 -41
  187. package/dist/cjs/constants/initialization.constants.d.ts +0 -40
  188. package/dist/cjs/constants/initialization.constants.js +0 -48
  189. package/dist/cjs/constants/limits.constants.d.ts +0 -25
  190. package/dist/cjs/constants/limits.constants.js +0 -40
  191. package/dist/cjs/constants/security.constants.d.ts +0 -1
  192. package/dist/cjs/constants/security.constants.js +0 -12
  193. package/dist/cjs/constants/timing.constants.d.ts +0 -22
  194. package/dist/cjs/constants/timing.constants.js +0 -34
  195. package/dist/cjs/constants/validation.constants.d.ts +0 -13
  196. package/dist/cjs/constants/validation.constants.js +0 -31
  197. package/dist/cjs/handlers/network.handler.d.ts +0 -16
  198. package/dist/cjs/handlers/network.handler.js +0 -136
  199. package/dist/cjs/managers/cross-tab-session.manager.d.ts +0 -170
  200. package/dist/cjs/managers/cross-tab-session.manager.js +0 -730
  201. package/dist/cjs/managers/sampling.manager.d.ts +0 -8
  202. package/dist/cjs/managers/sampling.manager.js +0 -53
  203. package/dist/cjs/managers/session-recovery.manager.d.ts +0 -65
  204. package/dist/cjs/managers/session-recovery.manager.js +0 -237
  205. package/dist/cjs/types/web-vitals.types.d.ts +0 -6
  206. package/dist/esm/constants/browser.constants.d.ts +0 -3
  207. package/dist/esm/constants/browser.constants.js +0 -38
  208. package/dist/esm/constants/initialization.constants.d.ts +0 -40
  209. package/dist/esm/constants/initialization.constants.js +0 -45
  210. package/dist/esm/constants/limits.constants.d.ts +0 -25
  211. package/dist/esm/constants/limits.constants.js +0 -37
  212. package/dist/esm/constants/security.constants.d.ts +0 -1
  213. package/dist/esm/constants/security.constants.js +0 -9
  214. package/dist/esm/constants/timing.constants.d.ts +0 -22
  215. package/dist/esm/constants/timing.constants.js +0 -31
  216. package/dist/esm/constants/validation.constants.d.ts +0 -13
  217. package/dist/esm/constants/validation.constants.js +0 -28
  218. package/dist/esm/handlers/network.handler.d.ts +0 -16
  219. package/dist/esm/handlers/network.handler.js +0 -132
  220. package/dist/esm/managers/cross-tab-session.manager.d.ts +0 -170
  221. package/dist/esm/managers/cross-tab-session.manager.js +0 -726
  222. package/dist/esm/managers/sampling.manager.d.ts +0 -8
  223. package/dist/esm/managers/sampling.manager.js +0 -49
  224. package/dist/esm/managers/session-recovery.manager.d.ts +0 -65
  225. package/dist/esm/managers/session-recovery.manager.js +0 -233
  226. package/dist/esm/types/web-vitals.types.d.ts +0 -6
  227. /package/dist/cjs/types/{web-vitals.types.js → test-bridge.types.js} +0 -0
  228. /package/dist/esm/types/{web-vitals.types.js → test-bridge.types.js} +0 -0
@@ -1,4 +1,4 @@
1
- import { SCROLL_DEBOUNCE_TIME_MS, SIGNIFICANT_SCROLL_DELTA } from '../constants';
1
+ import { MAX_SCROLL_EVENTS_PER_SESSION, MIN_SCROLL_DEPTH_CHANGE, SCROLL_DEBOUNCE_TIME_MS, SCROLL_MIN_EVENT_INTERVAL_MS, SIGNIFICANT_SCROLL_DELTA, } from '../constants';
2
2
  import { EventType, ScrollDirection } from '../types';
3
3
  import { StateManager } from '../managers/state.manager';
4
4
  import { debugLog } from '../utils/logging';
@@ -6,12 +6,18 @@ export class ScrollHandler extends StateManager {
6
6
  constructor(eventManager) {
7
7
  super();
8
8
  this.containers = [];
9
+ this.limitWarningLogged = false;
10
+ this.minDepthChange = MIN_SCROLL_DEPTH_CHANGE;
11
+ this.minIntervalMs = SCROLL_MIN_EVENT_INTERVAL_MS;
12
+ this.maxEventsPerSession = MAX_SCROLL_EVENTS_PER_SESSION;
9
13
  this.eventManager = eventManager;
10
14
  }
11
15
  startTracking() {
16
+ this.limitWarningLogged = false;
17
+ this.applyConfigOverrides();
18
+ this.set('scrollEventCount', 0);
12
19
  const raw = this.get('config').scrollContainerSelectors;
13
20
  const selectors = Array.isArray(raw) ? raw : typeof raw === 'string' ? [raw] : [];
14
- debugLog.debug('ScrollHandler', 'Starting scroll tracking', { selectorsCount: selectors.length });
15
21
  const elements = selectors
16
22
  .map((sel) => this.safeQuerySelector(sel))
17
23
  .filter((element) => element instanceof HTMLElement);
@@ -23,11 +29,8 @@ export class ScrollHandler extends StateManager {
23
29
  }
24
30
  }
25
31
  stopTracking() {
26
- debugLog.debug('ScrollHandler', 'Stopping scroll tracking', { containersCount: this.containers.length });
27
32
  for (const container of this.containers) {
28
- if (container.debounceTimer) {
29
- clearTimeout(container.debounceTimer);
30
- }
33
+ this.clearContainerTimer(container);
31
34
  if (container.element instanceof Window) {
32
35
  window.removeEventListener('scroll', container.listener);
33
36
  }
@@ -36,38 +39,38 @@ export class ScrollHandler extends StateManager {
36
39
  }
37
40
  }
38
41
  this.containers.length = 0;
42
+ this.set('scrollEventCount', 0);
43
+ this.limitWarningLogged = false;
39
44
  }
40
45
  setupScrollContainer(element) {
41
46
  // Skip setup for non-scrollable elements
42
47
  if (element !== window && !this.isElementScrollable(element)) {
43
48
  return;
44
49
  }
45
- const container = {
46
- element,
47
- lastScrollPos: this.getScrollTop(element),
48
- debounceTimer: null,
49
- listener: () => { },
50
- };
51
50
  const handleScroll = () => {
52
51
  if (this.get('suppressNextScroll')) {
53
- this.set('suppressNextScroll', false);
54
52
  return;
55
53
  }
56
- if (container.debounceTimer) {
57
- clearTimeout(container.debounceTimer);
58
- }
54
+ this.clearContainerTimer(container);
59
55
  container.debounceTimer = window.setTimeout(() => {
60
56
  const scrollData = this.calculateScrollData(container);
61
57
  if (scrollData) {
62
- this.eventManager.track({
63
- type: EventType.SCROLL,
64
- scroll_data: scrollData,
65
- });
58
+ const now = Date.now();
59
+ this.processScrollEvent(container, scrollData, now);
66
60
  }
67
61
  container.debounceTimer = null;
68
62
  }, SCROLL_DEBOUNCE_TIME_MS);
69
63
  };
70
- container.listener = handleScroll;
64
+ const initialScrollTop = this.getScrollTop(element);
65
+ const container = {
66
+ element,
67
+ lastScrollPos: initialScrollTop,
68
+ lastDepth: this.calculateScrollDepth(initialScrollTop, this.getScrollHeight(element), this.getViewportHeight(element)),
69
+ lastDirection: ScrollDirection.DOWN,
70
+ lastEventTime: 0,
71
+ debounceTimer: null,
72
+ listener: handleScroll,
73
+ };
71
74
  this.containers.push(container);
72
75
  if (element instanceof Window) {
73
76
  window.addEventListener('scroll', handleScroll, { passive: true });
@@ -76,24 +79,95 @@ export class ScrollHandler extends StateManager {
76
79
  element.addEventListener('scroll', handleScroll, { passive: true });
77
80
  }
78
81
  }
82
+ processScrollEvent(container, scrollData, timestamp) {
83
+ if (!this.shouldEmitScrollEvent(container, scrollData, timestamp)) {
84
+ return;
85
+ }
86
+ container.lastEventTime = timestamp;
87
+ container.lastDepth = scrollData.depth;
88
+ container.lastDirection = scrollData.direction;
89
+ const currentCount = this.get('scrollEventCount') ?? 0;
90
+ this.set('scrollEventCount', currentCount + 1);
91
+ this.eventManager.track({
92
+ type: EventType.SCROLL,
93
+ scroll_data: scrollData,
94
+ });
95
+ }
96
+ shouldEmitScrollEvent(container, scrollData, timestamp) {
97
+ if (this.hasReachedSessionLimit()) {
98
+ this.logLimitOnce();
99
+ return false;
100
+ }
101
+ if (!this.hasElapsedMinimumInterval(container, timestamp)) {
102
+ return false;
103
+ }
104
+ if (!this.hasSignificantDepthChange(container, scrollData.depth)) {
105
+ return false;
106
+ }
107
+ return true;
108
+ }
109
+ hasReachedSessionLimit() {
110
+ const currentCount = this.get('scrollEventCount') ?? 0;
111
+ return currentCount >= this.maxEventsPerSession;
112
+ }
113
+ hasElapsedMinimumInterval(container, timestamp) {
114
+ if (container.lastEventTime === 0) {
115
+ return true;
116
+ }
117
+ return timestamp - container.lastEventTime >= this.minIntervalMs;
118
+ }
119
+ hasSignificantDepthChange(container, newDepth) {
120
+ return Math.abs(newDepth - container.lastDepth) >= this.minDepthChange;
121
+ }
122
+ logLimitOnce() {
123
+ if (this.limitWarningLogged) {
124
+ return;
125
+ }
126
+ this.limitWarningLogged = true;
127
+ debugLog.warn('ScrollHandler', 'Max scroll events per session reached', {
128
+ limit: this.maxEventsPerSession,
129
+ });
130
+ }
131
+ applyConfigOverrides() {
132
+ this.minDepthChange = MIN_SCROLL_DEPTH_CHANGE;
133
+ this.minIntervalMs = SCROLL_MIN_EVENT_INTERVAL_MS;
134
+ this.maxEventsPerSession = MAX_SCROLL_EVENTS_PER_SESSION;
135
+ }
136
+ isWindowScrollable() {
137
+ return document.documentElement.scrollHeight > window.innerHeight;
138
+ }
139
+ clearContainerTimer(container) {
140
+ if (container.debounceTimer !== null) {
141
+ clearTimeout(container.debounceTimer);
142
+ container.debounceTimer = null;
143
+ }
144
+ }
145
+ getScrollDirection(current, previous) {
146
+ return current > previous ? ScrollDirection.DOWN : ScrollDirection.UP;
147
+ }
148
+ calculateScrollDepth(scrollTop, scrollHeight, viewportHeight) {
149
+ if (scrollHeight <= viewportHeight) {
150
+ return 0;
151
+ }
152
+ const maxScrollTop = scrollHeight - viewportHeight;
153
+ return Math.min(100, Math.max(0, Math.floor((scrollTop / maxScrollTop) * 100)));
154
+ }
79
155
  calculateScrollData(container) {
80
156
  const { element, lastScrollPos } = container;
81
157
  const scrollTop = this.getScrollTop(element);
82
- const viewportHeight = this.getViewportHeight(element);
83
- const scrollHeight = this.getScrollHeight(element);
84
- // Check if the page is actually scrollable at runtime
85
- if (element === window && scrollHeight <= viewportHeight) {
86
- return null;
87
- }
88
- const direction = scrollTop > lastScrollPos ? ScrollDirection.DOWN : ScrollDirection.UP;
89
- const depth = scrollHeight > viewportHeight
90
- ? Math.min(100, Math.max(0, Math.floor((scrollTop / (scrollHeight - viewportHeight)) * 100)))
91
- : 0;
92
- // Only update if scroll position changed significantly
158
+ // Early return: check significant movement first (cheapest check)
93
159
  const positionDelta = Math.abs(scrollTop - lastScrollPos);
94
160
  if (positionDelta < SIGNIFICANT_SCROLL_DELTA) {
95
161
  return null;
96
162
  }
163
+ // Early return: check if window is scrollable
164
+ if (element === window && !this.isWindowScrollable()) {
165
+ return null;
166
+ }
167
+ const viewportHeight = this.getViewportHeight(element);
168
+ const scrollHeight = this.getScrollHeight(element);
169
+ const direction = this.getScrollDirection(scrollTop, lastScrollPos);
170
+ const depth = this.calculateScrollDepth(scrollTop, scrollHeight, viewportHeight);
97
171
  container.lastScrollPos = scrollTop;
98
172
  return { depth, direction };
99
173
  }
@@ -4,26 +4,12 @@ import { StorageManager } from '../managers/storage.manager';
4
4
  export declare class SessionHandler extends StateManager {
5
5
  private readonly eventManager;
6
6
  private readonly storageManager;
7
- private readonly sessionStorageKey;
8
7
  private sessionManager;
9
- private recoveryManager;
10
- private _crossTabSessionManager;
11
- private heartbeatInterval;
12
- private _isInitializingCrossTab;
13
- private get crossTabSessionManager();
14
- private shouldUseCrossTabs;
8
+ private destroyed;
15
9
  constructor(storageManager: StorageManager, eventManager: EventManager);
16
- startTracking(): void;
17
- stopTracking(): void;
18
- private initializeSessionRecoveryManager;
19
- private initializeCrossTabSessionManager;
20
- private createOrJoinSession;
21
- private forceCleanupSession;
22
- private trackSession;
23
- private startInitialSession;
24
- private checkOrphanedSessions;
25
- private persistSession;
26
- private clearPersistedSession;
27
- private startHeartbeat;
28
- private stopHeartbeat;
10
+ startTracking(): Promise<void>;
11
+ private isActive;
12
+ private cleanupSessionManager;
13
+ stopTracking(): Promise<void>;
14
+ destroy(): void;
29
15
  }
@@ -1,353 +1,65 @@
1
- import { SESSION_HEARTBEAT_INTERVAL_MS, SESSION_STORAGE_KEY, DEFAULT_SESSION_TIMEOUT_MS, SESSION_SYNC_CONSTANTS, } from '../constants';
2
- import { EventType } from '../types';
3
1
  import { SessionManager } from '../managers/session.manager';
4
- import { SessionRecoveryManager } from '../managers/session-recovery.manager';
5
- import { CrossTabSessionManager } from '../managers/cross-tab-session.manager';
6
2
  import { StateManager } from '../managers/state.manager';
7
3
  import { debugLog } from '../utils/logging';
8
4
  export class SessionHandler extends StateManager {
9
- get crossTabSessionManager() {
10
- if (!this._crossTabSessionManager && !this._isInitializingCrossTab && this.shouldUseCrossTabs()) {
11
- this._isInitializingCrossTab = true;
12
- try {
13
- const projectId = this.get('config')?.id;
14
- if (projectId) {
15
- this.initializeCrossTabSessionManager(projectId);
16
- }
17
- }
18
- catch (error) {
19
- debugLog.error('SessionHandler', 'Failed to initialize cross-tab session manager', {
20
- error: error instanceof Error ? error.message : 'Unknown error',
21
- });
22
- }
23
- finally {
24
- this._isInitializingCrossTab = false;
25
- }
26
- }
27
- return this._crossTabSessionManager;
28
- }
29
- shouldUseCrossTabs() {
30
- // Only initialize if BroadcastChannel is supported (indicates potential for multiple tabs)
31
- // and ServiceWorker is available (better cross-tab coordination)
32
- return typeof BroadcastChannel !== 'undefined' && typeof navigator !== 'undefined' && 'serviceWorker' in navigator;
33
- }
34
5
  constructor(storageManager, eventManager) {
35
6
  super();
36
7
  this.sessionManager = null;
37
- this.recoveryManager = null;
38
- this._crossTabSessionManager = null;
39
- this.heartbeatInterval = null;
40
- this._isInitializingCrossTab = false;
8
+ this.destroyed = false;
41
9
  this.eventManager = eventManager;
42
10
  this.storageManager = storageManager;
43
- this.sessionStorageKey = SESSION_STORAGE_KEY(this.get('config')?.id);
44
- const projectId = this.get('config')?.id;
45
- if (projectId) {
46
- this.initializeSessionRecoveryManager(projectId);
47
- // CrossTabSessionManager will be initialized lazily when needed via the getter
48
- }
49
11
  }
50
- startTracking() {
51
- if (this.sessionManager) {
52
- debugLog.debug('SessionHandler', 'Session tracking already active');
12
+ async startTracking() {
13
+ if (this.isActive()) {
53
14
  return;
54
15
  }
55
- debugLog.debug('SessionHandler', 'Starting session tracking');
56
- this.checkOrphanedSessions();
57
- const onActivity = async () => {
58
- if (this.crossTabSessionManager) {
59
- this.crossTabSessionManager.updateSessionActivity();
60
- }
61
- if (this.get('sessionId')) {
62
- return;
63
- }
64
- try {
65
- const sessionResult = await this.createOrJoinSession();
66
- this.set('sessionId', sessionResult.sessionId);
67
- debugLog.info('SessionHandler', '🏁 Session started', {
68
- sessionId: sessionResult.sessionId,
69
- recovered: sessionResult.recovered,
70
- crossTabActive: !!this.crossTabSessionManager,
71
- });
72
- this.trackSession(EventType.SESSION_START, sessionResult.recovered);
73
- this.persistSession(sessionResult.sessionId);
74
- this.startHeartbeat();
75
- }
76
- catch (error) {
77
- debugLog.error('SessionHandler', `Session creation failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
78
- this.forceCleanupSession();
79
- }
80
- };
81
- const onInactivity = () => {
82
- if (!this.get('sessionId')) {
83
- return;
84
- }
85
- if (this.crossTabSessionManager && this.crossTabSessionManager.getEffectiveSessionTimeout() > 0) {
86
- if (this.get('config')?.mode === 'qa' || this.get('config')?.mode === 'debug') {
87
- debugLog.debug('SessionHandler', 'Session kept alive by cross-tab activity');
88
- }
89
- return;
90
- }
91
- this.sessionManager.endSessionManaged('inactivity')
92
- .then((result) => {
93
- debugLog.info('SessionHandler', '🛑 Session ended by inactivity', {
94
- sessionId: this.get('sessionId'),
95
- reason: result.reason,
96
- success: result.success,
97
- eventsFlushed: result.eventsFlushed,
98
- });
99
- if (this.crossTabSessionManager) {
100
- this.crossTabSessionManager.endSession('inactivity');
101
- }
102
- this.clearPersistedSession();
103
- this.stopHeartbeat();
104
- })
105
- .catch((error) => {
106
- debugLog.error('SessionHandler', `Session end failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
107
- this.forceCleanupSession();
108
- });
109
- };
110
- const sessionEndConfig = {
111
- enablePageUnloadHandlers: true,
112
- debugMode: (this.get('config')?.mode === 'qa' || this.get('config')?.mode === 'debug') ?? false,
113
- syncTimeoutMs: SESSION_SYNC_CONSTANTS.SYNC_TIMEOUT_MS,
114
- maxRetries: SESSION_SYNC_CONSTANTS.MAX_RETRY_ATTEMPTS,
115
- };
116
- this.sessionManager = new SessionManager(onActivity, onInactivity, this.eventManager, this.storageManager, sessionEndConfig);
117
- debugLog.debug('SessionHandler', 'Session manager initialized');
118
- this.startInitialSession();
119
- }
120
- stopTracking() {
121
- debugLog.info('SessionHandler', 'Stopping session tracking');
122
- if (this.sessionManager) {
123
- if (this.get('sessionId')) {
16
+ if (this.destroyed) {
17
+ debugLog.warn('SessionHandler', 'Cannot start tracking on destroyed handler');
18
+ return;
19
+ }
20
+ try {
21
+ this.sessionManager = new SessionManager(this.storageManager, this.eventManager);
22
+ await this.sessionManager.startTracking();
23
+ }
24
+ catch (error) {
25
+ // Cleanup on failure
26
+ if (this.sessionManager) {
124
27
  try {
125
- this.sessionManager.endSessionSafely('manual_stop', { forceSync: true });
126
- this.clearPersistedSession();
127
- this.stopHeartbeat();
28
+ this.sessionManager.destroy();
128
29
  }
129
- catch (error) {
130
- debugLog.error('SessionHandler', `Manual session stop failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
131
- this.forceCleanupSession();
30
+ catch {
31
+ // Ignore cleanup errors
132
32
  }
33
+ this.sessionManager = null;
133
34
  }
134
- this.sessionManager.destroy();
135
- this.sessionManager = null;
136
- }
137
- if (this._crossTabSessionManager) {
138
- this._crossTabSessionManager.destroy();
139
- this._crossTabSessionManager = null;
140
- }
141
- // Reset cross-tab initialization flag
142
- this._isInitializingCrossTab = false;
143
- if (this.recoveryManager) {
144
- this.recoveryManager.cleanupOldRecoveryAttempts();
145
- this.recoveryManager = null;
35
+ debugLog.error('SessionHandler', 'Failed to start session tracking', {
36
+ error: error instanceof Error ? error.message : 'Unknown error',
37
+ });
38
+ throw error;
146
39
  }
147
40
  }
148
- initializeSessionRecoveryManager(projectId) {
149
- this.recoveryManager = new SessionRecoveryManager(this.storageManager, projectId, this.eventManager);
150
- debugLog.debug('SessionHandler', 'Session recovery manager initialized', { projectId });
151
- }
152
- initializeCrossTabSessionManager(projectId) {
153
- const config = {
154
- debugMode: (this.get('config')?.mode === 'qa' || this.get('config')?.mode === 'debug') ?? false,
155
- };
156
- const onSessionStart = (sessionId) => {
157
- if (this.get('config')?.mode === 'qa' || this.get('config')?.mode === 'debug') {
158
- debugLog.debug('SessionHandler', `Cross-tab session started: ${sessionId}`);
159
- }
160
- this.set('sessionId', sessionId);
161
- this.trackSession(EventType.SESSION_START, false);
162
- this.persistSession(sessionId);
163
- this.startHeartbeat();
164
- };
165
- const onSessionEnd = (reason) => {
166
- if (this.get('config')?.mode === 'qa' || this.get('config')?.mode === 'debug') {
167
- debugLog.debug('SessionHandler', `Cross-tab session ended: ${reason}`);
168
- }
169
- this.clearPersistedSession();
170
- this.trackSession(EventType.SESSION_END, false, reason);
171
- };
172
- const onTabActivity = () => {
173
- if (this.get('config')?.mode === 'qa' || this.get('config')?.mode === 'debug') {
174
- debugLog.debug('SessionHandler', 'Cross-tab activity detected');
175
- }
176
- };
177
- const onCrossTabConflict = () => {
178
- if (this.get('config')?.mode === 'qa' || this.get('config')?.mode === 'debug') {
179
- debugLog.warn('SessionHandler', 'Cross-tab conflict detected');
180
- }
181
- // Track cross-tab conflict in session health
182
- if (this.sessionManager) {
183
- this.sessionManager.trackSessionHealth('conflict');
184
- }
185
- };
186
- const callbacks = {
187
- onSessionStart,
188
- onSessionEnd,
189
- onTabActivity,
190
- onCrossTabConflict,
191
- };
192
- this._crossTabSessionManager = new CrossTabSessionManager(this.storageManager, projectId, config, callbacks);
193
- debugLog.debug('SessionHandler', 'Cross-tab session manager initialized', { projectId });
194
- }
195
- async createOrJoinSession() {
196
- if (this.crossTabSessionManager) {
197
- const existingSessionId = this.crossTabSessionManager.getSessionId();
198
- if (existingSessionId) {
199
- return { sessionId: existingSessionId, recovered: false };
200
- }
201
- // If cross-tab manager exists but no session, create one through regular session manager
202
- // The cross-tab manager will coordinate with other tabs automatically
203
- const sessionResult = this.sessionManager.startSession();
204
- return { sessionId: sessionResult.sessionId, recovered: sessionResult.recovered ?? false };
205
- }
206
- // Fallback: create regular session when no cross-tab manager
207
- const sessionResult = this.sessionManager.startSession();
208
- return { sessionId: sessionResult.sessionId, recovered: sessionResult.recovered ?? false };
41
+ isActive() {
42
+ return this.sessionManager !== null && !this.destroyed;
209
43
  }
210
- forceCleanupSession() {
211
- // Clear session state
212
- this.set('sessionId', null);
213
- // Clear persisted session data
214
- this.clearPersistedSession();
215
- // Stop heartbeat timer
216
- this.stopHeartbeat();
217
- // Clean up cross-tab session if exists
218
- if (this.crossTabSessionManager) {
219
- try {
220
- this.crossTabSessionManager.endSession('orphaned_cleanup');
221
- }
222
- catch (error) {
223
- // Silent cleanup - we're already in error recovery
224
- if (this.get('config')?.mode === 'qa' || this.get('config')?.mode === 'debug') {
225
- debugLog.warn('SessionHandler', `Cross-tab cleanup failed during force cleanup: ${error instanceof Error ? error.message : 'Unknown error'}`);
226
- }
227
- }
228
- }
229
- // Track session end for analytics
230
- try {
231
- this.trackSession(EventType.SESSION_END, false, 'orphaned_cleanup');
232
- }
233
- catch (error) {
234
- // Silent tracking failure - we're already in error recovery
235
- if (this.get('config')?.mode === 'qa' || this.get('config')?.mode === 'debug') {
236
- debugLog.warn('SessionHandler', `Session tracking failed during force cleanup: ${error instanceof Error ? error.message : 'Unknown error'}`);
237
- }
238
- }
239
- if (this.get('config')?.mode === 'qa' || this.get('config')?.mode === 'debug') {
240
- debugLog.debug('SessionHandler', 'Forced session cleanup completed');
44
+ async cleanupSessionManager() {
45
+ if (this.sessionManager) {
46
+ await this.sessionManager.stopTracking();
47
+ this.sessionManager.destroy();
48
+ this.sessionManager = null;
241
49
  }
242
50
  }
243
- trackSession(eventType, sessionStartRecovered = false, sessionEndReason) {
244
- this.eventManager.track({
245
- type: eventType,
246
- ...(eventType === EventType.SESSION_START &&
247
- sessionStartRecovered && { session_start_recovered: sessionStartRecovered }),
248
- ...(eventType === EventType.SESSION_END && { session_end_reason: sessionEndReason ?? 'orphaned_cleanup' }),
249
- });
51
+ async stopTracking() {
52
+ await this.cleanupSessionManager();
250
53
  }
251
- startInitialSession() {
252
- if (this.get('sessionId')) {
253
- debugLog.debug('SessionHandler', 'Session already exists, skipping initial session creation');
54
+ destroy() {
55
+ if (this.destroyed) {
254
56
  return;
255
57
  }
256
- debugLog.debug('SessionHandler', 'Starting initial session');
257
- // Check if there's already a cross-tab session active
258
- if (this.crossTabSessionManager) {
259
- const existingSessionId = this.crossTabSessionManager.getSessionId();
260
- if (existingSessionId) {
261
- // Join existing cross-tab session
262
- this.set('sessionId', existingSessionId);
263
- this.persistSession(existingSessionId);
264
- this.startHeartbeat();
265
- return;
266
- }
267
- // No existing cross-tab session, so trigger activity to potentially create one
268
- // The cross-tab session manager will handle session creation when activity occurs
269
- return;
270
- }
271
- // Fallback: no cross-tab session manager, start regular session
272
- debugLog.debug('SessionHandler', 'Starting regular session (no cross-tab)');
273
- const sessionResult = this.sessionManager.startSession();
274
- this.set('sessionId', sessionResult.sessionId);
275
- this.trackSession(EventType.SESSION_START, sessionResult.recovered);
276
- this.persistSession(sessionResult.sessionId);
277
- this.startHeartbeat();
278
- }
279
- checkOrphanedSessions() {
280
- const storedSessionData = this.storageManager.getItem(this.sessionStorageKey);
281
- if (storedSessionData) {
282
- try {
283
- const session = JSON.parse(storedSessionData);
284
- const now = Date.now();
285
- const timeSinceLastHeartbeat = now - session.lastHeartbeat;
286
- const sessionTimeout = this.get('config')?.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT_MS;
287
- if (timeSinceLastHeartbeat > sessionTimeout) {
288
- const canRecover = this.recoveryManager?.hasRecoverableSession();
289
- if (canRecover) {
290
- if (this.recoveryManager) {
291
- const sessionContext = {
292
- sessionId: session.sessionId,
293
- startTime: session.startTime,
294
- lastActivity: session.lastHeartbeat,
295
- tabCount: 1,
296
- recoveryAttempts: 0,
297
- metadata: {
298
- userAgent: navigator.userAgent,
299
- pageUrl: this.get('pageUrl'),
300
- },
301
- };
302
- this.recoveryManager.storeSessionContextForRecovery(sessionContext);
303
- if (this.get('config')?.mode === 'qa' || this.get('config')?.mode === 'debug') {
304
- debugLog.debug('SessionHandler', `Orphaned session stored for recovery: ${session.sessionId}`);
305
- }
306
- }
307
- }
308
- this.trackSession(EventType.SESSION_END);
309
- this.clearPersistedSession();
310
- if (this.get('config')?.mode === 'qa' || this.get('config')?.mode === 'debug') {
311
- debugLog.debug('SessionHandler', `Orphaned session ended: ${session.sessionId}, recovery available: ${canRecover}`);
312
- }
313
- }
314
- }
315
- catch {
316
- this.clearPersistedSession();
317
- }
318
- }
319
- }
320
- persistSession(sessionId) {
321
- const sessionData = {
322
- sessionId,
323
- startTime: Date.now(),
324
- lastHeartbeat: Date.now(),
325
- };
326
- this.storageManager.setItem(this.sessionStorageKey, JSON.stringify(sessionData));
327
- }
328
- clearPersistedSession() {
329
- this.storageManager.removeItem(this.sessionStorageKey);
330
- }
331
- startHeartbeat() {
332
- this.stopHeartbeat();
333
- this.heartbeatInterval = setInterval(() => {
334
- const storedSessionData = this.storageManager.getItem(this.sessionStorageKey);
335
- if (storedSessionData) {
336
- try {
337
- const session = JSON.parse(storedSessionData);
338
- session.lastHeartbeat = Date.now();
339
- this.storageManager.setItem(this.sessionStorageKey, JSON.stringify(session));
340
- }
341
- catch {
342
- this.clearPersistedSession();
343
- }
344
- }
345
- }, SESSION_HEARTBEAT_INTERVAL_MS);
346
- }
347
- stopHeartbeat() {
348
- if (this.heartbeatInterval) {
349
- clearInterval(this.heartbeatInterval);
350
- this.heartbeatInterval = null;
58
+ if (this.sessionManager) {
59
+ this.sessionManager.destroy();
60
+ this.sessionManager = null;
351
61
  }
62
+ this.destroyed = true;
63
+ this.set('hasStartSession', false);
352
64
  }
353
65
  }
@@ -8,7 +8,6 @@ declare global {
8
8
  }
9
9
  export declare class GoogleAnalyticsIntegration extends StateManager {
10
10
  private isInitialized;
11
- constructor();
12
11
  initialize(): Promise<void>;
13
12
  trackEvent(eventName: string, metadata: Record<string, MetadataType>): void;
14
13
  cleanup(): void;