@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.
- package/dist/activity.d.ts +59 -0
- package/dist/activity.d.ts.map +1 -0
- package/dist/cjs/activity.d.ts +59 -0
- package/dist/cjs/activity.d.ts.map +1 -0
- package/dist/cjs/activity.js +131 -0
- package/dist/cjs/activity.js.map +1 -0
- package/dist/cjs/consent.d.ts +68 -0
- package/dist/cjs/consent.d.ts.map +1 -0
- package/dist/cjs/consent.js +191 -0
- package/dist/cjs/consent.js.map +1 -0
- package/dist/cjs/cookies.d.ts +28 -0
- package/dist/cjs/cookies.d.ts.map +1 -0
- package/dist/cjs/cookies.js +95 -0
- package/dist/cjs/cookies.js.map +1 -0
- package/dist/cjs/heartbeat.d.ts +42 -0
- package/dist/cjs/heartbeat.d.ts.map +1 -0
- package/dist/cjs/heartbeat.js +92 -0
- package/dist/cjs/heartbeat.js.map +1 -0
- package/dist/cjs/index.d.ts +100 -3
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/page-tracking.d.ts +60 -0
- package/dist/cjs/page-tracking.d.ts.map +1 -0
- package/dist/cjs/page-tracking.js +180 -0
- package/dist/cjs/page-tracking.js.map +1 -0
- package/dist/cjs/react/components/ConsentBanner.d.ts +16 -0
- package/dist/cjs/react/components/ConsentBanner.d.ts.map +1 -0
- package/dist/cjs/react/components/ConsentBanner.js +112 -0
- package/dist/cjs/react/components/ConsentBanner.js.map +1 -0
- package/dist/cjs/react/components/CookieNotice.d.ts +12 -0
- package/dist/cjs/react/components/CookieNotice.d.ts.map +1 -0
- package/dist/cjs/react/components/CookieNotice.js +62 -0
- package/dist/cjs/react/components/CookieNotice.js.map +1 -0
- package/dist/cjs/react/components/PrivacyPreferenceCenter.d.ts +12 -0
- package/dist/cjs/react/components/PrivacyPreferenceCenter.d.ts.map +1 -0
- package/dist/cjs/react/components/PrivacyPreferenceCenter.js +120 -0
- package/dist/cjs/react/components/PrivacyPreferenceCenter.js.map +1 -0
- package/dist/cjs/react/hooks/useConsent.d.ts +13 -0
- package/dist/cjs/react/hooks/useConsent.d.ts.map +1 -0
- package/dist/cjs/react/hooks/useConsent.js +84 -0
- package/dist/cjs/react/hooks/useConsent.js.map +1 -0
- package/dist/cjs/react/hooks/useDataDeletion.d.ts +17 -0
- package/dist/cjs/react/hooks/useDataDeletion.d.ts.map +1 -0
- package/dist/cjs/react/hooks/useDataDeletion.js +117 -0
- package/dist/cjs/react/hooks/useDataDeletion.js.map +1 -0
- package/dist/cjs/react/hooks/usePrivacyPreferences.d.ts +15 -0
- package/dist/cjs/react/hooks/usePrivacyPreferences.d.ts.map +1 -0
- package/dist/cjs/react/hooks/usePrivacyPreferences.js +82 -0
- package/dist/cjs/react/hooks/usePrivacyPreferences.js.map +1 -0
- package/dist/cjs/react/index.d.ts +11 -0
- package/dist/cjs/react/index.d.ts.map +1 -1
- package/dist/cjs/react/index.js +15 -1
- package/dist/cjs/react/index.js.map +1 -1
- package/dist/consent.d.ts +68 -0
- package/dist/consent.d.ts.map +1 -0
- package/dist/cookies.d.ts +28 -0
- package/dist/cookies.d.ts.map +1 -0
- package/dist/esm/activity.d.ts +59 -0
- package/dist/esm/activity.d.ts.map +1 -0
- package/dist/esm/activity.js +127 -0
- package/dist/esm/activity.js.map +1 -0
- package/dist/esm/consent.d.ts +68 -0
- package/dist/esm/consent.d.ts.map +1 -0
- package/dist/esm/consent.js +187 -0
- package/dist/esm/consent.js.map +1 -0
- package/dist/esm/cookies.d.ts +28 -0
- package/dist/esm/cookies.d.ts.map +1 -0
- package/dist/esm/cookies.js +89 -0
- package/dist/esm/cookies.js.map +1 -0
- package/dist/esm/heartbeat.d.ts +42 -0
- package/dist/esm/heartbeat.d.ts.map +1 -0
- package/dist/esm/heartbeat.js +88 -0
- package/dist/esm/heartbeat.js.map +1 -0
- package/dist/esm/index.d.ts +100 -3
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/page-tracking.d.ts +60 -0
- package/dist/esm/page-tracking.d.ts.map +1 -0
- package/dist/esm/page-tracking.js +176 -0
- package/dist/esm/page-tracking.js.map +1 -0
- package/dist/esm/react/components/ConsentBanner.d.ts +16 -0
- package/dist/esm/react/components/ConsentBanner.d.ts.map +1 -0
- package/dist/esm/react/components/ConsentBanner.js +76 -0
- package/dist/esm/react/components/ConsentBanner.js.map +1 -0
- package/dist/esm/react/components/CookieNotice.d.ts +12 -0
- package/dist/esm/react/components/CookieNotice.d.ts.map +1 -0
- package/dist/esm/react/components/CookieNotice.js +26 -0
- package/dist/esm/react/components/CookieNotice.js.map +1 -0
- package/dist/esm/react/components/PrivacyPreferenceCenter.d.ts +12 -0
- package/dist/esm/react/components/PrivacyPreferenceCenter.d.ts.map +1 -0
- package/dist/esm/react/components/PrivacyPreferenceCenter.js +84 -0
- package/dist/esm/react/components/PrivacyPreferenceCenter.js.map +1 -0
- package/dist/esm/react/hooks/useConsent.d.ts +13 -0
- package/dist/esm/react/hooks/useConsent.d.ts.map +1 -0
- package/dist/esm/react/hooks/useConsent.js +48 -0
- package/dist/esm/react/hooks/useConsent.js.map +1 -0
- package/dist/esm/react/hooks/useDataDeletion.d.ts +17 -0
- package/dist/esm/react/hooks/useDataDeletion.d.ts.map +1 -0
- package/dist/esm/react/hooks/useDataDeletion.js +81 -0
- package/dist/esm/react/hooks/useDataDeletion.js.map +1 -0
- package/dist/esm/react/hooks/usePrivacyPreferences.d.ts +15 -0
- package/dist/esm/react/hooks/usePrivacyPreferences.d.ts.map +1 -0
- package/dist/esm/react/hooks/usePrivacyPreferences.js +46 -0
- package/dist/esm/react/hooks/usePrivacyPreferences.js.map +1 -0
- package/dist/esm/react/index.d.ts +11 -0
- package/dist/esm/react/index.d.ts.map +1 -1
- package/dist/esm/react/index.js +8 -0
- package/dist/esm/react/index.js.map +1 -1
- package/dist/heartbeat.d.ts +42 -0
- package/dist/heartbeat.d.ts.map +1 -0
- package/dist/index.d.ts +100 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.dev.js +903 -12
- package/dist/index.global.dev.js.map +3 -3
- package/dist/index.global.js +2 -2
- package/dist/index.global.js.map +4 -4
- package/dist/index.js +321 -11
- package/dist/index.mjs +321 -11
- package/dist/page-tracking.d.ts +60 -0
- package/dist/page-tracking.d.ts.map +1 -0
- package/dist/react/activity.d.ts +59 -0
- package/dist/react/activity.d.ts.map +1 -0
- package/dist/react/activity.js +130 -0
- package/dist/react/activity.mjs +126 -0
- package/dist/react/consent.d.ts +68 -0
- package/dist/react/consent.d.ts.map +1 -0
- package/dist/react/consent.js +190 -0
- package/dist/react/consent.mjs +186 -0
- package/dist/react/cookies.d.ts +28 -0
- package/dist/react/cookies.d.ts.map +1 -0
- package/dist/react/cookies.js +94 -0
- package/dist/react/cookies.mjs +88 -0
- package/dist/react/heartbeat.d.ts +42 -0
- package/dist/react/heartbeat.d.ts.map +1 -0
- package/dist/react/heartbeat.js +91 -0
- package/dist/react/heartbeat.mjs +87 -0
- package/dist/react/index.d.ts +100 -3
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +321 -11
- package/dist/react/index.mjs +321 -11
- package/dist/react/page-tracking.d.ts +60 -0
- package/dist/react/page-tracking.d.ts.map +1 -0
- package/dist/react/page-tracking.js +179 -0
- package/dist/react/page-tracking.mjs +175 -0
- package/dist/react/react/components/ConsentBanner.d.ts +16 -0
- package/dist/react/react/components/ConsentBanner.d.ts.map +1 -0
- package/dist/react/react/components/ConsentBanner.js +78 -0
- package/dist/react/react/components/ConsentBanner.mjs +75 -0
- package/dist/react/react/components/CookieNotice.d.ts +12 -0
- package/dist/react/react/components/CookieNotice.d.ts.map +1 -0
- package/dist/react/react/components/CookieNotice.js +28 -0
- package/dist/react/react/components/CookieNotice.mjs +25 -0
- package/dist/react/react/components/PrivacyPreferenceCenter.d.ts +12 -0
- package/dist/react/react/components/PrivacyPreferenceCenter.d.ts.map +1 -0
- package/dist/react/react/components/PrivacyPreferenceCenter.js +86 -0
- package/dist/react/react/components/PrivacyPreferenceCenter.mjs +83 -0
- package/dist/react/react/hooks/useConsent.d.ts +13 -0
- package/dist/react/react/hooks/useConsent.d.ts.map +1 -0
- package/dist/react/react/hooks/useConsent.js +50 -0
- package/dist/react/react/hooks/useConsent.mjs +47 -0
- package/dist/react/react/hooks/useDataDeletion.d.ts +17 -0
- package/dist/react/react/hooks/useDataDeletion.d.ts.map +1 -0
- package/dist/react/react/hooks/useDataDeletion.js +83 -0
- package/dist/react/react/hooks/useDataDeletion.mjs +80 -0
- package/dist/react/react/hooks/usePrivacyPreferences.d.ts +15 -0
- package/dist/react/react/hooks/usePrivacyPreferences.d.ts.map +1 -0
- package/dist/react/react/hooks/usePrivacyPreferences.js +48 -0
- package/dist/react/react/hooks/usePrivacyPreferences.mjs +45 -0
- package/dist/react/react/index.d.ts +11 -0
- package/dist/react/react/index.d.ts.map +1 -1
- package/dist/react/react/index.js +15 -1
- package/dist/react/react/index.mjs +8 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,9 +6,15 @@
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.GrainAnalytics = void 0;
|
|
8
8
|
exports.createGrainAnalytics = createGrainAnalytics;
|
|
9
|
+
const consent_1 = require("./consent");
|
|
10
|
+
const cookies_1 = require("./cookies");
|
|
11
|
+
const activity_1 = require("./activity");
|
|
12
|
+
const heartbeat_1 = require("./heartbeat");
|
|
13
|
+
const page_tracking_1 = require("./page-tracking");
|
|
9
14
|
class GrainAnalytics {
|
|
10
15
|
constructor(config) {
|
|
11
16
|
this.eventQueue = [];
|
|
17
|
+
this.waitingForConsentQueue = [];
|
|
12
18
|
this.flushTimer = null;
|
|
13
19
|
this.isDestroyed = false;
|
|
14
20
|
this.globalUserId = null;
|
|
@@ -18,6 +24,13 @@ class GrainAnalytics {
|
|
|
18
24
|
this.configRefreshTimer = null;
|
|
19
25
|
this.configChangeListeners = [];
|
|
20
26
|
this.configFetchPromise = null;
|
|
27
|
+
this.cookiesEnabled = false;
|
|
28
|
+
// Automatic Tracking properties
|
|
29
|
+
this.activityDetector = null;
|
|
30
|
+
this.heartbeatManager = null;
|
|
31
|
+
this.pageTrackingManager = null;
|
|
32
|
+
this.ephemeralSessionId = null;
|
|
33
|
+
this.eventCountSinceLastHeartbeat = 0;
|
|
21
34
|
this.config = {
|
|
22
35
|
apiUrl: 'https://api.grainql.com',
|
|
23
36
|
authStrategy: 'NONE',
|
|
@@ -32,9 +45,30 @@ class GrainAnalytics {
|
|
|
32
45
|
configCacheKey: 'grain_config',
|
|
33
46
|
configRefreshInterval: 300000, // 5 minutes
|
|
34
47
|
enableConfigCache: true,
|
|
48
|
+
// Privacy defaults
|
|
49
|
+
consentMode: 'opt-out',
|
|
50
|
+
waitForConsent: false,
|
|
51
|
+
enableCookies: false,
|
|
52
|
+
anonymizeIP: false,
|
|
53
|
+
disableAutoProperties: false,
|
|
54
|
+
// Automatic Tracking defaults
|
|
55
|
+
enableHeartbeat: true,
|
|
56
|
+
heartbeatActiveInterval: 120000, // 2 minutes
|
|
57
|
+
heartbeatInactiveInterval: 300000, // 5 minutes
|
|
58
|
+
enableAutoPageView: true,
|
|
59
|
+
stripQueryParams: true,
|
|
35
60
|
...config,
|
|
36
61
|
tenantId: config.tenantId,
|
|
37
62
|
};
|
|
63
|
+
// Initialize consent manager
|
|
64
|
+
this.consentManager = new consent_1.ConsentManager(this.config.tenantId, this.config.consentMode);
|
|
65
|
+
// Check if cookies are enabled
|
|
66
|
+
if (this.config.enableCookies) {
|
|
67
|
+
this.cookiesEnabled = (0, cookies_1.areCookiesEnabled)();
|
|
68
|
+
if (!this.cookiesEnabled && this.config.debug) {
|
|
69
|
+
console.warn('[Grain Analytics] Cookies are not available, falling back to localStorage');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
38
72
|
// Set global userId if provided in config
|
|
39
73
|
if (config.userId) {
|
|
40
74
|
this.globalUserId = config.userId;
|
|
@@ -44,6 +78,18 @@ class GrainAnalytics {
|
|
|
44
78
|
this.setupBeforeUnload();
|
|
45
79
|
this.startFlushTimer();
|
|
46
80
|
this.initializeConfigCache();
|
|
81
|
+
// Initialize ephemeral session ID (memory-only, not persisted)
|
|
82
|
+
this.ephemeralSessionId = this.generateUUID();
|
|
83
|
+
// Initialize automatic tracking (browser only)
|
|
84
|
+
if (typeof window !== 'undefined') {
|
|
85
|
+
this.initializeAutomaticTracking();
|
|
86
|
+
}
|
|
87
|
+
// Set up consent change listener to flush waiting events and handle consent upgrade
|
|
88
|
+
this.consentManager.addListener((state) => {
|
|
89
|
+
if (state.granted) {
|
|
90
|
+
this.handleConsentGranted();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
47
93
|
}
|
|
48
94
|
validateConfig() {
|
|
49
95
|
if (!this.config.tenantId) {
|
|
@@ -77,22 +123,38 @@ class GrainAnalytics {
|
|
|
77
123
|
return this.generateUUID();
|
|
78
124
|
}
|
|
79
125
|
/**
|
|
80
|
-
* Initialize persistent anonymous user ID from
|
|
126
|
+
* Initialize persistent anonymous user ID from cookies or localStorage
|
|
127
|
+
* Priority: Cookie → localStorage → generate new
|
|
81
128
|
*/
|
|
82
129
|
initializePersistentAnonymousUserId() {
|
|
83
130
|
if (typeof window === 'undefined')
|
|
84
131
|
return;
|
|
85
132
|
const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
|
|
133
|
+
const cookieName = '_grain_uid';
|
|
86
134
|
try {
|
|
135
|
+
// Try to load from cookie first if enabled
|
|
136
|
+
if (this.cookiesEnabled) {
|
|
137
|
+
const cookieValue = (0, cookies_1.getCookie)(cookieName);
|
|
138
|
+
if (cookieValue) {
|
|
139
|
+
this.persistentAnonymousUserId = cookieValue;
|
|
140
|
+
this.log('Loaded persistent anonymous user ID from cookie:', this.persistentAnonymousUserId);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Fallback to localStorage
|
|
87
145
|
const stored = localStorage.getItem(storageKey);
|
|
88
146
|
if (stored) {
|
|
89
147
|
this.persistentAnonymousUserId = stored;
|
|
90
|
-
this.log('Loaded persistent anonymous user ID:', this.persistentAnonymousUserId);
|
|
148
|
+
this.log('Loaded persistent anonymous user ID from localStorage:', this.persistentAnonymousUserId);
|
|
149
|
+
// Migrate to cookie if enabled
|
|
150
|
+
if (this.cookiesEnabled) {
|
|
151
|
+
this.savePersistentAnonymousUserId(stored);
|
|
152
|
+
}
|
|
91
153
|
}
|
|
92
154
|
else {
|
|
93
155
|
// Generate new UUIDv4 anonymous user ID
|
|
94
156
|
this.persistentAnonymousUserId = this.generateAnonymousUserId();
|
|
95
|
-
|
|
157
|
+
this.savePersistentAnonymousUserId(this.persistentAnonymousUserId);
|
|
96
158
|
this.log('Generated new persistent anonymous user ID:', this.persistentAnonymousUserId);
|
|
97
159
|
}
|
|
98
160
|
}
|
|
@@ -102,10 +164,36 @@ class GrainAnalytics {
|
|
|
102
164
|
this.persistentAnonymousUserId = this.generateAnonymousUserId();
|
|
103
165
|
}
|
|
104
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Save persistent anonymous user ID to cookie and/or localStorage
|
|
169
|
+
*/
|
|
170
|
+
savePersistentAnonymousUserId(userId) {
|
|
171
|
+
if (typeof window === 'undefined')
|
|
172
|
+
return;
|
|
173
|
+
const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
|
|
174
|
+
const cookieName = '_grain_uid';
|
|
175
|
+
try {
|
|
176
|
+
// Save to cookie if enabled
|
|
177
|
+
if (this.cookiesEnabled) {
|
|
178
|
+
const cookieOptions = {
|
|
179
|
+
maxAge: 365 * 24 * 60 * 60, // 365 days
|
|
180
|
+
sameSite: 'lax',
|
|
181
|
+
secure: window.location.protocol === 'https:',
|
|
182
|
+
...this.config.cookieOptions,
|
|
183
|
+
};
|
|
184
|
+
(0, cookies_1.setCookie)(cookieName, userId, cookieOptions);
|
|
185
|
+
}
|
|
186
|
+
// Always save to localStorage as fallback
|
|
187
|
+
localStorage.setItem(storageKey, userId);
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
this.log('Failed to save persistent anonymous user ID:', error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
105
193
|
/**
|
|
106
194
|
* Get the effective user ID (global userId or persistent anonymous ID)
|
|
107
195
|
*/
|
|
108
|
-
|
|
196
|
+
getEffectiveUserIdInternal() {
|
|
109
197
|
if (this.globalUserId) {
|
|
110
198
|
return this.globalUserId;
|
|
111
199
|
}
|
|
@@ -253,7 +341,7 @@ class GrainAnalytics {
|
|
|
253
341
|
formatEvent(event) {
|
|
254
342
|
return {
|
|
255
343
|
eventName: event.eventName,
|
|
256
|
-
userId: event.userId || this.
|
|
344
|
+
userId: event.userId || this.getEffectiveUserIdInternal(),
|
|
257
345
|
properties: event.properties || {},
|
|
258
346
|
};
|
|
259
347
|
}
|
|
@@ -433,6 +521,120 @@ class GrainAnalytics {
|
|
|
433
521
|
}
|
|
434
522
|
});
|
|
435
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* Initialize automatic tracking (heartbeat and page views)
|
|
526
|
+
*/
|
|
527
|
+
initializeAutomaticTracking() {
|
|
528
|
+
if (this.config.enableHeartbeat) {
|
|
529
|
+
try {
|
|
530
|
+
this.activityDetector = new activity_1.ActivityDetector();
|
|
531
|
+
this.heartbeatManager = new heartbeat_1.HeartbeatManager(this, this.activityDetector, {
|
|
532
|
+
activeInterval: this.config.heartbeatActiveInterval,
|
|
533
|
+
inactiveInterval: this.config.heartbeatInactiveInterval,
|
|
534
|
+
debug: this.config.debug,
|
|
535
|
+
});
|
|
536
|
+
this.log('Heartbeat tracking initialized');
|
|
537
|
+
}
|
|
538
|
+
catch (error) {
|
|
539
|
+
this.log('Failed to initialize heartbeat tracking:', error);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (this.config.enableAutoPageView) {
|
|
543
|
+
try {
|
|
544
|
+
this.pageTrackingManager = new page_tracking_1.PageTrackingManager(this, {
|
|
545
|
+
stripQueryParams: this.config.stripQueryParams,
|
|
546
|
+
debug: this.config.debug,
|
|
547
|
+
});
|
|
548
|
+
this.log('Auto page view tracking initialized');
|
|
549
|
+
}
|
|
550
|
+
catch (error) {
|
|
551
|
+
this.log('Failed to initialize page view tracking:', error);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Handle consent granted - upgrade ephemeral session to persistent user
|
|
557
|
+
*/
|
|
558
|
+
handleConsentGranted() {
|
|
559
|
+
this.flushWaitingForConsentQueue();
|
|
560
|
+
// Track consent granted event with mapping
|
|
561
|
+
if (this.ephemeralSessionId) {
|
|
562
|
+
this.trackSystemEvent('_grain_consent_granted', {
|
|
563
|
+
previous_session_id: this.ephemeralSessionId,
|
|
564
|
+
new_user_id: this.getEffectiveUserId(),
|
|
565
|
+
timestamp: Date.now(),
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Track system events that bypass consent checks (for necessary/functional tracking)
|
|
571
|
+
*/
|
|
572
|
+
trackSystemEvent(eventName, properties) {
|
|
573
|
+
if (this.isDestroyed)
|
|
574
|
+
return;
|
|
575
|
+
const hasConsent = this.consentManager.hasConsent('analytics');
|
|
576
|
+
// Create event with appropriate user ID
|
|
577
|
+
const event = {
|
|
578
|
+
eventName,
|
|
579
|
+
userId: hasConsent ? this.getEffectiveUserId() : this.getEphemeralSessionId(),
|
|
580
|
+
properties: {
|
|
581
|
+
...properties,
|
|
582
|
+
_minimal: !hasConsent, // Flag to indicate minimal tracking
|
|
583
|
+
_consent_status: hasConsent ? 'granted' : 'pending',
|
|
584
|
+
},
|
|
585
|
+
};
|
|
586
|
+
// Bypass consent check for necessary system events
|
|
587
|
+
this.eventQueue.push(event);
|
|
588
|
+
this.eventCountSinceLastHeartbeat++;
|
|
589
|
+
this.log(`Queued system event: ${eventName}`, properties);
|
|
590
|
+
// Consider flushing
|
|
591
|
+
if (this.eventQueue.length >= this.config.batchSize) {
|
|
592
|
+
this.flush().catch((error) => {
|
|
593
|
+
const formattedError = this.formatError(error, 'flush system event');
|
|
594
|
+
this.logError(formattedError);
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Get ephemeral session ID (memory-only, not persisted)
|
|
600
|
+
*/
|
|
601
|
+
getEphemeralSessionId() {
|
|
602
|
+
if (!this.ephemeralSessionId) {
|
|
603
|
+
this.ephemeralSessionId = this.generateUUID();
|
|
604
|
+
}
|
|
605
|
+
return this.ephemeralSessionId;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Get the current page path from page tracker
|
|
609
|
+
*/
|
|
610
|
+
getCurrentPage() {
|
|
611
|
+
return this.pageTrackingManager?.getCurrentPage() || null;
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Get event count since last heartbeat
|
|
615
|
+
*/
|
|
616
|
+
getEventCountSinceLastHeartbeat() {
|
|
617
|
+
return this.eventCountSinceLastHeartbeat;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Reset event count since last heartbeat
|
|
621
|
+
*/
|
|
622
|
+
resetEventCountSinceLastHeartbeat() {
|
|
623
|
+
this.eventCountSinceLastHeartbeat = 0;
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Get the effective user ID (public method)
|
|
627
|
+
*/
|
|
628
|
+
getEffectiveUserId() {
|
|
629
|
+
return this.getEffectiveUserIdInternal();
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Get the session ID (ephemeral or persistent based on consent)
|
|
633
|
+
*/
|
|
634
|
+
getSessionId() {
|
|
635
|
+
const hasConsent = this.consentManager.hasConsent('analytics');
|
|
636
|
+
return hasConsent ? this.getEffectiveUserId() : this.getEphemeralSessionId();
|
|
637
|
+
}
|
|
436
638
|
async track(eventOrName, propertiesOrOptions, options) {
|
|
437
639
|
try {
|
|
438
640
|
if (this.isDestroyed) {
|
|
@@ -454,8 +656,30 @@ class GrainAnalytics {
|
|
|
454
656
|
event = eventOrName;
|
|
455
657
|
opts = propertiesOrOptions || {};
|
|
456
658
|
}
|
|
659
|
+
// Filter properties if whitelist is enabled
|
|
660
|
+
if (this.config.allowedProperties && event.properties) {
|
|
661
|
+
const filtered = {};
|
|
662
|
+
for (const key of this.config.allowedProperties) {
|
|
663
|
+
if (key in event.properties) {
|
|
664
|
+
filtered[key] = event.properties[key];
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
event.properties = filtered;
|
|
668
|
+
}
|
|
457
669
|
const formattedEvent = this.formatEvent(event);
|
|
670
|
+
// Check consent before tracking
|
|
671
|
+
if (this.consentManager.shouldWaitForConsent() && this.config.waitForConsent) {
|
|
672
|
+
// Queue event until consent is granted
|
|
673
|
+
this.waitingForConsentQueue.push(formattedEvent);
|
|
674
|
+
this.log(`Event waiting for consent: ${event.eventName}`, event.properties);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (!this.consentManager.hasConsent('analytics')) {
|
|
678
|
+
this.log(`Event blocked by consent: ${event.eventName}`);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
458
681
|
this.eventQueue.push(formattedEvent);
|
|
682
|
+
this.eventCountSinceLastHeartbeat++;
|
|
459
683
|
this.log(`Queued event: ${event.eventName}`, event.properties);
|
|
460
684
|
// Check if we should flush immediately
|
|
461
685
|
if (opts.flush || this.eventQueue.length >= this.config.batchSize) {
|
|
@@ -467,6 +691,22 @@ class GrainAnalytics {
|
|
|
467
691
|
this.logError(formattedError);
|
|
468
692
|
}
|
|
469
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* Flush events that were waiting for consent
|
|
696
|
+
*/
|
|
697
|
+
flushWaitingForConsentQueue() {
|
|
698
|
+
if (this.waitingForConsentQueue.length === 0)
|
|
699
|
+
return;
|
|
700
|
+
this.log(`Flushing ${this.waitingForConsentQueue.length} events waiting for consent`);
|
|
701
|
+
// Move waiting events to main queue
|
|
702
|
+
this.eventQueue.push(...this.waitingForConsentQueue);
|
|
703
|
+
this.waitingForConsentQueue = [];
|
|
704
|
+
// Flush immediately
|
|
705
|
+
this.flush().catch((error) => {
|
|
706
|
+
const formattedError = this.formatError(error, 'flush waiting for consent queue');
|
|
707
|
+
this.logError(formattedError);
|
|
708
|
+
});
|
|
709
|
+
}
|
|
470
710
|
/**
|
|
471
711
|
* Identify a user (sets userId for subsequent events)
|
|
472
712
|
*/
|
|
@@ -513,7 +753,7 @@ class GrainAnalytics {
|
|
|
513
753
|
* Get current effective user ID (global userId or persistent anonymous ID)
|
|
514
754
|
*/
|
|
515
755
|
getEffectiveUserIdPublic() {
|
|
516
|
-
return this.
|
|
756
|
+
return this.getEffectiveUserIdInternal();
|
|
517
757
|
}
|
|
518
758
|
/**
|
|
519
759
|
* Login with auth token or userId on the fly
|
|
@@ -563,7 +803,7 @@ class GrainAnalytics {
|
|
|
563
803
|
this.log(`Login: Setting auth strategy to ${options.authStrategy}`);
|
|
564
804
|
this.config.authStrategy = options.authStrategy;
|
|
565
805
|
}
|
|
566
|
-
this.log(`Login successful. Effective user ID: ${this.
|
|
806
|
+
this.log(`Login successful. Effective user ID: ${this.getEffectiveUserIdInternal()}`);
|
|
567
807
|
}
|
|
568
808
|
catch (error) {
|
|
569
809
|
const formattedError = this.formatError(error, 'login');
|
|
@@ -608,7 +848,7 @@ class GrainAnalytics {
|
|
|
608
848
|
}
|
|
609
849
|
}
|
|
610
850
|
}
|
|
611
|
-
this.log(`Logout successful. Effective user ID: ${this.
|
|
851
|
+
this.log(`Logout successful. Effective user ID: ${this.getEffectiveUserIdInternal()}`);
|
|
612
852
|
}
|
|
613
853
|
catch (error) {
|
|
614
854
|
const formattedError = this.formatError(error, 'logout');
|
|
@@ -626,7 +866,7 @@ class GrainAnalytics {
|
|
|
626
866
|
this.logError(formattedError);
|
|
627
867
|
return;
|
|
628
868
|
}
|
|
629
|
-
const userId = options?.userId || this.
|
|
869
|
+
const userId = options?.userId || this.getEffectiveUserIdInternal();
|
|
630
870
|
// Validate property count (max 4 properties)
|
|
631
871
|
const propertyKeys = Object.keys(properties);
|
|
632
872
|
if (propertyKeys.length > 4) {
|
|
@@ -906,7 +1146,7 @@ class GrainAnalytics {
|
|
|
906
1146
|
this.logError(formattedError);
|
|
907
1147
|
return null;
|
|
908
1148
|
}
|
|
909
|
-
const userId = options.userId || this.
|
|
1149
|
+
const userId = options.userId || this.getEffectiveUserIdInternal();
|
|
910
1150
|
const immediateKeys = options.immediateKeys || [];
|
|
911
1151
|
const properties = options.properties || {};
|
|
912
1152
|
const request = {
|
|
@@ -1108,7 +1348,7 @@ class GrainAnalytics {
|
|
|
1108
1348
|
async preloadConfig(immediateKeys = [], properties) {
|
|
1109
1349
|
try {
|
|
1110
1350
|
// Use effective userId (will be generated if not set)
|
|
1111
|
-
const effectiveUserId = this.
|
|
1351
|
+
const effectiveUserId = this.getEffectiveUserIdInternal();
|
|
1112
1352
|
this.log(`Preloading config for user: ${effectiveUserId}`);
|
|
1113
1353
|
const response = await this.fetchConfig({ immediateKeys, properties });
|
|
1114
1354
|
if (response) {
|
|
@@ -1130,6 +1370,63 @@ class GrainAnalytics {
|
|
|
1130
1370
|
}
|
|
1131
1371
|
return chunks;
|
|
1132
1372
|
}
|
|
1373
|
+
// Privacy & Consent Methods
|
|
1374
|
+
/**
|
|
1375
|
+
* Grant consent for tracking
|
|
1376
|
+
* @param categories - Optional array of consent categories (e.g., ['analytics', 'functional'])
|
|
1377
|
+
*/
|
|
1378
|
+
grantConsent(categories) {
|
|
1379
|
+
try {
|
|
1380
|
+
this.consentManager.grantConsent(categories);
|
|
1381
|
+
this.log('Consent granted', categories);
|
|
1382
|
+
}
|
|
1383
|
+
catch (error) {
|
|
1384
|
+
const formattedError = this.formatError(error, 'grantConsent');
|
|
1385
|
+
this.logError(formattedError);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Revoke consent for tracking (opt-out)
|
|
1390
|
+
* @param categories - Optional array of categories to revoke (if not provided, revokes all)
|
|
1391
|
+
*/
|
|
1392
|
+
revokeConsent(categories) {
|
|
1393
|
+
try {
|
|
1394
|
+
this.consentManager.revokeConsent(categories);
|
|
1395
|
+
this.log('Consent revoked', categories);
|
|
1396
|
+
// Clear queued events when consent is revoked
|
|
1397
|
+
this.eventQueue = [];
|
|
1398
|
+
this.waitingForConsentQueue = [];
|
|
1399
|
+
}
|
|
1400
|
+
catch (error) {
|
|
1401
|
+
const formattedError = this.formatError(error, 'revokeConsent');
|
|
1402
|
+
this.logError(formattedError);
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Get current consent state
|
|
1407
|
+
*/
|
|
1408
|
+
getConsentState() {
|
|
1409
|
+
return this.consentManager.getConsentState();
|
|
1410
|
+
}
|
|
1411
|
+
/**
|
|
1412
|
+
* Check if user has granted consent
|
|
1413
|
+
* @param category - Optional category to check (if not provided, checks general consent)
|
|
1414
|
+
*/
|
|
1415
|
+
hasConsent(category) {
|
|
1416
|
+
return this.consentManager.hasConsent(category);
|
|
1417
|
+
}
|
|
1418
|
+
/**
|
|
1419
|
+
* Add listener for consent state changes
|
|
1420
|
+
*/
|
|
1421
|
+
onConsentChange(listener) {
|
|
1422
|
+
this.consentManager.addListener(listener);
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Remove consent change listener
|
|
1426
|
+
*/
|
|
1427
|
+
offConsentChange(listener) {
|
|
1428
|
+
this.consentManager.removeListener(listener);
|
|
1429
|
+
}
|
|
1133
1430
|
/**
|
|
1134
1431
|
* Destroy the client and clean up resources
|
|
1135
1432
|
*/
|
|
@@ -1143,6 +1440,19 @@ class GrainAnalytics {
|
|
|
1143
1440
|
this.stopConfigRefreshTimer();
|
|
1144
1441
|
// Clear config change listeners
|
|
1145
1442
|
this.configChangeListeners = [];
|
|
1443
|
+
// Destroy automatic tracking managers
|
|
1444
|
+
if (this.heartbeatManager) {
|
|
1445
|
+
this.heartbeatManager.destroy();
|
|
1446
|
+
this.heartbeatManager = null;
|
|
1447
|
+
}
|
|
1448
|
+
if (this.pageTrackingManager) {
|
|
1449
|
+
this.pageTrackingManager.destroy();
|
|
1450
|
+
this.pageTrackingManager = null;
|
|
1451
|
+
}
|
|
1452
|
+
if (this.activityDetector) {
|
|
1453
|
+
this.activityDetector.destroy();
|
|
1454
|
+
this.activityDetector = null;
|
|
1455
|
+
}
|
|
1146
1456
|
// Send any remaining events (in chunks if necessary)
|
|
1147
1457
|
if (this.eventQueue.length > 0) {
|
|
1148
1458
|
const eventsToSend = [...this.eventQueue];
|