@grainql/analytics-web 2.8.0 → 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 (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
@@ -1,4 +1,4 @@
1
- /* Grain Analytics Web SDK v2.8.0 | MIT License | Development Build */
1
+ /* Grain Analytics Web SDK v2.9.0 | MIT License | Development Build */
2
2
  "use strict";
3
3
  var Grain = (() => {
4
4
  var __defProp = Object.defineProperty;
@@ -2700,7 +2700,7 @@ var Grain = (() => {
2700
2700
  var DEFAULT_CONSENT_CATEGORIES = ["necessary", "analytics", "functional"];
2701
2701
  var CONSENT_VERSION = "1.0.0";
2702
2702
  var ConsentManager = class {
2703
- constructor(tenantId, consentMode = "opt-out") {
2703
+ constructor(tenantId, consentMode = "cookieless") {
2704
2704
  this.consentState = null;
2705
2705
  this.listeners = [];
2706
2706
  this.consentMode = consentMode;
@@ -2710,9 +2710,8 @@ var Grain = (() => {
2710
2710
  /**
2711
2711
  * Load consent state from localStorage
2712
2712
  *
2713
- * GDPR Compliance: In opt-in mode, we can use localStorage for consent preferences
2714
- * since storing consent choices is a legitimate interest and necessary for compliance.
2715
- * The consent preference itself is not tracking data.
2713
+ * GDPR Compliance: localStorage only used for storing consent preferences
2714
+ * (not for tracking), which is a legitimate interest for compliance.
2716
2715
  */
2717
2716
  loadConsentState() {
2718
2717
  if (typeof window === "undefined")
@@ -2725,7 +2724,7 @@ var Grain = (() => {
2725
2724
  ...parsed,
2726
2725
  timestamp: new Date(parsed.timestamp)
2727
2726
  };
2728
- } else if (this.consentMode === "opt-out" || this.consentMode === "disabled") {
2727
+ } else if (this.consentMode === "gdpr-opt-out") {
2729
2728
  this.consentState = {
2730
2729
  granted: true,
2731
2730
  categories: DEFAULT_CONSENT_CATEGORIES,
@@ -2798,28 +2797,67 @@ var Grain = (() => {
2798
2797
  return this.consentState ? { ...this.consentState } : null;
2799
2798
  }
2800
2799
  /**
2801
- * Check if user has granted consent
2800
+ * Check if user has granted consent for permanent IDs
2802
2801
  */
2803
2802
  hasConsent(category) {
2804
- if (this.consentMode === "disabled") {
2805
- return true;
2806
- }
2807
- if (this.consentMode === "opt-in" && !this.consentState) {
2803
+ if (this.consentMode === "cookieless") {
2808
2804
  return false;
2809
2805
  }
2810
- if (!this.consentState?.granted) {
2811
- return false;
2806
+ if (this.consentMode === "gdpr-strict") {
2807
+ if (!this.consentState?.granted) {
2808
+ return false;
2809
+ }
2812
2810
  }
2813
- if (category) {
2811
+ if (this.consentMode === "gdpr-opt-out") {
2812
+ if (!this.consentState) {
2813
+ return true;
2814
+ }
2815
+ if (!this.consentState.granted) {
2816
+ return false;
2817
+ }
2818
+ }
2819
+ if (category && this.consentState) {
2814
2820
  return this.consentState.categories.includes(category);
2815
2821
  }
2822
+ return this.consentState?.granted ?? this.consentMode === "gdpr-opt-out";
2823
+ }
2824
+ /**
2825
+ * Check if permanent IDs are allowed
2826
+ */
2827
+ shouldUsePermanentId() {
2828
+ return this.hasConsent();
2829
+ }
2830
+ /**
2831
+ * Check if we should strip query parameters from URLs
2832
+ * Query params stripped unless:
2833
+ * - Mode is gdpr-opt-out, OR
2834
+ * - Mode is gdpr-strict AND consent given
2835
+ */
2836
+ shouldStripQueryParams() {
2837
+ if (this.consentMode === "cookieless") {
2838
+ return true;
2839
+ }
2840
+ if (this.consentMode === "gdpr-strict") {
2841
+ return !this.hasConsent();
2842
+ }
2843
+ if (this.consentMode === "gdpr-opt-out") {
2844
+ return false;
2845
+ }
2846
+ return true;
2847
+ }
2848
+ /**
2849
+ * Check if we can track events (always true in v2.0)
2850
+ * Even cookieless mode allows basic analytics with daily IDs
2851
+ */
2852
+ canTrack() {
2816
2853
  return true;
2817
2854
  }
2818
2855
  /**
2819
2856
  * Check if we should wait for consent before tracking
2857
+ * Only relevant for GDPR Strict mode
2820
2858
  */
2821
2859
  shouldWaitForConsent() {
2822
- return this.consentMode === "opt-in" && !this.consentState?.granted;
2860
+ return this.consentMode === "gdpr-strict" && !this.consentState?.granted;
2823
2861
  }
2824
2862
  /**
2825
2863
  * Add consent change listener
@@ -2861,6 +2899,19 @@ var Grain = (() => {
2861
2899
  } catch (error) {
2862
2900
  }
2863
2901
  }
2902
+ /**
2903
+ * Get current consent mode
2904
+ */
2905
+ getConsentMode() {
2906
+ return this.consentMode;
2907
+ }
2908
+ /**
2909
+ * Get ID mode based on consent state
2910
+ * Returns 'cookieless' or 'permanent'
2911
+ */
2912
+ getIdMode() {
2913
+ return this.shouldUsePermanentId() ? "permanent" : "cookieless";
2914
+ }
2864
2915
  };
2865
2916
 
2866
2917
  // src/cookies.ts
@@ -2903,36 +2954,6 @@ var Grain = (() => {
2903
2954
  }
2904
2955
  return null;
2905
2956
  }
2906
- function deleteCookie(name, config) {
2907
- if (typeof document === "undefined")
2908
- return;
2909
- const parts = [
2910
- `${encodeURIComponent(name)}=`,
2911
- "max-age=0"
2912
- ];
2913
- if (config?.domain) {
2914
- parts.push(`domain=${config.domain}`);
2915
- }
2916
- if (config?.path) {
2917
- parts.push(`path=${config.path}`);
2918
- } else {
2919
- parts.push("path=/");
2920
- }
2921
- document.cookie = parts.join("; ");
2922
- }
2923
- function areCookiesEnabled() {
2924
- if (typeof document === "undefined")
2925
- return false;
2926
- try {
2927
- const testCookie = "_grain_cookie_test";
2928
- setCookie(testCookie, "test", { maxAge: 1 });
2929
- const result = getCookie(testCookie) === "test";
2930
- deleteCookie(testCookie);
2931
- return result;
2932
- } catch {
2933
- return false;
2934
- }
2935
- }
2936
2957
 
2937
2958
  // src/activity.ts
2938
2959
  var ActivityDetector = class {
@@ -6312,7 +6333,7 @@ var Grain = (() => {
6312
6333
  };
6313
6334
  if (hasConsent) {
6314
6335
  properties.title = document.title || "";
6315
- properties.full_url = currentUrl;
6336
+ properties.full_url = this.cleanUrl(currentUrl);
6316
6337
  properties.session_id = this.tracker.getSessionId();
6317
6338
  if (referrer) {
6318
6339
  properties.referrer = referrer;
@@ -6417,14 +6438,18 @@ var Grain = (() => {
6417
6438
  }
6418
6439
  /**
6419
6440
  * Extract path from URL, optionally stripping query parameters
6441
+ * Privacy-first: strips query params by default
6420
6442
  */
6421
6443
  extractPath(url) {
6422
6444
  try {
6423
6445
  const urlObj = new URL(url);
6424
- let path = urlObj.pathname + urlObj.hash;
6446
+ let path = urlObj.pathname;
6425
6447
  if (!this.config.stripQueryParams && urlObj.search) {
6426
6448
  path += urlObj.search;
6427
6449
  }
6450
+ if (!this.config.stripHash && urlObj.hash) {
6451
+ path += urlObj.hash;
6452
+ }
6428
6453
  return path;
6429
6454
  } catch (error) {
6430
6455
  if (this.config.debug) {
@@ -6433,6 +6458,20 @@ var Grain = (() => {
6433
6458
  return url;
6434
6459
  }
6435
6460
  }
6461
+ /**
6462
+ * Clean URL for privacy (strip query params based on config)
6463
+ */
6464
+ cleanUrl(url) {
6465
+ if (!this.config.stripQueryParams) {
6466
+ return url;
6467
+ }
6468
+ try {
6469
+ const urlObj = new URL(url);
6470
+ return `${urlObj.origin}${urlObj.pathname}${this.config.stripHash ? "" : urlObj.hash}`;
6471
+ } catch (error) {
6472
+ return url;
6473
+ }
6474
+ }
6436
6475
  /**
6437
6476
  * Get the current page path
6438
6477
  */
@@ -6501,6 +6540,172 @@ var Grain = (() => {
6501
6540
  }
6502
6541
  };
6503
6542
 
6543
+ // src/id-manager.ts
6544
+ function simpleHash(str) {
6545
+ let hash = 0;
6546
+ for (let i = 0; i < str.length; i++) {
6547
+ const char = str.charCodeAt(i);
6548
+ hash = (hash << 5) - hash + char;
6549
+ hash = hash & hash;
6550
+ }
6551
+ return Math.abs(hash).toString(36);
6552
+ }
6553
+ function generateUUID() {
6554
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
6555
+ return crypto.randomUUID();
6556
+ }
6557
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
6558
+ const r = Math.random() * 16 | 0;
6559
+ const v = c === "x" ? r : r & 3 | 8;
6560
+ return v.toString(16);
6561
+ });
6562
+ }
6563
+ function getBrowserFingerprint() {
6564
+ if (typeof window === "undefined")
6565
+ return "server";
6566
+ const components = [
6567
+ screen.width?.toString() || "",
6568
+ screen.height?.toString() || "",
6569
+ navigator.language || "",
6570
+ Intl.DateTimeFormat().resolvedOptions().timeZone || ""
6571
+ ];
6572
+ return simpleHash(components.join("|"));
6573
+ }
6574
+ function getLocalDateString() {
6575
+ const now = /* @__PURE__ */ new Date();
6576
+ const year = now.getFullYear();
6577
+ const month = String(now.getMonth() + 1).padStart(2, "0");
6578
+ const day = String(now.getDate()).padStart(2, "0");
6579
+ return `${year}-${month}-${day}`;
6580
+ }
6581
+ var IdManager = class {
6582
+ constructor(config) {
6583
+ this.cachedDailyId = null;
6584
+ this.dailyIdDate = null;
6585
+ this.permanentId = null;
6586
+ this.config = config;
6587
+ if (config.mode === "permanent" && config.useLocalStorage) {
6588
+ this.loadPermanentId();
6589
+ }
6590
+ }
6591
+ /**
6592
+ * Generate a daily rotating ID
6593
+ * Rotates at midnight in user's local timezone
6594
+ * Provides same-day continuity without persistent tracking
6595
+ */
6596
+ generateDailyRotatingId() {
6597
+ const currentDate = getLocalDateString();
6598
+ if (this.cachedDailyId && this.dailyIdDate === currentDate) {
6599
+ return this.cachedDailyId;
6600
+ }
6601
+ const fingerprint = getBrowserFingerprint();
6602
+ const seed = `${this.config.tenantId}|${currentDate}|${fingerprint}`;
6603
+ const dailyId = `daily_${simpleHash(seed)}_${simpleHash(Date.now().toString())}`;
6604
+ this.cachedDailyId = dailyId;
6605
+ this.dailyIdDate = currentDate;
6606
+ return dailyId;
6607
+ }
6608
+ /**
6609
+ * Generate or retrieve permanent user ID
6610
+ * Only used when consent is given
6611
+ */
6612
+ generatePermanentId() {
6613
+ if (this.permanentId) {
6614
+ return this.permanentId;
6615
+ }
6616
+ if (this.config.useLocalStorage) {
6617
+ const stored = this.loadPermanentId();
6618
+ if (stored) {
6619
+ return stored;
6620
+ }
6621
+ }
6622
+ const newId = generateUUID();
6623
+ this.permanentId = newId;
6624
+ if (this.config.useLocalStorage) {
6625
+ this.savePermanentId(newId);
6626
+ }
6627
+ return newId;
6628
+ }
6629
+ /**
6630
+ * Get the current user ID based on mode
6631
+ */
6632
+ getCurrentUserId() {
6633
+ if (this.config.mode === "cookieless") {
6634
+ return this.generateDailyRotatingId();
6635
+ } else {
6636
+ return this.generatePermanentId();
6637
+ }
6638
+ }
6639
+ /**
6640
+ * Switch ID mode (e.g., when consent is granted/revoked)
6641
+ */
6642
+ setMode(mode) {
6643
+ this.config.mode = mode;
6644
+ if (mode === "permanent") {
6645
+ this.cachedDailyId = null;
6646
+ this.dailyIdDate = null;
6647
+ }
6648
+ if (mode === "cookieless") {
6649
+ this.permanentId = null;
6650
+ if (this.config.useLocalStorage) {
6651
+ this.clearPermanentId();
6652
+ }
6653
+ }
6654
+ }
6655
+ /**
6656
+ * Load permanent ID from localStorage
6657
+ */
6658
+ loadPermanentId() {
6659
+ if (typeof window === "undefined")
6660
+ return null;
6661
+ try {
6662
+ const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
6663
+ const stored = localStorage.getItem(storageKey);
6664
+ if (stored) {
6665
+ this.permanentId = stored;
6666
+ return stored;
6667
+ }
6668
+ } catch (error) {
6669
+ }
6670
+ return null;
6671
+ }
6672
+ /**
6673
+ * Save permanent ID to localStorage
6674
+ */
6675
+ savePermanentId(id) {
6676
+ if (typeof window === "undefined")
6677
+ return;
6678
+ try {
6679
+ const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
6680
+ localStorage.setItem(storageKey, id);
6681
+ } catch (error) {
6682
+ }
6683
+ }
6684
+ /**
6685
+ * Clear permanent ID from localStorage
6686
+ */
6687
+ clearPermanentId() {
6688
+ if (typeof window === "undefined")
6689
+ return;
6690
+ try {
6691
+ const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
6692
+ localStorage.removeItem(storageKey);
6693
+ } catch (error) {
6694
+ }
6695
+ }
6696
+ /**
6697
+ * Get info about current ID for debugging
6698
+ */
6699
+ getIdInfo() {
6700
+ const id = this.getCurrentUserId();
6701
+ return {
6702
+ mode: this.config.mode,
6703
+ id,
6704
+ isDailyRotating: id.startsWith("daily_")
6705
+ };
6706
+ }
6707
+ };
6708
+
6504
6709
  // src/index.ts
6505
6710
  var GrainAnalytics = class {
6506
6711
  constructor(config) {
@@ -6510,12 +6715,14 @@ var Grain = (() => {
6510
6715
  this.isDestroyed = false;
6511
6716
  this.globalUserId = null;
6512
6717
  this.persistentAnonymousUserId = null;
6718
+ // Deprecated: use idManager instead
6513
6719
  // Remote Config properties
6514
6720
  this.configCache = null;
6515
6721
  this.configRefreshTimer = null;
6516
6722
  this.configChangeListeners = [];
6517
6723
  this.configFetchPromise = null;
6518
6724
  this.cookiesEnabled = false;
6725
+ // Deprecated: cookies no longer used for IDs
6519
6726
  // Automatic Tracking properties
6520
6727
  this.activityDetector = null;
6521
6728
  this.heartbeatManager = null;
@@ -6550,11 +6757,10 @@ var Grain = (() => {
6550
6757
  configRefreshInterval: 3e5,
6551
6758
  // 5 minutes
6552
6759
  enableConfigCache: true,
6553
- // Privacy defaults
6554
- consentMode: "opt-out",
6760
+ // Privacy defaults (v2.0)
6761
+ consentMode: "cookieless",
6762
+ // Default: privacy-first, no permanent tracking
6555
6763
  waitForConsent: false,
6556
- enableCookies: false,
6557
- anonymizeIP: false,
6558
6764
  disableAutoProperties: false,
6559
6765
  // Automatic Tracking defaults
6560
6766
  enableHeartbeat: true,
@@ -6564,23 +6770,25 @@ var Grain = (() => {
6564
6770
  // 5 minutes
6565
6771
  enableAutoPageView: true,
6566
6772
  stripQueryParams: true,
6773
+ // Privacy-first: strip by default
6774
+ stripHash: false,
6567
6775
  // Heatmap Tracking defaults
6568
6776
  enableHeatmapTracking: true,
6569
6777
  ...config,
6570
6778
  tenantId: config.tenantId
6571
6779
  };
6572
6780
  this.consentManager = new ConsentManager(this.config.tenantId, this.config.consentMode);
6573
- if (this.config.enableCookies) {
6574
- this.cookiesEnabled = areCookiesEnabled();
6575
- if (!this.cookiesEnabled && this.config.debug) {
6576
- console.warn("[Grain Analytics] Cookies are not available, falling back to localStorage");
6577
- }
6578
- }
6781
+ const idMode = this.consentManager.getIdMode();
6782
+ this.idManager = new IdManager({
6783
+ mode: idMode,
6784
+ tenantId: this.config.tenantId,
6785
+ useLocalStorage: true
6786
+ // For permanent IDs when consented
6787
+ });
6579
6788
  if (config.userId) {
6580
6789
  this.globalUserId = config.userId;
6581
6790
  }
6582
6791
  this.validateConfig();
6583
- this.initializePersistentAnonymousUserId();
6584
6792
  this.setupBeforeUnload();
6585
6793
  this.startFlushTimer();
6586
6794
  this.initializeConfigCache();
@@ -6594,6 +6802,8 @@ var Grain = (() => {
6594
6802
  }
6595
6803
  }
6596
6804
  this.consentManager.addListener((state) => {
6805
+ const idMode2 = this.consentManager.getIdMode();
6806
+ this.idManager.setMode(idMode2);
6597
6807
  if (state.granted) {
6598
6808
  this.handleConsentGranted();
6599
6809
  }
@@ -6634,10 +6844,12 @@ var Grain = (() => {
6634
6844
  */
6635
6845
  shouldAllowPersistentStorage() {
6636
6846
  const hasConsent = this.consentManager.hasConsent("analytics");
6637
- const isOptInMode = this.config.consentMode === "opt-in";
6847
+ const isCookieless = this.config.consentMode === "cookieless";
6638
6848
  const userExplicitlyIdentified = !!this.globalUserId;
6639
6849
  const isJWTAuth = this.config.authStrategy === "JWT";
6640
- return hasConsent || !isOptInMode || userExplicitlyIdentified || isJWTAuth;
6850
+ if (isCookieless)
6851
+ return false;
6852
+ return hasConsent || userExplicitlyIdentified || isJWTAuth;
6641
6853
  }
6642
6854
  /**
6643
6855
  * Generate a proper UUIDv4 identifier for anonymous user ID
@@ -6720,21 +6932,19 @@ var Grain = (() => {
6720
6932
  }
6721
6933
  }
6722
6934
  /**
6723
- * Get the effective user ID (global userId or persistent anonymous ID)
6935
+ * Get the effective user ID (v2.0)
6724
6936
  *
6725
- * GDPR Compliance: In opt-in mode without consent and no explicit user identification,
6726
- * this should not be called. Use getEphemeralSessionId() instead.
6937
+ * Privacy-first implementation:
6938
+ * - Returns global userId if explicitly set (via identify/login)
6939
+ * - Otherwise uses IdManager to generate:
6940
+ * - Daily rotating ID (cookieless mode)
6941
+ * - Permanent ID (with consent)
6727
6942
  */
6728
6943
  getEffectiveUserIdInternal() {
6729
6944
  if (this.globalUserId) {
6730
6945
  return this.globalUserId;
6731
6946
  }
6732
- if (this.persistentAnonymousUserId) {
6733
- return this.persistentAnonymousUserId;
6734
- }
6735
- this.persistentAnonymousUserId = this.generateAnonymousUserId();
6736
- this.savePersistentAnonymousUserId(this.persistentAnonymousUserId);
6737
- return this.persistentAnonymousUserId;
6947
+ return this.idManager.getCurrentUserId();
6738
6948
  }
6739
6949
  log(...args) {
6740
6950
  if (this.config.debug) {
@@ -7072,6 +7282,7 @@ var Grain = (() => {
7072
7282
  this,
7073
7283
  {
7074
7284
  stripQueryParams: this.config.stripQueryParams,
7285
+ stripHash: this.config.stripHash,
7075
7286
  debug: this.config.debug,
7076
7287
  tenantId: this.config.tenantId
7077
7288
  }
@@ -7365,11 +7576,12 @@ var Grain = (() => {
7365
7576
  const hasConsent = this.consentManager.hasConsent("analytics");
7366
7577
  const event = {
7367
7578
  eventName,
7368
- userId: hasConsent ? this.getEffectiveUserId() : this.getEphemeralSessionId(),
7579
+ userId: this.getEffectiveUserId(),
7580
+ // IdManager handles daily vs permanent based on consent
7369
7581
  properties: {
7370
7582
  ...properties,
7371
7583
  _minimal: !hasConsent,
7372
- // Flag to indicate minimal tracking
7584
+ // Flag to indicate minimal tracking (daily rotating ID)
7373
7585
  _consent_status: hasConsent ? "granted" : "pending"
7374
7586
  }
7375
7587
  };
@@ -7467,10 +7679,13 @@ var Grain = (() => {
7467
7679
  this.log(`Event waiting for consent: ${event.eventName}`, event.properties);
7468
7680
  return;
7469
7681
  }
7470
- if (!this.consentManager.hasConsent("analytics")) {
7471
- this.log(`Event blocked by consent: ${event.eventName}`);
7472
- return;
7473
- }
7682
+ const hasConsent = this.consentManager.hasConsent("analytics");
7683
+ formattedEvent.properties = {
7684
+ ...formattedEvent.properties,
7685
+ _minimal: !hasConsent,
7686
+ // Flag: true = daily rotating ID, false = permanent ID
7687
+ _consent_status: hasConsent ? "granted" : "pending"
7688
+ };
7474
7689
  this.eventQueue.push(formattedEvent);
7475
7690
  this.eventCountSinceLastHeartbeat++;
7476
7691
  this.sessionEventCount++;
@@ -8090,28 +8305,42 @@ var Grain = (() => {
8090
8305
  }
8091
8306
  // Privacy & Consent Methods
8092
8307
  /**
8093
- * Grant consent for tracking
8308
+ * Grant consent for tracking (v2.0)
8309
+ * Switches from cookie-less mode to permanent IDs
8094
8310
  * @param categories - Optional array of consent categories (e.g., ['analytics', 'functional'])
8095
8311
  */
8096
8312
  grantConsent(categories) {
8097
8313
  try {
8098
8314
  this.consentManager.grantConsent(categories);
8099
- this.log("Consent granted", categories);
8315
+ const idMode = this.consentManager.getIdMode();
8316
+ this.idManager.setMode(idMode);
8317
+ this.log("Consent granted, switched to permanent IDs", categories);
8318
+ if (this.waitingForConsentQueue.length > 0) {
8319
+ this.log(`Processing ${this.waitingForConsentQueue.length} queued events`);
8320
+ this.eventQueue.push(...this.waitingForConsentQueue);
8321
+ this.waitingForConsentQueue = [];
8322
+ this.flush();
8323
+ }
8100
8324
  } catch (error) {
8101
8325
  const formattedError = this.formatError(error, "grantConsent");
8102
8326
  this.logError(formattedError);
8103
8327
  }
8104
8328
  }
8105
8329
  /**
8106
- * Revoke consent for tracking (opt-out)
8330
+ * Revoke consent for tracking (v2.0)
8331
+ * Switches from permanent IDs to cookie-less mode
8107
8332
  * @param categories - Optional array of categories to revoke (if not provided, revokes all)
8108
8333
  */
8109
8334
  revokeConsent(categories) {
8110
8335
  try {
8111
8336
  this.consentManager.revokeConsent(categories);
8112
- this.log("Consent revoked", categories);
8113
- this.eventQueue = [];
8114
- this.waitingForConsentQueue = [];
8337
+ const idMode = this.consentManager.getIdMode();
8338
+ this.idManager.setMode(idMode);
8339
+ this.log("Consent revoked, switched to cookie-less mode", categories);
8340
+ if (!this.consentManager.hasConsent()) {
8341
+ this.eventQueue = [];
8342
+ this.waitingForConsentQueue = [];
8343
+ }
8115
8344
  } catch (error) {
8116
8345
  const formattedError = this.formatError(error, "revokeConsent");
8117
8346
  this.logError(formattedError);