@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,583 +1,328 @@
1
- import { DEFAULT_MOTION_THRESHOLD, DEFAULT_SESSION_TIMEOUT_MS, DEFAULT_THROTTLE_DELAY_MS, DEFAULT_VISIBILITY_TIMEOUT_MS, } from '../constants';
2
- import { DeviceType, EventType } from '../types';
3
- import { generateUUID, getDeviceType } from '../utils';
1
+ import { BROADCAST_CHANNEL_NAME, DEFAULT_SESSION_TIMEOUT, SESSION_STORAGE_KEY } from '../constants';
2
+ import { EventType } from '../types';
4
3
  import { debugLog } from '../utils/logging';
5
- import { ActivityListenerManager, KeyboardListenerManager, MouseListenerManager, TouchListenerManager, UnloadListenerManager, VisibilityListenerManager, } from '../listeners';
6
4
  import { StateManager } from './state.manager';
7
- import { SessionRecoveryManager } from './session-recovery.manager';
8
5
  export class SessionManager extends StateManager {
9
- constructor(onActivity, onInactivity, eventManager, storageManager, sessionEndConfig) {
6
+ constructor(storageManager, eventManager) {
10
7
  super();
11
- this.eventManager = null;
12
- this.storageManager = null;
13
- this.listenerManagers = [];
14
- // Recovery manager
15
- this.recoveryManager = null;
16
- this.isSessionActive = false;
17
- this.lastActivityTime = 0;
18
- this.inactivityTimer = null;
19
- this.sessionStartTime = 0;
20
- this.throttleTimeout = null;
21
- // Track visibility change timeout for proper cleanup
22
- this.visibilityChangeTimeout = null;
23
- // Session End Management
24
- this.pendingSessionEnd = false;
25
- this.sessionEndPromise = null;
26
- this.sessionEndLock = Promise.resolve({
27
- success: true,
28
- reason: 'manual_stop',
29
- timestamp: Date.now(),
30
- eventsFlushed: 0,
31
- method: 'async',
32
- });
33
- this.cleanupHandlers = [];
34
- this.sessionEndReason = null;
35
- this.sessionEndPriority = {
36
- page_unload: 4,
37
- manual_stop: 3,
38
- orphaned_cleanup: 2,
39
- inactivity: 1,
40
- tab_closed: 0,
41
- };
42
- this.sessionEndStats = {
43
- totalSessionEnds: 0,
44
- successfulEnds: 0,
45
- failedEnds: 0,
46
- duplicatePrevented: 0,
47
- reasonCounts: {
48
- inactivity: 0,
49
- page_unload: 0,
50
- manual_stop: 0,
51
- orphaned_cleanup: 0,
52
- tab_closed: 0,
53
- },
54
- };
55
- // Session health monitoring
56
- this.sessionHealth = {
57
- recoveryAttempts: 0,
58
- sessionTimeouts: 0,
59
- crossTabConflicts: 0,
60
- lastHealthCheck: Date.now(),
61
- };
62
- this.handleActivity = () => {
63
- const now = Date.now();
64
- if (now - this.lastActivityTime < this.config.throttleDelay) {
8
+ this.sessionTimeoutId = null;
9
+ this.broadcastChannel = null;
10
+ this.activityHandler = null;
11
+ this.visibilityChangeHandler = null;
12
+ this.beforeUnloadHandler = null;
13
+ this.isTracking = false;
14
+ this.storageManager = storageManager;
15
+ this.eventManager = eventManager;
16
+ }
17
+ /**
18
+ * Initialize cross-tab synchronization
19
+ */
20
+ initCrossTabSync() {
21
+ if (typeof BroadcastChannel === 'undefined') {
22
+ debugLog.warn('SessionManager', 'BroadcastChannel not supported');
23
+ return;
24
+ }
25
+ const projectId = this.getProjectId();
26
+ this.broadcastChannel = new BroadcastChannel(BROADCAST_CHANNEL_NAME(projectId));
27
+ this.broadcastChannel.onmessage = (event) => {
28
+ const { action, sessionId, timestamp, projectId: messageProjectId } = event.data ?? {};
29
+ if (messageProjectId !== projectId) {
65
30
  return;
66
31
  }
67
- this.lastActivityTime = now;
68
- if (this.isSessionActive) {
69
- // Always call onActivity to update cross-tab session activity
70
- this.onActivity();
71
- this.resetInactivityTimer();
32
+ if (action === 'session_end') {
33
+ debugLog.debug('SessionManager', 'Session end synced from another tab');
34
+ this.resetSessionState();
35
+ return;
72
36
  }
73
- else {
74
- if (this.throttleTimeout) {
75
- clearTimeout(this.throttleTimeout);
76
- this.throttleTimeout = null;
37
+ if (sessionId && typeof timestamp === 'number' && timestamp > Date.now() - 5000) {
38
+ this.set('sessionId', sessionId);
39
+ this.set('hasStartSession', true);
40
+ this.persistSession(sessionId, timestamp);
41
+ if (this.isTracking) {
42
+ this.setupSessionTimeout();
77
43
  }
78
- this.throttleTimeout = window.setTimeout(() => {
79
- this.onActivity();
80
- this.throttleTimeout = null;
81
- }, 100);
82
- }
83
- };
84
- this.handleInactivity = () => {
85
- // Track session timeout for health monitoring
86
- this.trackSessionHealth('timeout');
87
- this.onInactivity();
88
- };
89
- this.handleVisibilityChange = () => {
90
- if (document.hidden) {
91
- if (this.isSessionActive) {
92
- if (this.inactivityTimer) {
93
- clearTimeout(this.inactivityTimer);
94
- this.inactivityTimer = null;
95
- }
96
- this.inactivityTimer = window.setTimeout(this.handleInactivity, this.config.visibilityTimeout);
97
- }
98
- }
99
- else {
100
- this.handleActivity();
101
- }
102
- };
103
- this.resetInactivityTimer = () => {
104
- if (this.inactivityTimer) {
105
- clearTimeout(this.inactivityTimer);
106
- this.inactivityTimer = null;
44
+ debugLog.debug('SessionManager', 'Session synced from another tab', { sessionId });
107
45
  }
108
- if (this.isSessionActive) {
109
- this.inactivityTimer = window.setTimeout(() => {
110
- this.handleInactivity();
111
- }, this.config.timeout);
112
- }
113
- };
114
- this.config = {
115
- throttleDelay: DEFAULT_THROTTLE_DELAY_MS,
116
- visibilityTimeout: DEFAULT_VISIBILITY_TIMEOUT_MS,
117
- motionThreshold: DEFAULT_MOTION_THRESHOLD,
118
- timeout: this.get('config')?.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT_MS,
119
46
  };
120
- this.sessionEndConfig = {
121
- enablePageUnloadHandlers: true,
122
- syncTimeoutMs: 1000,
123
- maxRetries: 2,
124
- debugMode: false,
125
- ...sessionEndConfig,
126
- };
127
- this.onActivity = onActivity;
128
- this.onInactivity = onInactivity;
129
- this.eventManager = eventManager ?? null;
130
- this.storageManager = storageManager ?? null;
131
- this.deviceCapabilities = this.detectDeviceCapabilities();
132
- this.initializeRecoveryManager();
133
- this.initializeListenerManagers();
134
- this.setupAllListeners();
135
- if (this.sessionEndConfig.enablePageUnloadHandlers) {
136
- this.setupPageUnloadHandlers();
137
- }
138
- debugLog.debug('SessionManager', 'SessionManager initialized', {
139
- sessionTimeout: this.config.timeout,
140
- deviceCapabilities: this.deviceCapabilities,
141
- unloadHandlersEnabled: this.sessionEndConfig.enablePageUnloadHandlers,
142
- });
143
47
  }
144
48
  /**
145
- * Initialize recovery manager
49
+ * Share session with other tabs
146
50
  */
147
- initializeRecoveryManager() {
148
- if (!this.storageManager)
149
- return;
150
- const projectId = this.get('config')?.id;
151
- if (!projectId)
51
+ shareSession(sessionId) {
52
+ this.broadcastChannel?.postMessage({
53
+ action: 'session_start',
54
+ projectId: this.getProjectId(),
55
+ sessionId,
56
+ timestamp: Date.now(),
57
+ });
58
+ }
59
+ broadcastSessionEnd(sessionId, reason) {
60
+ if (!sessionId) {
152
61
  return;
153
- try {
154
- // Initialize session recovery manager (always enabled)
155
- this.recoveryManager = new SessionRecoveryManager(this.storageManager, projectId, this.eventManager ?? undefined);
156
- debugLog.debug('SessionManager', 'Recovery manager initialized', { projectId });
157
- }
158
- catch (error) {
159
- debugLog.error('SessionManager', 'Failed to initialize recovery manager', { error, projectId });
160
62
  }
63
+ this.broadcastChannel?.postMessage({
64
+ action: 'session_end',
65
+ projectId: this.getProjectId(),
66
+ sessionId,
67
+ reason,
68
+ timestamp: Date.now(),
69
+ });
161
70
  }
162
71
  /**
163
- * Store session context for recovery
72
+ * Cleanup cross-tab sync
164
73
  */
165
- storeSessionContextForRecovery() {
166
- if (!this.recoveryManager)
167
- return;
168
- const sessionId = this.get('sessionId');
169
- if (!sessionId)
170
- return;
171
- const sessionContext = {
172
- sessionId,
173
- startTime: this.sessionStartTime,
174
- lastActivity: this.lastActivityTime,
175
- tabCount: 1, // This will be updated by cross-tab manager
176
- recoveryAttempts: 0,
177
- metadata: {
178
- userAgent: navigator.userAgent,
179
- pageUrl: this.get('pageUrl'),
180
- },
181
- };
182
- this.recoveryManager.storeSessionContextForRecovery(sessionContext);
183
- }
184
- startSession() {
185
- const now = Date.now();
186
- // Attempt session recovery first
187
- let sessionId = '';
188
- let wasRecovered = false;
189
- if (this.recoveryManager?.hasRecoverableSession()) {
190
- const recoveryResult = this.recoveryManager.attemptSessionRecovery();
191
- if (recoveryResult.recovered && recoveryResult.recoveredSessionId) {
192
- sessionId = recoveryResult.recoveredSessionId;
193
- wasRecovered = true;
194
- // Track session recovery for health monitoring
195
- this.trackSessionHealth('recovery');
196
- // Update session timing from recovery context
197
- if (recoveryResult.context) {
198
- this.sessionStartTime = recoveryResult.context.startTime;
199
- this.lastActivityTime = now;
200
- }
201
- else {
202
- this.sessionStartTime = now;
203
- this.lastActivityTime = now;
204
- }
205
- debugLog.info('SessionManager', 'Session successfully recovered', {
206
- sessionId,
207
- recoveryAttempts: this.sessionHealth.recoveryAttempts,
208
- });
209
- }
210
- }
211
- // If no recovery, create new session
212
- if (!wasRecovered) {
213
- sessionId = generateUUID();
214
- this.sessionStartTime = now;
215
- this.lastActivityTime = now;
216
- debugLog.info('SessionManager', 'New session started', { sessionId });
74
+ cleanupCrossTabSync() {
75
+ if (this.broadcastChannel) {
76
+ this.broadcastChannel.close();
77
+ this.broadcastChannel = null;
217
78
  }
218
- this.isSessionActive = true;
219
- this.resetInactivityTimer();
220
- // Store session context for future recovery
221
- this.storeSessionContextForRecovery();
222
- return { sessionId, recovered: wasRecovered };
223
79
  }
224
- endSession() {
225
- if (this.sessionStartTime === 0) {
226
- return 0;
80
+ /**
81
+ * Recover session from localStorage if it exists and hasn't expired
82
+ */
83
+ recoverSession() {
84
+ const storedSession = this.loadStoredSession();
85
+ if (!storedSession) {
86
+ return null;
227
87
  }
228
- const durationMs = Date.now() - this.sessionStartTime;
229
- this.sessionStartTime = 0;
230
- this.isSessionActive = false;
231
- if (this.inactivityTimer) {
232
- clearTimeout(this.inactivityTimer);
233
- this.inactivityTimer = null;
88
+ const sessionTimeout = this.get('config')?.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT;
89
+ if (Date.now() - storedSession.lastActivity > sessionTimeout) {
90
+ debugLog.debug('SessionManager', 'Stored session expired');
91
+ this.clearStoredSession();
92
+ return null;
234
93
  }
235
- return durationMs;
94
+ debugLog.info('SessionManager', 'Session recovered from storage', { sessionId: storedSession.id });
95
+ return storedSession.id;
236
96
  }
237
- destroy() {
238
- this.clearTimers();
239
- this.cleanupAllListeners();
240
- this.resetState();
241
- this.cleanupHandlers.forEach((cleanup) => cleanup());
242
- this.cleanupHandlers = [];
243
- this.pendingSessionEnd = false;
244
- this.sessionEndPromise = null;
245
- this.sessionEndLock = Promise.resolve({
246
- success: true,
247
- reason: 'manual_stop',
248
- timestamp: Date.now(),
249
- eventsFlushed: 0,
250
- method: 'async',
97
+ /**
98
+ * Persist session data to localStorage
99
+ */
100
+ persistSession(sessionId, lastActivity = Date.now()) {
101
+ this.saveStoredSession({
102
+ id: sessionId,
103
+ lastActivity,
251
104
  });
252
- if (this.recoveryManager) {
253
- this.recoveryManager.cleanupOldRecoveryAttempts();
254
- this.recoveryManager = null;
255
- }
256
105
  }
257
- detectDeviceCapabilities() {
258
- const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
259
- const hasMouse = window.matchMedia('(pointer: fine)').matches;
260
- const hasKeyboard = !window.matchMedia('(pointer: coarse)').matches;
261
- const isMobile = getDeviceType() === DeviceType.Mobile;
262
- return { hasTouch, hasMouse, hasKeyboard, isMobile };
106
+ clearStoredSession() {
107
+ const storageKey = this.getSessionStorageKey();
108
+ this.storageManager.removeItem(storageKey);
263
109
  }
264
- initializeListenerManagers() {
265
- this.listenerManagers.push(new ActivityListenerManager(this.handleActivity));
266
- if (this.deviceCapabilities.hasTouch) {
267
- this.listenerManagers.push(new TouchListenerManager(this.handleActivity, this.config.motionThreshold));
110
+ loadStoredSession() {
111
+ const storageKey = this.getSessionStorageKey();
112
+ const storedData = this.storageManager.getItem(storageKey);
113
+ if (!storedData) {
114
+ return null;
268
115
  }
269
- if (this.deviceCapabilities.hasMouse) {
270
- this.listenerManagers.push(new MouseListenerManager(this.handleActivity));
116
+ try {
117
+ const parsed = JSON.parse(storedData);
118
+ if (!parsed.id || typeof parsed.lastActivity !== 'number') {
119
+ return null;
120
+ }
121
+ return parsed;
271
122
  }
272
- if (this.deviceCapabilities.hasKeyboard) {
273
- this.listenerManagers.push(new KeyboardListenerManager(this.handleActivity));
123
+ catch {
124
+ this.storageManager.removeItem(storageKey);
125
+ return null;
274
126
  }
275
- this.listenerManagers.push(new VisibilityListenerManager(this.handleActivity, this.handleVisibilityChange, this.deviceCapabilities.isMobile));
276
- this.listenerManagers.push(new UnloadListenerManager(this.handleInactivity));
277
127
  }
278
- setupAllListeners() {
279
- this.listenerManagers.forEach((manager) => manager.setup());
128
+ saveStoredSession(session) {
129
+ const storageKey = this.getSessionStorageKey();
130
+ this.storageManager.setItem(storageKey, JSON.stringify(session));
131
+ }
132
+ getSessionStorageKey() {
133
+ return SESSION_STORAGE_KEY(this.getProjectId());
280
134
  }
281
- cleanupAllListeners() {
282
- this.listenerManagers.forEach((manager) => manager.cleanup());
135
+ getProjectId() {
136
+ return this.get('config')?.id ?? '';
283
137
  }
284
- clearTimers() {
285
- if (this.inactivityTimer) {
286
- clearTimeout(this.inactivityTimer);
287
- this.inactivityTimer = null;
138
+ /**
139
+ * Start session tracking
140
+ */
141
+ async startTracking() {
142
+ if (this.isTracking) {
143
+ debugLog.warn('SessionManager', 'Session tracking already active');
144
+ return;
288
145
  }
289
- if (this.throttleTimeout) {
290
- clearTimeout(this.throttleTimeout);
291
- this.throttleTimeout = null;
146
+ const recoveredSessionId = this.recoverSession();
147
+ const sessionId = recoveredSessionId ?? this.generateSessionId();
148
+ const isRecovered = Boolean(recoveredSessionId);
149
+ this.isTracking = true;
150
+ try {
151
+ this.set('sessionId', sessionId);
152
+ this.persistSession(sessionId);
153
+ // Track session start event
154
+ this.eventManager.track({
155
+ type: EventType.SESSION_START,
156
+ ...(isRecovered && { session_start_recovered: true }),
157
+ });
158
+ // Initialize components
159
+ this.initCrossTabSync();
160
+ this.shareSession(sessionId);
161
+ this.setupSessionTimeout();
162
+ this.setupActivityListeners();
163
+ this.setupLifecycleListeners();
164
+ debugLog.info('SessionManager', 'Session tracking started', { sessionId, recovered: isRecovered });
165
+ }
166
+ catch (error) {
167
+ this.isTracking = false;
168
+ this.clearSessionTimeout();
169
+ this.cleanupActivityListeners();
170
+ this.cleanupLifecycleListeners();
171
+ this.cleanupCrossTabSync();
172
+ this.set('sessionId', null);
173
+ throw error;
292
174
  }
293
175
  }
294
- resetState() {
295
- this.isSessionActive = false;
296
- this.lastActivityTime = 0;
297
- this.sessionStartTime = 0;
176
+ /**
177
+ * Generate unique session ID
178
+ */
179
+ generateSessionId() {
180
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
298
181
  }
299
- clearInactivityTimer() {
300
- if (this.inactivityTimer) {
301
- clearTimeout(this.inactivityTimer);
302
- this.inactivityTimer = null;
303
- }
182
+ /**
183
+ * Setup session timeout
184
+ */
185
+ setupSessionTimeout() {
186
+ this.clearSessionTimeout();
187
+ const sessionTimeout = this.get('config')?.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT;
188
+ this.sessionTimeoutId = setTimeout(() => {
189
+ this.endSession('inactivity');
190
+ }, sessionTimeout);
304
191
  }
305
- shouldProceedWithSessionEnd(reason) {
306
- return !this.sessionEndReason || this.sessionEndPriority[reason] > this.sessionEndPriority[this.sessionEndReason];
192
+ /**
193
+ * Reset session timeout and update activity
194
+ */
195
+ resetSessionTimeout() {
196
+ this.setupSessionTimeout();
197
+ const sessionId = this.get('sessionId');
198
+ if (sessionId) {
199
+ this.persistSession(sessionId);
200
+ }
307
201
  }
308
- async waitForCompletion() {
309
- if (this.sessionEndPromise) {
310
- return await this.sessionEndPromise;
202
+ /**
203
+ * Clear session timeout
204
+ */
205
+ clearSessionTimeout() {
206
+ if (this.sessionTimeoutId) {
207
+ clearTimeout(this.sessionTimeoutId);
208
+ this.sessionTimeoutId = null;
311
209
  }
312
- return {
313
- success: false,
314
- reason: 'inactivity',
315
- timestamp: Date.now(),
316
- eventsFlushed: 0,
317
- method: 'async',
318
- };
319
210
  }
320
- async endSessionManaged(reason) {
321
- return (this.sessionEndLock = this.sessionEndLock.then(async () => {
322
- this.sessionEndStats.totalSessionEnds++;
323
- this.sessionEndStats.reasonCounts[reason]++;
324
- if (this.pendingSessionEnd) {
325
- this.sessionEndStats.duplicatePrevented++;
326
- debugLog.debug('SessionManager', 'Session end already pending, waiting for completion', { reason });
327
- return this.waitForCompletion();
328
- }
329
- if (!this.shouldProceedWithSessionEnd(reason)) {
330
- if (this.sessionEndConfig.debugMode) {
331
- debugLog.debug('SessionManager', `Session end skipped due to lower priority. Current: ${this.sessionEndReason}, Requested: ${reason}`);
332
- }
333
- return {
334
- success: false,
335
- reason,
336
- timestamp: Date.now(),
337
- eventsFlushed: 0,
338
- method: 'async',
339
- };
340
- }
341
- this.sessionEndReason = reason;
342
- this.pendingSessionEnd = true;
343
- this.sessionEndPromise = this.performSessionEnd(reason, 'async');
344
- try {
345
- const result = await this.sessionEndPromise;
346
- return result;
347
- }
348
- finally {
349
- this.pendingSessionEnd = false;
350
- this.sessionEndPromise = null;
351
- this.sessionEndReason = null;
352
- }
353
- }));
211
+ /**
212
+ * Setup activity listeners to track user engagement
213
+ */
214
+ setupActivityListeners() {
215
+ this.activityHandler = () => this.resetSessionTimeout();
216
+ document.addEventListener('click', this.activityHandler, { passive: true });
217
+ document.addEventListener('keydown', this.activityHandler, { passive: true });
218
+ document.addEventListener('scroll', this.activityHandler, { passive: true });
354
219
  }
355
- endSessionSafely(reason, options) {
356
- const shouldUseSync = options?.forceSync ?? (options?.allowSync && ['page_unload', 'tab_closed'].includes(reason));
357
- if (shouldUseSync) {
358
- return this.endSessionManagedSync(reason);
220
+ /**
221
+ * Clean up activity listeners
222
+ */
223
+ cleanupActivityListeners() {
224
+ if (this.activityHandler) {
225
+ document.removeEventListener('click', this.activityHandler);
226
+ document.removeEventListener('keydown', this.activityHandler);
227
+ document.removeEventListener('scroll', this.activityHandler);
228
+ this.activityHandler = null;
359
229
  }
360
- return this.endSessionManaged(reason);
361
- }
362
- isPendingSessionEnd() {
363
- return this.pendingSessionEnd;
364
230
  }
365
231
  /**
366
- * Track session health events for monitoring and diagnostics
232
+ * Setup page lifecycle listeners (visibility and unload)
367
233
  */
368
- trackSessionHealth(event) {
369
- const now = Date.now();
370
- // Update health counters
371
- switch (event) {
372
- case 'recovery':
373
- this.sessionHealth.recoveryAttempts++;
374
- break;
375
- case 'timeout':
376
- this.sessionHealth.sessionTimeouts++;
377
- break;
378
- case 'conflict':
379
- this.sessionHealth.crossTabConflicts++;
380
- break;
234
+ setupLifecycleListeners() {
235
+ if (this.visibilityChangeHandler || this.beforeUnloadHandler) {
236
+ return;
381
237
  }
382
- this.sessionHealth.lastHealthCheck = now;
383
- // Send health degradation event if recovery attempts are high
384
- if (this.sessionHealth.recoveryAttempts > 3 && this.eventManager) {
385
- this.eventManager.track({
386
- type: EventType.CUSTOM,
387
- custom_event: {
388
- name: 'session_health_degraded',
389
- metadata: {
390
- ...this.sessionHealth,
391
- event_trigger: event,
392
- },
393
- },
394
- });
395
- if (this.sessionEndConfig.debugMode) {
396
- debugLog.warn('SessionManager', `Session health degraded: ${this.sessionHealth.recoveryAttempts} recovery attempts`);
238
+ this.visibilityChangeHandler = () => {
239
+ if (document.hidden) {
240
+ this.clearSessionTimeout();
397
241
  }
242
+ else {
243
+ const sessionId = this.get('sessionId');
244
+ if (sessionId) {
245
+ this.setupSessionTimeout();
246
+ }
247
+ }
248
+ };
249
+ this.beforeUnloadHandler = () => {
250
+ this.eventManager.flushImmediatelySync();
251
+ };
252
+ // Handle tab visibility changes
253
+ document.addEventListener('visibilitychange', this.visibilityChangeHandler);
254
+ // Handle page unload
255
+ window.addEventListener('beforeunload', this.beforeUnloadHandler);
256
+ }
257
+ cleanupLifecycleListeners() {
258
+ if (this.visibilityChangeHandler) {
259
+ document.removeEventListener('visibilitychange', this.visibilityChangeHandler);
260
+ this.visibilityChangeHandler = null;
398
261
  }
399
- if (this.sessionEndConfig.debugMode) {
400
- debugLog.debug('SessionManager', `Session health event tracked: ${event}`);
262
+ if (this.beforeUnloadHandler) {
263
+ window.removeEventListener('beforeunload', this.beforeUnloadHandler);
264
+ this.beforeUnloadHandler = null;
401
265
  }
402
266
  }
403
- async performSessionEnd(reason, method) {
404
- const timestamp = Date.now();
405
- let eventsFlushed = 0;
406
- try {
407
- debugLog.info('SessionManager', 'Starting session end', { method, reason, timestamp });
408
- if (this.eventManager) {
409
- this.eventManager.track({
410
- type: EventType.SESSION_END,
411
- session_end_reason: reason,
412
- });
413
- eventsFlushed = this.eventManager.getQueueLength();
414
- const flushResult = await this.eventManager.flushImmediately();
415
- this.cleanupSession();
416
- const result = {
417
- success: flushResult,
418
- reason,
419
- timestamp,
420
- eventsFlushed,
421
- method,
422
- };
423
- if (flushResult) {
424
- this.sessionEndStats.successfulEnds++;
425
- }
426
- else {
427
- this.sessionEndStats.failedEnds++;
428
- }
429
- return result;
430
- }
431
- this.cleanupSession();
432
- const result = {
433
- success: true,
434
- reason,
435
- timestamp,
436
- eventsFlushed: 0,
437
- method,
438
- };
439
- this.sessionEndStats.successfulEnds++;
440
- return result;
267
+ /**
268
+ * End current session
269
+ */
270
+ endSession(reason) {
271
+ const sessionId = this.get('sessionId');
272
+ if (!sessionId) {
273
+ debugLog.warn('SessionManager', 'endSession called without active session', { reason });
274
+ this.resetSessionState();
275
+ return;
441
276
  }
442
- catch (error) {
443
- this.sessionEndStats.failedEnds++;
444
- debugLog.error('SessionManager', 'Session end failed', { error, reason, method });
445
- this.cleanupSession();
446
- return {
447
- success: false,
448
- reason,
449
- timestamp,
450
- eventsFlushed,
451
- method,
452
- };
277
+ debugLog.info('SessionManager', 'Ending session', { sessionId, reason });
278
+ this.eventManager.track({
279
+ type: EventType.SESSION_END,
280
+ session_end_reason: reason,
281
+ });
282
+ const finalize = () => {
283
+ this.broadcastSessionEnd(sessionId, reason);
284
+ this.resetSessionState();
285
+ };
286
+ const flushResult = this.eventManager.flushImmediatelySync();
287
+ if (flushResult) {
288
+ finalize();
289
+ return;
453
290
  }
291
+ this.eventManager
292
+ .flushImmediately()
293
+ .then(finalize)
294
+ .catch((error) => {
295
+ debugLog.warn('SessionManager', 'Async flush failed during session end', {
296
+ error: error instanceof Error ? error.message : 'Unknown error',
297
+ });
298
+ finalize();
299
+ });
454
300
  }
455
- cleanupSession() {
456
- this.endSession();
457
- this.clearTimers();
301
+ resetSessionState() {
302
+ this.clearSessionTimeout();
303
+ this.cleanupActivityListeners();
304
+ this.cleanupLifecycleListeners();
305
+ this.cleanupCrossTabSync();
306
+ this.clearStoredSession();
458
307
  this.set('sessionId', null);
459
308
  this.set('hasStartSession', false);
309
+ this.isTracking = false;
460
310
  }
461
- endSessionManagedSync(reason) {
462
- this.sessionEndStats.totalSessionEnds++;
463
- this.sessionEndStats.reasonCounts[reason]++;
464
- if (this.pendingSessionEnd) {
465
- this.sessionEndStats.duplicatePrevented++;
466
- debugLog.warn('SessionManager', 'Sync session end called while async end pending', { reason });
467
- }
468
- if (!this.shouldProceedWithSessionEnd(reason)) {
469
- if (this.sessionEndConfig.debugMode) {
470
- debugLog.debug('SessionManager', `Sync session end skipped due to lower priority. Current: ${this.sessionEndReason}, Requested: ${reason}`);
471
- }
472
- return {
473
- success: false,
474
- reason,
475
- timestamp: Date.now(),
476
- eventsFlushed: 0,
477
- method: 'sync',
478
- };
479
- }
480
- this.sessionEndReason = reason;
481
- this.pendingSessionEnd = true;
482
- try {
483
- return this.performSessionEndSync(reason);
484
- }
485
- finally {
486
- this.pendingSessionEnd = false;
487
- this.sessionEndPromise = null;
488
- this.sessionEndReason = null;
489
- }
490
- }
491
- performSessionEndSync(reason) {
492
- const timestamp = Date.now();
493
- let eventsFlushed = 0;
494
- try {
495
- if (this.eventManager) {
496
- this.eventManager.track({
497
- type: EventType.SESSION_END,
498
- session_end_reason: reason,
499
- });
500
- eventsFlushed = this.eventManager.getQueueLength();
501
- const success = this.eventManager.flushImmediatelySync();
502
- this.cleanupSession();
503
- const result = {
504
- success,
505
- reason,
506
- timestamp,
507
- eventsFlushed,
508
- method: 'sync',
509
- };
510
- if (success) {
511
- this.sessionEndStats.successfulEnds++;
512
- }
513
- else {
514
- this.sessionEndStats.failedEnds++;
515
- }
516
- return result;
517
- }
518
- this.cleanupSession();
519
- const result = {
520
- success: true,
521
- reason,
522
- timestamp,
523
- eventsFlushed: 0,
524
- method: 'sync',
525
- };
526
- this.sessionEndStats.successfulEnds++;
527
- return result;
528
- }
529
- catch (error) {
530
- this.sessionEndStats.failedEnds++;
531
- this.cleanupSession();
532
- debugLog.error('SessionManager', 'Sync session end failed', { error, reason });
533
- return {
534
- success: false,
535
- reason,
536
- timestamp,
537
- eventsFlushed,
538
- method: 'sync',
539
- };
540
- }
311
+ /**
312
+ * Stop session tracking
313
+ */
314
+ async stopTracking() {
315
+ this.endSession('manual_stop');
541
316
  }
542
- setupPageUnloadHandlers() {
543
- let unloadHandled = false;
544
- const handlePageUnload = () => {
545
- if (unloadHandled || !this.get('sessionId')) {
546
- return;
547
- }
548
- unloadHandled = true;
549
- this.clearInactivityTimer();
550
- this.endSessionSafely('page_unload', { forceSync: true });
551
- };
552
- // Primary handler for modern browsers
553
- const beforeUnloadHandler = () => {
554
- handlePageUnload();
555
- };
556
- // Fallback for older browsers and mobile Safari
557
- const pageHideHandler = (event) => {
558
- if (!event.persisted) {
559
- handlePageUnload();
560
- }
561
- };
562
- // Delayed handler for visibility changes (gives time for page transitions)
563
- const visibilityChangeHandler = () => {
564
- if (document.visibilityState === 'hidden' && this.get('sessionId') && !unloadHandled) {
565
- this.visibilityChangeTimeout = window.setTimeout(() => {
566
- if (document.visibilityState === 'hidden' && this.get('sessionId') && !unloadHandled) {
567
- handlePageUnload();
568
- }
569
- this.visibilityChangeTimeout = null;
570
- }, 1000);
571
- }
572
- };
573
- window.addEventListener('beforeunload', beforeUnloadHandler);
574
- window.addEventListener('pagehide', pageHideHandler);
575
- document.addEventListener('visibilitychange', visibilityChangeHandler);
576
- this.cleanupHandlers.push(() => window.removeEventListener('beforeunload', beforeUnloadHandler), () => window.removeEventListener('pagehide', pageHideHandler), () => document.removeEventListener('visibilitychange', visibilityChangeHandler), () => {
577
- if (this.visibilityChangeTimeout) {
578
- clearTimeout(this.visibilityChangeTimeout);
579
- this.visibilityChangeTimeout = null;
580
- }
581
- });
317
+ /**
318
+ * Clean up all resources
319
+ */
320
+ destroy() {
321
+ this.clearSessionTimeout();
322
+ this.cleanupActivityListeners();
323
+ this.cleanupCrossTabSync();
324
+ this.cleanupLifecycleListeners();
325
+ this.isTracking = false;
326
+ this.set('hasStartSession', false);
582
327
  }
583
328
  }