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.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 API endpoint
1747
+ * Configure the analytics service
1182
1748
  *
1183
1749
  * @param config - Configuration object
1184
1750
  * @param config.apiEndpoint - Your backend API endpoint URL
1185
- * - Relative path: '/api/analytics' (sends to same domain)
1186
- * - Full URL: 'https://your-server.com/api/analytics' (sends to your server)
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
- * // Use your own server
1760
+ * // Basic configuration
1191
1761
  * AnalyticsService.configure({
1192
1762
  * apiEndpoint: 'https://api.yourcompany.com/analytics'
1193
1763
  * });
1194
1764
  *
1195
- * // Or use relative path (same domain)
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
- try {
1228
- const res = await fetch(this.apiEndpoint, {
1229
- method: 'POST',
1230
- headers: { 'Content-Type': 'application/json' },
1231
- keepalive: true, // Allows sending during unload on some browsers
1232
- body: JSON.stringify(payload),
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
- // Don't break user experience - silently fail
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({ apiEndpoint: config.apiEndpoint });
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
- }, [config?.apiEndpoint]);
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