user-analytics-tracker 1.7.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/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 API endpoint
1751
+ * Configure the analytics service
1186
1752
  *
1187
1753
  * @param config - Configuration object
1188
1754
  * @param config.apiEndpoint - Your backend API endpoint URL
1189
- * - Relative path: '/api/analytics' (sends to same domain)
1190
- * - Full URL: 'https://your-server.com/api/analytics' (sends to your server)
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
- * // Use your own server
1764
+ * // Basic configuration
1195
1765
  * AnalyticsService.configure({
1196
1766
  * apiEndpoint: 'https://api.yourcompany.com/analytics'
1197
1767
  * });
1198
1768
  *
1199
- * // Or use relative path (same domain)
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
- try {
1232
- const res = await fetch(this.apiEndpoint, {
1233
- method: 'POST',
1234
- headers: { 'Content-Type': 'application/json' },
1235
- keepalive: true, // Allows sending during unload on some browsers
1236
- body: JSON.stringify(payload),
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
- // Don't break user experience - silently fail
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({ apiEndpoint: config.apiEndpoint });
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
- }, [config?.apiEndpoint]);
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