@grainql/analytics-web 2.7.1 → 2.9.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.
Files changed (83) hide show
  1. package/README.md +36 -3
  2. package/dist/cjs/consent.d.ts +38 -7
  3. package/dist/cjs/consent.d.ts.map +1 -1
  4. package/dist/cjs/consent.js +82 -23
  5. package/dist/cjs/consent.js.map +1 -1
  6. package/dist/cjs/debug-agent.d.ts +171 -0
  7. package/dist/cjs/debug-agent.d.ts.map +1 -0
  8. package/dist/cjs/debug-agent.js +1219 -0
  9. package/dist/cjs/debug-agent.js.map +1 -0
  10. package/dist/cjs/id-manager.d.ts +66 -0
  11. package/dist/cjs/id-manager.d.ts.map +1 -0
  12. package/dist/cjs/id-manager.js +212 -0
  13. package/dist/cjs/id-manager.js.map +1 -0
  14. package/dist/cjs/index.d.ts +26 -8
  15. package/dist/cjs/index.d.ts.map +1 -1
  16. package/dist/cjs/index.js.map +1 -1
  17. package/dist/cjs/interaction-tracking.d.ts +6 -0
  18. package/dist/cjs/interaction-tracking.d.ts.map +1 -1
  19. package/dist/cjs/interaction-tracking.js +55 -5
  20. package/dist/cjs/interaction-tracking.js.map +1 -1
  21. package/dist/cjs/page-tracking.d.ts +6 -0
  22. package/dist/cjs/page-tracking.d.ts.map +1 -1
  23. package/dist/cjs/page-tracking.js +23 -2
  24. package/dist/cjs/page-tracking.js.map +1 -1
  25. package/dist/cjs/react/hooks/useConsent.d.ts +18 -2
  26. package/dist/cjs/react/hooks/useConsent.d.ts.map +1 -1
  27. package/dist/cjs/react/hooks/useConsent.js +52 -1
  28. package/dist/cjs/react/hooks/useConsent.js.map +1 -1
  29. package/dist/consent.d.ts +38 -7
  30. package/dist/consent.d.ts.map +1 -1
  31. package/dist/consent.js +82 -23
  32. package/dist/debug-agent.d.ts +171 -0
  33. package/dist/debug-agent.d.ts.map +1 -0
  34. package/dist/debug-agent.js +1219 -0
  35. package/dist/esm/consent.d.ts +38 -7
  36. package/dist/esm/consent.d.ts.map +1 -1
  37. package/dist/esm/consent.js +82 -23
  38. package/dist/esm/consent.js.map +1 -1
  39. package/dist/esm/debug-agent.d.ts +171 -0
  40. package/dist/esm/debug-agent.d.ts.map +1 -0
  41. package/dist/esm/debug-agent.js +1215 -0
  42. package/dist/esm/debug-agent.js.map +1 -0
  43. package/dist/esm/id-manager.d.ts +66 -0
  44. package/dist/esm/id-manager.d.ts.map +1 -0
  45. package/dist/esm/id-manager.js +208 -0
  46. package/dist/esm/id-manager.js.map +1 -0
  47. package/dist/esm/index.d.ts +26 -8
  48. package/dist/esm/index.d.ts.map +1 -1
  49. package/dist/esm/index.js.map +1 -1
  50. package/dist/esm/interaction-tracking.d.ts +6 -0
  51. package/dist/esm/interaction-tracking.d.ts.map +1 -1
  52. package/dist/esm/interaction-tracking.js +55 -5
  53. package/dist/esm/interaction-tracking.js.map +1 -1
  54. package/dist/esm/page-tracking.d.ts +6 -0
  55. package/dist/esm/page-tracking.d.ts.map +1 -1
  56. package/dist/esm/page-tracking.js +23 -2
  57. package/dist/esm/page-tracking.js.map +1 -1
  58. package/dist/esm/react/hooks/useConsent.d.ts +18 -2
  59. package/dist/esm/react/hooks/useConsent.d.ts.map +1 -1
  60. package/dist/esm/react/hooks/useConsent.js +49 -1
  61. package/dist/esm/react/hooks/useConsent.js.map +1 -1
  62. package/dist/id-manager.d.ts +66 -0
  63. package/dist/id-manager.d.ts.map +1 -0
  64. package/dist/id-manager.js +212 -0
  65. package/dist/index.d.ts +26 -8
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.global.dev.js +1635 -86
  68. package/dist/index.global.dev.js.map +4 -4
  69. package/dist/index.global.js +506 -2
  70. package/dist/index.global.js.map +4 -4
  71. package/dist/index.js +171 -44
  72. package/dist/index.mjs +172 -45
  73. package/dist/interaction-tracking.d.ts +6 -0
  74. package/dist/interaction-tracking.d.ts.map +1 -1
  75. package/dist/interaction-tracking.js +55 -5
  76. package/dist/page-tracking.d.ts +6 -0
  77. package/dist/page-tracking.d.ts.map +1 -1
  78. package/dist/page-tracking.js +23 -2
  79. package/dist/react/hooks/useConsent.d.ts +18 -2
  80. package/dist/react/hooks/useConsent.d.ts.map +1 -1
  81. package/dist/react/hooks/useConsent.js +52 -1
  82. package/dist/react/hooks/useConsent.mjs +49 -1
  83. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -44,6 +44,7 @@ const cookies_1 = require("./cookies");
44
44
  const activity_1 = require("./activity");
45
45
  const heartbeat_1 = require("./heartbeat");
46
46
  const page_tracking_1 = require("./page-tracking");
47
+ const id_manager_1 = require("./id-manager");
47
48
  const attribution_1 = require("./attribution");
48
49
  Object.defineProperty(exports, "categorizeReferrer", { enumerable: true, get: function () { return attribution_1.categorizeReferrer; } });
49
50
  Object.defineProperty(exports, "parseUTMParameters", { enumerable: true, get: function () { return attribution_1.parseUTMParameters; } });
@@ -59,13 +60,13 @@ class GrainAnalytics {
59
60
  this.flushTimer = null;
60
61
  this.isDestroyed = false;
61
62
  this.globalUserId = null;
62
- this.persistentAnonymousUserId = null;
63
+ this.persistentAnonymousUserId = null; // Deprecated: use idManager instead
63
64
  // Remote Config properties
64
65
  this.configCache = null;
65
66
  this.configRefreshTimer = null;
66
67
  this.configChangeListeners = [];
67
68
  this.configFetchPromise = null;
68
- this.cookiesEnabled = false;
69
+ this.cookiesEnabled = false; // Deprecated: cookies no longer used for IDs
69
70
  // Automatic Tracking properties
70
71
  this.activityDetector = null;
71
72
  this.heartbeatManager = null;
@@ -79,6 +80,9 @@ class GrainAnalytics {
79
80
  // Session tracking
80
81
  this.sessionStartTime = Date.now();
81
82
  this.sessionEventCount = 0;
83
+ // Debug mode properties
84
+ this.debugAgent = null;
85
+ this.isDebugMode = false;
82
86
  this.config = {
83
87
  apiUrl: 'https://api.grainql.com',
84
88
  authStrategy: 'NONE',
@@ -93,38 +97,37 @@ class GrainAnalytics {
93
97
  configCacheKey: 'grain_config',
94
98
  configRefreshInterval: 300000, // 5 minutes
95
99
  enableConfigCache: true,
96
- // Privacy defaults
97
- consentMode: 'opt-out',
100
+ // Privacy defaults (v2.0)
101
+ consentMode: 'cookieless', // Default: privacy-first, no permanent tracking
98
102
  waitForConsent: false,
99
- enableCookies: false,
100
- anonymizeIP: false,
101
103
  disableAutoProperties: false,
102
104
  // Automatic Tracking defaults
103
105
  enableHeartbeat: true,
104
106
  heartbeatActiveInterval: 120000, // 2 minutes
105
107
  heartbeatInactiveInterval: 300000, // 5 minutes
106
108
  enableAutoPageView: true,
107
- stripQueryParams: true,
109
+ stripQueryParams: true, // Privacy-first: strip by default
110
+ stripHash: false,
108
111
  // Heatmap Tracking defaults
109
112
  enableHeatmapTracking: true,
110
113
  ...config,
111
114
  tenantId: config.tenantId,
112
115
  };
113
- // Initialize consent manager
116
+ // Initialize consent manager (v2.0)
114
117
  this.consentManager = new consent_1.ConsentManager(this.config.tenantId, this.config.consentMode);
115
- // Check if cookies are enabled
116
- if (this.config.enableCookies) {
117
- this.cookiesEnabled = (0, cookies_1.areCookiesEnabled)();
118
- if (!this.cookiesEnabled && this.config.debug) {
119
- console.warn('[Grain Analytics] Cookies are not available, falling back to localStorage');
120
- }
121
- }
118
+ // Initialize ID manager (v2.0)
119
+ const idMode = this.consentManager.getIdMode();
120
+ this.idManager = new id_manager_1.IdManager({
121
+ mode: idMode,
122
+ tenantId: this.config.tenantId,
123
+ useLocalStorage: true, // For permanent IDs when consented
124
+ });
122
125
  // Set global userId if provided in config
123
126
  if (config.userId) {
124
127
  this.globalUserId = config.userId;
125
128
  }
126
129
  this.validateConfig();
127
- this.initializePersistentAnonymousUserId();
130
+ // Deprecated: initializePersistentAnonymousUserId() - now handled by IdManager
128
131
  this.setupBeforeUnload();
129
132
  this.startFlushTimer();
130
133
  this.initializeConfigCache();
@@ -132,6 +135,8 @@ class GrainAnalytics {
132
135
  this.ephemeralSessionId = this.generateUUID();
133
136
  // Initialize automatic tracking (browser only)
134
137
  if (typeof window !== 'undefined') {
138
+ // Check for debug mode before initializing tracking
139
+ this.checkAndInitializeDebugMode();
135
140
  this.initializeAutomaticTracking();
136
141
  // Track session start
137
142
  this.trackSessionStart();
@@ -140,8 +145,11 @@ class GrainAnalytics {
140
145
  this.initializeHeatmapTracking();
141
146
  }
142
147
  }
143
- // Set up consent change listener to flush waiting events and handle consent upgrade
148
+ // Set up consent change listener to sync IdManager and flush waiting events (v2.0)
144
149
  this.consentManager.addListener((state) => {
150
+ // Sync IdManager with consent state
151
+ const idMode = this.consentManager.getIdMode();
152
+ this.idManager.setMode(idMode);
145
153
  if (state.granted) {
146
154
  this.handleConsentGranted();
147
155
  }
@@ -183,11 +191,14 @@ class GrainAnalytics {
183
191
  */
184
192
  shouldAllowPersistentStorage() {
185
193
  const hasConsent = this.consentManager.hasConsent('analytics');
186
- const isOptInMode = this.config.consentMode === 'opt-in';
194
+ const isCookieless = this.config.consentMode === 'cookieless';
187
195
  const userExplicitlyIdentified = !!this.globalUserId;
188
196
  const isJWTAuth = this.config.authStrategy === 'JWT';
197
+ // Never allow persistent storage in cookieless mode
198
+ if (isCookieless)
199
+ return false;
189
200
  // Allow persistent storage if any of these conditions are met
190
- return hasConsent || !isOptInMode || userExplicitlyIdentified || isJWTAuth;
201
+ return hasConsent || userExplicitlyIdentified || isJWTAuth;
191
202
  }
192
203
  /**
193
204
  * Generate a proper UUIDv4 identifier for anonymous user ID
@@ -281,23 +292,21 @@ class GrainAnalytics {
281
292
  }
282
293
  }
283
294
  /**
284
- * Get the effective user ID (global userId or persistent anonymous ID)
295
+ * Get the effective user ID (v2.0)
285
296
  *
286
- * GDPR Compliance: In opt-in mode without consent and no explicit user identification,
287
- * this should not be called. Use getEphemeralSessionId() instead.
297
+ * Privacy-first implementation:
298
+ * - Returns global userId if explicitly set (via identify/login)
299
+ * - Otherwise uses IdManager to generate:
300
+ * - Daily rotating ID (cookieless mode)
301
+ * - Permanent ID (with consent)
288
302
  */
289
303
  getEffectiveUserIdInternal() {
304
+ // Explicit user identification always takes precedence
290
305
  if (this.globalUserId) {
291
306
  return this.globalUserId;
292
307
  }
293
- if (this.persistentAnonymousUserId) {
294
- return this.persistentAnonymousUserId;
295
- }
296
- // Generate a new UUIDv4 identifier as fallback
297
- this.persistentAnonymousUserId = this.generateAnonymousUserId();
298
- // Try to persist it (will be skipped in opt-in mode without consent)
299
- this.savePersistentAnonymousUserId(this.persistentAnonymousUserId);
300
- return this.persistentAnonymousUserId;
308
+ // Use IdManager to generate appropriate ID based on consent
309
+ return this.idManager.getCurrentUserId();
301
310
  }
302
311
  log(...args) {
303
312
  if (this.config.debug) {
@@ -668,6 +677,7 @@ class GrainAnalytics {
668
677
  try {
669
678
  this.pageTrackingManager = new page_tracking_1.PageTrackingManager(this, {
670
679
  stripQueryParams: this.config.stripQueryParams,
680
+ stripHash: this.config.stripHash,
671
681
  debug: this.config.debug,
672
682
  tenantId: this.config.tenantId,
673
683
  });
@@ -760,6 +770,8 @@ class GrainAnalytics {
760
770
  debug: this.config.debug,
761
771
  enableMutationObserver: true,
762
772
  mutationDebounceDelay: 500,
773
+ tenantId: this.config.tenantId,
774
+ apiUrl: this.config.apiUrl,
763
775
  });
764
776
  this.log('Interaction tracking initialized');
765
777
  }
@@ -963,12 +975,13 @@ class GrainAnalytics {
963
975
  return;
964
976
  const hasConsent = this.consentManager.hasConsent('analytics');
965
977
  // Create event with appropriate user ID
978
+ // v2.0: Always use IdManager which returns daily rotating ID or permanent ID based on consent
966
979
  const event = {
967
980
  eventName,
968
- userId: hasConsent ? this.getEffectiveUserId() : this.getEphemeralSessionId(),
981
+ userId: this.getEffectiveUserId(), // IdManager handles daily vs permanent based on consent
969
982
  properties: {
970
983
  ...properties,
971
- _minimal: !hasConsent, // Flag to indicate minimal tracking
984
+ _minimal: !hasConsent, // Flag to indicate minimal tracking (daily rotating ID)
972
985
  _consent_status: hasConsent ? 'granted' : 'pending',
973
986
  },
974
987
  };
@@ -1065,17 +1078,22 @@ class GrainAnalytics {
1065
1078
  event.properties = filtered;
1066
1079
  }
1067
1080
  const formattedEvent = this.formatEvent(event);
1068
- // Check consent before tracking
1081
+ // Check if we should wait for consent (only if explicitly configured)
1069
1082
  if (this.consentManager.shouldWaitForConsent() && this.config.waitForConsent) {
1070
1083
  // Queue event until consent is granted
1071
1084
  this.waitingForConsentQueue.push(formattedEvent);
1072
1085
  this.log(`Event waiting for consent: ${event.eventName}`, event.properties);
1073
1086
  return;
1074
1087
  }
1075
- if (!this.consentManager.hasConsent('analytics')) {
1076
- this.log(`Event blocked by consent: ${event.eventName}`);
1077
- return;
1078
- }
1088
+ // v2.0: GDPR Strict falls back to cookie-less mode (daily rotating IDs)
1089
+ // Events are never blocked - IdManager already provides correct ID (daily or permanent)
1090
+ const hasConsent = this.consentManager.hasConsent('analytics');
1091
+ // Add tracking flags to indicate consent status
1092
+ formattedEvent.properties = {
1093
+ ...formattedEvent.properties,
1094
+ _minimal: !hasConsent, // Flag: true = daily rotating ID, false = permanent ID
1095
+ _consent_status: hasConsent ? 'granted' : 'pending',
1096
+ };
1079
1097
  this.eventQueue.push(formattedEvent);
1080
1098
  this.eventCountSinceLastHeartbeat++;
1081
1099
  this.sessionEventCount++;
@@ -1763,13 +1781,24 @@ class GrainAnalytics {
1763
1781
  }
1764
1782
  // Privacy & Consent Methods
1765
1783
  /**
1766
- * Grant consent for tracking
1784
+ * Grant consent for tracking (v2.0)
1785
+ * Switches from cookie-less mode to permanent IDs
1767
1786
  * @param categories - Optional array of consent categories (e.g., ['analytics', 'functional'])
1768
1787
  */
1769
1788
  grantConsent(categories) {
1770
1789
  try {
1771
1790
  this.consentManager.grantConsent(categories);
1772
- this.log('Consent granted', categories);
1791
+ // Sync ID manager with consent state
1792
+ const idMode = this.consentManager.getIdMode();
1793
+ this.idManager.setMode(idMode);
1794
+ this.log('Consent granted, switched to permanent IDs', categories);
1795
+ // Process any queued events waiting for consent
1796
+ if (this.waitingForConsentQueue.length > 0) {
1797
+ this.log(`Processing ${this.waitingForConsentQueue.length} queued events`);
1798
+ this.eventQueue.push(...this.waitingForConsentQueue);
1799
+ this.waitingForConsentQueue = [];
1800
+ this.flush();
1801
+ }
1773
1802
  }
1774
1803
  catch (error) {
1775
1804
  const formattedError = this.formatError(error, 'grantConsent');
@@ -1777,16 +1806,22 @@ class GrainAnalytics {
1777
1806
  }
1778
1807
  }
1779
1808
  /**
1780
- * Revoke consent for tracking (opt-out)
1809
+ * Revoke consent for tracking (v2.0)
1810
+ * Switches from permanent IDs to cookie-less mode
1781
1811
  * @param categories - Optional array of categories to revoke (if not provided, revokes all)
1782
1812
  */
1783
1813
  revokeConsent(categories) {
1784
1814
  try {
1785
1815
  this.consentManager.revokeConsent(categories);
1786
- this.log('Consent revoked', categories);
1787
- // Clear queued events when consent is revoked
1788
- this.eventQueue = [];
1789
- this.waitingForConsentQueue = [];
1816
+ // Sync ID manager with consent state
1817
+ const idMode = this.consentManager.getIdMode();
1818
+ this.idManager.setMode(idMode);
1819
+ this.log('Consent revoked, switched to cookie-less mode', categories);
1820
+ // Clear queued events when consent is fully revoked
1821
+ if (!this.consentManager.hasConsent()) {
1822
+ this.eventQueue = [];
1823
+ this.waitingForConsentQueue = [];
1824
+ }
1790
1825
  }
1791
1826
  catch (error) {
1792
1827
  const formattedError = this.formatError(error, 'revokeConsent');
@@ -1818,6 +1853,93 @@ class GrainAnalytics {
1818
1853
  offConsentChange(listener) {
1819
1854
  this.consentManager.removeListener(listener);
1820
1855
  }
1856
+ /**
1857
+ * Check for debug mode parameters and initialize debug agent if valid
1858
+ */
1859
+ checkAndInitializeDebugMode() {
1860
+ if (typeof window === 'undefined')
1861
+ return;
1862
+ try {
1863
+ const urlParams = new URLSearchParams(window.location.search);
1864
+ const isDebug = urlParams.get('grain_debug') === '1';
1865
+ const sessionId = urlParams.get('grain_session');
1866
+ if (!isDebug || !sessionId) {
1867
+ return;
1868
+ }
1869
+ this.log('Debug mode detected, verifying session:', sessionId);
1870
+ // Verify session with API
1871
+ this.verifyDebugSession(sessionId, window.location.hostname)
1872
+ .then((valid) => {
1873
+ if (valid) {
1874
+ this.log('Debug session verified, initializing debug agent');
1875
+ this.isDebugMode = true;
1876
+ this.initializeDebugAgent(sessionId);
1877
+ }
1878
+ else {
1879
+ this.log('Debug session verification failed');
1880
+ }
1881
+ })
1882
+ .catch((error) => {
1883
+ this.log('Failed to verify debug session:', error);
1884
+ });
1885
+ }
1886
+ catch (error) {
1887
+ this.log('Error checking debug mode:', error);
1888
+ }
1889
+ }
1890
+ /**
1891
+ * Verify debug session with API
1892
+ */
1893
+ async verifyDebugSession(sessionId, domain) {
1894
+ try {
1895
+ const url = `${this.config.apiUrl}/v1/tenant/${encodeURIComponent(this.config.tenantId)}/debug-sessions/verify`;
1896
+ const response = await fetch(url, {
1897
+ method: 'POST',
1898
+ headers: {
1899
+ 'Content-Type': 'application/json',
1900
+ },
1901
+ body: JSON.stringify({
1902
+ sessionId,
1903
+ domain,
1904
+ }),
1905
+ });
1906
+ if (!response.ok) {
1907
+ return false;
1908
+ }
1909
+ const result = await response.json();
1910
+ return result.valid === true;
1911
+ }
1912
+ catch (error) {
1913
+ this.log('Debug session verification error:', error);
1914
+ return false;
1915
+ }
1916
+ }
1917
+ /**
1918
+ * Initialize debug agent
1919
+ */
1920
+ initializeDebugAgent(sessionId) {
1921
+ if (typeof window === 'undefined')
1922
+ return;
1923
+ try {
1924
+ this.log('Loading debug agent module');
1925
+ Promise.resolve().then(() => __importStar(require('./debug-agent'))).then(({ DebugAgent }) => {
1926
+ try {
1927
+ this.debugAgent = new DebugAgent(this, sessionId, this.config.tenantId, this.config.apiUrl, {
1928
+ debug: this.config.debug,
1929
+ });
1930
+ this.log('Debug agent initialized');
1931
+ }
1932
+ catch (error) {
1933
+ this.log('Failed to initialize debug agent:', error);
1934
+ }
1935
+ }).catch((error) => {
1936
+ this.log('Failed to load debug agent module:', error);
1937
+ });
1938
+ }
1939
+ catch (error) {
1940
+ this.log('Error initializing debug agent:', error);
1941
+ }
1942
+ }
1821
1943
  /**
1822
1944
  * Destroy the client and clean up resources
1823
1945
  */
@@ -1857,6 +1979,11 @@ class GrainAnalytics {
1857
1979
  this.heatmapTrackingManager.destroy();
1858
1980
  this.heatmapTrackingManager = null;
1859
1981
  }
1982
+ // Destroy debug agent
1983
+ if (this.debugAgent) {
1984
+ this.debugAgent.destroy();
1985
+ this.debugAgent = null;
1986
+ }
1860
1987
  // Send any remaining events (in chunks if necessary)
1861
1988
  if (this.eventQueue.length > 0) {
1862
1989
  const eventsToSend = [...this.eventQueue];