@grainql/analytics-web 2.0.0 → 2.1.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 (173) hide show
  1. package/dist/activity.d.ts +59 -0
  2. package/dist/activity.d.ts.map +1 -0
  3. package/dist/cjs/activity.d.ts +59 -0
  4. package/dist/cjs/activity.d.ts.map +1 -0
  5. package/dist/cjs/activity.js +131 -0
  6. package/dist/cjs/activity.js.map +1 -0
  7. package/dist/cjs/consent.d.ts +68 -0
  8. package/dist/cjs/consent.d.ts.map +1 -0
  9. package/dist/cjs/consent.js +191 -0
  10. package/dist/cjs/consent.js.map +1 -0
  11. package/dist/cjs/cookies.d.ts +28 -0
  12. package/dist/cjs/cookies.d.ts.map +1 -0
  13. package/dist/cjs/cookies.js +95 -0
  14. package/dist/cjs/cookies.js.map +1 -0
  15. package/dist/cjs/heartbeat.d.ts +42 -0
  16. package/dist/cjs/heartbeat.d.ts.map +1 -0
  17. package/dist/cjs/heartbeat.js +92 -0
  18. package/dist/cjs/heartbeat.js.map +1 -0
  19. package/dist/cjs/index.d.ts +100 -3
  20. package/dist/cjs/index.d.ts.map +1 -1
  21. package/dist/cjs/index.js.map +1 -1
  22. package/dist/cjs/page-tracking.d.ts +60 -0
  23. package/dist/cjs/page-tracking.d.ts.map +1 -0
  24. package/dist/cjs/page-tracking.js +180 -0
  25. package/dist/cjs/page-tracking.js.map +1 -0
  26. package/dist/cjs/react/components/ConsentBanner.d.ts +16 -0
  27. package/dist/cjs/react/components/ConsentBanner.d.ts.map +1 -0
  28. package/dist/cjs/react/components/ConsentBanner.js +112 -0
  29. package/dist/cjs/react/components/ConsentBanner.js.map +1 -0
  30. package/dist/cjs/react/components/CookieNotice.d.ts +12 -0
  31. package/dist/cjs/react/components/CookieNotice.d.ts.map +1 -0
  32. package/dist/cjs/react/components/CookieNotice.js +62 -0
  33. package/dist/cjs/react/components/CookieNotice.js.map +1 -0
  34. package/dist/cjs/react/components/PrivacyPreferenceCenter.d.ts +12 -0
  35. package/dist/cjs/react/components/PrivacyPreferenceCenter.d.ts.map +1 -0
  36. package/dist/cjs/react/components/PrivacyPreferenceCenter.js +120 -0
  37. package/dist/cjs/react/components/PrivacyPreferenceCenter.js.map +1 -0
  38. package/dist/cjs/react/hooks/useConsent.d.ts +13 -0
  39. package/dist/cjs/react/hooks/useConsent.d.ts.map +1 -0
  40. package/dist/cjs/react/hooks/useConsent.js +84 -0
  41. package/dist/cjs/react/hooks/useConsent.js.map +1 -0
  42. package/dist/cjs/react/hooks/useDataDeletion.d.ts +17 -0
  43. package/dist/cjs/react/hooks/useDataDeletion.d.ts.map +1 -0
  44. package/dist/cjs/react/hooks/useDataDeletion.js +117 -0
  45. package/dist/cjs/react/hooks/useDataDeletion.js.map +1 -0
  46. package/dist/cjs/react/hooks/usePrivacyPreferences.d.ts +15 -0
  47. package/dist/cjs/react/hooks/usePrivacyPreferences.d.ts.map +1 -0
  48. package/dist/cjs/react/hooks/usePrivacyPreferences.js +82 -0
  49. package/dist/cjs/react/hooks/usePrivacyPreferences.js.map +1 -0
  50. package/dist/cjs/react/index.d.ts +11 -0
  51. package/dist/cjs/react/index.d.ts.map +1 -1
  52. package/dist/cjs/react/index.js +15 -1
  53. package/dist/cjs/react/index.js.map +1 -1
  54. package/dist/consent.d.ts +68 -0
  55. package/dist/consent.d.ts.map +1 -0
  56. package/dist/cookies.d.ts +28 -0
  57. package/dist/cookies.d.ts.map +1 -0
  58. package/dist/esm/activity.d.ts +59 -0
  59. package/dist/esm/activity.d.ts.map +1 -0
  60. package/dist/esm/activity.js +127 -0
  61. package/dist/esm/activity.js.map +1 -0
  62. package/dist/esm/consent.d.ts +68 -0
  63. package/dist/esm/consent.d.ts.map +1 -0
  64. package/dist/esm/consent.js +187 -0
  65. package/dist/esm/consent.js.map +1 -0
  66. package/dist/esm/cookies.d.ts +28 -0
  67. package/dist/esm/cookies.d.ts.map +1 -0
  68. package/dist/esm/cookies.js +89 -0
  69. package/dist/esm/cookies.js.map +1 -0
  70. package/dist/esm/heartbeat.d.ts +42 -0
  71. package/dist/esm/heartbeat.d.ts.map +1 -0
  72. package/dist/esm/heartbeat.js +88 -0
  73. package/dist/esm/heartbeat.js.map +1 -0
  74. package/dist/esm/index.d.ts +100 -3
  75. package/dist/esm/index.d.ts.map +1 -1
  76. package/dist/esm/index.js.map +1 -1
  77. package/dist/esm/page-tracking.d.ts +60 -0
  78. package/dist/esm/page-tracking.d.ts.map +1 -0
  79. package/dist/esm/page-tracking.js +176 -0
  80. package/dist/esm/page-tracking.js.map +1 -0
  81. package/dist/esm/react/components/ConsentBanner.d.ts +16 -0
  82. package/dist/esm/react/components/ConsentBanner.d.ts.map +1 -0
  83. package/dist/esm/react/components/ConsentBanner.js +76 -0
  84. package/dist/esm/react/components/ConsentBanner.js.map +1 -0
  85. package/dist/esm/react/components/CookieNotice.d.ts +12 -0
  86. package/dist/esm/react/components/CookieNotice.d.ts.map +1 -0
  87. package/dist/esm/react/components/CookieNotice.js +26 -0
  88. package/dist/esm/react/components/CookieNotice.js.map +1 -0
  89. package/dist/esm/react/components/PrivacyPreferenceCenter.d.ts +12 -0
  90. package/dist/esm/react/components/PrivacyPreferenceCenter.d.ts.map +1 -0
  91. package/dist/esm/react/components/PrivacyPreferenceCenter.js +84 -0
  92. package/dist/esm/react/components/PrivacyPreferenceCenter.js.map +1 -0
  93. package/dist/esm/react/hooks/useConsent.d.ts +13 -0
  94. package/dist/esm/react/hooks/useConsent.d.ts.map +1 -0
  95. package/dist/esm/react/hooks/useConsent.js +48 -0
  96. package/dist/esm/react/hooks/useConsent.js.map +1 -0
  97. package/dist/esm/react/hooks/useDataDeletion.d.ts +17 -0
  98. package/dist/esm/react/hooks/useDataDeletion.d.ts.map +1 -0
  99. package/dist/esm/react/hooks/useDataDeletion.js +81 -0
  100. package/dist/esm/react/hooks/useDataDeletion.js.map +1 -0
  101. package/dist/esm/react/hooks/usePrivacyPreferences.d.ts +15 -0
  102. package/dist/esm/react/hooks/usePrivacyPreferences.d.ts.map +1 -0
  103. package/dist/esm/react/hooks/usePrivacyPreferences.js +46 -0
  104. package/dist/esm/react/hooks/usePrivacyPreferences.js.map +1 -0
  105. package/dist/esm/react/index.d.ts +11 -0
  106. package/dist/esm/react/index.d.ts.map +1 -1
  107. package/dist/esm/react/index.js +8 -0
  108. package/dist/esm/react/index.js.map +1 -1
  109. package/dist/heartbeat.d.ts +42 -0
  110. package/dist/heartbeat.d.ts.map +1 -0
  111. package/dist/index.d.ts +100 -3
  112. package/dist/index.d.ts.map +1 -1
  113. package/dist/index.global.dev.js +903 -12
  114. package/dist/index.global.dev.js.map +3 -3
  115. package/dist/index.global.js +2 -2
  116. package/dist/index.global.js.map +4 -4
  117. package/dist/index.js +321 -11
  118. package/dist/index.mjs +321 -11
  119. package/dist/page-tracking.d.ts +60 -0
  120. package/dist/page-tracking.d.ts.map +1 -0
  121. package/dist/react/activity.d.ts +59 -0
  122. package/dist/react/activity.d.ts.map +1 -0
  123. package/dist/react/activity.js +130 -0
  124. package/dist/react/activity.mjs +126 -0
  125. package/dist/react/consent.d.ts +68 -0
  126. package/dist/react/consent.d.ts.map +1 -0
  127. package/dist/react/consent.js +190 -0
  128. package/dist/react/consent.mjs +186 -0
  129. package/dist/react/cookies.d.ts +28 -0
  130. package/dist/react/cookies.d.ts.map +1 -0
  131. package/dist/react/cookies.js +94 -0
  132. package/dist/react/cookies.mjs +88 -0
  133. package/dist/react/heartbeat.d.ts +42 -0
  134. package/dist/react/heartbeat.d.ts.map +1 -0
  135. package/dist/react/heartbeat.js +91 -0
  136. package/dist/react/heartbeat.mjs +87 -0
  137. package/dist/react/index.d.ts +100 -3
  138. package/dist/react/index.d.ts.map +1 -1
  139. package/dist/react/index.js +321 -11
  140. package/dist/react/index.mjs +321 -11
  141. package/dist/react/page-tracking.d.ts +60 -0
  142. package/dist/react/page-tracking.d.ts.map +1 -0
  143. package/dist/react/page-tracking.js +179 -0
  144. package/dist/react/page-tracking.mjs +175 -0
  145. package/dist/react/react/components/ConsentBanner.d.ts +16 -0
  146. package/dist/react/react/components/ConsentBanner.d.ts.map +1 -0
  147. package/dist/react/react/components/ConsentBanner.js +78 -0
  148. package/dist/react/react/components/ConsentBanner.mjs +75 -0
  149. package/dist/react/react/components/CookieNotice.d.ts +12 -0
  150. package/dist/react/react/components/CookieNotice.d.ts.map +1 -0
  151. package/dist/react/react/components/CookieNotice.js +28 -0
  152. package/dist/react/react/components/CookieNotice.mjs +25 -0
  153. package/dist/react/react/components/PrivacyPreferenceCenter.d.ts +12 -0
  154. package/dist/react/react/components/PrivacyPreferenceCenter.d.ts.map +1 -0
  155. package/dist/react/react/components/PrivacyPreferenceCenter.js +86 -0
  156. package/dist/react/react/components/PrivacyPreferenceCenter.mjs +83 -0
  157. package/dist/react/react/hooks/useConsent.d.ts +13 -0
  158. package/dist/react/react/hooks/useConsent.d.ts.map +1 -0
  159. package/dist/react/react/hooks/useConsent.js +50 -0
  160. package/dist/react/react/hooks/useConsent.mjs +47 -0
  161. package/dist/react/react/hooks/useDataDeletion.d.ts +17 -0
  162. package/dist/react/react/hooks/useDataDeletion.d.ts.map +1 -0
  163. package/dist/react/react/hooks/useDataDeletion.js +83 -0
  164. package/dist/react/react/hooks/useDataDeletion.mjs +80 -0
  165. package/dist/react/react/hooks/usePrivacyPreferences.d.ts +15 -0
  166. package/dist/react/react/hooks/usePrivacyPreferences.d.ts.map +1 -0
  167. package/dist/react/react/hooks/usePrivacyPreferences.js +48 -0
  168. package/dist/react/react/hooks/usePrivacyPreferences.mjs +45 -0
  169. package/dist/react/react/index.d.ts +11 -0
  170. package/dist/react/react/index.d.ts.map +1 -1
  171. package/dist/react/react/index.js +15 -1
  172. package/dist/react/react/index.mjs +8 -0
  173. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- /* Grain Analytics Web SDK v2.0.0 | MIT License | Development Build */
1
+ /* Grain Analytics Web SDK v2.1.0 | MIT License | Development Build */
2
2
  "use strict";
3
3
  var Grain = (() => {
4
4
  var __defProp = Object.defineProperty;
@@ -26,9 +26,611 @@ var Grain = (() => {
26
26
  createGrainAnalytics: () => createGrainAnalytics,
27
27
  default: () => src_default
28
28
  });
29
+
30
+ // src/consent.ts
31
+ var DEFAULT_CONSENT_CATEGORIES = ["necessary", "analytics", "functional"];
32
+ var CONSENT_VERSION = "1.0.0";
33
+ var ConsentManager = class {
34
+ constructor(tenantId, consentMode = "opt-out") {
35
+ this.consentState = null;
36
+ this.listeners = [];
37
+ this.consentMode = consentMode;
38
+ this.storageKey = `grain_consent_${tenantId}`;
39
+ this.loadConsentState();
40
+ }
41
+ /**
42
+ * Load consent state from localStorage
43
+ */
44
+ loadConsentState() {
45
+ if (typeof window === "undefined")
46
+ return;
47
+ try {
48
+ const stored = localStorage.getItem(this.storageKey);
49
+ if (stored) {
50
+ const parsed = JSON.parse(stored);
51
+ this.consentState = {
52
+ ...parsed,
53
+ timestamp: new Date(parsed.timestamp)
54
+ };
55
+ } else if (this.consentMode === "opt-out" || this.consentMode === "disabled") {
56
+ this.consentState = {
57
+ granted: true,
58
+ categories: DEFAULT_CONSENT_CATEGORIES,
59
+ timestamp: /* @__PURE__ */ new Date(),
60
+ version: CONSENT_VERSION
61
+ };
62
+ this.saveConsentState();
63
+ }
64
+ } catch (error) {
65
+ console.error("[Grain Consent] Failed to load consent state:", error);
66
+ }
67
+ }
68
+ /**
69
+ * Save consent state to localStorage
70
+ */
71
+ saveConsentState() {
72
+ if (typeof window === "undefined" || !this.consentState)
73
+ return;
74
+ try {
75
+ localStorage.setItem(this.storageKey, JSON.stringify(this.consentState));
76
+ } catch (error) {
77
+ console.error("[Grain Consent] Failed to save consent state:", error);
78
+ }
79
+ }
80
+ /**
81
+ * Grant consent with optional categories
82
+ */
83
+ grantConsent(categories) {
84
+ const grantedCategories = categories || DEFAULT_CONSENT_CATEGORIES;
85
+ this.consentState = {
86
+ granted: true,
87
+ categories: grantedCategories,
88
+ timestamp: /* @__PURE__ */ new Date(),
89
+ version: CONSENT_VERSION
90
+ };
91
+ this.saveConsentState();
92
+ this.notifyListeners();
93
+ }
94
+ /**
95
+ * Revoke consent (opt-out)
96
+ */
97
+ revokeConsent(categories) {
98
+ if (!this.consentState) {
99
+ this.consentState = {
100
+ granted: false,
101
+ categories: [],
102
+ timestamp: /* @__PURE__ */ new Date(),
103
+ version: CONSENT_VERSION
104
+ };
105
+ } else if (categories) {
106
+ this.consentState = {
107
+ ...this.consentState,
108
+ categories: this.consentState.categories.filter((cat) => !categories.includes(cat)),
109
+ granted: this.consentState.categories.length > 0,
110
+ timestamp: /* @__PURE__ */ new Date()
111
+ };
112
+ } else {
113
+ this.consentState = {
114
+ granted: false,
115
+ categories: [],
116
+ timestamp: /* @__PURE__ */ new Date(),
117
+ version: CONSENT_VERSION
118
+ };
119
+ }
120
+ this.saveConsentState();
121
+ this.notifyListeners();
122
+ }
123
+ /**
124
+ * Get current consent state
125
+ */
126
+ getConsentState() {
127
+ return this.consentState ? { ...this.consentState } : null;
128
+ }
129
+ /**
130
+ * Check if user has granted consent
131
+ */
132
+ hasConsent(category) {
133
+ if (this.consentMode === "disabled") {
134
+ return true;
135
+ }
136
+ if (this.consentMode === "opt-in" && !this.consentState) {
137
+ return false;
138
+ }
139
+ if (!this.consentState?.granted) {
140
+ return false;
141
+ }
142
+ if (category) {
143
+ return this.consentState.categories.includes(category);
144
+ }
145
+ return true;
146
+ }
147
+ /**
148
+ * Check if we should wait for consent before tracking
149
+ */
150
+ shouldWaitForConsent() {
151
+ return this.consentMode === "opt-in" && !this.consentState?.granted;
152
+ }
153
+ /**
154
+ * Add consent change listener
155
+ */
156
+ addListener(listener) {
157
+ this.listeners.push(listener);
158
+ }
159
+ /**
160
+ * Remove consent change listener
161
+ */
162
+ removeListener(listener) {
163
+ const index = this.listeners.indexOf(listener);
164
+ if (index > -1) {
165
+ this.listeners.splice(index, 1);
166
+ }
167
+ }
168
+ /**
169
+ * Notify all listeners of consent state change
170
+ */
171
+ notifyListeners() {
172
+ if (!this.consentState)
173
+ return;
174
+ this.listeners.forEach((listener) => {
175
+ try {
176
+ listener(this.consentState);
177
+ } catch (error) {
178
+ console.error("[Grain Consent] Listener error:", error);
179
+ }
180
+ });
181
+ }
182
+ /**
183
+ * Clear all consent data
184
+ */
185
+ clearConsent() {
186
+ if (typeof window === "undefined")
187
+ return;
188
+ try {
189
+ localStorage.removeItem(this.storageKey);
190
+ this.consentState = null;
191
+ } catch (error) {
192
+ console.error("[Grain Consent] Failed to clear consent:", error);
193
+ }
194
+ }
195
+ };
196
+
197
+ // src/cookies.ts
198
+ function setCookie(name, value, config) {
199
+ if (typeof document === "undefined")
200
+ return;
201
+ const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];
202
+ if (config?.maxAge !== void 0) {
203
+ parts.push(`max-age=${config.maxAge}`);
204
+ }
205
+ if (config?.domain) {
206
+ parts.push(`domain=${config.domain}`);
207
+ }
208
+ if (config?.path) {
209
+ parts.push(`path=${config.path}`);
210
+ } else {
211
+ parts.push("path=/");
212
+ }
213
+ if (config?.sameSite) {
214
+ parts.push(`samesite=${config.sameSite}`);
215
+ }
216
+ if (config?.secure) {
217
+ parts.push("secure");
218
+ }
219
+ document.cookie = parts.join("; ");
220
+ }
221
+ function getCookie(name) {
222
+ if (typeof document === "undefined")
223
+ return null;
224
+ const nameEQ = encodeURIComponent(name) + "=";
225
+ const cookies = document.cookie.split(";");
226
+ for (let i = 0; i < cookies.length; i++) {
227
+ let cookie = cookies[i];
228
+ while (cookie.charAt(0) === " ") {
229
+ cookie = cookie.substring(1);
230
+ }
231
+ if (cookie.indexOf(nameEQ) === 0) {
232
+ return decodeURIComponent(cookie.substring(nameEQ.length));
233
+ }
234
+ }
235
+ return null;
236
+ }
237
+ function deleteCookie(name, config) {
238
+ if (typeof document === "undefined")
239
+ return;
240
+ const parts = [
241
+ `${encodeURIComponent(name)}=`,
242
+ "max-age=0"
243
+ ];
244
+ if (config?.domain) {
245
+ parts.push(`domain=${config.domain}`);
246
+ }
247
+ if (config?.path) {
248
+ parts.push(`path=${config.path}`);
249
+ } else {
250
+ parts.push("path=/");
251
+ }
252
+ document.cookie = parts.join("; ");
253
+ }
254
+ function areCookiesEnabled() {
255
+ if (typeof document === "undefined")
256
+ return false;
257
+ try {
258
+ const testCookie = "_grain_cookie_test";
259
+ setCookie(testCookie, "test", { maxAge: 1 });
260
+ const result = getCookie(testCookie) === "test";
261
+ deleteCookie(testCookie);
262
+ return result;
263
+ } catch {
264
+ return false;
265
+ }
266
+ }
267
+
268
+ // src/activity.ts
269
+ var ActivityDetector = class {
270
+ constructor() {
271
+ this.activityThreshold = 3e4;
272
+ // 30 seconds
273
+ this.listeners = [];
274
+ this.isDestroyed = false;
275
+ // Events that indicate user activity
276
+ this.activityEvents = [
277
+ "mousemove",
278
+ "mousedown",
279
+ "keydown",
280
+ "scroll",
281
+ "touchstart",
282
+ "click"
283
+ ];
284
+ this.lastActivityTime = Date.now();
285
+ this.boundActivityHandler = this.debounce(this.handleActivity.bind(this), 500);
286
+ this.setupListeners();
287
+ }
288
+ /**
289
+ * Setup event listeners for activity detection
290
+ */
291
+ setupListeners() {
292
+ if (typeof window === "undefined")
293
+ return;
294
+ for (const event of this.activityEvents) {
295
+ window.addEventListener(event, this.boundActivityHandler, { passive: true });
296
+ }
297
+ }
298
+ /**
299
+ * Handle activity event
300
+ */
301
+ handleActivity() {
302
+ if (this.isDestroyed)
303
+ return;
304
+ this.lastActivityTime = Date.now();
305
+ this.notifyListeners();
306
+ }
307
+ /**
308
+ * Debounce function to limit how often activity handler is called
309
+ */
310
+ debounce(func, wait) {
311
+ let timeout = null;
312
+ return () => {
313
+ if (timeout !== null) {
314
+ clearTimeout(timeout);
315
+ }
316
+ timeout = window.setTimeout(() => {
317
+ func();
318
+ timeout = null;
319
+ }, wait);
320
+ };
321
+ }
322
+ /**
323
+ * Check if user is currently active
324
+ * @param threshold Time in ms to consider user inactive (default: 30s)
325
+ */
326
+ isActive(threshold) {
327
+ const thresholdToUse = threshold ?? this.activityThreshold;
328
+ const now = Date.now();
329
+ return now - this.lastActivityTime < thresholdToUse;
330
+ }
331
+ /**
332
+ * Get time since last activity in milliseconds
333
+ */
334
+ getTimeSinceLastActivity() {
335
+ return Date.now() - this.lastActivityTime;
336
+ }
337
+ /**
338
+ * Get last activity timestamp
339
+ */
340
+ getLastActivityTime() {
341
+ return this.lastActivityTime;
342
+ }
343
+ /**
344
+ * Set activity threshold
345
+ */
346
+ setActivityThreshold(threshold) {
347
+ this.activityThreshold = threshold;
348
+ }
349
+ /**
350
+ * Add listener for activity changes
351
+ */
352
+ addListener(listener) {
353
+ this.listeners.push(listener);
354
+ }
355
+ /**
356
+ * Remove listener
357
+ */
358
+ removeListener(listener) {
359
+ const index = this.listeners.indexOf(listener);
360
+ if (index > -1) {
361
+ this.listeners.splice(index, 1);
362
+ }
363
+ }
364
+ /**
365
+ * Notify all listeners
366
+ */
367
+ notifyListeners() {
368
+ for (const listener of this.listeners) {
369
+ try {
370
+ listener();
371
+ } catch (error) {
372
+ console.error("[Activity Detector] Listener error:", error);
373
+ }
374
+ }
375
+ }
376
+ /**
377
+ * Cleanup and remove listeners
378
+ */
379
+ destroy() {
380
+ if (this.isDestroyed)
381
+ return;
382
+ if (typeof window !== "undefined") {
383
+ for (const event of this.activityEvents) {
384
+ window.removeEventListener(event, this.boundActivityHandler);
385
+ }
386
+ }
387
+ this.listeners = [];
388
+ this.isDestroyed = true;
389
+ }
390
+ };
391
+
392
+ // src/heartbeat.ts
393
+ var HeartbeatManager = class {
394
+ constructor(tracker, activityDetector, config) {
395
+ this.heartbeatTimer = null;
396
+ this.isDestroyed = false;
397
+ this.tracker = tracker;
398
+ this.activityDetector = activityDetector;
399
+ this.config = config;
400
+ this.lastHeartbeatTime = Date.now();
401
+ this.currentInterval = config.activeInterval;
402
+ this.scheduleNextHeartbeat();
403
+ }
404
+ /**
405
+ * Schedule the next heartbeat based on current activity
406
+ */
407
+ scheduleNextHeartbeat() {
408
+ if (this.isDestroyed)
409
+ return;
410
+ if (this.heartbeatTimer !== null) {
411
+ clearTimeout(this.heartbeatTimer);
412
+ }
413
+ const isActive = this.activityDetector.isActive(6e4);
414
+ this.currentInterval = isActive ? this.config.activeInterval : this.config.inactiveInterval;
415
+ this.heartbeatTimer = window.setTimeout(() => {
416
+ this.sendHeartbeat();
417
+ this.scheduleNextHeartbeat();
418
+ }, this.currentInterval);
419
+ if (this.config.debug) {
420
+ console.log(
421
+ `[Heartbeat] Scheduled next heartbeat in ${this.currentInterval / 1e3}s (${isActive ? "active" : "inactive"})`
422
+ );
423
+ }
424
+ }
425
+ /**
426
+ * Send heartbeat event
427
+ */
428
+ sendHeartbeat() {
429
+ if (this.isDestroyed)
430
+ return;
431
+ const now = Date.now();
432
+ const isActive = this.activityDetector.isActive(6e4);
433
+ const hasConsent = this.tracker.hasConsent("analytics");
434
+ const properties = {
435
+ type: "heartbeat",
436
+ status: isActive ? "active" : "inactive",
437
+ timestamp: now
438
+ };
439
+ if (hasConsent) {
440
+ const page = this.tracker.getCurrentPage();
441
+ if (page) {
442
+ properties.page = page;
443
+ }
444
+ properties.duration = now - this.lastHeartbeatTime;
445
+ properties.event_count = this.tracker.getEventCountSinceLastHeartbeat();
446
+ this.tracker.resetEventCountSinceLastHeartbeat();
447
+ }
448
+ this.tracker.trackSystemEvent("_grain_heartbeat", properties);
449
+ this.lastHeartbeatTime = now;
450
+ if (this.config.debug) {
451
+ console.log("[Heartbeat] Sent heartbeat:", properties);
452
+ }
453
+ }
454
+ /**
455
+ * Destroy the heartbeat manager
456
+ */
457
+ destroy() {
458
+ if (this.isDestroyed)
459
+ return;
460
+ if (this.heartbeatTimer !== null) {
461
+ clearTimeout(this.heartbeatTimer);
462
+ this.heartbeatTimer = null;
463
+ }
464
+ this.isDestroyed = true;
465
+ if (this.config.debug) {
466
+ console.log("[Heartbeat] Destroyed");
467
+ }
468
+ }
469
+ };
470
+
471
+ // src/page-tracking.ts
472
+ var PageTrackingManager = class {
473
+ constructor(tracker, config) {
474
+ this.isDestroyed = false;
475
+ this.currentPath = null;
476
+ this.originalPushState = null;
477
+ this.originalReplaceState = null;
478
+ /**
479
+ * Handle popstate event (back/forward navigation)
480
+ */
481
+ this.handlePopState = () => {
482
+ if (this.isDestroyed)
483
+ return;
484
+ this.trackCurrentPage();
485
+ };
486
+ /**
487
+ * Handle hash change event
488
+ */
489
+ this.handleHashChange = () => {
490
+ if (this.isDestroyed)
491
+ return;
492
+ this.trackCurrentPage();
493
+ };
494
+ this.tracker = tracker;
495
+ this.config = config;
496
+ this.trackCurrentPage();
497
+ this.setupHistoryListeners();
498
+ this.setupHashChangeListener();
499
+ }
500
+ /**
501
+ * Setup History API listeners (pushState, replaceState, popstate)
502
+ */
503
+ setupHistoryListeners() {
504
+ if (typeof window === "undefined" || typeof history === "undefined")
505
+ return;
506
+ this.originalPushState = history.pushState;
507
+ history.pushState = (state, title, url) => {
508
+ this.originalPushState?.call(history, state, title, url);
509
+ this.trackCurrentPage();
510
+ };
511
+ this.originalReplaceState = history.replaceState;
512
+ history.replaceState = (state, title, url) => {
513
+ this.originalReplaceState?.call(history, state, title, url);
514
+ this.trackCurrentPage();
515
+ };
516
+ window.addEventListener("popstate", this.handlePopState);
517
+ }
518
+ /**
519
+ * Setup hash change listener
520
+ */
521
+ setupHashChangeListener() {
522
+ if (typeof window === "undefined")
523
+ return;
524
+ window.addEventListener("hashchange", this.handleHashChange);
525
+ }
526
+ /**
527
+ * Track the current page
528
+ */
529
+ trackCurrentPage() {
530
+ if (this.isDestroyed || typeof window === "undefined")
531
+ return;
532
+ const page = this.extractPath(window.location.href);
533
+ if (page === this.currentPath) {
534
+ return;
535
+ }
536
+ this.currentPath = page;
537
+ const hasConsent = this.tracker.hasConsent("analytics");
538
+ const properties = {
539
+ page,
540
+ timestamp: Date.now()
541
+ };
542
+ if (hasConsent) {
543
+ properties.referrer = document.referrer || "";
544
+ properties.title = document.title || "";
545
+ properties.full_url = window.location.href;
546
+ }
547
+ this.tracker.trackSystemEvent("page_view", properties);
548
+ if (this.config.debug) {
549
+ console.log("[Page Tracking] Tracked page view:", properties);
550
+ }
551
+ }
552
+ /**
553
+ * Extract path from URL, optionally stripping query parameters
554
+ */
555
+ extractPath(url) {
556
+ try {
557
+ const urlObj = new URL(url);
558
+ let path = urlObj.pathname + urlObj.hash;
559
+ if (!this.config.stripQueryParams && urlObj.search) {
560
+ path += urlObj.search;
561
+ }
562
+ return path;
563
+ } catch (error) {
564
+ if (this.config.debug) {
565
+ console.warn("[Page Tracking] Failed to parse URL:", url, error);
566
+ }
567
+ return url;
568
+ }
569
+ }
570
+ /**
571
+ * Get the current page path
572
+ */
573
+ getCurrentPage() {
574
+ return this.currentPath;
575
+ }
576
+ /**
577
+ * Manually track a page view (for custom navigation)
578
+ */
579
+ trackPage(page, properties) {
580
+ if (this.isDestroyed)
581
+ return;
582
+ const hasConsent = this.tracker.hasConsent("analytics");
583
+ const baseProperties = {
584
+ page,
585
+ timestamp: Date.now(),
586
+ ...properties
587
+ };
588
+ if (hasConsent && typeof document !== "undefined") {
589
+ if (!baseProperties.referrer) {
590
+ baseProperties.referrer = document.referrer || "";
591
+ }
592
+ if (!baseProperties.title) {
593
+ baseProperties.title = document.title || "";
594
+ }
595
+ if (!baseProperties.full_url && typeof window !== "undefined") {
596
+ baseProperties.full_url = window.location.href;
597
+ }
598
+ }
599
+ this.tracker.trackSystemEvent("page_view", baseProperties);
600
+ if (this.config.debug) {
601
+ console.log("[Page Tracking] Manually tracked page:", baseProperties);
602
+ }
603
+ }
604
+ /**
605
+ * Destroy the page tracker
606
+ */
607
+ destroy() {
608
+ if (this.isDestroyed)
609
+ return;
610
+ if (typeof history !== "undefined") {
611
+ if (this.originalPushState) {
612
+ history.pushState = this.originalPushState;
613
+ }
614
+ if (this.originalReplaceState) {
615
+ history.replaceState = this.originalReplaceState;
616
+ }
617
+ }
618
+ if (typeof window !== "undefined") {
619
+ window.removeEventListener("popstate", this.handlePopState);
620
+ window.removeEventListener("hashchange", this.handleHashChange);
621
+ }
622
+ this.isDestroyed = true;
623
+ if (this.config.debug) {
624
+ console.log("[Page Tracking] Destroyed");
625
+ }
626
+ }
627
+ };
628
+
629
+ // src/index.ts
29
630
  var GrainAnalytics = class {
30
631
  constructor(config) {
31
632
  this.eventQueue = [];
633
+ this.waitingForConsentQueue = [];
32
634
  this.flushTimer = null;
33
635
  this.isDestroyed = false;
34
636
  this.globalUserId = null;
@@ -38,6 +640,13 @@ var Grain = (() => {
38
640
  this.configRefreshTimer = null;
39
641
  this.configChangeListeners = [];
40
642
  this.configFetchPromise = null;
643
+ this.cookiesEnabled = false;
644
+ // Automatic Tracking properties
645
+ this.activityDetector = null;
646
+ this.heartbeatManager = null;
647
+ this.pageTrackingManager = null;
648
+ this.ephemeralSessionId = null;
649
+ this.eventCountSinceLastHeartbeat = 0;
41
650
  this.config = {
42
651
  apiUrl: "https://api.grainql.com",
43
652
  authStrategy: "NONE",
@@ -56,9 +665,30 @@ var Grain = (() => {
56
665
  configRefreshInterval: 3e5,
57
666
  // 5 minutes
58
667
  enableConfigCache: true,
668
+ // Privacy defaults
669
+ consentMode: "opt-out",
670
+ waitForConsent: false,
671
+ enableCookies: false,
672
+ anonymizeIP: false,
673
+ disableAutoProperties: false,
674
+ // Automatic Tracking defaults
675
+ enableHeartbeat: true,
676
+ heartbeatActiveInterval: 12e4,
677
+ // 2 minutes
678
+ heartbeatInactiveInterval: 3e5,
679
+ // 5 minutes
680
+ enableAutoPageView: true,
681
+ stripQueryParams: true,
59
682
  ...config,
60
683
  tenantId: config.tenantId
61
684
  };
685
+ this.consentManager = new ConsentManager(this.config.tenantId, this.config.consentMode);
686
+ if (this.config.enableCookies) {
687
+ this.cookiesEnabled = areCookiesEnabled();
688
+ if (!this.cookiesEnabled && this.config.debug) {
689
+ console.warn("[Grain Analytics] Cookies are not available, falling back to localStorage");
690
+ }
691
+ }
62
692
  if (config.userId) {
63
693
  this.globalUserId = config.userId;
64
694
  }
@@ -67,6 +697,15 @@ var Grain = (() => {
67
697
  this.setupBeforeUnload();
68
698
  this.startFlushTimer();
69
699
  this.initializeConfigCache();
700
+ this.ephemeralSessionId = this.generateUUID();
701
+ if (typeof window !== "undefined") {
702
+ this.initializeAutomaticTracking();
703
+ }
704
+ this.consentManager.addListener((state) => {
705
+ if (state.granted) {
706
+ this.handleConsentGranted();
707
+ }
708
+ });
70
709
  }
71
710
  validateConfig() {
72
711
  if (!this.config.tenantId) {
@@ -99,20 +738,33 @@ var Grain = (() => {
99
738
  return this.generateUUID();
100
739
  }
101
740
  /**
102
- * Initialize persistent anonymous user ID from localStorage or create new one
741
+ * Initialize persistent anonymous user ID from cookies or localStorage
742
+ * Priority: Cookie → localStorage → generate new
103
743
  */
104
744
  initializePersistentAnonymousUserId() {
105
745
  if (typeof window === "undefined")
106
746
  return;
107
747
  const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
748
+ const cookieName = "_grain_uid";
108
749
  try {
750
+ if (this.cookiesEnabled) {
751
+ const cookieValue = getCookie(cookieName);
752
+ if (cookieValue) {
753
+ this.persistentAnonymousUserId = cookieValue;
754
+ this.log("Loaded persistent anonymous user ID from cookie:", this.persistentAnonymousUserId);
755
+ return;
756
+ }
757
+ }
109
758
  const stored = localStorage.getItem(storageKey);
110
759
  if (stored) {
111
760
  this.persistentAnonymousUserId = stored;
112
- this.log("Loaded persistent anonymous user ID:", this.persistentAnonymousUserId);
761
+ this.log("Loaded persistent anonymous user ID from localStorage:", this.persistentAnonymousUserId);
762
+ if (this.cookiesEnabled) {
763
+ this.savePersistentAnonymousUserId(stored);
764
+ }
113
765
  } else {
114
766
  this.persistentAnonymousUserId = this.generateAnonymousUserId();
115
- localStorage.setItem(storageKey, this.persistentAnonymousUserId);
767
+ this.savePersistentAnonymousUserId(this.persistentAnonymousUserId);
116
768
  this.log("Generated new persistent anonymous user ID:", this.persistentAnonymousUserId);
117
769
  }
118
770
  } catch (error) {
@@ -120,10 +772,34 @@ var Grain = (() => {
120
772
  this.persistentAnonymousUserId = this.generateAnonymousUserId();
121
773
  }
122
774
  }
775
+ /**
776
+ * Save persistent anonymous user ID to cookie and/or localStorage
777
+ */
778
+ savePersistentAnonymousUserId(userId) {
779
+ if (typeof window === "undefined")
780
+ return;
781
+ const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
782
+ const cookieName = "_grain_uid";
783
+ try {
784
+ if (this.cookiesEnabled) {
785
+ const cookieOptions = {
786
+ maxAge: 365 * 24 * 60 * 60,
787
+ // 365 days
788
+ sameSite: "lax",
789
+ secure: window.location.protocol === "https:",
790
+ ...this.config.cookieOptions
791
+ };
792
+ setCookie(cookieName, userId, cookieOptions);
793
+ }
794
+ localStorage.setItem(storageKey, userId);
795
+ } catch (error) {
796
+ this.log("Failed to save persistent anonymous user ID:", error);
797
+ }
798
+ }
123
799
  /**
124
800
  * Get the effective user ID (global userId or persistent anonymous ID)
125
801
  */
126
- getEffectiveUserId() {
802
+ getEffectiveUserIdInternal() {
127
803
  if (this.globalUserId) {
128
804
  return this.globalUserId;
129
805
  }
@@ -256,7 +932,7 @@ var Grain = (() => {
256
932
  formatEvent(event) {
257
933
  return {
258
934
  eventName: event.eventName,
259
- userId: event.userId || this.getEffectiveUserId(),
935
+ userId: event.userId || this.getEffectiveUserIdInternal(),
260
936
  properties: event.properties || {}
261
937
  };
262
938
  }
@@ -419,6 +1095,122 @@ var Grain = (() => {
419
1095
  }
420
1096
  });
421
1097
  }
1098
+ /**
1099
+ * Initialize automatic tracking (heartbeat and page views)
1100
+ */
1101
+ initializeAutomaticTracking() {
1102
+ if (this.config.enableHeartbeat) {
1103
+ try {
1104
+ this.activityDetector = new ActivityDetector();
1105
+ this.heartbeatManager = new HeartbeatManager(
1106
+ this,
1107
+ this.activityDetector,
1108
+ {
1109
+ activeInterval: this.config.heartbeatActiveInterval,
1110
+ inactiveInterval: this.config.heartbeatInactiveInterval,
1111
+ debug: this.config.debug
1112
+ }
1113
+ );
1114
+ this.log("Heartbeat tracking initialized");
1115
+ } catch (error) {
1116
+ this.log("Failed to initialize heartbeat tracking:", error);
1117
+ }
1118
+ }
1119
+ if (this.config.enableAutoPageView) {
1120
+ try {
1121
+ this.pageTrackingManager = new PageTrackingManager(
1122
+ this,
1123
+ {
1124
+ stripQueryParams: this.config.stripQueryParams,
1125
+ debug: this.config.debug
1126
+ }
1127
+ );
1128
+ this.log("Auto page view tracking initialized");
1129
+ } catch (error) {
1130
+ this.log("Failed to initialize page view tracking:", error);
1131
+ }
1132
+ }
1133
+ }
1134
+ /**
1135
+ * Handle consent granted - upgrade ephemeral session to persistent user
1136
+ */
1137
+ handleConsentGranted() {
1138
+ this.flushWaitingForConsentQueue();
1139
+ if (this.ephemeralSessionId) {
1140
+ this.trackSystemEvent("_grain_consent_granted", {
1141
+ previous_session_id: this.ephemeralSessionId,
1142
+ new_user_id: this.getEffectiveUserId(),
1143
+ timestamp: Date.now()
1144
+ });
1145
+ }
1146
+ }
1147
+ /**
1148
+ * Track system events that bypass consent checks (for necessary/functional tracking)
1149
+ */
1150
+ trackSystemEvent(eventName, properties) {
1151
+ if (this.isDestroyed)
1152
+ return;
1153
+ const hasConsent = this.consentManager.hasConsent("analytics");
1154
+ const event = {
1155
+ eventName,
1156
+ userId: hasConsent ? this.getEffectiveUserId() : this.getEphemeralSessionId(),
1157
+ properties: {
1158
+ ...properties,
1159
+ _minimal: !hasConsent,
1160
+ // Flag to indicate minimal tracking
1161
+ _consent_status: hasConsent ? "granted" : "pending"
1162
+ }
1163
+ };
1164
+ this.eventQueue.push(event);
1165
+ this.eventCountSinceLastHeartbeat++;
1166
+ this.log(`Queued system event: ${eventName}`, properties);
1167
+ if (this.eventQueue.length >= this.config.batchSize) {
1168
+ this.flush().catch((error) => {
1169
+ const formattedError = this.formatError(error, "flush system event");
1170
+ this.logError(formattedError);
1171
+ });
1172
+ }
1173
+ }
1174
+ /**
1175
+ * Get ephemeral session ID (memory-only, not persisted)
1176
+ */
1177
+ getEphemeralSessionId() {
1178
+ if (!this.ephemeralSessionId) {
1179
+ this.ephemeralSessionId = this.generateUUID();
1180
+ }
1181
+ return this.ephemeralSessionId;
1182
+ }
1183
+ /**
1184
+ * Get the current page path from page tracker
1185
+ */
1186
+ getCurrentPage() {
1187
+ return this.pageTrackingManager?.getCurrentPage() || null;
1188
+ }
1189
+ /**
1190
+ * Get event count since last heartbeat
1191
+ */
1192
+ getEventCountSinceLastHeartbeat() {
1193
+ return this.eventCountSinceLastHeartbeat;
1194
+ }
1195
+ /**
1196
+ * Reset event count since last heartbeat
1197
+ */
1198
+ resetEventCountSinceLastHeartbeat() {
1199
+ this.eventCountSinceLastHeartbeat = 0;
1200
+ }
1201
+ /**
1202
+ * Get the effective user ID (public method)
1203
+ */
1204
+ getEffectiveUserId() {
1205
+ return this.getEffectiveUserIdInternal();
1206
+ }
1207
+ /**
1208
+ * Get the session ID (ephemeral or persistent based on consent)
1209
+ */
1210
+ getSessionId() {
1211
+ const hasConsent = this.consentManager.hasConsent("analytics");
1212
+ return hasConsent ? this.getEffectiveUserId() : this.getEphemeralSessionId();
1213
+ }
422
1214
  async track(eventOrName, propertiesOrOptions, options) {
423
1215
  try {
424
1216
  if (this.isDestroyed) {
@@ -439,8 +1231,27 @@ var Grain = (() => {
439
1231
  event = eventOrName;
440
1232
  opts = propertiesOrOptions || {};
441
1233
  }
1234
+ if (this.config.allowedProperties && event.properties) {
1235
+ const filtered = {};
1236
+ for (const key of this.config.allowedProperties) {
1237
+ if (key in event.properties) {
1238
+ filtered[key] = event.properties[key];
1239
+ }
1240
+ }
1241
+ event.properties = filtered;
1242
+ }
442
1243
  const formattedEvent = this.formatEvent(event);
1244
+ if (this.consentManager.shouldWaitForConsent() && this.config.waitForConsent) {
1245
+ this.waitingForConsentQueue.push(formattedEvent);
1246
+ this.log(`Event waiting for consent: ${event.eventName}`, event.properties);
1247
+ return;
1248
+ }
1249
+ if (!this.consentManager.hasConsent("analytics")) {
1250
+ this.log(`Event blocked by consent: ${event.eventName}`);
1251
+ return;
1252
+ }
443
1253
  this.eventQueue.push(formattedEvent);
1254
+ this.eventCountSinceLastHeartbeat++;
444
1255
  this.log(`Queued event: ${event.eventName}`, event.properties);
445
1256
  if (opts.flush || this.eventQueue.length >= this.config.batchSize) {
446
1257
  await this.flush();
@@ -450,6 +1261,20 @@ var Grain = (() => {
450
1261
  this.logError(formattedError);
451
1262
  }
452
1263
  }
1264
+ /**
1265
+ * Flush events that were waiting for consent
1266
+ */
1267
+ flushWaitingForConsentQueue() {
1268
+ if (this.waitingForConsentQueue.length === 0)
1269
+ return;
1270
+ this.log(`Flushing ${this.waitingForConsentQueue.length} events waiting for consent`);
1271
+ this.eventQueue.push(...this.waitingForConsentQueue);
1272
+ this.waitingForConsentQueue = [];
1273
+ this.flush().catch((error) => {
1274
+ const formattedError = this.formatError(error, "flush waiting for consent queue");
1275
+ this.logError(formattedError);
1276
+ });
1277
+ }
453
1278
  /**
454
1279
  * Identify a user (sets userId for subsequent events)
455
1280
  */
@@ -490,7 +1315,7 @@ var Grain = (() => {
490
1315
  * Get current effective user ID (global userId or persistent anonymous ID)
491
1316
  */
492
1317
  getEffectiveUserIdPublic() {
493
- return this.getEffectiveUserId();
1318
+ return this.getEffectiveUserIdInternal();
494
1319
  }
495
1320
  /**
496
1321
  * Login with auth token or userId on the fly
@@ -534,7 +1359,7 @@ var Grain = (() => {
534
1359
  this.log(`Login: Setting auth strategy to ${options.authStrategy}`);
535
1360
  this.config.authStrategy = options.authStrategy;
536
1361
  }
537
- this.log(`Login successful. Effective user ID: ${this.getEffectiveUserId()}`);
1362
+ this.log(`Login successful. Effective user ID: ${this.getEffectiveUserIdInternal()}`);
538
1363
  } catch (error) {
539
1364
  const formattedError = this.formatError(error, "login");
540
1365
  this.logError(formattedError);
@@ -573,7 +1398,7 @@ var Grain = (() => {
573
1398
  }
574
1399
  }
575
1400
  }
576
- this.log(`Logout successful. Effective user ID: ${this.getEffectiveUserId()}`);
1401
+ this.log(`Logout successful. Effective user ID: ${this.getEffectiveUserIdInternal()}`);
577
1402
  } catch (error) {
578
1403
  const formattedError = this.formatError(error, "logout");
579
1404
  this.logError(formattedError);
@@ -590,7 +1415,7 @@ var Grain = (() => {
590
1415
  this.logError(formattedError);
591
1416
  return;
592
1417
  }
593
- const userId = options?.userId || this.getEffectiveUserId();
1418
+ const userId = options?.userId || this.getEffectiveUserIdInternal();
594
1419
  const propertyKeys = Object.keys(properties);
595
1420
  if (propertyKeys.length > 4) {
596
1421
  const error = new Error("Grain Analytics: Maximum 4 properties allowed per request");
@@ -846,7 +1671,7 @@ var Grain = (() => {
846
1671
  this.logError(formattedError);
847
1672
  return null;
848
1673
  }
849
- const userId = options.userId || this.getEffectiveUserId();
1674
+ const userId = options.userId || this.getEffectiveUserIdInternal();
850
1675
  const immediateKeys = options.immediateKeys || [];
851
1676
  const properties = options.properties || {};
852
1677
  const request = {
@@ -1027,7 +1852,7 @@ var Grain = (() => {
1027
1852
  */
1028
1853
  async preloadConfig(immediateKeys = [], properties) {
1029
1854
  try {
1030
- const effectiveUserId = this.getEffectiveUserId();
1855
+ const effectiveUserId = this.getEffectiveUserIdInternal();
1031
1856
  this.log(`Preloading config for user: ${effectiveUserId}`);
1032
1857
  const response = await this.fetchConfig({ immediateKeys, properties });
1033
1858
  if (response) {
@@ -1048,6 +1873,60 @@ var Grain = (() => {
1048
1873
  }
1049
1874
  return chunks;
1050
1875
  }
1876
+ // Privacy & Consent Methods
1877
+ /**
1878
+ * Grant consent for tracking
1879
+ * @param categories - Optional array of consent categories (e.g., ['analytics', 'functional'])
1880
+ */
1881
+ grantConsent(categories) {
1882
+ try {
1883
+ this.consentManager.grantConsent(categories);
1884
+ this.log("Consent granted", categories);
1885
+ } catch (error) {
1886
+ const formattedError = this.formatError(error, "grantConsent");
1887
+ this.logError(formattedError);
1888
+ }
1889
+ }
1890
+ /**
1891
+ * Revoke consent for tracking (opt-out)
1892
+ * @param categories - Optional array of categories to revoke (if not provided, revokes all)
1893
+ */
1894
+ revokeConsent(categories) {
1895
+ try {
1896
+ this.consentManager.revokeConsent(categories);
1897
+ this.log("Consent revoked", categories);
1898
+ this.eventQueue = [];
1899
+ this.waitingForConsentQueue = [];
1900
+ } catch (error) {
1901
+ const formattedError = this.formatError(error, "revokeConsent");
1902
+ this.logError(formattedError);
1903
+ }
1904
+ }
1905
+ /**
1906
+ * Get current consent state
1907
+ */
1908
+ getConsentState() {
1909
+ return this.consentManager.getConsentState();
1910
+ }
1911
+ /**
1912
+ * Check if user has granted consent
1913
+ * @param category - Optional category to check (if not provided, checks general consent)
1914
+ */
1915
+ hasConsent(category) {
1916
+ return this.consentManager.hasConsent(category);
1917
+ }
1918
+ /**
1919
+ * Add listener for consent state changes
1920
+ */
1921
+ onConsentChange(listener) {
1922
+ this.consentManager.addListener(listener);
1923
+ }
1924
+ /**
1925
+ * Remove consent change listener
1926
+ */
1927
+ offConsentChange(listener) {
1928
+ this.consentManager.removeListener(listener);
1929
+ }
1051
1930
  /**
1052
1931
  * Destroy the client and clean up resources
1053
1932
  */
@@ -1059,6 +1938,18 @@ var Grain = (() => {
1059
1938
  }
1060
1939
  this.stopConfigRefreshTimer();
1061
1940
  this.configChangeListeners = [];
1941
+ if (this.heartbeatManager) {
1942
+ this.heartbeatManager.destroy();
1943
+ this.heartbeatManager = null;
1944
+ }
1945
+ if (this.pageTrackingManager) {
1946
+ this.pageTrackingManager.destroy();
1947
+ this.pageTrackingManager = null;
1948
+ }
1949
+ if (this.activityDetector) {
1950
+ this.activityDetector.destroy();
1951
+ this.activityDetector = null;
1952
+ }
1062
1953
  if (this.eventQueue.length > 0) {
1063
1954
  const eventsToSend = [...this.eventQueue];
1064
1955
  this.eventQueue = [];