@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.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;
@@ -38,6 +39,9 @@ export class GrainAnalytics {
38
39
  // Session tracking
39
40
  this.sessionStartTime = Date.now();
40
41
  this.sessionEventCount = 0;
42
+ // Debug mode properties
43
+ this.debugAgent = null;
44
+ this.isDebugMode = false;
41
45
  this.config = {
42
46
  apiUrl: 'https://api.grainql.com',
43
47
  authStrategy: 'NONE',
@@ -52,38 +56,37 @@ export class GrainAnalytics {
52
56
  configCacheKey: 'grain_config',
53
57
  configRefreshInterval: 300000, // 5 minutes
54
58
  enableConfigCache: true,
55
- // Privacy defaults
56
- consentMode: 'opt-out',
59
+ // Privacy defaults (v2.0)
60
+ consentMode: 'cookieless', // Default: privacy-first, no permanent tracking
57
61
  waitForConsent: false,
58
- enableCookies: false,
59
- anonymizeIP: false,
60
62
  disableAutoProperties: false,
61
63
  // Automatic Tracking defaults
62
64
  enableHeartbeat: true,
63
65
  heartbeatActiveInterval: 120000, // 2 minutes
64
66
  heartbeatInactiveInterval: 300000, // 5 minutes
65
67
  enableAutoPageView: true,
66
- stripQueryParams: true,
68
+ stripQueryParams: true, // Privacy-first: strip by default
69
+ stripHash: false,
67
70
  // Heatmap Tracking defaults
68
71
  enableHeatmapTracking: true,
69
72
  ...config,
70
73
  tenantId: config.tenantId,
71
74
  };
72
- // Initialize consent manager
75
+ // Initialize consent manager (v2.0)
73
76
  this.consentManager = new ConsentManager(this.config.tenantId, this.config.consentMode);
74
- // Check if cookies are enabled
75
- if (this.config.enableCookies) {
76
- this.cookiesEnabled = areCookiesEnabled();
77
- if (!this.cookiesEnabled && this.config.debug) {
78
- console.warn('[Grain Analytics] Cookies are not available, falling back to localStorage');
79
- }
80
- }
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
+ });
81
84
  // Set global userId if provided in config
82
85
  if (config.userId) {
83
86
  this.globalUserId = config.userId;
84
87
  }
85
88
  this.validateConfig();
86
- this.initializePersistentAnonymousUserId();
89
+ // Deprecated: initializePersistentAnonymousUserId() - now handled by IdManager
87
90
  this.setupBeforeUnload();
88
91
  this.startFlushTimer();
89
92
  this.initializeConfigCache();
@@ -91,6 +94,8 @@ export class GrainAnalytics {
91
94
  this.ephemeralSessionId = this.generateUUID();
92
95
  // Initialize automatic tracking (browser only)
93
96
  if (typeof window !== 'undefined') {
97
+ // Check for debug mode before initializing tracking
98
+ this.checkAndInitializeDebugMode();
94
99
  this.initializeAutomaticTracking();
95
100
  // Track session start
96
101
  this.trackSessionStart();
@@ -99,8 +104,11 @@ export class GrainAnalytics {
99
104
  this.initializeHeatmapTracking();
100
105
  }
101
106
  }
102
- // 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)
103
108
  this.consentManager.addListener((state) => {
109
+ // Sync IdManager with consent state
110
+ const idMode = this.consentManager.getIdMode();
111
+ this.idManager.setMode(idMode);
104
112
  if (state.granted) {
105
113
  this.handleConsentGranted();
106
114
  }
@@ -142,11 +150,14 @@ export class GrainAnalytics {
142
150
  */
143
151
  shouldAllowPersistentStorage() {
144
152
  const hasConsent = this.consentManager.hasConsent('analytics');
145
- const isOptInMode = this.config.consentMode === 'opt-in';
153
+ const isCookieless = this.config.consentMode === 'cookieless';
146
154
  const userExplicitlyIdentified = !!this.globalUserId;
147
155
  const isJWTAuth = this.config.authStrategy === 'JWT';
156
+ // Never allow persistent storage in cookieless mode
157
+ if (isCookieless)
158
+ return false;
148
159
  // Allow persistent storage if any of these conditions are met
149
- return hasConsent || !isOptInMode || userExplicitlyIdentified || isJWTAuth;
160
+ return hasConsent || userExplicitlyIdentified || isJWTAuth;
150
161
  }
151
162
  /**
152
163
  * Generate a proper UUIDv4 identifier for anonymous user ID
@@ -240,23 +251,21 @@ export class GrainAnalytics {
240
251
  }
241
252
  }
242
253
  /**
243
- * Get the effective user ID (global userId or persistent anonymous ID)
254
+ * Get the effective user ID (v2.0)
244
255
  *
245
- * GDPR Compliance: In opt-in mode without consent and no explicit user identification,
246
- * 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)
247
261
  */
248
262
  getEffectiveUserIdInternal() {
263
+ // Explicit user identification always takes precedence
249
264
  if (this.globalUserId) {
250
265
  return this.globalUserId;
251
266
  }
252
- if (this.persistentAnonymousUserId) {
253
- return this.persistentAnonymousUserId;
254
- }
255
- // Generate a new UUIDv4 identifier as fallback
256
- this.persistentAnonymousUserId = this.generateAnonymousUserId();
257
- // Try to persist it (will be skipped in opt-in mode without consent)
258
- this.savePersistentAnonymousUserId(this.persistentAnonymousUserId);
259
- return this.persistentAnonymousUserId;
267
+ // Use IdManager to generate appropriate ID based on consent
268
+ return this.idManager.getCurrentUserId();
260
269
  }
261
270
  log(...args) {
262
271
  if (this.config.debug) {
@@ -627,6 +636,7 @@ export class GrainAnalytics {
627
636
  try {
628
637
  this.pageTrackingManager = new PageTrackingManager(this, {
629
638
  stripQueryParams: this.config.stripQueryParams,
639
+ stripHash: this.config.stripHash,
630
640
  debug: this.config.debug,
631
641
  tenantId: this.config.tenantId,
632
642
  });
@@ -719,6 +729,8 @@ export class GrainAnalytics {
719
729
  debug: this.config.debug,
720
730
  enableMutationObserver: true,
721
731
  mutationDebounceDelay: 500,
732
+ tenantId: this.config.tenantId,
733
+ apiUrl: this.config.apiUrl,
722
734
  });
723
735
  this.log('Interaction tracking initialized');
724
736
  }
@@ -922,12 +934,13 @@ export class GrainAnalytics {
922
934
  return;
923
935
  const hasConsent = this.consentManager.hasConsent('analytics');
924
936
  // Create event with appropriate user ID
937
+ // v2.0: Always use IdManager which returns daily rotating ID or permanent ID based on consent
925
938
  const event = {
926
939
  eventName,
927
- userId: hasConsent ? this.getEffectiveUserId() : this.getEphemeralSessionId(),
940
+ userId: this.getEffectiveUserId(), // IdManager handles daily vs permanent based on consent
928
941
  properties: {
929
942
  ...properties,
930
- _minimal: !hasConsent, // Flag to indicate minimal tracking
943
+ _minimal: !hasConsent, // Flag to indicate minimal tracking (daily rotating ID)
931
944
  _consent_status: hasConsent ? 'granted' : 'pending',
932
945
  },
933
946
  };
@@ -1024,17 +1037,22 @@ export class GrainAnalytics {
1024
1037
  event.properties = filtered;
1025
1038
  }
1026
1039
  const formattedEvent = this.formatEvent(event);
1027
- // Check consent before tracking
1040
+ // Check if we should wait for consent (only if explicitly configured)
1028
1041
  if (this.consentManager.shouldWaitForConsent() && this.config.waitForConsent) {
1029
1042
  // Queue event until consent is granted
1030
1043
  this.waitingForConsentQueue.push(formattedEvent);
1031
1044
  this.log(`Event waiting for consent: ${event.eventName}`, event.properties);
1032
1045
  return;
1033
1046
  }
1034
- if (!this.consentManager.hasConsent('analytics')) {
1035
- this.log(`Event blocked by consent: ${event.eventName}`);
1036
- return;
1037
- }
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
+ };
1038
1056
  this.eventQueue.push(formattedEvent);
1039
1057
  this.eventCountSinceLastHeartbeat++;
1040
1058
  this.sessionEventCount++;
@@ -1722,13 +1740,24 @@ export class GrainAnalytics {
1722
1740
  }
1723
1741
  // Privacy & Consent Methods
1724
1742
  /**
1725
- * Grant consent for tracking
1743
+ * Grant consent for tracking (v2.0)
1744
+ * Switches from cookie-less mode to permanent IDs
1726
1745
  * @param categories - Optional array of consent categories (e.g., ['analytics', 'functional'])
1727
1746
  */
1728
1747
  grantConsent(categories) {
1729
1748
  try {
1730
1749
  this.consentManager.grantConsent(categories);
1731
- 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
+ }
1732
1761
  }
1733
1762
  catch (error) {
1734
1763
  const formattedError = this.formatError(error, 'grantConsent');
@@ -1736,16 +1765,22 @@ export class GrainAnalytics {
1736
1765
  }
1737
1766
  }
1738
1767
  /**
1739
- * Revoke consent for tracking (opt-out)
1768
+ * Revoke consent for tracking (v2.0)
1769
+ * Switches from permanent IDs to cookie-less mode
1740
1770
  * @param categories - Optional array of categories to revoke (if not provided, revokes all)
1741
1771
  */
1742
1772
  revokeConsent(categories) {
1743
1773
  try {
1744
1774
  this.consentManager.revokeConsent(categories);
1745
- this.log('Consent revoked', categories);
1746
- // Clear queued events when consent is revoked
1747
- this.eventQueue = [];
1748
- 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
+ }
1749
1784
  }
1750
1785
  catch (error) {
1751
1786
  const formattedError = this.formatError(error, 'revokeConsent');
@@ -1777,6 +1812,93 @@ export class GrainAnalytics {
1777
1812
  offConsentChange(listener) {
1778
1813
  this.consentManager.removeListener(listener);
1779
1814
  }
1815
+ /**
1816
+ * Check for debug mode parameters and initialize debug agent if valid
1817
+ */
1818
+ checkAndInitializeDebugMode() {
1819
+ if (typeof window === 'undefined')
1820
+ return;
1821
+ try {
1822
+ const urlParams = new URLSearchParams(window.location.search);
1823
+ const isDebug = urlParams.get('grain_debug') === '1';
1824
+ const sessionId = urlParams.get('grain_session');
1825
+ if (!isDebug || !sessionId) {
1826
+ return;
1827
+ }
1828
+ this.log('Debug mode detected, verifying session:', sessionId);
1829
+ // Verify session with API
1830
+ this.verifyDebugSession(sessionId, window.location.hostname)
1831
+ .then((valid) => {
1832
+ if (valid) {
1833
+ this.log('Debug session verified, initializing debug agent');
1834
+ this.isDebugMode = true;
1835
+ this.initializeDebugAgent(sessionId);
1836
+ }
1837
+ else {
1838
+ this.log('Debug session verification failed');
1839
+ }
1840
+ })
1841
+ .catch((error) => {
1842
+ this.log('Failed to verify debug session:', error);
1843
+ });
1844
+ }
1845
+ catch (error) {
1846
+ this.log('Error checking debug mode:', error);
1847
+ }
1848
+ }
1849
+ /**
1850
+ * Verify debug session with API
1851
+ */
1852
+ async verifyDebugSession(sessionId, domain) {
1853
+ try {
1854
+ const url = `${this.config.apiUrl}/v1/tenant/${encodeURIComponent(this.config.tenantId)}/debug-sessions/verify`;
1855
+ const response = await fetch(url, {
1856
+ method: 'POST',
1857
+ headers: {
1858
+ 'Content-Type': 'application/json',
1859
+ },
1860
+ body: JSON.stringify({
1861
+ sessionId,
1862
+ domain,
1863
+ }),
1864
+ });
1865
+ if (!response.ok) {
1866
+ return false;
1867
+ }
1868
+ const result = await response.json();
1869
+ return result.valid === true;
1870
+ }
1871
+ catch (error) {
1872
+ this.log('Debug session verification error:', error);
1873
+ return false;
1874
+ }
1875
+ }
1876
+ /**
1877
+ * Initialize debug agent
1878
+ */
1879
+ initializeDebugAgent(sessionId) {
1880
+ if (typeof window === 'undefined')
1881
+ return;
1882
+ try {
1883
+ this.log('Loading debug agent module');
1884
+ import('./debug-agent').then(({ DebugAgent }) => {
1885
+ try {
1886
+ this.debugAgent = new DebugAgent(this, sessionId, this.config.tenantId, this.config.apiUrl, {
1887
+ debug: this.config.debug,
1888
+ });
1889
+ this.log('Debug agent initialized');
1890
+ }
1891
+ catch (error) {
1892
+ this.log('Failed to initialize debug agent:', error);
1893
+ }
1894
+ }).catch((error) => {
1895
+ this.log('Failed to load debug agent module:', error);
1896
+ });
1897
+ }
1898
+ catch (error) {
1899
+ this.log('Error initializing debug agent:', error);
1900
+ }
1901
+ }
1780
1902
  /**
1781
1903
  * Destroy the client and clean up resources
1782
1904
  */
@@ -1816,6 +1938,11 @@ export class GrainAnalytics {
1816
1938
  this.heatmapTrackingManager.destroy();
1817
1939
  this.heatmapTrackingManager = null;
1818
1940
  }
1941
+ // Destroy debug agent
1942
+ if (this.debugAgent) {
1943
+ this.debugAgent.destroy();
1944
+ this.debugAgent = null;
1945
+ }
1819
1946
  // Send any remaining events (in chunks if necessary)
1820
1947
  if (this.eventQueue.length > 0) {
1821
1948
  const eventsToSend = [...this.eventQueue];
@@ -15,6 +15,8 @@ export interface InteractionTrackingConfig {
15
15
  debug?: boolean;
16
16
  enableMutationObserver?: boolean;
17
17
  mutationDebounceDelay?: number;
18
+ tenantId?: string;
19
+ apiUrl?: string;
18
20
  }
19
21
  export declare class InteractionTrackingManager {
20
22
  private tracker;
@@ -26,6 +28,10 @@ export declare class InteractionTrackingManager {
26
28
  private mutationObserver;
27
29
  private mutationDebounceTimer;
28
30
  constructor(tracker: InteractionTracker, interactions: InteractionConfig[], config?: InteractionTrackingConfig);
31
+ /**
32
+ * Fetch trackers from API and merge with existing interactions
33
+ */
34
+ private fetchAndMergeTrackers;
29
35
  /**
30
36
  * Attach listeners to all configured interactions
31
37
  */
@@ -1 +1 @@
1
- {"version":3,"file":"interaction-tracking.d.ts","sourceRoot":"","sources":["../src/interaction-tracking.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAG/D,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjH,UAAU,CAAC,QAAQ,EAAE,WAAW,GAAG,WAAW,GAAG,YAAY,GAAG,OAAO,CAAC;IACxE,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,qBAAa,0BAA0B;IACrC,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,iBAAiB,CAA6E;IACtG,OAAO,CAAC,UAAU,CAA0C;IAC5D,OAAO,CAAC,gBAAgB,CAAiC;IACzD,OAAO,CAAC,qBAAqB,CAAuB;gBAGlD,OAAO,EAAE,kBAAkB,EAC3B,YAAY,EAAE,iBAAiB,EAAE,EACjC,MAAM,GAAE,yBAA8B;IA0BxC;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAU1B;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAiCjC;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAiC9B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAkB9B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAyC1B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IA0B7B;;OAEG;IACH,OAAO,CAAC,eAAe;IA+BvB;;OAEG;IACH,OAAO,CAAC,eAAe;IAWvB;;OAEG;IACH,OAAO,CAAC,GAAG;IAMX;;OAEG;IACH,kBAAkB,CAAC,YAAY,EAAE,iBAAiB,EAAE,GAAG,IAAI;IAoB3D;;OAEG;IACH,OAAO,IAAI,IAAI;CA4BhB"}
1
+ {"version":3,"file":"interaction-tracking.d.ts","sourceRoot":"","sources":["../src/interaction-tracking.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAG/D,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjH,UAAU,CAAC,QAAQ,EAAE,WAAW,GAAG,WAAW,GAAG,YAAY,GAAG,OAAO,CAAC;IACxE,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,qBAAa,0BAA0B;IACrC,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,iBAAiB,CAA6E;IACtG,OAAO,CAAC,UAAU,CAA0C;IAC5D,OAAO,CAAC,gBAAgB,CAAiC;IACzD,OAAO,CAAC,qBAAqB,CAAuB;gBAGlD,OAAO,EAAE,kBAAkB,EAC3B,YAAY,EAAE,iBAAiB,EAAE,EACjC,MAAM,GAAE,yBAA8B;IAmCxC;;OAEG;YACW,qBAAqB;IA6CnC;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAU1B;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAiCjC;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAiC9B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAkB9B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAyC1B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IA0B7B;;OAEG;IACH,OAAO,CAAC,eAAe;IA+BvB;;OAEG;IACH,OAAO,CAAC,eAAe;IAWvB;;OAEG;IACH,OAAO,CAAC,GAAG;IAMX;;OAEG;IACH,kBAAkB,CAAC,YAAY,EAAE,iBAAiB,EAAE,GAAG,IAAI;IAoB3D;;OAEG;IACH,OAAO,IAAI,IAAI;CA4BhB"}
@@ -19,15 +19,25 @@ class InteractionTrackingManager {
19
19
  debug: config.debug ?? false,
20
20
  enableMutationObserver: config.enableMutationObserver ?? true,
21
21
  mutationDebounceDelay: config.mutationDebounceDelay ?? 500,
22
+ tenantId: config.tenantId,
23
+ apiUrl: config.apiUrl,
22
24
  };
23
25
  if (typeof window !== 'undefined' && typeof document !== 'undefined') {
24
- // Attach listeners after DOM is ready
25
- if (document.readyState === 'loading') {
26
- document.addEventListener('DOMContentLoaded', () => this.attachAllListeners());
26
+ // Fetch and merge trackers if configured
27
+ if (this.config.tenantId && this.config.apiUrl) {
28
+ this.fetchAndMergeTrackers().then(() => {
29
+ this.attachAllListeners();
30
+ });
27
31
  }
28
32
  else {
29
- // DOM already loaded
30
- setTimeout(() => this.attachAllListeners(), 0);
33
+ // Attach listeners after DOM is ready
34
+ if (document.readyState === 'loading') {
35
+ document.addEventListener('DOMContentLoaded', () => this.attachAllListeners());
36
+ }
37
+ else {
38
+ // DOM already loaded
39
+ setTimeout(() => this.attachAllListeners(), 0);
40
+ }
31
41
  }
32
42
  // Setup mutation observer for dynamic content
33
43
  if (this.config.enableMutationObserver) {
@@ -35,6 +45,46 @@ class InteractionTrackingManager {
35
45
  }
36
46
  }
37
47
  }
48
+ /**
49
+ * Fetch trackers from API and merge with existing interactions
50
+ */
51
+ async fetchAndMergeTrackers() {
52
+ if (!this.config.tenantId || !this.config.apiUrl)
53
+ return;
54
+ try {
55
+ const currentUrl = typeof window !== 'undefined' ? window.location.href : '';
56
+ const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/trackers?url=${encodeURIComponent(currentUrl)}`;
57
+ this.log('Fetching trackers from:', url);
58
+ const response = await fetch(url, {
59
+ method: 'GET',
60
+ headers: {
61
+ 'Content-Type': 'application/json',
62
+ },
63
+ });
64
+ if (!response.ok) {
65
+ this.log('Failed to fetch trackers:', response.status);
66
+ return;
67
+ }
68
+ const result = await response.json();
69
+ if (result.trackers && Array.isArray(result.trackers)) {
70
+ this.log('Fetched', result.trackers.length, 'trackers');
71
+ // Convert trackers to InteractionConfig format
72
+ const trackerInteractions = result.trackers.map((tracker) => ({
73
+ eventName: tracker.eventName,
74
+ selector: tracker.selector,
75
+ priority: 5, // High priority for manually created trackers
76
+ label: tracker.eventName,
77
+ description: `Tracker: ${tracker.eventName}`,
78
+ }));
79
+ // Merge with existing interactions (trackers take precedence)
80
+ this.interactions = [...trackerInteractions, ...this.interactions];
81
+ this.log('Merged trackers, total interactions:', this.interactions.length);
82
+ }
83
+ }
84
+ catch (error) {
85
+ this.log('Error fetching trackers:', error);
86
+ }
87
+ }
38
88
  /**
39
89
  * Attach listeners to all configured interactions
40
90
  */
@@ -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"}