@tracelog/lib 0.4.0 → 0.5.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.
- package/dist/browser/tracelog.js +620 -658
- package/dist/cjs/api.d.ts +1 -53
- package/dist/cjs/api.js +0 -59
- package/dist/cjs/app.constants.d.ts +1 -1
- package/dist/cjs/app.d.ts +1 -5
- package/dist/cjs/app.js +4 -12
- package/dist/cjs/constants/api.constants.d.ts +5 -2
- package/dist/cjs/constants/api.constants.js +5 -14
- package/dist/cjs/constants/config.constants.d.ts +3 -3
- package/dist/cjs/constants/config.constants.js +3 -3
- package/dist/cjs/constants/error.constants.d.ts +7 -2
- package/dist/cjs/constants/error.constants.js +13 -2
- package/dist/cjs/handlers/click.handler.js +0 -6
- package/dist/cjs/handlers/error.handler.js +9 -0
- package/dist/cjs/handlers/scroll.handler.js +0 -5
- package/dist/cjs/handlers/session.handler.js +5 -2
- package/dist/cjs/integrations/google-analytics.integration.d.ts +1 -1
- package/dist/cjs/integrations/google-analytics.integration.js +2 -1
- package/dist/cjs/managers/api.manager.d.ts +1 -1
- package/dist/cjs/managers/api.manager.js +3 -3
- package/dist/cjs/managers/config.builder.d.ts +33 -0
- package/dist/cjs/managers/config.builder.js +116 -0
- package/dist/cjs/managers/config.manager.d.ts +13 -14
- package/dist/cjs/managers/config.manager.js +52 -58
- package/dist/cjs/managers/event.manager.d.ts +1 -46
- package/dist/cjs/managers/event.manager.js +15 -70
- package/dist/cjs/managers/sender.manager.d.ts +1 -28
- package/dist/cjs/managers/sender.manager.js +43 -73
- package/dist/cjs/managers/session.manager.d.ts +2 -49
- package/dist/cjs/managers/session.manager.js +42 -83
- package/dist/cjs/managers/state.manager.d.ts +1 -28
- package/dist/cjs/managers/state.manager.js +5 -33
- package/dist/cjs/managers/storage.manager.d.ts +6 -0
- package/dist/cjs/managers/storage.manager.js +18 -1
- package/dist/cjs/public-api.d.ts +1 -1
- package/dist/cjs/test-bridge.d.ts +3 -2
- package/dist/cjs/test-bridge.js +34 -7
- package/dist/cjs/types/api.types.d.ts +24 -8
- package/dist/cjs/types/api.types.js +24 -8
- package/dist/cjs/types/event.types.d.ts +2 -4
- package/dist/cjs/types/event.types.js +0 -1
- package/dist/cjs/types/test-bridge.types.d.ts +2 -1
- package/dist/cjs/utils/logging/debug-logger.utils.d.ts +1 -2
- package/dist/cjs/utils/logging/debug-logger.utils.js +2 -3
- package/dist/cjs/utils/validations/config-validations.utils.d.ts +1 -26
- package/dist/cjs/utils/validations/config-validations.utils.js +5 -117
- package/dist/cjs/utils/validations/event-validations.utils.d.ts +2 -2
- package/dist/cjs/utils/validations/metadata-validations.utils.d.ts +3 -3
- package/dist/cjs/utils/validations/metadata-validations.utils.js +41 -3
- package/dist/esm/api.d.ts +1 -53
- package/dist/esm/api.js +0 -59
- package/dist/esm/app.constants.d.ts +1 -1
- package/dist/esm/app.d.ts +1 -5
- package/dist/esm/app.js +5 -13
- package/dist/esm/constants/api.constants.d.ts +5 -2
- package/dist/esm/constants/api.constants.js +5 -13
- package/dist/esm/constants/config.constants.d.ts +3 -3
- package/dist/esm/constants/config.constants.js +3 -3
- package/dist/esm/constants/error.constants.d.ts +7 -2
- package/dist/esm/constants/error.constants.js +12 -1
- package/dist/esm/handlers/click.handler.js +0 -6
- package/dist/esm/handlers/error.handler.js +10 -1
- package/dist/esm/handlers/scroll.handler.js +0 -5
- package/dist/esm/handlers/session.handler.js +5 -2
- package/dist/esm/integrations/google-analytics.integration.d.ts +1 -1
- package/dist/esm/integrations/google-analytics.integration.js +2 -1
- package/dist/esm/managers/api.manager.d.ts +1 -1
- package/dist/esm/managers/api.manager.js +3 -3
- package/dist/esm/managers/config.builder.d.ts +33 -0
- package/dist/esm/managers/config.builder.js +112 -0
- package/dist/esm/managers/config.manager.d.ts +13 -14
- package/dist/esm/managers/config.manager.js +54 -60
- package/dist/esm/managers/event.manager.d.ts +1 -46
- package/dist/esm/managers/event.manager.js +15 -70
- package/dist/esm/managers/sender.manager.d.ts +1 -28
- package/dist/esm/managers/sender.manager.js +44 -74
- package/dist/esm/managers/session.manager.d.ts +2 -49
- package/dist/esm/managers/session.manager.js +42 -83
- package/dist/esm/managers/state.manager.d.ts +1 -28
- package/dist/esm/managers/state.manager.js +4 -33
- package/dist/esm/managers/storage.manager.d.ts +6 -0
- package/dist/esm/managers/storage.manager.js +18 -1
- package/dist/esm/public-api.d.ts +1 -1
- package/dist/esm/test-bridge.d.ts +3 -2
- package/dist/esm/test-bridge.js +34 -7
- package/dist/esm/types/api.types.d.ts +24 -8
- package/dist/esm/types/api.types.js +24 -8
- package/dist/esm/types/event.types.d.ts +2 -4
- package/dist/esm/types/event.types.js +0 -1
- package/dist/esm/types/test-bridge.types.d.ts +2 -1
- package/dist/esm/utils/logging/debug-logger.utils.d.ts +1 -2
- package/dist/esm/utils/logging/debug-logger.utils.js +3 -4
- package/dist/esm/utils/validations/config-validations.utils.d.ts +1 -26
- package/dist/esm/utils/validations/config-validations.utils.js +5 -114
- package/dist/esm/utils/validations/event-validations.utils.d.ts +2 -2
- package/dist/esm/utils/validations/metadata-validations.utils.d.ts +3 -3
- package/dist/esm/utils/validations/metadata-validations.utils.js +41 -3
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { QUEUE_KEY, EVENT_EXPIRY_HOURS,
|
|
1
|
+
import { QUEUE_KEY, EVENT_EXPIRY_HOURS, MAX_RETRIES, RETRY_DELAY_MS, REQUEST_TIMEOUT_MS } from '../constants';
|
|
2
2
|
import { SpecialProjectId } from '../types';
|
|
3
3
|
import { debugLog } from '../utils';
|
|
4
4
|
import { StateManager } from './state.manager';
|
|
@@ -15,40 +15,34 @@ export class SenderManager extends StateManager {
|
|
|
15
15
|
const userId = this.get('userId') || 'anonymous';
|
|
16
16
|
return `${QUEUE_KEY(projectId)}:${userId}`;
|
|
17
17
|
}
|
|
18
|
-
/**
|
|
19
|
-
* Send events synchronously using sendBeacon or XHR fallback
|
|
20
|
-
* Used primarily for page unload scenarios
|
|
21
|
-
*/
|
|
22
18
|
sendEventsQueueSync(body) {
|
|
23
|
-
// For skip mode, simulate success immediately (sync version)
|
|
24
19
|
if (this.shouldSkipSend()) {
|
|
25
|
-
this.clearPersistedEvents();
|
|
26
20
|
this.resetRetryState();
|
|
27
21
|
return true;
|
|
28
22
|
}
|
|
23
|
+
const config = this.get('config');
|
|
24
|
+
if (config?.id === SpecialProjectId.Fail) {
|
|
25
|
+
debugLog.warn('SenderManager', 'Fail mode: simulating network failure (sync)', {
|
|
26
|
+
events: body.events.length,
|
|
27
|
+
});
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
29
30
|
const success = this.sendQueueSyncInternal(body);
|
|
30
31
|
if (success) {
|
|
31
|
-
this.clearPersistedEvents();
|
|
32
32
|
this.resetRetryState();
|
|
33
33
|
}
|
|
34
34
|
return success;
|
|
35
35
|
}
|
|
36
|
-
/**
|
|
37
|
-
* Send events asynchronously with persistence and retry logic
|
|
38
|
-
* Main method for sending events during normal operation
|
|
39
|
-
*/
|
|
40
36
|
async sendEventsQueue(body, callbacks) {
|
|
41
|
-
// First, try to persist events for recovery (even in skip mode for consistency)
|
|
42
37
|
const persisted = this.persistEvents(body);
|
|
43
38
|
if (!persisted && !this.shouldSkipSend()) {
|
|
44
39
|
debugLog.warn('SenderManager', 'Failed to persist events, attempting immediate send');
|
|
45
40
|
}
|
|
46
|
-
// Attempt to send events (or simulate in skip mode)
|
|
47
41
|
const success = await this.send(body);
|
|
48
42
|
if (success) {
|
|
49
43
|
this.clearPersistedEvents();
|
|
50
44
|
this.resetRetryState();
|
|
51
|
-
callbacks?.onSuccess?.(body.events.length);
|
|
45
|
+
callbacks?.onSuccess?.(body.events.length, body.events, body);
|
|
52
46
|
}
|
|
53
47
|
else {
|
|
54
48
|
this.scheduleRetry(body, callbacks);
|
|
@@ -56,10 +50,6 @@ export class SenderManager extends StateManager {
|
|
|
56
50
|
}
|
|
57
51
|
return success;
|
|
58
52
|
}
|
|
59
|
-
/**
|
|
60
|
-
* Recover and send previously persisted events
|
|
61
|
-
* Called during initialization to handle events from previous session
|
|
62
|
-
*/
|
|
63
53
|
async recoverPersistedEvents(callbacks) {
|
|
64
54
|
try {
|
|
65
55
|
const persistedData = this.getPersistedData();
|
|
@@ -72,7 +62,7 @@ export class SenderManager extends StateManager {
|
|
|
72
62
|
if (success) {
|
|
73
63
|
this.clearPersistedEvents();
|
|
74
64
|
this.resetRetryState();
|
|
75
|
-
callbacks?.onSuccess?.(persistedData.events.length);
|
|
65
|
+
callbacks?.onSuccess?.(persistedData.events.length, persistedData.events, body);
|
|
76
66
|
}
|
|
77
67
|
else {
|
|
78
68
|
this.scheduleRetry(body, callbacks);
|
|
@@ -81,35 +71,30 @@ export class SenderManager extends StateManager {
|
|
|
81
71
|
}
|
|
82
72
|
catch (error) {
|
|
83
73
|
debugLog.error('SenderManager', 'Failed to recover persisted events', { error });
|
|
84
|
-
this.clearPersistedEvents();
|
|
74
|
+
this.clearPersistedEvents();
|
|
85
75
|
}
|
|
86
76
|
}
|
|
87
|
-
/**
|
|
88
|
-
* Persist events for recovery in case of failure
|
|
89
|
-
*/
|
|
90
77
|
persistEventsForRecovery(body) {
|
|
91
78
|
return this.persistEvents(body);
|
|
92
79
|
}
|
|
93
|
-
/**
|
|
94
|
-
* Legacy method for backward compatibility
|
|
95
|
-
* @deprecated Use sendEventsQueue instead
|
|
96
|
-
*/
|
|
97
80
|
async sendEventsQueueAsync(body) {
|
|
98
81
|
return this.sendEventsQueue(body);
|
|
99
82
|
}
|
|
100
|
-
/**
|
|
101
|
-
* Stop the sender manager and clean up resources
|
|
102
|
-
*/
|
|
103
83
|
stop() {
|
|
104
84
|
this.clearRetryTimeout();
|
|
105
85
|
this.resetRetryState();
|
|
106
|
-
// Clear any persisted events on shutdown to prevent stale data
|
|
107
|
-
this.clearPersistedEvents();
|
|
108
86
|
}
|
|
109
87
|
async send(body) {
|
|
110
88
|
if (this.shouldSkipSend()) {
|
|
111
89
|
return this.simulateSuccessfulSend();
|
|
112
90
|
}
|
|
91
|
+
const config = this.get('config');
|
|
92
|
+
if (config?.id === SpecialProjectId.Fail) {
|
|
93
|
+
debugLog.warn('SenderManager', 'Fail mode: simulating network failure', {
|
|
94
|
+
events: body.events.length,
|
|
95
|
+
});
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
113
98
|
const { url, payload } = this.prepareRequest(body);
|
|
114
99
|
try {
|
|
115
100
|
const response = await this.sendWithTimeout(url, payload);
|
|
@@ -120,7 +105,7 @@ export class SenderManager extends StateManager {
|
|
|
120
105
|
debugLog.error('SenderManager', 'Send request failed', {
|
|
121
106
|
error: errorMessage,
|
|
122
107
|
events: body.events.length,
|
|
123
|
-
url: url.replace(/\/\/[^/]+/, '//[DOMAIN]'),
|
|
108
|
+
url: url.replace(/\/\/[^/]+/, '//[DOMAIN]'),
|
|
124
109
|
});
|
|
125
110
|
return false;
|
|
126
111
|
}
|
|
@@ -128,11 +113,13 @@ export class SenderManager extends StateManager {
|
|
|
128
113
|
async sendWithTimeout(url, payload) {
|
|
129
114
|
const controller = new AbortController();
|
|
130
115
|
const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
116
|
+
const config = this.get('config');
|
|
131
117
|
try {
|
|
132
118
|
const response = await fetch(url, {
|
|
133
119
|
method: 'POST',
|
|
134
120
|
headers: {
|
|
135
121
|
'Content-Type': 'application/json',
|
|
122
|
+
'X-TraceLog-Project': config?.id || 'unknown',
|
|
136
123
|
},
|
|
137
124
|
body: payload,
|
|
138
125
|
keepalive: true,
|
|
@@ -151,42 +138,33 @@ export class SenderManager extends StateManager {
|
|
|
151
138
|
sendQueueSyncInternal(body) {
|
|
152
139
|
const { url, payload } = this.prepareRequest(body);
|
|
153
140
|
const blob = new Blob([payload], { type: 'application/json' });
|
|
154
|
-
if (this.isSendBeaconAvailable()
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
sendSyncXHR(url, payload) {
|
|
160
|
-
const xhr = new XMLHttpRequest();
|
|
161
|
-
try {
|
|
162
|
-
xhr.open('POST', url, false);
|
|
163
|
-
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
164
|
-
xhr.withCredentials = true;
|
|
165
|
-
xhr.timeout = SYNC_XHR_TIMEOUT_MS;
|
|
166
|
-
xhr.send(payload);
|
|
167
|
-
const success = xhr.status >= 200 && xhr.status < 300;
|
|
168
|
-
if (!success) {
|
|
169
|
-
debugLog.warn('SenderManager', 'Sync XHR failed', {
|
|
170
|
-
status: xhr.status,
|
|
171
|
-
statusText: xhr.statusText || 'Unknown error',
|
|
172
|
-
});
|
|
141
|
+
if (this.isSendBeaconAvailable()) {
|
|
142
|
+
const success = navigator.sendBeacon(url, blob);
|
|
143
|
+
if (success) {
|
|
144
|
+
return true;
|
|
173
145
|
}
|
|
174
|
-
|
|
146
|
+
debugLog.warn('SenderManager', 'sendBeacon failed, persisting events for recovery');
|
|
175
147
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
debugLog.warn('SenderManager', 'Sync XHR error', {
|
|
179
|
-
error: errorMessage,
|
|
180
|
-
status: xhr.status || 'unknown',
|
|
181
|
-
});
|
|
182
|
-
return false;
|
|
148
|
+
else {
|
|
149
|
+
debugLog.warn('SenderManager', 'sendBeacon not available, persisting events for recovery');
|
|
183
150
|
}
|
|
151
|
+
this.persistEventsForRecovery(body);
|
|
152
|
+
return false;
|
|
184
153
|
}
|
|
185
154
|
prepareRequest(body) {
|
|
186
155
|
const url = `${this.get('apiUrl')}/collect`;
|
|
156
|
+
// Enrich payload with metadata for sendBeacon() fallback
|
|
157
|
+
// sendBeacon() doesn't send custom headers, so we include referer in payload
|
|
158
|
+
const enrichedBody = {
|
|
159
|
+
...body,
|
|
160
|
+
_metadata: {
|
|
161
|
+
referer: typeof window !== 'undefined' ? window.location.href : undefined,
|
|
162
|
+
timestamp: Date.now(),
|
|
163
|
+
},
|
|
164
|
+
};
|
|
187
165
|
return {
|
|
188
166
|
url,
|
|
189
|
-
payload: JSON.stringify(
|
|
167
|
+
payload: JSON.stringify(enrichedBody),
|
|
190
168
|
};
|
|
191
169
|
}
|
|
192
170
|
getPersistedData() {
|
|
@@ -199,7 +177,6 @@ export class SenderManager extends StateManager {
|
|
|
199
177
|
}
|
|
200
178
|
catch (error) {
|
|
201
179
|
debugLog.warn('SenderManager', 'Failed to parse persisted data', { error });
|
|
202
|
-
// Clean up corrupted data
|
|
203
180
|
this.clearPersistedEvents();
|
|
204
181
|
}
|
|
205
182
|
return null;
|
|
@@ -242,7 +219,8 @@ export class SenderManager extends StateManager {
|
|
|
242
219
|
}
|
|
243
220
|
clearPersistedEvents() {
|
|
244
221
|
try {
|
|
245
|
-
this.
|
|
222
|
+
const key = this.getQueueStorageKey();
|
|
223
|
+
this.storeManager.removeItem(key);
|
|
246
224
|
}
|
|
247
225
|
catch (error) {
|
|
248
226
|
debugLog.warn('SenderManager', 'Failed to clear persisted events', { error });
|
|
@@ -265,13 +243,10 @@ export class SenderManager extends StateManager {
|
|
|
265
243
|
return;
|
|
266
244
|
}
|
|
267
245
|
const retryDelay = RETRY_DELAY_MS * Math.pow(2, this.retryCount); // Exponential backoff
|
|
246
|
+
this.isRetrying = true;
|
|
268
247
|
this.retryTimeoutId = window.setTimeout(async () => {
|
|
269
248
|
this.retryTimeoutId = null;
|
|
270
|
-
if (this.isRetrying) {
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
249
|
this.retryCount++;
|
|
274
|
-
this.isRetrying = true;
|
|
275
250
|
try {
|
|
276
251
|
const success = await this.send(body);
|
|
277
252
|
if (success) {
|
|
@@ -303,15 +278,10 @@ export class SenderManager extends StateManager {
|
|
|
303
278
|
const { id } = config || {};
|
|
304
279
|
return id === SpecialProjectId.Skip;
|
|
305
280
|
}
|
|
306
|
-
/**
|
|
307
|
-
* Simulate a successful send operation for skip mode
|
|
308
|
-
* Provides realistic timing and behavior without making HTTP requests
|
|
309
|
-
*/
|
|
310
281
|
async simulateSuccessfulSend() {
|
|
311
|
-
// Simulate realistic network delay (100-500ms)
|
|
312
282
|
const delay = Math.random() * 400 + 100;
|
|
313
283
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
314
|
-
return true;
|
|
284
|
+
return true;
|
|
315
285
|
}
|
|
316
286
|
isSendBeaconAvailable() {
|
|
317
287
|
return typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function';
|
|
@@ -4,83 +4,36 @@ import { EventManager } from './event.manager';
|
|
|
4
4
|
export declare class SessionManager extends StateManager {
|
|
5
5
|
private readonly storageManager;
|
|
6
6
|
private readonly eventManager;
|
|
7
|
+
private readonly projectId;
|
|
7
8
|
private sessionTimeoutId;
|
|
8
9
|
private broadcastChannel;
|
|
9
10
|
private activityHandler;
|
|
10
11
|
private visibilityChangeHandler;
|
|
11
12
|
private beforeUnloadHandler;
|
|
12
13
|
private isTracking;
|
|
13
|
-
constructor(storageManager: StorageManager, eventManager: EventManager);
|
|
14
|
-
/**
|
|
15
|
-
* Initialize cross-tab synchronization
|
|
16
|
-
*/
|
|
14
|
+
constructor(storageManager: StorageManager, eventManager: EventManager, projectId: string);
|
|
17
15
|
private initCrossTabSync;
|
|
18
|
-
/**
|
|
19
|
-
* Share session with other tabs
|
|
20
|
-
*/
|
|
21
16
|
private shareSession;
|
|
22
17
|
private broadcastSessionEnd;
|
|
23
|
-
/**
|
|
24
|
-
* Cleanup cross-tab sync
|
|
25
|
-
*/
|
|
26
18
|
private cleanupCrossTabSync;
|
|
27
|
-
/**
|
|
28
|
-
* Recover session from localStorage if it exists and hasn't expired
|
|
29
|
-
*/
|
|
30
19
|
private recoverSession;
|
|
31
|
-
/**
|
|
32
|
-
* Persist session data to localStorage
|
|
33
|
-
*/
|
|
34
20
|
private persistSession;
|
|
35
21
|
private clearStoredSession;
|
|
36
22
|
private loadStoredSession;
|
|
37
23
|
private saveStoredSession;
|
|
38
24
|
private getSessionStorageKey;
|
|
39
25
|
private getProjectId;
|
|
40
|
-
/**
|
|
41
|
-
* Start session tracking
|
|
42
|
-
*/
|
|
43
26
|
startTracking(): Promise<void>;
|
|
44
|
-
/**
|
|
45
|
-
* Generate unique session ID
|
|
46
|
-
*/
|
|
47
27
|
private generateSessionId;
|
|
48
|
-
/**
|
|
49
|
-
* Setup session timeout
|
|
50
|
-
*/
|
|
51
28
|
private setupSessionTimeout;
|
|
52
|
-
/**
|
|
53
|
-
* Reset session timeout and update activity
|
|
54
|
-
*/
|
|
55
29
|
private resetSessionTimeout;
|
|
56
|
-
/**
|
|
57
|
-
* Clear session timeout
|
|
58
|
-
*/
|
|
59
30
|
private clearSessionTimeout;
|
|
60
|
-
/**
|
|
61
|
-
* Setup activity listeners to track user engagement
|
|
62
|
-
*/
|
|
63
31
|
private setupActivityListeners;
|
|
64
|
-
/**
|
|
65
|
-
* Clean up activity listeners
|
|
66
|
-
*/
|
|
67
32
|
private cleanupActivityListeners;
|
|
68
|
-
/**
|
|
69
|
-
* Setup page lifecycle listeners (visibility and unload)
|
|
70
|
-
*/
|
|
71
33
|
private setupLifecycleListeners;
|
|
72
34
|
private cleanupLifecycleListeners;
|
|
73
|
-
/**
|
|
74
|
-
* End current session
|
|
75
|
-
*/
|
|
76
35
|
private endSession;
|
|
77
36
|
private resetSessionState;
|
|
78
|
-
/**
|
|
79
|
-
* Stop session tracking
|
|
80
|
-
*/
|
|
81
37
|
stopTracking(): Promise<void>;
|
|
82
|
-
/**
|
|
83
|
-
* Clean up all resources
|
|
84
|
-
*/
|
|
85
38
|
destroy(): void;
|
|
86
39
|
}
|
|
@@ -3,7 +3,7 @@ import { EventType } from '../types';
|
|
|
3
3
|
import { debugLog } from '../utils/logging';
|
|
4
4
|
import { StateManager } from './state.manager';
|
|
5
5
|
export class SessionManager extends StateManager {
|
|
6
|
-
constructor(storageManager, eventManager) {
|
|
6
|
+
constructor(storageManager, eventManager, projectId) {
|
|
7
7
|
super();
|
|
8
8
|
this.sessionTimeoutId = null;
|
|
9
9
|
this.broadcastChannel = null;
|
|
@@ -13,10 +13,8 @@ export class SessionManager extends StateManager {
|
|
|
13
13
|
this.isTracking = false;
|
|
14
14
|
this.storageManager = storageManager;
|
|
15
15
|
this.eventManager = eventManager;
|
|
16
|
+
this.projectId = projectId;
|
|
16
17
|
}
|
|
17
|
-
/**
|
|
18
|
-
* Initialize cross-tab synchronization
|
|
19
|
-
*/
|
|
20
18
|
initCrossTabSync() {
|
|
21
19
|
if (typeof BroadcastChannel === 'undefined') {
|
|
22
20
|
debugLog.warn('SessionManager', 'BroadcastChannel not supported');
|
|
@@ -45,41 +43,38 @@ export class SessionManager extends StateManager {
|
|
|
45
43
|
}
|
|
46
44
|
};
|
|
47
45
|
}
|
|
48
|
-
/**
|
|
49
|
-
* Share session with other tabs
|
|
50
|
-
*/
|
|
51
46
|
shareSession(sessionId) {
|
|
52
|
-
this.broadcastChannel
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
47
|
+
if (this.broadcastChannel && typeof this.broadcastChannel.postMessage === 'function') {
|
|
48
|
+
this.broadcastChannel.postMessage({
|
|
49
|
+
action: 'session_start',
|
|
50
|
+
projectId: this.getProjectId(),
|
|
51
|
+
sessionId,
|
|
52
|
+
timestamp: Date.now(),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
58
55
|
}
|
|
59
56
|
broadcastSessionEnd(sessionId, reason) {
|
|
60
57
|
if (!sessionId) {
|
|
61
58
|
return;
|
|
62
59
|
}
|
|
63
|
-
this.broadcastChannel
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
60
|
+
if (this.broadcastChannel && typeof this.broadcastChannel.postMessage === 'function') {
|
|
61
|
+
this.broadcastChannel.postMessage({
|
|
62
|
+
action: 'session_end',
|
|
63
|
+
projectId: this.getProjectId(),
|
|
64
|
+
sessionId,
|
|
65
|
+
reason,
|
|
66
|
+
timestamp: Date.now(),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
70
69
|
}
|
|
71
|
-
/**
|
|
72
|
-
* Cleanup cross-tab sync
|
|
73
|
-
*/
|
|
74
70
|
cleanupCrossTabSync() {
|
|
75
71
|
if (this.broadcastChannel) {
|
|
76
|
-
this.broadcastChannel.close
|
|
72
|
+
if (typeof this.broadcastChannel.close === 'function') {
|
|
73
|
+
this.broadcastChannel.close();
|
|
74
|
+
}
|
|
77
75
|
this.broadcastChannel = null;
|
|
78
76
|
}
|
|
79
77
|
}
|
|
80
|
-
/**
|
|
81
|
-
* Recover session from localStorage if it exists and hasn't expired
|
|
82
|
-
*/
|
|
83
78
|
recoverSession() {
|
|
84
79
|
const storedSession = this.loadStoredSession();
|
|
85
80
|
if (!storedSession) {
|
|
@@ -94,9 +89,6 @@ export class SessionManager extends StateManager {
|
|
|
94
89
|
debugLog.info('SessionManager', 'Session recovered from storage', { sessionId: storedSession.id });
|
|
95
90
|
return storedSession.id;
|
|
96
91
|
}
|
|
97
|
-
/**
|
|
98
|
-
* Persist session data to localStorage
|
|
99
|
-
*/
|
|
100
92
|
persistSession(sessionId, lastActivity = Date.now()) {
|
|
101
93
|
this.saveStoredSession({
|
|
102
94
|
id: sessionId,
|
|
@@ -133,11 +125,8 @@ export class SessionManager extends StateManager {
|
|
|
133
125
|
return SESSION_STORAGE_KEY(this.getProjectId());
|
|
134
126
|
}
|
|
135
127
|
getProjectId() {
|
|
136
|
-
return this.
|
|
128
|
+
return this.projectId;
|
|
137
129
|
}
|
|
138
|
-
/**
|
|
139
|
-
* Start session tracking
|
|
140
|
-
*/
|
|
141
130
|
async startTracking() {
|
|
142
131
|
if (this.isTracking) {
|
|
143
132
|
debugLog.warn('SessionManager', 'Session tracking already active');
|
|
@@ -150,12 +139,11 @@ export class SessionManager extends StateManager {
|
|
|
150
139
|
try {
|
|
151
140
|
this.set('sessionId', sessionId);
|
|
152
141
|
this.persistSession(sessionId);
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
// Initialize components
|
|
142
|
+
if (!isRecovered) {
|
|
143
|
+
this.eventManager.track({
|
|
144
|
+
type: EventType.SESSION_START,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
159
147
|
this.initCrossTabSync();
|
|
160
148
|
this.shareSession(sessionId);
|
|
161
149
|
this.setupSessionTimeout();
|
|
@@ -173,15 +161,9 @@ export class SessionManager extends StateManager {
|
|
|
173
161
|
throw error;
|
|
174
162
|
}
|
|
175
163
|
}
|
|
176
|
-
/**
|
|
177
|
-
* Generate unique session ID
|
|
178
|
-
*/
|
|
179
164
|
generateSessionId() {
|
|
180
165
|
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
181
166
|
}
|
|
182
|
-
/**
|
|
183
|
-
* Setup session timeout
|
|
184
|
-
*/
|
|
185
167
|
setupSessionTimeout() {
|
|
186
168
|
this.clearSessionTimeout();
|
|
187
169
|
const sessionTimeout = this.get('config')?.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT;
|
|
@@ -189,9 +171,6 @@ export class SessionManager extends StateManager {
|
|
|
189
171
|
this.endSession('inactivity');
|
|
190
172
|
}, sessionTimeout);
|
|
191
173
|
}
|
|
192
|
-
/**
|
|
193
|
-
* Reset session timeout and update activity
|
|
194
|
-
*/
|
|
195
174
|
resetSessionTimeout() {
|
|
196
175
|
this.setupSessionTimeout();
|
|
197
176
|
const sessionId = this.get('sessionId');
|
|
@@ -199,27 +178,18 @@ export class SessionManager extends StateManager {
|
|
|
199
178
|
this.persistSession(sessionId);
|
|
200
179
|
}
|
|
201
180
|
}
|
|
202
|
-
/**
|
|
203
|
-
* Clear session timeout
|
|
204
|
-
*/
|
|
205
181
|
clearSessionTimeout() {
|
|
206
182
|
if (this.sessionTimeoutId) {
|
|
207
183
|
clearTimeout(this.sessionTimeoutId);
|
|
208
184
|
this.sessionTimeoutId = null;
|
|
209
185
|
}
|
|
210
186
|
}
|
|
211
|
-
/**
|
|
212
|
-
* Setup activity listeners to track user engagement
|
|
213
|
-
*/
|
|
214
187
|
setupActivityListeners() {
|
|
215
188
|
this.activityHandler = () => this.resetSessionTimeout();
|
|
216
189
|
document.addEventListener('click', this.activityHandler, { passive: true });
|
|
217
190
|
document.addEventListener('keydown', this.activityHandler, { passive: true });
|
|
218
191
|
document.addEventListener('scroll', this.activityHandler, { passive: true });
|
|
219
192
|
}
|
|
220
|
-
/**
|
|
221
|
-
* Clean up activity listeners
|
|
222
|
-
*/
|
|
223
193
|
cleanupActivityListeners() {
|
|
224
194
|
if (this.activityHandler) {
|
|
225
195
|
document.removeEventListener('click', this.activityHandler);
|
|
@@ -228,9 +198,6 @@ export class SessionManager extends StateManager {
|
|
|
228
198
|
this.activityHandler = null;
|
|
229
199
|
}
|
|
230
200
|
}
|
|
231
|
-
/**
|
|
232
|
-
* Setup page lifecycle listeners (visibility and unload)
|
|
233
|
-
*/
|
|
234
201
|
setupLifecycleListeners() {
|
|
235
202
|
if (this.visibilityChangeHandler || this.beforeUnloadHandler) {
|
|
236
203
|
return;
|
|
@@ -249,9 +216,7 @@ export class SessionManager extends StateManager {
|
|
|
249
216
|
this.beforeUnloadHandler = () => {
|
|
250
217
|
this.endSession('page_unload');
|
|
251
218
|
};
|
|
252
|
-
// Handle tab visibility changes
|
|
253
219
|
document.addEventListener('visibilitychange', this.visibilityChangeHandler);
|
|
254
|
-
// Handle page unload
|
|
255
220
|
window.addEventListener('beforeunload', this.beforeUnloadHandler);
|
|
256
221
|
}
|
|
257
222
|
cleanupLifecycleListeners() {
|
|
@@ -264,14 +229,11 @@ export class SessionManager extends StateManager {
|
|
|
264
229
|
this.beforeUnloadHandler = null;
|
|
265
230
|
}
|
|
266
231
|
}
|
|
267
|
-
|
|
268
|
-
* End current session
|
|
269
|
-
*/
|
|
270
|
-
endSession(reason) {
|
|
232
|
+
async endSession(reason) {
|
|
271
233
|
const sessionId = this.get('sessionId');
|
|
272
234
|
if (!sessionId) {
|
|
273
235
|
debugLog.warn('SessionManager', 'endSession called without active session', { reason });
|
|
274
|
-
this.resetSessionState();
|
|
236
|
+
this.resetSessionState(reason);
|
|
275
237
|
return;
|
|
276
238
|
}
|
|
277
239
|
debugLog.info('SessionManager', 'Ending session', { sessionId, reason });
|
|
@@ -281,42 +243,39 @@ export class SessionManager extends StateManager {
|
|
|
281
243
|
});
|
|
282
244
|
const finalize = () => {
|
|
283
245
|
this.broadcastSessionEnd(sessionId, reason);
|
|
284
|
-
this.resetSessionState();
|
|
246
|
+
this.resetSessionState(reason);
|
|
285
247
|
};
|
|
286
248
|
const flushResult = this.eventManager.flushImmediatelySync();
|
|
287
249
|
if (flushResult) {
|
|
288
250
|
finalize();
|
|
289
251
|
return;
|
|
290
252
|
}
|
|
291
|
-
|
|
292
|
-
.flushImmediately()
|
|
293
|
-
|
|
294
|
-
|
|
253
|
+
try {
|
|
254
|
+
await this.eventManager.flushImmediately();
|
|
255
|
+
finalize();
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
295
258
|
debugLog.warn('SessionManager', 'Async flush failed during session end', {
|
|
296
259
|
error: error instanceof Error ? error.message : 'Unknown error',
|
|
297
260
|
});
|
|
298
261
|
finalize();
|
|
299
|
-
}
|
|
262
|
+
}
|
|
300
263
|
}
|
|
301
|
-
resetSessionState() {
|
|
264
|
+
resetSessionState(reason) {
|
|
302
265
|
this.clearSessionTimeout();
|
|
303
266
|
this.cleanupActivityListeners();
|
|
304
267
|
this.cleanupLifecycleListeners();
|
|
305
268
|
this.cleanupCrossTabSync();
|
|
306
|
-
|
|
269
|
+
if (reason !== 'page_unload') {
|
|
270
|
+
this.clearStoredSession();
|
|
271
|
+
}
|
|
307
272
|
this.set('sessionId', null);
|
|
308
273
|
this.set('hasStartSession', false);
|
|
309
274
|
this.isTracking = false;
|
|
310
275
|
}
|
|
311
|
-
/**
|
|
312
|
-
* Stop session tracking
|
|
313
|
-
*/
|
|
314
276
|
async stopTracking() {
|
|
315
|
-
this.endSession('manual_stop');
|
|
277
|
+
await this.endSession('manual_stop');
|
|
316
278
|
}
|
|
317
|
-
/**
|
|
318
|
-
* Clean up all resources
|
|
319
|
-
*/
|
|
320
279
|
destroy() {
|
|
321
280
|
this.clearSessionTimeout();
|
|
322
281
|
this.cleanupActivityListeners();
|
|
@@ -1,38 +1,11 @@
|
|
|
1
1
|
import { State } from '../types';
|
|
2
|
-
|
|
3
|
-
* Resets the global state to its initial empty state.
|
|
4
|
-
* Used primarily for testing and cleanup scenarios.
|
|
5
|
-
*/
|
|
2
|
+
export declare function getGlobalState(): Readonly<State>;
|
|
6
3
|
export declare function resetGlobalState(): void;
|
|
7
|
-
/**
|
|
8
|
-
* Abstract base class providing state management capabilities to TraceLog components.
|
|
9
|
-
*
|
|
10
|
-
* All managers and handlers extend this class to access and modify the shared global state.
|
|
11
|
-
* State operations are synchronous and thread-safe within the single-threaded browser environment.
|
|
12
|
-
*/
|
|
13
4
|
export declare abstract class StateManager {
|
|
14
|
-
/**
|
|
15
|
-
* Gets a value from the global state
|
|
16
|
-
*/
|
|
17
5
|
protected get<T extends keyof State>(key: T): State[T];
|
|
18
|
-
/**
|
|
19
|
-
* Sets a value in the global state
|
|
20
|
-
*/
|
|
21
6
|
protected set<T extends keyof State>(key: T, value: State[T]): void;
|
|
22
|
-
/**
|
|
23
|
-
* Gets the entire state object (for debugging purposes)
|
|
24
|
-
*/
|
|
25
7
|
protected getState(): Readonly<State>;
|
|
26
|
-
/**
|
|
27
|
-
* Checks if a state key is considered critical for logging
|
|
28
|
-
*/
|
|
29
8
|
private isCriticalStateKey;
|
|
30
|
-
/**
|
|
31
|
-
* Determines if a state change should be logged
|
|
32
|
-
*/
|
|
33
9
|
private shouldLog;
|
|
34
|
-
/**
|
|
35
|
-
* Formats values for logging (avoiding large object dumps)
|
|
36
|
-
*/
|
|
37
10
|
private formatLogValue;
|
|
38
11
|
}
|