@grainql/analytics-web 2.8.0 → 3.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.
Files changed (61) 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/id-manager.d.ts +66 -0
  7. package/dist/cjs/id-manager.d.ts.map +1 -0
  8. package/dist/cjs/id-manager.js +212 -0
  9. package/dist/cjs/id-manager.js.map +1 -0
  10. package/dist/cjs/index.d.ts +12 -8
  11. package/dist/cjs/index.d.ts.map +1 -1
  12. package/dist/cjs/index.js.map +1 -1
  13. package/dist/cjs/page-tracking.d.ts +6 -0
  14. package/dist/cjs/page-tracking.d.ts.map +1 -1
  15. package/dist/cjs/page-tracking.js +23 -2
  16. package/dist/cjs/page-tracking.js.map +1 -1
  17. package/dist/cjs/react/hooks/useConsent.d.ts +18 -2
  18. package/dist/cjs/react/hooks/useConsent.d.ts.map +1 -1
  19. package/dist/cjs/react/hooks/useConsent.js +52 -1
  20. package/dist/cjs/react/hooks/useConsent.js.map +1 -1
  21. package/dist/consent.d.ts +38 -7
  22. package/dist/consent.d.ts.map +1 -1
  23. package/dist/consent.js +82 -23
  24. package/dist/esm/consent.d.ts +38 -7
  25. package/dist/esm/consent.d.ts.map +1 -1
  26. package/dist/esm/consent.js +82 -23
  27. package/dist/esm/consent.js.map +1 -1
  28. package/dist/esm/id-manager.d.ts +66 -0
  29. package/dist/esm/id-manager.d.ts.map +1 -0
  30. package/dist/esm/id-manager.js +208 -0
  31. package/dist/esm/id-manager.js.map +1 -0
  32. package/dist/esm/index.d.ts +12 -8
  33. package/dist/esm/index.d.ts.map +1 -1
  34. package/dist/esm/index.js.map +1 -1
  35. package/dist/esm/page-tracking.d.ts +6 -0
  36. package/dist/esm/page-tracking.d.ts.map +1 -1
  37. package/dist/esm/page-tracking.js +23 -2
  38. package/dist/esm/page-tracking.js.map +1 -1
  39. package/dist/esm/react/hooks/useConsent.d.ts +18 -2
  40. package/dist/esm/react/hooks/useConsent.d.ts.map +1 -1
  41. package/dist/esm/react/hooks/useConsent.js +49 -1
  42. package/dist/esm/react/hooks/useConsent.js.map +1 -1
  43. package/dist/id-manager.d.ts +66 -0
  44. package/dist/id-manager.d.ts.map +1 -0
  45. package/dist/id-manager.js +212 -0
  46. package/dist/index.d.ts +12 -8
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.global.dev.js +310 -81
  49. package/dist/index.global.dev.js.map +4 -4
  50. package/dist/index.global.js +8 -8
  51. package/dist/index.global.js.map +4 -4
  52. package/dist/index.js +72 -44
  53. package/dist/index.mjs +73 -45
  54. package/dist/page-tracking.d.ts +6 -0
  55. package/dist/page-tracking.d.ts.map +1 -1
  56. package/dist/page-tracking.js +23 -2
  57. package/dist/react/hooks/useConsent.d.ts +18 -2
  58. package/dist/react/hooks/useConsent.d.ts.map +1 -1
  59. package/dist/react/hooks/useConsent.js +52 -1
  60. package/dist/react/hooks/useConsent.mjs +49 -1
  61. 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;
@@ -96,38 +97,37 @@ class GrainAnalytics {
96
97
  configCacheKey: 'grain_config',
97
98
  configRefreshInterval: 300000, // 5 minutes
98
99
  enableConfigCache: true,
99
- // Privacy defaults
100
- consentMode: 'opt-out',
100
+ // Privacy defaults (v2.0)
101
+ consentMode: 'cookieless', // Default: privacy-first, no permanent tracking
101
102
  waitForConsent: false,
102
- enableCookies: false,
103
- anonymizeIP: false,
104
103
  disableAutoProperties: false,
105
104
  // Automatic Tracking defaults
106
105
  enableHeartbeat: true,
107
106
  heartbeatActiveInterval: 120000, // 2 minutes
108
107
  heartbeatInactiveInterval: 300000, // 5 minutes
109
108
  enableAutoPageView: true,
110
- stripQueryParams: true,
109
+ stripQueryParams: true, // Privacy-first: strip by default
110
+ stripHash: false,
111
111
  // Heatmap Tracking defaults
112
112
  enableHeatmapTracking: true,
113
113
  ...config,
114
114
  tenantId: config.tenantId,
115
115
  };
116
- // Initialize consent manager
116
+ // Initialize consent manager (v2.0)
117
117
  this.consentManager = new consent_1.ConsentManager(this.config.tenantId, this.config.consentMode);
118
- // Check if cookies are enabled
119
- if (this.config.enableCookies) {
120
- this.cookiesEnabled = (0, cookies_1.areCookiesEnabled)();
121
- if (!this.cookiesEnabled && this.config.debug) {
122
- console.warn('[Grain Analytics] Cookies are not available, falling back to localStorage');
123
- }
124
- }
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
+ });
125
125
  // Set global userId if provided in config
126
126
  if (config.userId) {
127
127
  this.globalUserId = config.userId;
128
128
  }
129
129
  this.validateConfig();
130
- this.initializePersistentAnonymousUserId();
130
+ // Deprecated: initializePersistentAnonymousUserId() - now handled by IdManager
131
131
  this.setupBeforeUnload();
132
132
  this.startFlushTimer();
133
133
  this.initializeConfigCache();
@@ -145,8 +145,11 @@ class GrainAnalytics {
145
145
  this.initializeHeatmapTracking();
146
146
  }
147
147
  }
148
- // 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)
149
149
  this.consentManager.addListener((state) => {
150
+ // Sync IdManager with consent state
151
+ const idMode = this.consentManager.getIdMode();
152
+ this.idManager.setMode(idMode);
150
153
  if (state.granted) {
151
154
  this.handleConsentGranted();
152
155
  }
@@ -188,11 +191,14 @@ class GrainAnalytics {
188
191
  */
189
192
  shouldAllowPersistentStorage() {
190
193
  const hasConsent = this.consentManager.hasConsent('analytics');
191
- const isOptInMode = this.config.consentMode === 'opt-in';
194
+ const isCookieless = this.config.consentMode === 'cookieless';
192
195
  const userExplicitlyIdentified = !!this.globalUserId;
193
196
  const isJWTAuth = this.config.authStrategy === 'JWT';
197
+ // Never allow persistent storage in cookieless mode
198
+ if (isCookieless)
199
+ return false;
194
200
  // Allow persistent storage if any of these conditions are met
195
- return hasConsent || !isOptInMode || userExplicitlyIdentified || isJWTAuth;
201
+ return hasConsent || userExplicitlyIdentified || isJWTAuth;
196
202
  }
197
203
  /**
198
204
  * Generate a proper UUIDv4 identifier for anonymous user ID
@@ -286,23 +292,21 @@ class GrainAnalytics {
286
292
  }
287
293
  }
288
294
  /**
289
- * Get the effective user ID (global userId or persistent anonymous ID)
295
+ * Get the effective user ID (v2.0)
290
296
  *
291
- * GDPR Compliance: In opt-in mode without consent and no explicit user identification,
292
- * 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)
293
302
  */
294
303
  getEffectiveUserIdInternal() {
304
+ // Explicit user identification always takes precedence
295
305
  if (this.globalUserId) {
296
306
  return this.globalUserId;
297
307
  }
298
- if (this.persistentAnonymousUserId) {
299
- return this.persistentAnonymousUserId;
300
- }
301
- // Generate a new UUIDv4 identifier as fallback
302
- this.persistentAnonymousUserId = this.generateAnonymousUserId();
303
- // Try to persist it (will be skipped in opt-in mode without consent)
304
- this.savePersistentAnonymousUserId(this.persistentAnonymousUserId);
305
- return this.persistentAnonymousUserId;
308
+ // Use IdManager to generate appropriate ID based on consent
309
+ return this.idManager.getCurrentUserId();
306
310
  }
307
311
  log(...args) {
308
312
  if (this.config.debug) {
@@ -673,6 +677,7 @@ class GrainAnalytics {
673
677
  try {
674
678
  this.pageTrackingManager = new page_tracking_1.PageTrackingManager(this, {
675
679
  stripQueryParams: this.config.stripQueryParams,
680
+ stripHash: this.config.stripHash,
676
681
  debug: this.config.debug,
677
682
  tenantId: this.config.tenantId,
678
683
  });
@@ -970,12 +975,13 @@ class GrainAnalytics {
970
975
  return;
971
976
  const hasConsent = this.consentManager.hasConsent('analytics');
972
977
  // Create event with appropriate user ID
978
+ // v2.0: Always use IdManager which returns daily rotating ID or permanent ID based on consent
973
979
  const event = {
974
980
  eventName,
975
- userId: hasConsent ? this.getEffectiveUserId() : this.getEphemeralSessionId(),
981
+ userId: this.getEffectiveUserId(), // IdManager handles daily vs permanent based on consent
976
982
  properties: {
977
983
  ...properties,
978
- _minimal: !hasConsent, // Flag to indicate minimal tracking
984
+ _minimal: !hasConsent, // Flag to indicate minimal tracking (daily rotating ID)
979
985
  _consent_status: hasConsent ? 'granted' : 'pending',
980
986
  },
981
987
  };
@@ -1072,17 +1078,22 @@ class GrainAnalytics {
1072
1078
  event.properties = filtered;
1073
1079
  }
1074
1080
  const formattedEvent = this.formatEvent(event);
1075
- // Check consent before tracking
1081
+ // Check if we should wait for consent (only if explicitly configured)
1076
1082
  if (this.consentManager.shouldWaitForConsent() && this.config.waitForConsent) {
1077
1083
  // Queue event until consent is granted
1078
1084
  this.waitingForConsentQueue.push(formattedEvent);
1079
1085
  this.log(`Event waiting for consent: ${event.eventName}`, event.properties);
1080
1086
  return;
1081
1087
  }
1082
- if (!this.consentManager.hasConsent('analytics')) {
1083
- this.log(`Event blocked by consent: ${event.eventName}`);
1084
- return;
1085
- }
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
+ };
1086
1097
  this.eventQueue.push(formattedEvent);
1087
1098
  this.eventCountSinceLastHeartbeat++;
1088
1099
  this.sessionEventCount++;
@@ -1770,13 +1781,24 @@ class GrainAnalytics {
1770
1781
  }
1771
1782
  // Privacy & Consent Methods
1772
1783
  /**
1773
- * Grant consent for tracking
1784
+ * Grant consent for tracking (v2.0)
1785
+ * Switches from cookie-less mode to permanent IDs
1774
1786
  * @param categories - Optional array of consent categories (e.g., ['analytics', 'functional'])
1775
1787
  */
1776
1788
  grantConsent(categories) {
1777
1789
  try {
1778
1790
  this.consentManager.grantConsent(categories);
1779
- 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
+ }
1780
1802
  }
1781
1803
  catch (error) {
1782
1804
  const formattedError = this.formatError(error, 'grantConsent');
@@ -1784,16 +1806,22 @@ class GrainAnalytics {
1784
1806
  }
1785
1807
  }
1786
1808
  /**
1787
- * Revoke consent for tracking (opt-out)
1809
+ * Revoke consent for tracking (v2.0)
1810
+ * Switches from permanent IDs to cookie-less mode
1788
1811
  * @param categories - Optional array of categories to revoke (if not provided, revokes all)
1789
1812
  */
1790
1813
  revokeConsent(categories) {
1791
1814
  try {
1792
1815
  this.consentManager.revokeConsent(categories);
1793
- this.log('Consent revoked', categories);
1794
- // Clear queued events when consent is revoked
1795
- this.eventQueue = [];
1796
- 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
+ }
1797
1825
  }
1798
1826
  catch (error) {
1799
1827
  const formattedError = this.formatError(error, 'revokeConsent');
package/dist/index.mjs CHANGED
@@ -3,10 +3,11 @@
3
3
  * A lightweight, dependency-free TypeScript SDK for sending analytics events to Grain's REST API
4
4
  */
5
5
  import { ConsentManager } from './consent.js';
6
- import { setCookie, getCookie, areCookiesEnabled } from './cookies.js';
6
+ import { setCookie, getCookie } from './cookies.js';
7
7
  import { ActivityDetector } from './activity.js';
8
8
  import { HeartbeatManager } from './heartbeat.js';
9
9
  import { PageTrackingManager } from './page-tracking.js';
10
+ import { IdManager } from './id-manager.js';
10
11
  import { categorizeReferrer, parseUTMParameters, getOrCreateFirstTouchAttribution, getSessionUTMParameters, getFirstTouchAttribution, } from './attribution.js';
11
12
  export { categorizeReferrer, parseUTMParameters };
12
13
  // Re-export timezone-country utilities
@@ -18,13 +19,13 @@ export class GrainAnalytics {
18
19
  this.flushTimer = null;
19
20
  this.isDestroyed = false;
20
21
  this.globalUserId = null;
21
- this.persistentAnonymousUserId = null;
22
+ this.persistentAnonymousUserId = null; // Deprecated: use idManager instead
22
23
  // Remote Config properties
23
24
  this.configCache = null;
24
25
  this.configRefreshTimer = null;
25
26
  this.configChangeListeners = [];
26
27
  this.configFetchPromise = null;
27
- this.cookiesEnabled = false;
28
+ this.cookiesEnabled = false; // Deprecated: cookies no longer used for IDs
28
29
  // Automatic Tracking properties
29
30
  this.activityDetector = null;
30
31
  this.heartbeatManager = null;
@@ -55,38 +56,37 @@ export class GrainAnalytics {
55
56
  configCacheKey: 'grain_config',
56
57
  configRefreshInterval: 300000, // 5 minutes
57
58
  enableConfigCache: true,
58
- // Privacy defaults
59
- consentMode: 'opt-out',
59
+ // Privacy defaults (v2.0)
60
+ consentMode: 'cookieless', // Default: privacy-first, no permanent tracking
60
61
  waitForConsent: false,
61
- enableCookies: false,
62
- anonymizeIP: false,
63
62
  disableAutoProperties: false,
64
63
  // Automatic Tracking defaults
65
64
  enableHeartbeat: true,
66
65
  heartbeatActiveInterval: 120000, // 2 minutes
67
66
  heartbeatInactiveInterval: 300000, // 5 minutes
68
67
  enableAutoPageView: true,
69
- stripQueryParams: true,
68
+ stripQueryParams: true, // Privacy-first: strip by default
69
+ stripHash: false,
70
70
  // Heatmap Tracking defaults
71
71
  enableHeatmapTracking: true,
72
72
  ...config,
73
73
  tenantId: config.tenantId,
74
74
  };
75
- // Initialize consent manager
75
+ // Initialize consent manager (v2.0)
76
76
  this.consentManager = new ConsentManager(this.config.tenantId, this.config.consentMode);
77
- // Check if cookies are enabled
78
- if (this.config.enableCookies) {
79
- this.cookiesEnabled = areCookiesEnabled();
80
- if (!this.cookiesEnabled && this.config.debug) {
81
- console.warn('[Grain Analytics] Cookies are not available, falling back to localStorage');
82
- }
83
- }
77
+ // Initialize ID manager (v2.0)
78
+ const idMode = this.consentManager.getIdMode();
79
+ this.idManager = new IdManager({
80
+ mode: idMode,
81
+ tenantId: this.config.tenantId,
82
+ useLocalStorage: true, // For permanent IDs when consented
83
+ });
84
84
  // Set global userId if provided in config
85
85
  if (config.userId) {
86
86
  this.globalUserId = config.userId;
87
87
  }
88
88
  this.validateConfig();
89
- this.initializePersistentAnonymousUserId();
89
+ // Deprecated: initializePersistentAnonymousUserId() - now handled by IdManager
90
90
  this.setupBeforeUnload();
91
91
  this.startFlushTimer();
92
92
  this.initializeConfigCache();
@@ -104,8 +104,11 @@ export class GrainAnalytics {
104
104
  this.initializeHeatmapTracking();
105
105
  }
106
106
  }
107
- // Set up consent change listener to flush waiting events and handle consent upgrade
107
+ // Set up consent change listener to sync IdManager and flush waiting events (v2.0)
108
108
  this.consentManager.addListener((state) => {
109
+ // Sync IdManager with consent state
110
+ const idMode = this.consentManager.getIdMode();
111
+ this.idManager.setMode(idMode);
109
112
  if (state.granted) {
110
113
  this.handleConsentGranted();
111
114
  }
@@ -147,11 +150,14 @@ export class GrainAnalytics {
147
150
  */
148
151
  shouldAllowPersistentStorage() {
149
152
  const hasConsent = this.consentManager.hasConsent('analytics');
150
- const isOptInMode = this.config.consentMode === 'opt-in';
153
+ const isCookieless = this.config.consentMode === 'cookieless';
151
154
  const userExplicitlyIdentified = !!this.globalUserId;
152
155
  const isJWTAuth = this.config.authStrategy === 'JWT';
156
+ // Never allow persistent storage in cookieless mode
157
+ if (isCookieless)
158
+ return false;
153
159
  // Allow persistent storage if any of these conditions are met
154
- return hasConsent || !isOptInMode || userExplicitlyIdentified || isJWTAuth;
160
+ return hasConsent || userExplicitlyIdentified || isJWTAuth;
155
161
  }
156
162
  /**
157
163
  * Generate a proper UUIDv4 identifier for anonymous user ID
@@ -245,23 +251,21 @@ export class GrainAnalytics {
245
251
  }
246
252
  }
247
253
  /**
248
- * Get the effective user ID (global userId or persistent anonymous ID)
254
+ * Get the effective user ID (v2.0)
249
255
  *
250
- * GDPR Compliance: In opt-in mode without consent and no explicit user identification,
251
- * this should not be called. Use getEphemeralSessionId() instead.
256
+ * Privacy-first implementation:
257
+ * - Returns global userId if explicitly set (via identify/login)
258
+ * - Otherwise uses IdManager to generate:
259
+ * - Daily rotating ID (cookieless mode)
260
+ * - Permanent ID (with consent)
252
261
  */
253
262
  getEffectiveUserIdInternal() {
263
+ // Explicit user identification always takes precedence
254
264
  if (this.globalUserId) {
255
265
  return this.globalUserId;
256
266
  }
257
- if (this.persistentAnonymousUserId) {
258
- return this.persistentAnonymousUserId;
259
- }
260
- // Generate a new UUIDv4 identifier as fallback
261
- this.persistentAnonymousUserId = this.generateAnonymousUserId();
262
- // Try to persist it (will be skipped in opt-in mode without consent)
263
- this.savePersistentAnonymousUserId(this.persistentAnonymousUserId);
264
- return this.persistentAnonymousUserId;
267
+ // Use IdManager to generate appropriate ID based on consent
268
+ return this.idManager.getCurrentUserId();
265
269
  }
266
270
  log(...args) {
267
271
  if (this.config.debug) {
@@ -632,6 +636,7 @@ export class GrainAnalytics {
632
636
  try {
633
637
  this.pageTrackingManager = new PageTrackingManager(this, {
634
638
  stripQueryParams: this.config.stripQueryParams,
639
+ stripHash: this.config.stripHash,
635
640
  debug: this.config.debug,
636
641
  tenantId: this.config.tenantId,
637
642
  });
@@ -929,12 +934,13 @@ export class GrainAnalytics {
929
934
  return;
930
935
  const hasConsent = this.consentManager.hasConsent('analytics');
931
936
  // Create event with appropriate user ID
937
+ // v2.0: Always use IdManager which returns daily rotating ID or permanent ID based on consent
932
938
  const event = {
933
939
  eventName,
934
- userId: hasConsent ? this.getEffectiveUserId() : this.getEphemeralSessionId(),
940
+ userId: this.getEffectiveUserId(), // IdManager handles daily vs permanent based on consent
935
941
  properties: {
936
942
  ...properties,
937
- _minimal: !hasConsent, // Flag to indicate minimal tracking
943
+ _minimal: !hasConsent, // Flag to indicate minimal tracking (daily rotating ID)
938
944
  _consent_status: hasConsent ? 'granted' : 'pending',
939
945
  },
940
946
  };
@@ -1031,17 +1037,22 @@ export class GrainAnalytics {
1031
1037
  event.properties = filtered;
1032
1038
  }
1033
1039
  const formattedEvent = this.formatEvent(event);
1034
- // Check consent before tracking
1040
+ // Check if we should wait for consent (only if explicitly configured)
1035
1041
  if (this.consentManager.shouldWaitForConsent() && this.config.waitForConsent) {
1036
1042
  // Queue event until consent is granted
1037
1043
  this.waitingForConsentQueue.push(formattedEvent);
1038
1044
  this.log(`Event waiting for consent: ${event.eventName}`, event.properties);
1039
1045
  return;
1040
1046
  }
1041
- if (!this.consentManager.hasConsent('analytics')) {
1042
- this.log(`Event blocked by consent: ${event.eventName}`);
1043
- return;
1044
- }
1047
+ // v2.0: GDPR Strict falls back to cookie-less mode (daily rotating IDs)
1048
+ // Events are never blocked - IdManager already provides correct ID (daily or permanent)
1049
+ const hasConsent = this.consentManager.hasConsent('analytics');
1050
+ // Add tracking flags to indicate consent status
1051
+ formattedEvent.properties = {
1052
+ ...formattedEvent.properties,
1053
+ _minimal: !hasConsent, // Flag: true = daily rotating ID, false = permanent ID
1054
+ _consent_status: hasConsent ? 'granted' : 'pending',
1055
+ };
1045
1056
  this.eventQueue.push(formattedEvent);
1046
1057
  this.eventCountSinceLastHeartbeat++;
1047
1058
  this.sessionEventCount++;
@@ -1729,13 +1740,24 @@ export class GrainAnalytics {
1729
1740
  }
1730
1741
  // Privacy & Consent Methods
1731
1742
  /**
1732
- * Grant consent for tracking
1743
+ * Grant consent for tracking (v2.0)
1744
+ * Switches from cookie-less mode to permanent IDs
1733
1745
  * @param categories - Optional array of consent categories (e.g., ['analytics', 'functional'])
1734
1746
  */
1735
1747
  grantConsent(categories) {
1736
1748
  try {
1737
1749
  this.consentManager.grantConsent(categories);
1738
- this.log('Consent granted', categories);
1750
+ // Sync ID manager with consent state
1751
+ const idMode = this.consentManager.getIdMode();
1752
+ this.idManager.setMode(idMode);
1753
+ this.log('Consent granted, switched to permanent IDs', categories);
1754
+ // Process any queued events waiting for consent
1755
+ if (this.waitingForConsentQueue.length > 0) {
1756
+ this.log(`Processing ${this.waitingForConsentQueue.length} queued events`);
1757
+ this.eventQueue.push(...this.waitingForConsentQueue);
1758
+ this.waitingForConsentQueue = [];
1759
+ this.flush();
1760
+ }
1739
1761
  }
1740
1762
  catch (error) {
1741
1763
  const formattedError = this.formatError(error, 'grantConsent');
@@ -1743,16 +1765,22 @@ export class GrainAnalytics {
1743
1765
  }
1744
1766
  }
1745
1767
  /**
1746
- * Revoke consent for tracking (opt-out)
1768
+ * Revoke consent for tracking (v2.0)
1769
+ * Switches from permanent IDs to cookie-less mode
1747
1770
  * @param categories - Optional array of categories to revoke (if not provided, revokes all)
1748
1771
  */
1749
1772
  revokeConsent(categories) {
1750
1773
  try {
1751
1774
  this.consentManager.revokeConsent(categories);
1752
- this.log('Consent revoked', categories);
1753
- // Clear queued events when consent is revoked
1754
- this.eventQueue = [];
1755
- this.waitingForConsentQueue = [];
1775
+ // Sync ID manager with consent state
1776
+ const idMode = this.consentManager.getIdMode();
1777
+ this.idManager.setMode(idMode);
1778
+ this.log('Consent revoked, switched to cookie-less mode', categories);
1779
+ // Clear queued events when consent is fully revoked
1780
+ if (!this.consentManager.hasConsent()) {
1781
+ this.eventQueue = [];
1782
+ this.waitingForConsentQueue = [];
1783
+ }
1756
1784
  }
1757
1785
  catch (error) {
1758
1786
  const formattedError = this.formatError(error, 'revokeConsent');
@@ -4,6 +4,7 @@
4
4
  */
5
5
  export interface PageTrackingConfig {
6
6
  stripQueryParams: boolean;
7
+ stripHash?: boolean;
7
8
  debug?: boolean;
8
9
  tenantId: string;
9
10
  }
@@ -63,8 +64,13 @@ export declare class PageTrackingManager {
63
64
  private getDeviceType;
64
65
  /**
65
66
  * Extract path from URL, optionally stripping query parameters
67
+ * Privacy-first: strips query params by default
66
68
  */
67
69
  private extractPath;
70
+ /**
71
+ * Clean URL for privacy (strip query params based on config)
72
+ */
73
+ private cleanUrl;
68
74
  /**
69
75
  * Get the current page path
70
76
  */
@@ -1 +1 @@
1
- {"version":3,"file":"page-tracking.d.ts","sourceRoot":"","sources":["../src/page-tracking.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAaH,MAAM,WAAW,kBAAkB;IACjC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC/E,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACvC,kBAAkB,IAAI,MAAM,CAAC;IAC7B,qBAAqB,IAAI,MAAM,CAAC;IAChC,YAAY,IAAI,MAAM,CAAC;CACxB;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,iBAAiB,CAAyC;IAClE,OAAO,CAAC,oBAAoB,CAA4C;IACxE,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,aAAa,CAAK;gBAEd,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,kBAAkB;IAY5D;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAqB7B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAK/B;;OAEG;IACH,OAAO,CAAC,cAAc,CAGpB;IAEF;;OAEG;IACH,OAAO,CAAC,gBAAgB,CAGtB;IAEF;;OAEG;IACH,OAAO,CAAC,gBAAgB;IA4GxB;;OAEG;IACH,OAAO,CAAC,aAAa;IASrB;;OAEG;IACH,OAAO,CAAC,UAAU;IAUlB;;OAEG;IACH,OAAO,CAAC,KAAK;IAUb;;OAEG;IACH,OAAO,CAAC,aAAa;IAwBrB;;OAEG;IACH,OAAO,CAAC,WAAW;IAmBnB;;OAEG;IACH,cAAc,IAAI,MAAM,GAAG,IAAI;IAI/B;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAqCnE;;OAEG;IACH,gBAAgB,IAAI,MAAM;IAI1B;;OAEG;IACH,OAAO,IAAI,IAAI;CAqBhB"}
1
+ {"version":3,"file":"page-tracking.d.ts","sourceRoot":"","sources":["../src/page-tracking.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAaH,MAAM,WAAW,kBAAkB;IACjC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC/E,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACvC,kBAAkB,IAAI,MAAM,CAAC;IAC7B,qBAAqB,IAAI,MAAM,CAAC;IAChC,YAAY,IAAI,MAAM,CAAC;CACxB;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,iBAAiB,CAAyC;IAClE,OAAO,CAAC,oBAAoB,CAA4C;IACxE,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,aAAa,CAAK;gBAEd,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,kBAAkB;IAY5D;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAqB7B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAK/B;;OAEG;IACH,OAAO,CAAC,cAAc,CAGpB;IAEF;;OAEG;IACH,OAAO,CAAC,gBAAgB,CAGtB;IAEF;;OAEG;IACH,OAAO,CAAC,gBAAgB;IA4GxB;;OAEG;IACH,OAAO,CAAC,aAAa;IASrB;;OAEG;IACH,OAAO,CAAC,UAAU;IAUlB;;OAEG;IACH,OAAO,CAAC,KAAK;IAUb;;OAEG;IACH,OAAO,CAAC,aAAa;IAwBrB;;;OAGG;IACH,OAAO,CAAC,WAAW;IAyBnB;;OAEG;IACH,OAAO,CAAC,QAAQ;IAahB;;OAEG;IACH,cAAc,IAAI,MAAM,GAAG,IAAI;IAI/B;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAqCnE;;OAEG;IACH,gBAAgB,IAAI,MAAM;IAI1B;;OAEG;IACH,OAAO,IAAI,IAAI;CAqBhB"}
@@ -113,7 +113,7 @@ class PageTrackingManager {
113
113
  // Enhanced properties when consent is granted
114
114
  if (hasConsent) {
115
115
  properties.title = document.title || '';
116
- properties.full_url = currentUrl;
116
+ properties.full_url = this.cleanUrl(currentUrl); // Clean URL based on privacy settings
117
117
  properties.session_id = this.tracker.getSessionId();
118
118
  // Add referrer info
119
119
  if (referrer) {
@@ -231,14 +231,20 @@ class PageTrackingManager {
231
231
  }
232
232
  /**
233
233
  * Extract path from URL, optionally stripping query parameters
234
+ * Privacy-first: strips query params by default
234
235
  */
235
236
  extractPath(url) {
236
237
  try {
237
238
  const urlObj = new URL(url);
238
- let path = urlObj.pathname + urlObj.hash;
239
+ let path = urlObj.pathname;
240
+ // Include query params only if not stripping
239
241
  if (!this.config.stripQueryParams && urlObj.search) {
240
242
  path += urlObj.search;
241
243
  }
244
+ // Include hash only if not stripping
245
+ if (!this.config.stripHash && urlObj.hash) {
246
+ path += urlObj.hash;
247
+ }
242
248
  return path;
243
249
  }
244
250
  catch (error) {
@@ -249,6 +255,21 @@ class PageTrackingManager {
249
255
  return url;
250
256
  }
251
257
  }
258
+ /**
259
+ * Clean URL for privacy (strip query params based on config)
260
+ */
261
+ cleanUrl(url) {
262
+ if (!this.config.stripQueryParams) {
263
+ return url;
264
+ }
265
+ try {
266
+ const urlObj = new URL(url);
267
+ return `${urlObj.origin}${urlObj.pathname}${this.config.stripHash ? '' : urlObj.hash}`;
268
+ }
269
+ catch (error) {
270
+ return url;
271
+ }
272
+ }
252
273
  /**
253
274
  * Get the current page path
254
275
  */
@@ -1,7 +1,8 @@
1
1
  /**
2
- * useConsent - Hook for managing user consent
2
+ * useConsent - Hook for managing user consent (v2.0)
3
+ * Updated for new consent modes: cookieless, gdpr-strict, gdpr-opt-out
3
4
  */
4
- import type { ConsentState } from '../../consent';
5
+ import type { ConsentState, ConsentMode } from '../../consent';
5
6
  export declare function useConsent(): {
6
7
  consentState: ConsentState;
7
8
  grantConsent: (categories?: string[]) => void;
@@ -10,4 +11,19 @@ export declare function useConsent(): {
10
11
  isGranted: boolean;
11
12
  categories: string[];
12
13
  };
14
+ /**
15
+ * useConsentMode - Hook to get current consent mode
16
+ * v2.0: Returns 'cookieless' | 'gdpr-strict' | 'gdpr-opt-out'
17
+ */
18
+ export declare function useConsentMode(): ConsentMode | null;
19
+ /**
20
+ * useTrackingId - Hook to get current tracking ID
21
+ * v2.0: Returns daily rotating ID or permanent ID based on consent
22
+ */
23
+ export declare function useTrackingId(): string | null;
24
+ /**
25
+ * useCanTrack - Hook to check if tracking is allowed
26
+ * v2.0: Always returns true (even cookieless mode allows basic tracking)
27
+ */
28
+ export declare function useCanTrack(): boolean;
13
29
  //# sourceMappingURL=useConsent.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useConsent.d.ts","sourceRoot":"","sources":["../../../../src/react/hooks/useConsent.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,wBAAgB,UAAU;;gCAwBR,MAAM,EAAE;iCASR,MAAM,EAAE;4BASV,MAAM;;;EAerB"}
1
+ {"version":3,"file":"useConsent.d.ts","sourceRoot":"","sources":["../../../../src/react/hooks/useConsent.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE/D,wBAAgB,UAAU;;gCAwBR,MAAM,EAAE;iCASR,MAAM,EAAE;4BASV,MAAM;;;EAerB;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,WAAW,GAAG,IAAI,CAanD;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,MAAM,GAAG,IAAI,CAiB7C;AAED;;;GAGG;AACH,wBAAgB,WAAW,IAAI,OAAO,CAKrC"}