user-analytics-tracker 1.2.0 → 2.0.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/CHANGELOG.md +69 -0
- package/README.md +290 -7
- package/dist/index.cjs.js +926 -23
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.cts +215 -9
- package/dist/index.d.ts +215 -9
- package/dist/index.esm.js +920 -24
- package/dist/index.esm.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs.js
CHANGED
|
@@ -1017,15 +1017,91 @@ function trackPageVisit() {
|
|
|
1017
1017
|
localStorage.setItem('analytics:pageVisits', newCount.toString());
|
|
1018
1018
|
return newCount;
|
|
1019
1019
|
}
|
|
1020
|
+
const SESSION_STORAGE_KEY = 'analytics:session';
|
|
1021
|
+
const DEFAULT_SESSION_TIMEOUT = 30 * 60 * 1000; // 30 minutes
|
|
1022
|
+
/**
|
|
1023
|
+
* Get or create a session
|
|
1024
|
+
*/
|
|
1025
|
+
function getOrCreateSession(timeout = DEFAULT_SESSION_TIMEOUT) {
|
|
1026
|
+
if (typeof window === 'undefined') {
|
|
1027
|
+
return {
|
|
1028
|
+
sessionId: `server-${Date.now()}`,
|
|
1029
|
+
startTime: Date.now(),
|
|
1030
|
+
lastActivity: Date.now(),
|
|
1031
|
+
pageViews: 1,
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
const stored = loadJSON(SESSION_STORAGE_KEY);
|
|
1035
|
+
const now = Date.now();
|
|
1036
|
+
// Check if session expired
|
|
1037
|
+
if (stored && now - stored.lastActivity < timeout) {
|
|
1038
|
+
// Update last activity
|
|
1039
|
+
const updated = {
|
|
1040
|
+
...stored,
|
|
1041
|
+
lastActivity: now,
|
|
1042
|
+
pageViews: stored.pageViews + 1,
|
|
1043
|
+
};
|
|
1044
|
+
saveJSON(SESSION_STORAGE_KEY, updated);
|
|
1045
|
+
return updated;
|
|
1046
|
+
}
|
|
1047
|
+
// Create new session
|
|
1048
|
+
const sessionId = `session-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
|
1049
|
+
const newSession = {
|
|
1050
|
+
sessionId,
|
|
1051
|
+
startTime: now,
|
|
1052
|
+
lastActivity: now,
|
|
1053
|
+
pageViews: 1,
|
|
1054
|
+
};
|
|
1055
|
+
saveJSON(SESSION_STORAGE_KEY, newSession);
|
|
1056
|
+
return newSession;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* Update session activity
|
|
1060
|
+
*/
|
|
1061
|
+
function updateSessionActivity() {
|
|
1062
|
+
if (typeof window === 'undefined')
|
|
1063
|
+
return;
|
|
1064
|
+
const stored = loadJSON(SESSION_STORAGE_KEY);
|
|
1065
|
+
if (stored) {
|
|
1066
|
+
const updated = {
|
|
1067
|
+
...stored,
|
|
1068
|
+
lastActivity: Date.now(),
|
|
1069
|
+
};
|
|
1070
|
+
saveJSON(SESSION_STORAGE_KEY, updated);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
/**
|
|
1074
|
+
* Get current session info
|
|
1075
|
+
*/
|
|
1076
|
+
function getSession() {
|
|
1077
|
+
return loadJSON(SESSION_STORAGE_KEY);
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Clear session
|
|
1081
|
+
*/
|
|
1082
|
+
function clearSession() {
|
|
1083
|
+
if (typeof window === 'undefined')
|
|
1084
|
+
return;
|
|
1085
|
+
try {
|
|
1086
|
+
localStorage.removeItem(SESSION_STORAGE_KEY);
|
|
1087
|
+
}
|
|
1088
|
+
catch {
|
|
1089
|
+
// Silently fail
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1020
1092
|
|
|
1021
1093
|
var storage = /*#__PURE__*/Object.freeze({
|
|
1022
1094
|
__proto__: null,
|
|
1095
|
+
clearSession: clearSession,
|
|
1096
|
+
getOrCreateSession: getOrCreateSession,
|
|
1023
1097
|
getOrCreateUserId: getOrCreateUserId,
|
|
1098
|
+
getSession: getSession,
|
|
1024
1099
|
loadJSON: loadJSON,
|
|
1025
1100
|
loadSessionJSON: loadSessionJSON,
|
|
1026
1101
|
saveJSON: saveJSON,
|
|
1027
1102
|
saveSessionJSON: saveSessionJSON,
|
|
1028
|
-
trackPageVisit: trackPageVisit
|
|
1103
|
+
trackPageVisit: trackPageVisit,
|
|
1104
|
+
updateSessionActivity: updateSessionActivity
|
|
1029
1105
|
});
|
|
1030
1106
|
|
|
1031
1107
|
const UTM_KEYS = [
|
|
@@ -1174,36 +1250,584 @@ var attributionDetector = /*#__PURE__*/Object.freeze({
|
|
|
1174
1250
|
AttributionDetector: AttributionDetector
|
|
1175
1251
|
});
|
|
1176
1252
|
|
|
1253
|
+
/**
|
|
1254
|
+
* Logger utility for analytics tracker
|
|
1255
|
+
* Provides configurable logging levels for development and production
|
|
1256
|
+
*/
|
|
1257
|
+
class Logger {
|
|
1258
|
+
constructor() {
|
|
1259
|
+
this.level = 'warn';
|
|
1260
|
+
this.isDevelopment =
|
|
1261
|
+
typeof process !== 'undefined' &&
|
|
1262
|
+
process.env?.NODE_ENV === 'development';
|
|
1263
|
+
// Default to 'info' in development, 'warn' in production
|
|
1264
|
+
if (this.isDevelopment && this.level === 'warn') {
|
|
1265
|
+
this.level = 'info';
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
setLevel(level) {
|
|
1269
|
+
this.level = level;
|
|
1270
|
+
}
|
|
1271
|
+
getLevel() {
|
|
1272
|
+
return this.level;
|
|
1273
|
+
}
|
|
1274
|
+
shouldLog(level) {
|
|
1275
|
+
const levels = ['silent', 'error', 'warn', 'info', 'debug'];
|
|
1276
|
+
const currentIndex = levels.indexOf(this.level);
|
|
1277
|
+
const messageIndex = levels.indexOf(level);
|
|
1278
|
+
return messageIndex >= 0 && messageIndex <= currentIndex;
|
|
1279
|
+
}
|
|
1280
|
+
error(message, ...args) {
|
|
1281
|
+
if (this.shouldLog('error')) {
|
|
1282
|
+
console.error(`[Analytics] ${message}`, ...args);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
warn(message, ...args) {
|
|
1286
|
+
if (this.shouldLog('warn')) {
|
|
1287
|
+
console.warn(`[Analytics] ${message}`, ...args);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
info(message, ...args) {
|
|
1291
|
+
if (this.shouldLog('info')) {
|
|
1292
|
+
console.log(`[Analytics] ${message}`, ...args);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
debug(message, ...args) {
|
|
1296
|
+
if (this.shouldLog('debug')) {
|
|
1297
|
+
console.log(`[Analytics] [DEBUG] ${message}`, ...args);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
const logger = new Logger();
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Queue Manager for Analytics Events
|
|
1305
|
+
* Handles batching, persistence, and offline support
|
|
1306
|
+
*/
|
|
1307
|
+
class QueueManager {
|
|
1308
|
+
constructor(config) {
|
|
1309
|
+
this.queue = [];
|
|
1310
|
+
this.flushTimer = null;
|
|
1311
|
+
this.isFlushing = false;
|
|
1312
|
+
this.flushCallback = null;
|
|
1313
|
+
this.config = config;
|
|
1314
|
+
this.loadFromStorage();
|
|
1315
|
+
this.startAutoFlush();
|
|
1316
|
+
this.setupPageUnloadHandler();
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* Set the callback function to flush events
|
|
1320
|
+
*/
|
|
1321
|
+
setFlushCallback(callback) {
|
|
1322
|
+
this.flushCallback = callback;
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Add an event to the queue
|
|
1326
|
+
*/
|
|
1327
|
+
enqueue(event) {
|
|
1328
|
+
if (this.queue.length >= this.config.maxQueueSize) {
|
|
1329
|
+
logger.warn(`Queue full (${this.config.maxQueueSize} events). Dropping oldest event.`);
|
|
1330
|
+
this.queue.shift(); // Remove oldest event
|
|
1331
|
+
}
|
|
1332
|
+
const queuedEvent = {
|
|
1333
|
+
event,
|
|
1334
|
+
retries: 0,
|
|
1335
|
+
timestamp: Date.now(),
|
|
1336
|
+
id: `${Date.now()}-${Math.random().toString(36).substring(7)}`,
|
|
1337
|
+
};
|
|
1338
|
+
this.queue.push(queuedEvent);
|
|
1339
|
+
this.saveToStorage();
|
|
1340
|
+
logger.debug(`Event queued. Queue size: ${this.queue.length}`);
|
|
1341
|
+
// Auto-flush if batch size reached
|
|
1342
|
+
if (this.queue.length >= this.config.batchSize) {
|
|
1343
|
+
this.flush();
|
|
1344
|
+
}
|
|
1345
|
+
return true;
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Flush events from the queue
|
|
1349
|
+
*/
|
|
1350
|
+
async flush() {
|
|
1351
|
+
if (this.isFlushing || this.queue.length === 0 || !this.flushCallback) {
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
this.isFlushing = true;
|
|
1355
|
+
const eventsToFlush = this.queue.splice(0, this.config.batchSize);
|
|
1356
|
+
if (eventsToFlush.length === 0) {
|
|
1357
|
+
this.isFlushing = false;
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
try {
|
|
1361
|
+
const events = eventsToFlush.map((q) => q.event);
|
|
1362
|
+
await this.flushCallback(events);
|
|
1363
|
+
// Remove successfully flushed events from storage
|
|
1364
|
+
this.saveToStorage();
|
|
1365
|
+
logger.debug(`Flushed ${events.length} events. Queue size: ${this.queue.length}`);
|
|
1366
|
+
}
|
|
1367
|
+
catch (error) {
|
|
1368
|
+
// On failure, put events back in queue (they'll be retried)
|
|
1369
|
+
this.queue.unshift(...eventsToFlush);
|
|
1370
|
+
this.saveToStorage();
|
|
1371
|
+
logger.warn(`Failed to flush events. Re-queued ${eventsToFlush.length} events.`, error);
|
|
1372
|
+
throw error;
|
|
1373
|
+
}
|
|
1374
|
+
finally {
|
|
1375
|
+
this.isFlushing = false;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Get current queue size
|
|
1380
|
+
*/
|
|
1381
|
+
getQueueSize() {
|
|
1382
|
+
return this.queue.length;
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Get all queued events (for debugging)
|
|
1386
|
+
*/
|
|
1387
|
+
getQueue() {
|
|
1388
|
+
return [...this.queue];
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Clear the queue
|
|
1392
|
+
*/
|
|
1393
|
+
clear() {
|
|
1394
|
+
this.queue = [];
|
|
1395
|
+
this.saveToStorage();
|
|
1396
|
+
logger.debug('Queue cleared');
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Load queue from localStorage
|
|
1400
|
+
*/
|
|
1401
|
+
loadFromStorage() {
|
|
1402
|
+
if (typeof window === 'undefined')
|
|
1403
|
+
return;
|
|
1404
|
+
try {
|
|
1405
|
+
const stored = loadJSON(this.config.storageKey);
|
|
1406
|
+
if (stored && Array.isArray(stored)) {
|
|
1407
|
+
// Only load events from last 24 hours to prevent stale data
|
|
1408
|
+
const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000;
|
|
1409
|
+
this.queue = stored.filter((q) => q.timestamp > oneDayAgo);
|
|
1410
|
+
if (this.queue.length > 0) {
|
|
1411
|
+
logger.debug(`Loaded ${this.queue.length} events from storage`);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
catch (error) {
|
|
1416
|
+
logger.warn('Failed to load queue from storage', error);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
/**
|
|
1420
|
+
* Save queue to localStorage
|
|
1421
|
+
*/
|
|
1422
|
+
saveToStorage() {
|
|
1423
|
+
if (typeof window === 'undefined')
|
|
1424
|
+
return;
|
|
1425
|
+
try {
|
|
1426
|
+
saveJSON(this.config.storageKey, this.queue);
|
|
1427
|
+
}
|
|
1428
|
+
catch (error) {
|
|
1429
|
+
logger.warn('Failed to save queue to storage', error);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Start auto-flush timer
|
|
1434
|
+
*/
|
|
1435
|
+
startAutoFlush() {
|
|
1436
|
+
if (typeof window === 'undefined')
|
|
1437
|
+
return;
|
|
1438
|
+
if (this.flushTimer) {
|
|
1439
|
+
clearInterval(this.flushTimer);
|
|
1440
|
+
}
|
|
1441
|
+
this.flushTimer = setInterval(() => {
|
|
1442
|
+
if (this.queue.length > 0) {
|
|
1443
|
+
this.flush().catch((error) => {
|
|
1444
|
+
logger.warn('Auto-flush failed', error);
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
}, this.config.batchInterval);
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Setup page unload handler to flush events
|
|
1451
|
+
*/
|
|
1452
|
+
setupPageUnloadHandler() {
|
|
1453
|
+
if (typeof window === 'undefined')
|
|
1454
|
+
return;
|
|
1455
|
+
// Use sendBeacon for reliable delivery on page unload
|
|
1456
|
+
window.addEventListener('beforeunload', () => {
|
|
1457
|
+
if (this.queue.length > 0 && this.flushCallback) {
|
|
1458
|
+
const events = this.queue.splice(0, this.config.batchSize).map((q) => q.event);
|
|
1459
|
+
// Try to send via sendBeacon (more reliable on unload)
|
|
1460
|
+
if (navigator.sendBeacon) {
|
|
1461
|
+
try {
|
|
1462
|
+
const blob = new Blob([JSON.stringify(events)], {
|
|
1463
|
+
type: 'application/json',
|
|
1464
|
+
});
|
|
1465
|
+
navigator.sendBeacon(this.getEndpointFromCallback(), blob);
|
|
1466
|
+
this.saveToStorage();
|
|
1467
|
+
}
|
|
1468
|
+
catch (error) {
|
|
1469
|
+
// Fallback: put events back in queue
|
|
1470
|
+
this.queue.unshift(...events.map((e) => ({
|
|
1471
|
+
event: e,
|
|
1472
|
+
retries: 0,
|
|
1473
|
+
timestamp: Date.now(),
|
|
1474
|
+
id: `${Date.now()}-${Math.random().toString(36).substring(7)}`,
|
|
1475
|
+
})));
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
// Also use visibilitychange for better mobile support
|
|
1481
|
+
document.addEventListener('visibilitychange', () => {
|
|
1482
|
+
if (document.visibilityState === 'hidden' && this.queue.length > 0) {
|
|
1483
|
+
this.flush().catch(() => {
|
|
1484
|
+
// Silently fail on visibility change
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Get endpoint for sendBeacon
|
|
1491
|
+
*/
|
|
1492
|
+
getEndpointFromCallback() {
|
|
1493
|
+
// Try to get from window or return default
|
|
1494
|
+
if (typeof window !== 'undefined' && window.__analyticsEndpoint) {
|
|
1495
|
+
return window.__analyticsEndpoint;
|
|
1496
|
+
}
|
|
1497
|
+
return '/api/analytics';
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Update storage key (for configuration changes)
|
|
1501
|
+
*/
|
|
1502
|
+
updateConfig(config) {
|
|
1503
|
+
this.config = { ...this.config, ...config };
|
|
1504
|
+
if (config.batchInterval) {
|
|
1505
|
+
this.startAutoFlush();
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Cleanup resources
|
|
1510
|
+
*/
|
|
1511
|
+
destroy() {
|
|
1512
|
+
if (this.flushTimer) {
|
|
1513
|
+
clearInterval(this.flushTimer);
|
|
1514
|
+
this.flushTimer = null;
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* Plugin Manager for Analytics Tracker
|
|
1521
|
+
* Manages plugin registration and execution
|
|
1522
|
+
*/
|
|
1523
|
+
class PluginManager {
|
|
1524
|
+
constructor() {
|
|
1525
|
+
this.plugins = [];
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* Register a plugin
|
|
1529
|
+
*/
|
|
1530
|
+
register(plugin) {
|
|
1531
|
+
if (!plugin.name) {
|
|
1532
|
+
throw new Error('Plugin must have a name');
|
|
1533
|
+
}
|
|
1534
|
+
this.plugins.push(plugin);
|
|
1535
|
+
logger.debug(`Plugin registered: ${plugin.name}`);
|
|
1536
|
+
}
|
|
1537
|
+
/**
|
|
1538
|
+
* Unregister a plugin
|
|
1539
|
+
*/
|
|
1540
|
+
unregister(pluginName) {
|
|
1541
|
+
const index = this.plugins.findIndex((p) => p.name === pluginName);
|
|
1542
|
+
if (index !== -1) {
|
|
1543
|
+
this.plugins.splice(index, 1);
|
|
1544
|
+
logger.debug(`Plugin unregistered: ${pluginName}`);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
/**
|
|
1548
|
+
* Get all registered plugins
|
|
1549
|
+
*/
|
|
1550
|
+
getPlugins() {
|
|
1551
|
+
return [...this.plugins];
|
|
1552
|
+
}
|
|
1553
|
+
/**
|
|
1554
|
+
* Execute beforeSend hooks for all plugins
|
|
1555
|
+
* Returns the transformed event, or null if filtered out
|
|
1556
|
+
*/
|
|
1557
|
+
async executeBeforeSend(event) {
|
|
1558
|
+
let transformedEvent = event;
|
|
1559
|
+
for (const plugin of this.plugins) {
|
|
1560
|
+
if (!plugin.beforeSend)
|
|
1561
|
+
continue;
|
|
1562
|
+
try {
|
|
1563
|
+
const result = await plugin.beforeSend(transformedEvent);
|
|
1564
|
+
// If plugin returns null/undefined, filter out the event
|
|
1565
|
+
if (result === null || result === undefined) {
|
|
1566
|
+
logger.debug(`Event filtered out by plugin: ${plugin.name}`);
|
|
1567
|
+
return null;
|
|
1568
|
+
}
|
|
1569
|
+
transformedEvent = result;
|
|
1570
|
+
}
|
|
1571
|
+
catch (error) {
|
|
1572
|
+
logger.warn(`Plugin ${plugin.name} beforeSend hook failed:`, error);
|
|
1573
|
+
// Continue with other plugins even if one fails
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
return transformedEvent;
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* Execute afterSend hooks for all plugins
|
|
1580
|
+
*/
|
|
1581
|
+
async executeAfterSend(event) {
|
|
1582
|
+
for (const plugin of this.plugins) {
|
|
1583
|
+
if (!plugin.afterSend)
|
|
1584
|
+
continue;
|
|
1585
|
+
try {
|
|
1586
|
+
await plugin.afterSend(event);
|
|
1587
|
+
}
|
|
1588
|
+
catch (error) {
|
|
1589
|
+
logger.warn(`Plugin ${plugin.name} afterSend hook failed:`, error);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Execute onError hooks for all plugins
|
|
1595
|
+
*/
|
|
1596
|
+
async executeOnError(event, error) {
|
|
1597
|
+
for (const plugin of this.plugins) {
|
|
1598
|
+
if (!plugin.onError)
|
|
1599
|
+
continue;
|
|
1600
|
+
try {
|
|
1601
|
+
await plugin.onError(event, error);
|
|
1602
|
+
}
|
|
1603
|
+
catch (err) {
|
|
1604
|
+
logger.warn(`Plugin ${plugin.name} onError hook failed:`, err);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Clear all plugins
|
|
1610
|
+
*/
|
|
1611
|
+
clear() {
|
|
1612
|
+
this.plugins = [];
|
|
1613
|
+
logger.debug('All plugins cleared');
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
// Global plugin manager instance
|
|
1617
|
+
const pluginManager = new PluginManager();
|
|
1618
|
+
|
|
1619
|
+
/**
|
|
1620
|
+
* Metrics collection for analytics tracker
|
|
1621
|
+
* Tracks performance and usage statistics
|
|
1622
|
+
*/
|
|
1623
|
+
class MetricsCollector {
|
|
1624
|
+
constructor() {
|
|
1625
|
+
this.metrics = {
|
|
1626
|
+
eventsSent: 0,
|
|
1627
|
+
eventsQueued: 0,
|
|
1628
|
+
eventsFailed: 0,
|
|
1629
|
+
eventsFiltered: 0,
|
|
1630
|
+
averageSendTime: 0,
|
|
1631
|
+
retryCount: 0,
|
|
1632
|
+
queueSize: 0,
|
|
1633
|
+
lastFlushTime: null,
|
|
1634
|
+
errors: [],
|
|
1635
|
+
};
|
|
1636
|
+
this.sendTimes = [];
|
|
1637
|
+
this.maxErrors = 100; // Keep last 100 errors
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Record an event being queued
|
|
1641
|
+
*/
|
|
1642
|
+
recordQueued() {
|
|
1643
|
+
this.metrics.eventsQueued++;
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* Record an event being sent
|
|
1647
|
+
*/
|
|
1648
|
+
recordSent(sendTime) {
|
|
1649
|
+
this.metrics.eventsSent++;
|
|
1650
|
+
this.sendTimes.push(sendTime);
|
|
1651
|
+
// Keep only last 100 send times for average calculation
|
|
1652
|
+
if (this.sendTimes.length > 100) {
|
|
1653
|
+
this.sendTimes.shift();
|
|
1654
|
+
}
|
|
1655
|
+
// Calculate average
|
|
1656
|
+
const sum = this.sendTimes.reduce((a, b) => a + b, 0);
|
|
1657
|
+
this.metrics.averageSendTime = sum / this.sendTimes.length;
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Record a failed event
|
|
1661
|
+
*/
|
|
1662
|
+
recordFailed(error) {
|
|
1663
|
+
this.metrics.eventsFailed++;
|
|
1664
|
+
if (error) {
|
|
1665
|
+
this.metrics.errors.push({
|
|
1666
|
+
timestamp: Date.now(),
|
|
1667
|
+
error: error.message || String(error),
|
|
1668
|
+
});
|
|
1669
|
+
// Keep only last N errors
|
|
1670
|
+
if (this.metrics.errors.length > this.maxErrors) {
|
|
1671
|
+
this.metrics.errors.shift();
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
/**
|
|
1676
|
+
* Record a filtered event
|
|
1677
|
+
*/
|
|
1678
|
+
recordFiltered() {
|
|
1679
|
+
this.metrics.eventsFiltered++;
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Record a retry
|
|
1683
|
+
*/
|
|
1684
|
+
recordRetry() {
|
|
1685
|
+
this.metrics.retryCount++;
|
|
1686
|
+
}
|
|
1687
|
+
/**
|
|
1688
|
+
* Update queue size
|
|
1689
|
+
*/
|
|
1690
|
+
updateQueueSize(size) {
|
|
1691
|
+
this.metrics.queueSize = size;
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Record flush time
|
|
1695
|
+
*/
|
|
1696
|
+
recordFlush() {
|
|
1697
|
+
this.metrics.lastFlushTime = Date.now();
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* Get current metrics
|
|
1701
|
+
*/
|
|
1702
|
+
getMetrics() {
|
|
1703
|
+
return {
|
|
1704
|
+
...this.metrics,
|
|
1705
|
+
errors: [...this.metrics.errors], // Return copy
|
|
1706
|
+
};
|
|
1707
|
+
}
|
|
1708
|
+
/**
|
|
1709
|
+
* Reset metrics
|
|
1710
|
+
*/
|
|
1711
|
+
reset() {
|
|
1712
|
+
this.metrics = {
|
|
1713
|
+
eventsSent: 0,
|
|
1714
|
+
eventsQueued: 0,
|
|
1715
|
+
eventsFailed: 0,
|
|
1716
|
+
eventsFiltered: 0,
|
|
1717
|
+
averageSendTime: 0,
|
|
1718
|
+
retryCount: 0,
|
|
1719
|
+
queueSize: 0,
|
|
1720
|
+
lastFlushTime: null,
|
|
1721
|
+
errors: [],
|
|
1722
|
+
};
|
|
1723
|
+
this.sendTimes = [];
|
|
1724
|
+
logger.debug('Metrics reset');
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Get metrics summary (for logging)
|
|
1728
|
+
*/
|
|
1729
|
+
getSummary() {
|
|
1730
|
+
const m = this.metrics;
|
|
1731
|
+
return `Metrics: ${m.eventsSent} sent, ${m.eventsQueued} queued, ${m.eventsFailed} failed, ${m.eventsFiltered} filtered, ${m.retryCount} retries, avg send time: ${m.averageSendTime.toFixed(2)}ms`;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
// Global metrics collector instance
|
|
1735
|
+
const metricsCollector = new MetricsCollector();
|
|
1736
|
+
|
|
1177
1737
|
/**
|
|
1178
1738
|
* Analytics Service
|
|
1179
1739
|
* Sends analytics events to your backend API
|
|
1180
1740
|
*
|
|
1181
1741
|
* Supports both relative paths (e.g., '/api/analytics') and full URLs (e.g., 'https://your-server.com/api/analytics')
|
|
1742
|
+
*
|
|
1743
|
+
* Features:
|
|
1744
|
+
* - Event batching and queueing
|
|
1745
|
+
* - Automatic retry with exponential backoff
|
|
1746
|
+
* - Offline support with localStorage persistence
|
|
1747
|
+
* - Configurable logging levels
|
|
1182
1748
|
*/
|
|
1183
1749
|
class AnalyticsService {
|
|
1184
1750
|
/**
|
|
1185
|
-
* Configure the analytics
|
|
1751
|
+
* Configure the analytics service
|
|
1186
1752
|
*
|
|
1187
1753
|
* @param config - Configuration object
|
|
1188
1754
|
* @param config.apiEndpoint - Your backend API endpoint URL
|
|
1189
|
-
*
|
|
1190
|
-
*
|
|
1755
|
+
* @param config.batchSize - Events per batch (default: 10)
|
|
1756
|
+
* @param config.batchInterval - Flush interval in ms (default: 5000)
|
|
1757
|
+
* @param config.maxQueueSize - Max queued events (default: 100)
|
|
1758
|
+
* @param config.maxRetries - Max retry attempts (default: 3)
|
|
1759
|
+
* @param config.retryDelay - Initial retry delay in ms (default: 1000)
|
|
1760
|
+
* @param config.logLevel - Logging verbosity (default: 'warn')
|
|
1191
1761
|
*
|
|
1192
1762
|
* @example
|
|
1193
1763
|
* ```typescript
|
|
1194
|
-
* //
|
|
1764
|
+
* // Basic configuration
|
|
1195
1765
|
* AnalyticsService.configure({
|
|
1196
1766
|
* apiEndpoint: 'https://api.yourcompany.com/analytics'
|
|
1197
1767
|
* });
|
|
1198
1768
|
*
|
|
1199
|
-
* //
|
|
1769
|
+
* // Advanced configuration
|
|
1200
1770
|
* AnalyticsService.configure({
|
|
1201
|
-
* apiEndpoint: '/api/analytics'
|
|
1771
|
+
* apiEndpoint: '/api/analytics',
|
|
1772
|
+
* batchSize: 20,
|
|
1773
|
+
* batchInterval: 10000,
|
|
1774
|
+
* maxRetries: 5,
|
|
1775
|
+
* logLevel: 'info'
|
|
1202
1776
|
* });
|
|
1203
1777
|
* ```
|
|
1204
1778
|
*/
|
|
1205
1779
|
static configure(config) {
|
|
1206
1780
|
this.apiEndpoint = config.apiEndpoint;
|
|
1781
|
+
this.config = {
|
|
1782
|
+
batchSize: 10,
|
|
1783
|
+
batchInterval: 5000,
|
|
1784
|
+
maxQueueSize: 100,
|
|
1785
|
+
maxRetries: 3,
|
|
1786
|
+
retryDelay: 1000,
|
|
1787
|
+
logLevel: 'warn',
|
|
1788
|
+
...config,
|
|
1789
|
+
};
|
|
1790
|
+
// Set log level
|
|
1791
|
+
if (this.config.logLevel) {
|
|
1792
|
+
logger.setLevel(this.config.logLevel);
|
|
1793
|
+
}
|
|
1794
|
+
// Initialize queue manager
|
|
1795
|
+
this.initializeQueue();
|
|
1796
|
+
// Store endpoint for sendBeacon
|
|
1797
|
+
if (typeof window !== 'undefined') {
|
|
1798
|
+
window.__analyticsEndpoint = this.apiEndpoint;
|
|
1799
|
+
}
|
|
1800
|
+
// Reset metrics if enabled
|
|
1801
|
+
if (this.config.enableMetrics) {
|
|
1802
|
+
metricsCollector.reset();
|
|
1803
|
+
}
|
|
1804
|
+
this.isInitialized = true;
|
|
1805
|
+
}
|
|
1806
|
+
/**
|
|
1807
|
+
* Initialize the queue manager
|
|
1808
|
+
*/
|
|
1809
|
+
static initializeQueue() {
|
|
1810
|
+
if (typeof window === 'undefined')
|
|
1811
|
+
return;
|
|
1812
|
+
const batchSize = this.config.batchSize ?? 10;
|
|
1813
|
+
const batchInterval = this.config.batchInterval ?? 5000;
|
|
1814
|
+
const maxQueueSize = this.config.maxQueueSize ?? 100;
|
|
1815
|
+
this.queueManager = new QueueManager({
|
|
1816
|
+
batchSize,
|
|
1817
|
+
batchInterval,
|
|
1818
|
+
maxQueueSize,
|
|
1819
|
+
storageKey: 'analytics:eventQueue',
|
|
1820
|
+
});
|
|
1821
|
+
// Set flush callback
|
|
1822
|
+
this.queueManager.setFlushCallback(async (events) => {
|
|
1823
|
+
await this.sendBatch(events);
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
/**
|
|
1827
|
+
* Get queue manager instance
|
|
1828
|
+
*/
|
|
1829
|
+
static getQueueManager() {
|
|
1830
|
+
return this.queueManager;
|
|
1207
1831
|
}
|
|
1208
1832
|
/**
|
|
1209
1833
|
* Generate a random event ID
|
|
@@ -1219,8 +1843,131 @@ class AnalyticsService {
|
|
|
1219
1843
|
// Fallback for environments without crypto
|
|
1220
1844
|
return Math.random().toString(36).substring(2) + Date.now().toString(36);
|
|
1221
1845
|
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Send a batch of events with retry logic
|
|
1848
|
+
*/
|
|
1849
|
+
static async sendBatch(events) {
|
|
1850
|
+
if (events.length === 0)
|
|
1851
|
+
return;
|
|
1852
|
+
// Apply plugin transformations
|
|
1853
|
+
const transformedEvents = [];
|
|
1854
|
+
for (const event of events) {
|
|
1855
|
+
const transformed = await pluginManager.executeBeforeSend(event);
|
|
1856
|
+
if (transformed) {
|
|
1857
|
+
transformedEvents.push(transformed);
|
|
1858
|
+
}
|
|
1859
|
+
else {
|
|
1860
|
+
if (this.config.enableMetrics) {
|
|
1861
|
+
metricsCollector.recordFiltered();
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
if (transformedEvents.length === 0) {
|
|
1866
|
+
logger.debug('All events filtered out by plugins');
|
|
1867
|
+
return;
|
|
1868
|
+
}
|
|
1869
|
+
const maxRetries = this.config.maxRetries ?? 3;
|
|
1870
|
+
const retryDelay = this.config.retryDelay ?? 1000;
|
|
1871
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
1872
|
+
try {
|
|
1873
|
+
const res = await fetch(this.apiEndpoint, {
|
|
1874
|
+
method: 'POST',
|
|
1875
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1876
|
+
keepalive: true,
|
|
1877
|
+
body: JSON.stringify(transformedEvents),
|
|
1878
|
+
});
|
|
1879
|
+
if (res.ok) {
|
|
1880
|
+
const sendTime = Date.now(); // Approximate send time
|
|
1881
|
+
logger.debug(`Successfully sent batch of ${transformedEvents.length} events`);
|
|
1882
|
+
// Record metrics
|
|
1883
|
+
if (this.config.enableMetrics) {
|
|
1884
|
+
for (let i = 0; i < transformedEvents.length; i++) {
|
|
1885
|
+
metricsCollector.recordSent(sendTime);
|
|
1886
|
+
}
|
|
1887
|
+
metricsCollector.recordFlush();
|
|
1888
|
+
}
|
|
1889
|
+
// Execute afterSend hooks
|
|
1890
|
+
for (const event of transformedEvents) {
|
|
1891
|
+
await pluginManager.executeAfterSend(event);
|
|
1892
|
+
}
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
// Don't retry on client errors (4xx)
|
|
1896
|
+
if (res.status >= 400 && res.status < 500) {
|
|
1897
|
+
const errorText = await res.text().catch(() => 'Unknown error');
|
|
1898
|
+
logger.warn(`Client error (${res.status}): ${errorText}`);
|
|
1899
|
+
// Record metrics
|
|
1900
|
+
if (this.config.enableMetrics) {
|
|
1901
|
+
const error = new Error(`Client error: ${errorText}`);
|
|
1902
|
+
for (let i = 0; i < transformedEvents.length; i++) {
|
|
1903
|
+
metricsCollector.recordFailed(error);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
return;
|
|
1907
|
+
}
|
|
1908
|
+
// Retry on server errors (5xx) or network errors
|
|
1909
|
+
if (attempt < maxRetries) {
|
|
1910
|
+
const delay = retryDelay * Math.pow(2, attempt); // Exponential backoff
|
|
1911
|
+
logger.debug(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
|
|
1912
|
+
if (this.config.enableMetrics) {
|
|
1913
|
+
metricsCollector.recordRetry();
|
|
1914
|
+
}
|
|
1915
|
+
await this.sleep(delay);
|
|
1916
|
+
}
|
|
1917
|
+
else {
|
|
1918
|
+
const errorText = await res.text().catch(() => 'Unknown error');
|
|
1919
|
+
const error = new Error(`Failed after ${maxRetries} retries: ${errorText}`);
|
|
1920
|
+
// Record metrics
|
|
1921
|
+
if (this.config.enableMetrics) {
|
|
1922
|
+
for (let i = 0; i < transformedEvents.length; i++) {
|
|
1923
|
+
metricsCollector.recordFailed(error);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
// Execute onError hooks
|
|
1927
|
+
for (const event of transformedEvents) {
|
|
1928
|
+
await pluginManager.executeOnError(event, error);
|
|
1929
|
+
}
|
|
1930
|
+
throw error;
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
catch (err) {
|
|
1934
|
+
// Network error - retry if attempts remaining
|
|
1935
|
+
if (attempt < maxRetries) {
|
|
1936
|
+
const delay = retryDelay * Math.pow(2, attempt);
|
|
1937
|
+
logger.debug(`Network error, retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
|
|
1938
|
+
if (this.config.enableMetrics) {
|
|
1939
|
+
metricsCollector.recordRetry();
|
|
1940
|
+
}
|
|
1941
|
+
await this.sleep(delay);
|
|
1942
|
+
}
|
|
1943
|
+
else {
|
|
1944
|
+
logger.error(`Failed to send batch after ${maxRetries} retries:`, err);
|
|
1945
|
+
// Record metrics
|
|
1946
|
+
if (this.config.enableMetrics) {
|
|
1947
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1948
|
+
for (let i = 0; i < transformedEvents.length; i++) {
|
|
1949
|
+
metricsCollector.recordFailed(error);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
// Execute onError hooks
|
|
1953
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1954
|
+
for (const event of transformedEvents) {
|
|
1955
|
+
await pluginManager.executeOnError(event, error);
|
|
1956
|
+
}
|
|
1957
|
+
throw err;
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Sleep utility for retry delays
|
|
1964
|
+
*/
|
|
1965
|
+
static sleep(ms) {
|
|
1966
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1967
|
+
}
|
|
1222
1968
|
/**
|
|
1223
1969
|
* Track user journey/analytics event
|
|
1970
|
+
* Events are automatically queued and batched
|
|
1224
1971
|
*/
|
|
1225
1972
|
static async trackEvent(event) {
|
|
1226
1973
|
const payload = {
|
|
@@ -1228,23 +1975,22 @@ class AnalyticsService {
|
|
|
1228
1975
|
timestamp: new Date(),
|
|
1229
1976
|
eventId: this.generateEventId(),
|
|
1230
1977
|
};
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
if (!res.ok) {
|
|
1239
|
-
console.warn('[Analytics] Send failed:', await res.text());
|
|
1240
|
-
}
|
|
1241
|
-
else if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') {
|
|
1242
|
-
console.log('[Analytics] Event sent successfully');
|
|
1978
|
+
// If queue is available, use it (browser environment)
|
|
1979
|
+
if (this.queueManager && typeof window !== 'undefined') {
|
|
1980
|
+
this.queueManager.enqueue(payload);
|
|
1981
|
+
// Record metrics
|
|
1982
|
+
if (this.config.enableMetrics) {
|
|
1983
|
+
metricsCollector.recordQueued();
|
|
1984
|
+
metricsCollector.updateQueueSize(this.queueManager.getQueueSize());
|
|
1243
1985
|
}
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
// Fallback: send immediately (SSR or queue not initialized)
|
|
1989
|
+
try {
|
|
1990
|
+
await this.sendBatch([payload]);
|
|
1244
1991
|
}
|
|
1245
1992
|
catch (err) {
|
|
1246
|
-
|
|
1247
|
-
console.warn('[Analytics] Failed to send event:', err);
|
|
1993
|
+
logger.warn('Failed to send event:', err);
|
|
1248
1994
|
}
|
|
1249
1995
|
}
|
|
1250
1996
|
/**
|
|
@@ -1373,8 +2119,123 @@ class AnalyticsService {
|
|
|
1373
2119
|
...parameters,
|
|
1374
2120
|
});
|
|
1375
2121
|
}
|
|
2122
|
+
/**
|
|
2123
|
+
* Manually flush the event queue
|
|
2124
|
+
* Useful for ensuring events are sent before page unload
|
|
2125
|
+
*/
|
|
2126
|
+
static async flushQueue() {
|
|
2127
|
+
if (this.queueManager) {
|
|
2128
|
+
await this.queueManager.flush();
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
/**
|
|
2132
|
+
* Get current queue size
|
|
2133
|
+
*/
|
|
2134
|
+
static getQueueSize() {
|
|
2135
|
+
return this.queueManager?.getQueueSize() ?? 0;
|
|
2136
|
+
}
|
|
2137
|
+
/**
|
|
2138
|
+
* Clear the event queue
|
|
2139
|
+
*/
|
|
2140
|
+
static clearQueue() {
|
|
2141
|
+
this.queueManager?.clear();
|
|
2142
|
+
if (this.config.enableMetrics) {
|
|
2143
|
+
metricsCollector.updateQueueSize(0);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* Get metrics (if enabled)
|
|
2148
|
+
*/
|
|
2149
|
+
static getMetrics() {
|
|
2150
|
+
if (!this.config.enableMetrics) {
|
|
2151
|
+
logger.warn('Metrics collection is not enabled. Set enableMetrics: true in config.');
|
|
2152
|
+
return null;
|
|
2153
|
+
}
|
|
2154
|
+
return metricsCollector.getMetrics();
|
|
2155
|
+
}
|
|
1376
2156
|
}
|
|
1377
2157
|
AnalyticsService.apiEndpoint = '/api/analytics';
|
|
2158
|
+
AnalyticsService.queueManager = null;
|
|
2159
|
+
AnalyticsService.config = {};
|
|
2160
|
+
AnalyticsService.isInitialized = false;
|
|
2161
|
+
|
|
2162
|
+
/**
|
|
2163
|
+
* Debug utilities for analytics tracker
|
|
2164
|
+
* Provides debugging tools in development mode
|
|
2165
|
+
*/
|
|
2166
|
+
/**
|
|
2167
|
+
* Initialize debug utilities (only in development)
|
|
2168
|
+
*/
|
|
2169
|
+
function initDebug() {
|
|
2170
|
+
if (typeof window === 'undefined')
|
|
2171
|
+
return;
|
|
2172
|
+
const isDevelopment = typeof process !== 'undefined' &&
|
|
2173
|
+
process.env?.NODE_ENV === 'development';
|
|
2174
|
+
if (!isDevelopment) {
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
const debug = {
|
|
2178
|
+
/**
|
|
2179
|
+
* Get current queue state
|
|
2180
|
+
*/
|
|
2181
|
+
getQueue: () => {
|
|
2182
|
+
const queueManager = AnalyticsService.getQueueManager();
|
|
2183
|
+
if (!queueManager) {
|
|
2184
|
+
logger.warn('Queue manager not initialized');
|
|
2185
|
+
return [];
|
|
2186
|
+
}
|
|
2187
|
+
return queueManager.getQueue();
|
|
2188
|
+
},
|
|
2189
|
+
/**
|
|
2190
|
+
* Get queue size
|
|
2191
|
+
*/
|
|
2192
|
+
getQueueSize: () => {
|
|
2193
|
+
return AnalyticsService.getQueueSize();
|
|
2194
|
+
},
|
|
2195
|
+
/**
|
|
2196
|
+
* Manually flush the queue
|
|
2197
|
+
*/
|
|
2198
|
+
flushQueue: async () => {
|
|
2199
|
+
logger.info('Manually flushing queue...');
|
|
2200
|
+
await AnalyticsService.flushQueue();
|
|
2201
|
+
logger.info('Queue flushed');
|
|
2202
|
+
},
|
|
2203
|
+
/**
|
|
2204
|
+
* Clear the queue
|
|
2205
|
+
*/
|
|
2206
|
+
clearQueue: () => {
|
|
2207
|
+
logger.info('Clearing queue...');
|
|
2208
|
+
AnalyticsService.clearQueue();
|
|
2209
|
+
logger.info('Queue cleared');
|
|
2210
|
+
},
|
|
2211
|
+
/**
|
|
2212
|
+
* Get debug statistics
|
|
2213
|
+
*/
|
|
2214
|
+
getStats: () => {
|
|
2215
|
+
const queueManager = AnalyticsService.getQueueManager();
|
|
2216
|
+
const metrics = AnalyticsService.getMetrics();
|
|
2217
|
+
return {
|
|
2218
|
+
queueSize: AnalyticsService.getQueueSize(),
|
|
2219
|
+
queue: queueManager?.getQueue() ?? [],
|
|
2220
|
+
config: {
|
|
2221
|
+
metrics: metrics,
|
|
2222
|
+
metricsSummary: metrics ? metricsCollector.getSummary() : 'Metrics disabled',
|
|
2223
|
+
},
|
|
2224
|
+
};
|
|
2225
|
+
},
|
|
2226
|
+
/**
|
|
2227
|
+
* Set log level
|
|
2228
|
+
*/
|
|
2229
|
+
setLogLevel: (level) => {
|
|
2230
|
+
logger.setLevel(level);
|
|
2231
|
+
logger.info(`Log level set to: ${level}`);
|
|
2232
|
+
},
|
|
2233
|
+
};
|
|
2234
|
+
// Expose to window for console access
|
|
2235
|
+
window.__analyticsDebug = debug;
|
|
2236
|
+
logger.info('Analytics debug tools available at window.__analyticsDebug');
|
|
2237
|
+
logger.info('Available methods: getQueue(), getQueueSize(), flushQueue(), clearQueue(), getStats(), setLogLevel()');
|
|
2238
|
+
}
|
|
1378
2239
|
|
|
1379
2240
|
/**
|
|
1380
2241
|
* React Hook for Analytics Tracking
|
|
@@ -1396,9 +2257,29 @@ function useAnalytics(options = {}) {
|
|
|
1396
2257
|
// Configure analytics service if endpoint provided
|
|
1397
2258
|
react.useEffect(() => {
|
|
1398
2259
|
if (config?.apiEndpoint) {
|
|
1399
|
-
AnalyticsService.configure({
|
|
2260
|
+
AnalyticsService.configure({
|
|
2261
|
+
apiEndpoint: config.apiEndpoint,
|
|
2262
|
+
batchSize: config.batchSize,
|
|
2263
|
+
batchInterval: config.batchInterval,
|
|
2264
|
+
maxQueueSize: config.maxQueueSize,
|
|
2265
|
+
maxRetries: config.maxRetries,
|
|
2266
|
+
retryDelay: config.retryDelay,
|
|
2267
|
+
logLevel: config.logLevel,
|
|
2268
|
+
enableMetrics: config.enableMetrics,
|
|
2269
|
+
sessionTimeout: config.sessionTimeout,
|
|
2270
|
+
});
|
|
1400
2271
|
}
|
|
1401
|
-
}, [
|
|
2272
|
+
}, [
|
|
2273
|
+
config?.apiEndpoint,
|
|
2274
|
+
config?.batchSize,
|
|
2275
|
+
config?.batchInterval,
|
|
2276
|
+
config?.maxQueueSize,
|
|
2277
|
+
config?.maxRetries,
|
|
2278
|
+
config?.retryDelay,
|
|
2279
|
+
config?.logLevel,
|
|
2280
|
+
config?.enableMetrics,
|
|
2281
|
+
config?.sessionTimeout,
|
|
2282
|
+
]);
|
|
1402
2283
|
const [networkInfo, setNetworkInfo] = react.useState(null);
|
|
1403
2284
|
const [deviceInfo, setDeviceInfo] = react.useState(null);
|
|
1404
2285
|
const [attribution, setAttribution] = react.useState(null);
|
|
@@ -1423,6 +2304,10 @@ function useAnalytics(options = {}) {
|
|
|
1423
2304
|
};
|
|
1424
2305
|
}
|
|
1425
2306
|
}, []);
|
|
2307
|
+
// Initialize debug tools in development
|
|
2308
|
+
react.useEffect(() => {
|
|
2309
|
+
initDebug();
|
|
2310
|
+
}, []);
|
|
1426
2311
|
const refresh = react.useCallback(async () => {
|
|
1427
2312
|
const net = NetworkDetector.detect();
|
|
1428
2313
|
const dev = await DeviceDetector.detect();
|
|
@@ -1621,6 +2506,17 @@ function useAnalytics(options = {}) {
|
|
|
1621
2506
|
const incrementInteraction = react.useCallback(() => {
|
|
1622
2507
|
setInteractions((n) => n + 1);
|
|
1623
2508
|
}, []);
|
|
2509
|
+
// Session management
|
|
2510
|
+
react.useEffect(() => {
|
|
2511
|
+
if (config?.sessionTimeout) {
|
|
2512
|
+
getOrCreateSession(config.sessionTimeout);
|
|
2513
|
+
// Update session activity on user interactions
|
|
2514
|
+
const activityInterval = setInterval(() => {
|
|
2515
|
+
updateSessionActivity();
|
|
2516
|
+
}, 60000); // Update every minute
|
|
2517
|
+
return () => clearInterval(activityInterval);
|
|
2518
|
+
}
|
|
2519
|
+
}, [config?.sessionTimeout]);
|
|
1624
2520
|
return react.useMemo(() => ({
|
|
1625
2521
|
sessionId,
|
|
1626
2522
|
networkInfo,
|
|
@@ -1816,20 +2712,27 @@ exports.AttributionDetector = AttributionDetector;
|
|
|
1816
2712
|
exports.DeviceDetector = DeviceDetector;
|
|
1817
2713
|
exports.LocationDetector = LocationDetector;
|
|
1818
2714
|
exports.NetworkDetector = NetworkDetector;
|
|
2715
|
+
exports.QueueManager = QueueManager;
|
|
1819
2716
|
exports.checkAndSetLocationConsent = checkAndSetLocationConsent;
|
|
1820
2717
|
exports.clearLocationConsent = clearLocationConsent;
|
|
2718
|
+
exports.clearSession = clearSession;
|
|
1821
2719
|
exports.default = useAnalytics;
|
|
1822
2720
|
exports.getIPFromRequest = getIPFromRequest;
|
|
1823
2721
|
exports.getIPLocation = getIPLocation;
|
|
1824
2722
|
exports.getLocationConsentTimestamp = getLocationConsentTimestamp;
|
|
2723
|
+
exports.getOrCreateSession = getOrCreateSession;
|
|
1825
2724
|
exports.getOrCreateUserId = getOrCreateUserId;
|
|
1826
2725
|
exports.getPublicIP = getPublicIP;
|
|
2726
|
+
exports.getSession = getSession;
|
|
1827
2727
|
exports.hasLocationConsent = hasLocationConsent;
|
|
2728
|
+
exports.initDebug = initDebug;
|
|
1828
2729
|
exports.loadJSON = loadJSON;
|
|
1829
2730
|
exports.loadSessionJSON = loadSessionJSON;
|
|
2731
|
+
exports.logger = logger;
|
|
1830
2732
|
exports.saveJSON = saveJSON;
|
|
1831
2733
|
exports.saveSessionJSON = saveSessionJSON;
|
|
1832
2734
|
exports.setLocationConsentGranted = setLocationConsentGranted;
|
|
1833
2735
|
exports.trackPageVisit = trackPageVisit;
|
|
2736
|
+
exports.updateSessionActivity = updateSessionActivity;
|
|
1834
2737
|
exports.useAnalytics = useAnalytics;
|
|
1835
2738
|
//# sourceMappingURL=index.cjs.js.map
|