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