@tracelog/lib 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -24
- package/dist/browser/tracelog.js +1928 -3297
- package/dist/cjs/api.d.ts +33 -19
- package/dist/cjs/api.js +111 -156
- package/dist/cjs/app.constants.d.ts +74 -1
- package/dist/cjs/app.constants.js +77 -3
- package/dist/cjs/app.d.ts +29 -44
- package/dist/cjs/app.js +114 -212
- package/dist/cjs/app.types.d.ts +2 -7
- package/dist/cjs/app.types.js +10 -21
- package/dist/cjs/constants/api.constants.js +11 -5
- package/dist/cjs/constants/config.constants.d.ts +75 -0
- package/dist/cjs/constants/config.constants.js +178 -0
- package/dist/cjs/constants/error.constants.d.ts +29 -0
- package/dist/cjs/constants/error.constants.js +50 -0
- package/dist/cjs/constants/index.d.ts +3 -6
- package/dist/cjs/constants/index.js +3 -6
- package/dist/cjs/constants/performance.constants.d.ts +28 -0
- package/dist/cjs/constants/performance.constants.js +43 -0
- package/dist/cjs/handlers/click.handler.d.ts +1 -0
- package/dist/cjs/handlers/click.handler.js +30 -49
- package/dist/cjs/handlers/error.handler.d.ts +11 -6
- package/dist/cjs/handlers/error.handler.js +91 -51
- package/dist/cjs/handlers/page-view.handler.js +38 -29
- package/dist/cjs/handlers/performance.handler.d.ts +3 -0
- package/dist/cjs/handlers/performance.handler.js +76 -37
- package/dist/cjs/handlers/scroll.handler.d.ts +16 -10
- package/dist/cjs/handlers/scroll.handler.js +119 -185
- package/dist/cjs/handlers/session.handler.d.ts +6 -20
- package/dist/cjs/handlers/session.handler.js +38 -326
- package/dist/cjs/integrations/google-analytics.integration.d.ts +0 -1
- package/dist/cjs/integrations/google-analytics.integration.js +27 -98
- package/dist/cjs/listeners/input-listener-managers.d.ts +18 -9
- package/dist/cjs/listeners/input-listener-managers.js +24 -33
- package/dist/cjs/listeners/touch-listener-manager.d.ts +1 -3
- package/dist/cjs/listeners/touch-listener-manager.js +1 -23
- package/dist/cjs/listeners/visibility-listener-manager.d.ts +1 -4
- package/dist/cjs/listeners/visibility-listener-manager.js +6 -42
- package/dist/cjs/managers/api.manager.d.ts +13 -3
- package/dist/cjs/managers/api.manager.js +35 -5
- package/dist/cjs/managers/config.manager.d.ts +53 -3
- package/dist/cjs/managers/config.manager.js +131 -62
- package/dist/cjs/managers/event.manager.d.ts +57 -36
- package/dist/cjs/managers/event.manager.js +266 -417
- package/dist/cjs/managers/sender.manager.d.ts +40 -22
- package/dist/cjs/managers/sender.manager.js +200 -198
- package/dist/cjs/managers/session.manager.d.ts +80 -66
- package/dist/cjs/managers/session.manager.js +267 -522
- package/dist/cjs/managers/state.manager.d.ts +33 -0
- package/dist/cjs/managers/state.manager.js +79 -6
- package/dist/cjs/managers/storage.manager.d.ts +26 -2
- package/dist/cjs/managers/storage.manager.js +67 -34
- package/dist/cjs/managers/tags.manager.d.ts +31 -7
- package/dist/cjs/managers/tags.manager.js +123 -241
- package/dist/cjs/managers/user.manager.d.ts +14 -5
- package/dist/cjs/managers/user.manager.js +17 -9
- package/dist/cjs/public-api.d.ts +10 -1
- package/dist/cjs/public-api.js +18 -24
- package/dist/cjs/test-bridge.d.ts +48 -0
- package/dist/cjs/test-bridge.js +110 -0
- package/dist/cjs/types/api.types.d.ts +21 -6
- package/dist/cjs/types/api.types.js +21 -6
- package/dist/cjs/types/config.types.d.ts +22 -84
- package/dist/cjs/types/emitter.types.d.ts +11 -0
- package/dist/cjs/types/emitter.types.js +8 -0
- package/dist/cjs/types/event.types.d.ts +8 -11
- package/dist/cjs/types/index.d.ts +3 -1
- package/dist/cjs/types/index.js +3 -1
- package/dist/cjs/types/queue.types.d.ts +1 -0
- package/dist/cjs/types/session.types.d.ts +0 -64
- package/dist/cjs/types/state.types.d.ts +1 -0
- package/dist/cjs/types/test-bridge.types.d.ts +38 -0
- package/dist/cjs/types/validation-error.types.d.ts +7 -0
- package/dist/cjs/types/validation-error.types.js +11 -1
- package/dist/cjs/types/window.types.d.ts +1 -8
- package/dist/cjs/utils/data/uuid.utils.d.ts +1 -1
- package/dist/cjs/utils/data/uuid.utils.js +7 -5
- package/dist/cjs/utils/emitter.utils.d.ts +8 -0
- package/dist/cjs/utils/emitter.utils.js +33 -0
- package/dist/cjs/utils/index.d.ts +1 -0
- package/dist/cjs/utils/index.js +1 -0
- package/dist/cjs/utils/logging/debug-logger.utils.d.ts +10 -51
- package/dist/cjs/utils/logging/debug-logger.utils.js +36 -127
- package/dist/cjs/utils/network/fetch-with-timeout.utils.d.ts +4 -0
- package/dist/cjs/utils/network/fetch-with-timeout.utils.js +25 -0
- package/dist/cjs/utils/network/index.d.ts +1 -0
- package/dist/cjs/utils/network/index.js +1 -0
- package/dist/cjs/utils/network/url.utils.js +2 -42
- package/dist/cjs/utils/security/sanitize.utils.d.ts +1 -8
- package/dist/cjs/utils/security/sanitize.utils.js +7 -41
- package/dist/cjs/utils/validations/config-validations.utils.d.ts +7 -0
- package/dist/cjs/utils/validations/config-validations.utils.js +77 -22
- package/dist/esm/api.d.ts +33 -19
- package/dist/esm/api.js +105 -118
- package/dist/esm/app.constants.d.ts +74 -1
- package/dist/esm/app.constants.js +76 -1
- package/dist/esm/app.d.ts +29 -44
- package/dist/esm/app.js +115 -213
- package/dist/esm/app.types.d.ts +2 -7
- package/dist/esm/app.types.js +1 -7
- package/dist/esm/constants/api.constants.js +10 -4
- package/dist/esm/constants/config.constants.d.ts +75 -0
- package/dist/esm/constants/config.constants.js +174 -0
- package/dist/esm/constants/error.constants.d.ts +29 -0
- package/dist/esm/constants/error.constants.js +47 -0
- package/dist/esm/constants/index.d.ts +3 -6
- package/dist/esm/constants/index.js +3 -6
- package/dist/esm/constants/performance.constants.d.ts +28 -0
- package/dist/esm/constants/performance.constants.js +40 -0
- package/dist/esm/handlers/click.handler.d.ts +1 -0
- package/dist/esm/handlers/click.handler.js +30 -49
- package/dist/esm/handlers/error.handler.d.ts +11 -6
- package/dist/esm/handlers/error.handler.js +91 -51
- package/dist/esm/handlers/page-view.handler.js +38 -29
- package/dist/esm/handlers/performance.handler.d.ts +3 -0
- package/dist/esm/handlers/performance.handler.js +71 -32
- package/dist/esm/handlers/scroll.handler.d.ts +16 -10
- package/dist/esm/handlers/scroll.handler.js +120 -186
- package/dist/esm/handlers/session.handler.d.ts +6 -20
- package/dist/esm/handlers/session.handler.js +38 -326
- package/dist/esm/integrations/google-analytics.integration.d.ts +0 -1
- package/dist/esm/integrations/google-analytics.integration.js +27 -98
- package/dist/esm/listeners/input-listener-managers.d.ts +18 -9
- package/dist/esm/listeners/input-listener-managers.js +23 -32
- package/dist/esm/listeners/touch-listener-manager.d.ts +1 -3
- package/dist/esm/listeners/touch-listener-manager.js +1 -23
- package/dist/esm/listeners/visibility-listener-manager.d.ts +1 -4
- package/dist/esm/listeners/visibility-listener-manager.js +6 -42
- package/dist/esm/managers/api.manager.d.ts +13 -3
- package/dist/esm/managers/api.manager.js +34 -3
- package/dist/esm/managers/config.manager.d.ts +53 -3
- package/dist/esm/managers/config.manager.js +133 -64
- package/dist/esm/managers/event.manager.d.ts +57 -36
- package/dist/esm/managers/event.manager.js +268 -419
- package/dist/esm/managers/sender.manager.d.ts +40 -22
- package/dist/esm/managers/sender.manager.js +201 -199
- package/dist/esm/managers/session.manager.d.ts +80 -66
- package/dist/esm/managers/session.manager.js +269 -524
- package/dist/esm/managers/state.manager.d.ts +33 -0
- package/dist/esm/managers/state.manager.js +78 -6
- package/dist/esm/managers/storage.manager.d.ts +26 -2
- package/dist/esm/managers/storage.manager.js +66 -33
- package/dist/esm/managers/tags.manager.d.ts +31 -7
- package/dist/esm/managers/tags.manager.js +124 -242
- package/dist/esm/managers/user.manager.d.ts +14 -5
- package/dist/esm/managers/user.manager.js +17 -9
- package/dist/esm/public-api.d.ts +10 -1
- package/dist/esm/public-api.js +14 -1
- package/dist/esm/test-bridge.d.ts +48 -0
- package/dist/esm/test-bridge.js +106 -0
- package/dist/esm/types/api.types.d.ts +21 -6
- package/dist/esm/types/api.types.js +21 -6
- package/dist/esm/types/config.types.d.ts +22 -84
- package/dist/esm/types/emitter.types.d.ts +11 -0
- package/dist/esm/types/emitter.types.js +5 -0
- package/dist/esm/types/event.types.d.ts +8 -11
- package/dist/esm/types/index.d.ts +3 -1
- package/dist/esm/types/index.js +3 -1
- package/dist/esm/types/queue.types.d.ts +1 -0
- package/dist/esm/types/session.types.d.ts +0 -64
- package/dist/esm/types/state.types.d.ts +1 -0
- package/dist/esm/types/test-bridge.types.d.ts +38 -0
- package/dist/esm/types/validation-error.types.d.ts +7 -0
- package/dist/esm/types/validation-error.types.js +9 -0
- package/dist/esm/types/window.types.d.ts +1 -8
- package/dist/esm/utils/data/uuid.utils.d.ts +1 -1
- package/dist/esm/utils/data/uuid.utils.js +7 -5
- package/dist/esm/utils/emitter.utils.d.ts +8 -0
- package/dist/esm/utils/emitter.utils.js +29 -0
- package/dist/esm/utils/index.d.ts +1 -0
- package/dist/esm/utils/index.js +1 -0
- package/dist/esm/utils/logging/debug-logger.utils.d.ts +10 -51
- package/dist/esm/utils/logging/debug-logger.utils.js +36 -127
- package/dist/esm/utils/network/fetch-with-timeout.utils.d.ts +4 -0
- package/dist/esm/utils/network/fetch-with-timeout.utils.js +22 -0
- package/dist/esm/utils/network/index.d.ts +1 -0
- package/dist/esm/utils/network/index.js +1 -0
- package/dist/esm/utils/network/url.utils.js +2 -42
- package/dist/esm/utils/security/sanitize.utils.d.ts +1 -8
- package/dist/esm/utils/security/sanitize.utils.js +6 -39
- package/dist/esm/utils/validations/config-validations.utils.d.ts +7 -0
- package/dist/esm/utils/validations/config-validations.utils.js +76 -22
- package/package.json +23 -16
- package/dist/browser/web-vitals-CCnqwnC8.mjs +0 -198
- package/dist/cjs/constants/browser.constants.d.ts +0 -3
- package/dist/cjs/constants/browser.constants.js +0 -41
- package/dist/cjs/constants/initialization.constants.d.ts +0 -40
- package/dist/cjs/constants/initialization.constants.js +0 -48
- package/dist/cjs/constants/limits.constants.d.ts +0 -27
- package/dist/cjs/constants/limits.constants.js +0 -43
- package/dist/cjs/constants/security.constants.d.ts +0 -1
- package/dist/cjs/constants/security.constants.js +0 -12
- package/dist/cjs/constants/timing.constants.d.ts +0 -22
- package/dist/cjs/constants/timing.constants.js +0 -34
- package/dist/cjs/constants/validation.constants.d.ts +0 -13
- package/dist/cjs/constants/validation.constants.js +0 -31
- package/dist/cjs/handlers/network.handler.d.ts +0 -16
- package/dist/cjs/handlers/network.handler.js +0 -136
- package/dist/cjs/managers/cross-tab-session.manager.d.ts +0 -170
- package/dist/cjs/managers/cross-tab-session.manager.js +0 -730
- package/dist/cjs/managers/sampling.manager.d.ts +0 -8
- package/dist/cjs/managers/sampling.manager.js +0 -53
- package/dist/cjs/managers/session-recovery.manager.d.ts +0 -65
- package/dist/cjs/managers/session-recovery.manager.js +0 -237
- package/dist/cjs/types/web-vitals.types.d.ts +0 -6
- package/dist/esm/constants/browser.constants.d.ts +0 -3
- package/dist/esm/constants/browser.constants.js +0 -38
- package/dist/esm/constants/initialization.constants.d.ts +0 -40
- package/dist/esm/constants/initialization.constants.js +0 -45
- package/dist/esm/constants/limits.constants.d.ts +0 -27
- package/dist/esm/constants/limits.constants.js +0 -40
- package/dist/esm/constants/security.constants.d.ts +0 -1
- package/dist/esm/constants/security.constants.js +0 -9
- package/dist/esm/constants/timing.constants.d.ts +0 -22
- package/dist/esm/constants/timing.constants.js +0 -31
- package/dist/esm/constants/validation.constants.d.ts +0 -13
- package/dist/esm/constants/validation.constants.js +0 -28
- package/dist/esm/handlers/network.handler.d.ts +0 -16
- package/dist/esm/handlers/network.handler.js +0 -132
- package/dist/esm/managers/cross-tab-session.manager.d.ts +0 -170
- package/dist/esm/managers/cross-tab-session.manager.js +0 -726
- package/dist/esm/managers/sampling.manager.d.ts +0 -8
- package/dist/esm/managers/sampling.manager.js +0 -49
- package/dist/esm/managers/session-recovery.manager.d.ts +0 -65
- package/dist/esm/managers/session-recovery.manager.js +0 -233
- package/dist/esm/types/web-vitals.types.d.ts +0 -6
- /package/dist/cjs/types/{web-vitals.types.js → test-bridge.types.js} +0 -0
- /package/dist/esm/types/{web-vitals.types.js → test-bridge.types.js} +0 -0
|
@@ -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.
|
|
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.
|
|
52
|
-
debugLog.debug('SessionHandler', 'Session tracking already active');
|
|
12
|
+
async startTracking() {
|
|
13
|
+
if (this.isActive()) {
|
|
53
14
|
return;
|
|
54
15
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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.
|
|
126
|
-
this.clearPersistedSession();
|
|
127
|
-
this.stopHeartbeat();
|
|
28
|
+
this.sessionManager.destroy();
|
|
128
29
|
}
|
|
129
|
-
catch
|
|
130
|
-
|
|
131
|
-
this.forceCleanupSession();
|
|
30
|
+
catch {
|
|
31
|
+
// Ignore cleanup errors
|
|
132
32
|
}
|
|
33
|
+
this.sessionManager = null;
|
|
133
34
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
149
|
-
this.
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
244
|
-
this.
|
|
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
|
-
|
|
252
|
-
if (this.
|
|
253
|
-
debugLog.debug('SessionHandler', 'Session already exists, skipping initial session creation');
|
|
54
|
+
destroy() {
|
|
55
|
+
if (this.destroyed) {
|
|
254
56
|
return;
|
|
255
57
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { debugLog } from '../utils
|
|
1
|
+
import { debugLog } from '../utils';
|
|
2
2
|
import { StateManager } from '../managers/state.manager';
|
|
3
3
|
export class GoogleAnalyticsIntegration extends StateManager {
|
|
4
4
|
constructor() {
|
|
5
|
-
super();
|
|
5
|
+
super(...arguments);
|
|
6
6
|
this.isInitialized = false;
|
|
7
7
|
}
|
|
8
8
|
async initialize() {
|
|
@@ -10,69 +10,35 @@ export class GoogleAnalyticsIntegration extends StateManager {
|
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
12
|
const measurementId = this.get('config').integrations?.googleAnalytics?.measurementId;
|
|
13
|
-
if (!measurementId?.trim()) {
|
|
14
|
-
debugLog.clientWarn('GoogleAnalytics', 'Google Analytics integration disabled - measurementId not configured', {
|
|
15
|
-
hasIntegrations: !!this.get('config').integrations,
|
|
16
|
-
hasGoogleAnalytics: !!this.get('config').integrations?.googleAnalytics,
|
|
17
|
-
});
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
13
|
const userId = this.get('userId');
|
|
21
|
-
if (!userId?.trim()) {
|
|
22
|
-
debugLog.warn('GoogleAnalytics', 'Google Analytics initialization delayed - userId not available', {
|
|
23
|
-
measurementId: measurementId.substring(0, 8) + '...',
|
|
24
|
-
});
|
|
14
|
+
if (!measurementId?.trim() || !userId?.trim()) {
|
|
25
15
|
return;
|
|
26
16
|
}
|
|
27
17
|
try {
|
|
28
18
|
if (this.isScriptAlreadyLoaded()) {
|
|
29
|
-
debugLog.info('GoogleAnalytics', 'Google Analytics script already loaded', { measurementId });
|
|
30
19
|
this.isInitialized = true;
|
|
31
20
|
return;
|
|
32
21
|
}
|
|
33
22
|
await this.loadScript(measurementId);
|
|
34
23
|
this.configureGtag(measurementId, userId);
|
|
35
24
|
this.isInitialized = true;
|
|
36
|
-
debugLog.info('GoogleAnalytics', 'Google Analytics integration initialized successfully', {
|
|
37
|
-
measurementId,
|
|
38
|
-
userId,
|
|
39
|
-
});
|
|
40
25
|
}
|
|
41
26
|
catch (error) {
|
|
42
|
-
debugLog.error('
|
|
27
|
+
debugLog.error('GoogleAnalyticsIntegration', 'Initialization failed', {
|
|
43
28
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
44
|
-
measurementId,
|
|
45
|
-
userId,
|
|
46
29
|
});
|
|
47
30
|
}
|
|
48
31
|
}
|
|
49
32
|
trackEvent(eventName, metadata) {
|
|
50
|
-
if (!eventName?.trim()) {
|
|
51
|
-
debugLog.clientWarn('GoogleAnalytics', 'Event tracking skipped - invalid event name provided', {
|
|
52
|
-
eventName,
|
|
53
|
-
hasMetadata: !!metadata && Object.keys(metadata).length > 0,
|
|
54
|
-
});
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
|
-
if (!this.isInitialized) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
if (typeof window.gtag !== 'function') {
|
|
61
|
-
debugLog.warn('GoogleAnalytics', 'Event tracking failed - gtag function not available', {
|
|
62
|
-
eventName,
|
|
63
|
-
hasGtag: typeof window.gtag,
|
|
64
|
-
hasDataLayer: Array.isArray(window.dataLayer),
|
|
65
|
-
});
|
|
33
|
+
if (!eventName?.trim() || !this.isInitialized || typeof window.gtag !== 'function') {
|
|
66
34
|
return;
|
|
67
35
|
}
|
|
68
36
|
try {
|
|
69
37
|
window.gtag('event', eventName, metadata);
|
|
70
38
|
}
|
|
71
39
|
catch (error) {
|
|
72
|
-
debugLog.error('
|
|
73
|
-
eventName,
|
|
40
|
+
debugLog.error('GoogleAnalyticsIntegration', 'Event tracking failed', {
|
|
74
41
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
75
|
-
metadataKeys: Object.keys(metadata || {}),
|
|
76
42
|
});
|
|
77
43
|
}
|
|
78
44
|
}
|
|
@@ -82,74 +48,37 @@ export class GoogleAnalyticsIntegration extends StateManager {
|
|
|
82
48
|
if (script) {
|
|
83
49
|
script.remove();
|
|
84
50
|
}
|
|
85
|
-
debugLog.info('GoogleAnalytics', 'Google Analytics integration cleanup completed');
|
|
86
51
|
}
|
|
87
52
|
isScriptAlreadyLoaded() {
|
|
88
|
-
|
|
89
|
-
if (
|
|
53
|
+
// Check if we already loaded the script
|
|
54
|
+
if (document.getElementById('tracelog-ga-script')) {
|
|
90
55
|
return true;
|
|
91
56
|
}
|
|
57
|
+
// Check if GA is already loaded by another source
|
|
92
58
|
const existingGAScript = document.querySelector('script[src*="googletagmanager.com/gtag/js"]');
|
|
93
|
-
|
|
94
|
-
debugLog.clientWarn('GoogleAnalytics', 'Google Analytics script already loaded from external source', {
|
|
95
|
-
scriptSrc: existingGAScript.getAttribute('src'),
|
|
96
|
-
hasGtag: typeof window.gtag === 'function',
|
|
97
|
-
});
|
|
98
|
-
return true;
|
|
99
|
-
}
|
|
100
|
-
return false;
|
|
59
|
+
return !!existingGAScript;
|
|
101
60
|
}
|
|
102
61
|
async loadScript(measurementId) {
|
|
103
62
|
return new Promise((resolve, reject) => {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
};
|
|
112
|
-
script.onerror = () => {
|
|
113
|
-
const error = new Error('Failed to load Google Analytics script');
|
|
114
|
-
debugLog.error('GoogleAnalytics', 'Google Analytics script load failed', {
|
|
115
|
-
measurementId,
|
|
116
|
-
error: error.message,
|
|
117
|
-
scriptSrc: script.src,
|
|
118
|
-
});
|
|
119
|
-
reject(error);
|
|
120
|
-
};
|
|
121
|
-
document.head.appendChild(script);
|
|
122
|
-
}
|
|
123
|
-
catch (error) {
|
|
124
|
-
const errorMsg = error instanceof Error ? error : new Error(String(error));
|
|
125
|
-
debugLog.error('GoogleAnalytics', 'Error creating Google Analytics script', {
|
|
126
|
-
measurementId,
|
|
127
|
-
error: errorMsg.message,
|
|
128
|
-
});
|
|
129
|
-
reject(errorMsg);
|
|
130
|
-
}
|
|
63
|
+
const script = document.createElement('script');
|
|
64
|
+
script.id = 'tracelog-ga-script';
|
|
65
|
+
script.async = true;
|
|
66
|
+
script.src = `https://www.googletagmanager.com/gtag/js?id=${measurementId}`;
|
|
67
|
+
script.onload = () => resolve();
|
|
68
|
+
script.onerror = () => reject(new Error('Failed to load Google Analytics script'));
|
|
69
|
+
document.head.appendChild(script);
|
|
131
70
|
});
|
|
132
71
|
}
|
|
133
72
|
configureGtag(measurementId, userId) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
document.head.appendChild(gaScriptConfig);
|
|
145
|
-
}
|
|
146
|
-
catch (error) {
|
|
147
|
-
debugLog.error('GoogleAnalytics', 'Failed to configure Google Analytics', {
|
|
148
|
-
measurementId,
|
|
149
|
-
userId,
|
|
150
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
151
|
-
});
|
|
152
|
-
throw error;
|
|
153
|
-
}
|
|
73
|
+
const gaScriptConfig = document.createElement('script');
|
|
74
|
+
gaScriptConfig.innerHTML = `
|
|
75
|
+
window.dataLayer = window.dataLayer || [];
|
|
76
|
+
function gtag(){dataLayer.push(arguments);}
|
|
77
|
+
gtag('js', new Date());
|
|
78
|
+
gtag('config', '${measurementId}', {
|
|
79
|
+
'user_id': '${userId}'
|
|
80
|
+
});
|
|
81
|
+
`;
|
|
82
|
+
document.head.appendChild(gaScriptConfig);
|
|
154
83
|
}
|
|
155
84
|
}
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { EventListenerManager } from './listeners.types';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Base class for input listener managers to reduce code duplication
|
|
4
|
+
*/
|
|
5
|
+
declare abstract class BaseInputListenerManager implements EventListenerManager {
|
|
6
|
+
protected readonly onActivity: () => void;
|
|
7
|
+
protected readonly options: {
|
|
8
|
+
passive: boolean;
|
|
9
|
+
};
|
|
10
|
+
protected abstract readonly events: string[];
|
|
11
|
+
protected abstract readonly logPrefix: string;
|
|
5
12
|
constructor(onActivity: () => void);
|
|
6
13
|
setup(): void;
|
|
7
14
|
cleanup(): void;
|
|
8
15
|
}
|
|
9
|
-
export declare class
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
export declare class MouseListenerManager extends BaseInputListenerManager {
|
|
17
|
+
protected readonly events: string[];
|
|
18
|
+
protected readonly logPrefix = "MouseListenerManager";
|
|
19
|
+
}
|
|
20
|
+
export declare class KeyboardListenerManager extends BaseInputListenerManager {
|
|
21
|
+
protected readonly events: string[];
|
|
22
|
+
protected readonly logPrefix = "KeyboardListenerManager";
|
|
15
23
|
}
|
|
24
|
+
export {};
|