@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.mjs
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
* Grain Analytics Web SDK
|
|
3
3
|
* A lightweight, dependency-free TypeScript SDK for sending analytics events to Grain's REST API
|
|
4
4
|
*/
|
|
5
|
+
import { ConsentManager } from './consent';
|
|
6
|
+
import { setCookie, getCookie, areCookiesEnabled } from './cookies';
|
|
7
|
+
import { ActivityDetector } from './activity';
|
|
8
|
+
import { HeartbeatManager } from './heartbeat';
|
|
9
|
+
import { PageTrackingManager } from './page-tracking';
|
|
5
10
|
export class GrainAnalytics {
|
|
6
11
|
constructor(config) {
|
|
7
12
|
this.eventQueue = [];
|
|
13
|
+
this.waitingForConsentQueue = [];
|
|
8
14
|
this.flushTimer = null;
|
|
9
15
|
this.isDestroyed = false;
|
|
10
16
|
this.globalUserId = null;
|
|
@@ -14,6 +20,13 @@ export class GrainAnalytics {
|
|
|
14
20
|
this.configRefreshTimer = null;
|
|
15
21
|
this.configChangeListeners = [];
|
|
16
22
|
this.configFetchPromise = null;
|
|
23
|
+
this.cookiesEnabled = false;
|
|
24
|
+
// Automatic Tracking properties
|
|
25
|
+
this.activityDetector = null;
|
|
26
|
+
this.heartbeatManager = null;
|
|
27
|
+
this.pageTrackingManager = null;
|
|
28
|
+
this.ephemeralSessionId = null;
|
|
29
|
+
this.eventCountSinceLastHeartbeat = 0;
|
|
17
30
|
this.config = {
|
|
18
31
|
apiUrl: 'https://api.grainql.com',
|
|
19
32
|
authStrategy: 'NONE',
|
|
@@ -28,9 +41,30 @@ export class GrainAnalytics {
|
|
|
28
41
|
configCacheKey: 'grain_config',
|
|
29
42
|
configRefreshInterval: 300000, // 5 minutes
|
|
30
43
|
enableConfigCache: true,
|
|
44
|
+
// Privacy defaults
|
|
45
|
+
consentMode: 'opt-out',
|
|
46
|
+
waitForConsent: false,
|
|
47
|
+
enableCookies: false,
|
|
48
|
+
anonymizeIP: false,
|
|
49
|
+
disableAutoProperties: false,
|
|
50
|
+
// Automatic Tracking defaults
|
|
51
|
+
enableHeartbeat: true,
|
|
52
|
+
heartbeatActiveInterval: 120000, // 2 minutes
|
|
53
|
+
heartbeatInactiveInterval: 300000, // 5 minutes
|
|
54
|
+
enableAutoPageView: true,
|
|
55
|
+
stripQueryParams: true,
|
|
31
56
|
...config,
|
|
32
57
|
tenantId: config.tenantId,
|
|
33
58
|
};
|
|
59
|
+
// Initialize consent manager
|
|
60
|
+
this.consentManager = new ConsentManager(this.config.tenantId, this.config.consentMode);
|
|
61
|
+
// Check if cookies are enabled
|
|
62
|
+
if (this.config.enableCookies) {
|
|
63
|
+
this.cookiesEnabled = areCookiesEnabled();
|
|
64
|
+
if (!this.cookiesEnabled && this.config.debug) {
|
|
65
|
+
console.warn('[Grain Analytics] Cookies are not available, falling back to localStorage');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
34
68
|
// Set global userId if provided in config
|
|
35
69
|
if (config.userId) {
|
|
36
70
|
this.globalUserId = config.userId;
|
|
@@ -40,6 +74,18 @@ export class GrainAnalytics {
|
|
|
40
74
|
this.setupBeforeUnload();
|
|
41
75
|
this.startFlushTimer();
|
|
42
76
|
this.initializeConfigCache();
|
|
77
|
+
// Initialize ephemeral session ID (memory-only, not persisted)
|
|
78
|
+
this.ephemeralSessionId = this.generateUUID();
|
|
79
|
+
// Initialize automatic tracking (browser only)
|
|
80
|
+
if (typeof window !== 'undefined') {
|
|
81
|
+
this.initializeAutomaticTracking();
|
|
82
|
+
}
|
|
83
|
+
// Set up consent change listener to flush waiting events and handle consent upgrade
|
|
84
|
+
this.consentManager.addListener((state) => {
|
|
85
|
+
if (state.granted) {
|
|
86
|
+
this.handleConsentGranted();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
43
89
|
}
|
|
44
90
|
validateConfig() {
|
|
45
91
|
if (!this.config.tenantId) {
|
|
@@ -73,22 +119,38 @@ export class GrainAnalytics {
|
|
|
73
119
|
return this.generateUUID();
|
|
74
120
|
}
|
|
75
121
|
/**
|
|
76
|
-
* Initialize persistent anonymous user ID from
|
|
122
|
+
* Initialize persistent anonymous user ID from cookies or localStorage
|
|
123
|
+
* Priority: Cookie → localStorage → generate new
|
|
77
124
|
*/
|
|
78
125
|
initializePersistentAnonymousUserId() {
|
|
79
126
|
if (typeof window === 'undefined')
|
|
80
127
|
return;
|
|
81
128
|
const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
|
|
129
|
+
const cookieName = '_grain_uid';
|
|
82
130
|
try {
|
|
131
|
+
// Try to load from cookie first if enabled
|
|
132
|
+
if (this.cookiesEnabled) {
|
|
133
|
+
const cookieValue = getCookie(cookieName);
|
|
134
|
+
if (cookieValue) {
|
|
135
|
+
this.persistentAnonymousUserId = cookieValue;
|
|
136
|
+
this.log('Loaded persistent anonymous user ID from cookie:', this.persistentAnonymousUserId);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Fallback to localStorage
|
|
83
141
|
const stored = localStorage.getItem(storageKey);
|
|
84
142
|
if (stored) {
|
|
85
143
|
this.persistentAnonymousUserId = stored;
|
|
86
|
-
this.log('Loaded persistent anonymous user ID:', this.persistentAnonymousUserId);
|
|
144
|
+
this.log('Loaded persistent anonymous user ID from localStorage:', this.persistentAnonymousUserId);
|
|
145
|
+
// Migrate to cookie if enabled
|
|
146
|
+
if (this.cookiesEnabled) {
|
|
147
|
+
this.savePersistentAnonymousUserId(stored);
|
|
148
|
+
}
|
|
87
149
|
}
|
|
88
150
|
else {
|
|
89
151
|
// Generate new UUIDv4 anonymous user ID
|
|
90
152
|
this.persistentAnonymousUserId = this.generateAnonymousUserId();
|
|
91
|
-
|
|
153
|
+
this.savePersistentAnonymousUserId(this.persistentAnonymousUserId);
|
|
92
154
|
this.log('Generated new persistent anonymous user ID:', this.persistentAnonymousUserId);
|
|
93
155
|
}
|
|
94
156
|
}
|
|
@@ -98,10 +160,36 @@ export class GrainAnalytics {
|
|
|
98
160
|
this.persistentAnonymousUserId = this.generateAnonymousUserId();
|
|
99
161
|
}
|
|
100
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* Save persistent anonymous user ID to cookie and/or localStorage
|
|
165
|
+
*/
|
|
166
|
+
savePersistentAnonymousUserId(userId) {
|
|
167
|
+
if (typeof window === 'undefined')
|
|
168
|
+
return;
|
|
169
|
+
const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
|
|
170
|
+
const cookieName = '_grain_uid';
|
|
171
|
+
try {
|
|
172
|
+
// Save to cookie if enabled
|
|
173
|
+
if (this.cookiesEnabled) {
|
|
174
|
+
const cookieOptions = {
|
|
175
|
+
maxAge: 365 * 24 * 60 * 60, // 365 days
|
|
176
|
+
sameSite: 'lax',
|
|
177
|
+
secure: window.location.protocol === 'https:',
|
|
178
|
+
...this.config.cookieOptions,
|
|
179
|
+
};
|
|
180
|
+
setCookie(cookieName, userId, cookieOptions);
|
|
181
|
+
}
|
|
182
|
+
// Always save to localStorage as fallback
|
|
183
|
+
localStorage.setItem(storageKey, userId);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
this.log('Failed to save persistent anonymous user ID:', error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
101
189
|
/**
|
|
102
190
|
* Get the effective user ID (global userId or persistent anonymous ID)
|
|
103
191
|
*/
|
|
104
|
-
|
|
192
|
+
getEffectiveUserIdInternal() {
|
|
105
193
|
if (this.globalUserId) {
|
|
106
194
|
return this.globalUserId;
|
|
107
195
|
}
|
|
@@ -249,7 +337,7 @@ export class GrainAnalytics {
|
|
|
249
337
|
formatEvent(event) {
|
|
250
338
|
return {
|
|
251
339
|
eventName: event.eventName,
|
|
252
|
-
userId: event.userId || this.
|
|
340
|
+
userId: event.userId || this.getEffectiveUserIdInternal(),
|
|
253
341
|
properties: event.properties || {},
|
|
254
342
|
};
|
|
255
343
|
}
|
|
@@ -429,6 +517,120 @@ export class GrainAnalytics {
|
|
|
429
517
|
}
|
|
430
518
|
});
|
|
431
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Initialize automatic tracking (heartbeat and page views)
|
|
522
|
+
*/
|
|
523
|
+
initializeAutomaticTracking() {
|
|
524
|
+
if (this.config.enableHeartbeat) {
|
|
525
|
+
try {
|
|
526
|
+
this.activityDetector = new ActivityDetector();
|
|
527
|
+
this.heartbeatManager = new HeartbeatManager(this, this.activityDetector, {
|
|
528
|
+
activeInterval: this.config.heartbeatActiveInterval,
|
|
529
|
+
inactiveInterval: this.config.heartbeatInactiveInterval,
|
|
530
|
+
debug: this.config.debug,
|
|
531
|
+
});
|
|
532
|
+
this.log('Heartbeat tracking initialized');
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
this.log('Failed to initialize heartbeat tracking:', error);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
if (this.config.enableAutoPageView) {
|
|
539
|
+
try {
|
|
540
|
+
this.pageTrackingManager = new PageTrackingManager(this, {
|
|
541
|
+
stripQueryParams: this.config.stripQueryParams,
|
|
542
|
+
debug: this.config.debug,
|
|
543
|
+
});
|
|
544
|
+
this.log('Auto page view tracking initialized');
|
|
545
|
+
}
|
|
546
|
+
catch (error) {
|
|
547
|
+
this.log('Failed to initialize page view tracking:', error);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Handle consent granted - upgrade ephemeral session to persistent user
|
|
553
|
+
*/
|
|
554
|
+
handleConsentGranted() {
|
|
555
|
+
this.flushWaitingForConsentQueue();
|
|
556
|
+
// Track consent granted event with mapping
|
|
557
|
+
if (this.ephemeralSessionId) {
|
|
558
|
+
this.trackSystemEvent('_grain_consent_granted', {
|
|
559
|
+
previous_session_id: this.ephemeralSessionId,
|
|
560
|
+
new_user_id: this.getEffectiveUserId(),
|
|
561
|
+
timestamp: Date.now(),
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Track system events that bypass consent checks (for necessary/functional tracking)
|
|
567
|
+
*/
|
|
568
|
+
trackSystemEvent(eventName, properties) {
|
|
569
|
+
if (this.isDestroyed)
|
|
570
|
+
return;
|
|
571
|
+
const hasConsent = this.consentManager.hasConsent('analytics');
|
|
572
|
+
// Create event with appropriate user ID
|
|
573
|
+
const event = {
|
|
574
|
+
eventName,
|
|
575
|
+
userId: hasConsent ? this.getEffectiveUserId() : this.getEphemeralSessionId(),
|
|
576
|
+
properties: {
|
|
577
|
+
...properties,
|
|
578
|
+
_minimal: !hasConsent, // Flag to indicate minimal tracking
|
|
579
|
+
_consent_status: hasConsent ? 'granted' : 'pending',
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
// Bypass consent check for necessary system events
|
|
583
|
+
this.eventQueue.push(event);
|
|
584
|
+
this.eventCountSinceLastHeartbeat++;
|
|
585
|
+
this.log(`Queued system event: ${eventName}`, properties);
|
|
586
|
+
// Consider flushing
|
|
587
|
+
if (this.eventQueue.length >= this.config.batchSize) {
|
|
588
|
+
this.flush().catch((error) => {
|
|
589
|
+
const formattedError = this.formatError(error, 'flush system event');
|
|
590
|
+
this.logError(formattedError);
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Get ephemeral session ID (memory-only, not persisted)
|
|
596
|
+
*/
|
|
597
|
+
getEphemeralSessionId() {
|
|
598
|
+
if (!this.ephemeralSessionId) {
|
|
599
|
+
this.ephemeralSessionId = this.generateUUID();
|
|
600
|
+
}
|
|
601
|
+
return this.ephemeralSessionId;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Get the current page path from page tracker
|
|
605
|
+
*/
|
|
606
|
+
getCurrentPage() {
|
|
607
|
+
return this.pageTrackingManager?.getCurrentPage() || null;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Get event count since last heartbeat
|
|
611
|
+
*/
|
|
612
|
+
getEventCountSinceLastHeartbeat() {
|
|
613
|
+
return this.eventCountSinceLastHeartbeat;
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* Reset event count since last heartbeat
|
|
617
|
+
*/
|
|
618
|
+
resetEventCountSinceLastHeartbeat() {
|
|
619
|
+
this.eventCountSinceLastHeartbeat = 0;
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Get the effective user ID (public method)
|
|
623
|
+
*/
|
|
624
|
+
getEffectiveUserId() {
|
|
625
|
+
return this.getEffectiveUserIdInternal();
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Get the session ID (ephemeral or persistent based on consent)
|
|
629
|
+
*/
|
|
630
|
+
getSessionId() {
|
|
631
|
+
const hasConsent = this.consentManager.hasConsent('analytics');
|
|
632
|
+
return hasConsent ? this.getEffectiveUserId() : this.getEphemeralSessionId();
|
|
633
|
+
}
|
|
432
634
|
async track(eventOrName, propertiesOrOptions, options) {
|
|
433
635
|
try {
|
|
434
636
|
if (this.isDestroyed) {
|
|
@@ -450,8 +652,30 @@ export class GrainAnalytics {
|
|
|
450
652
|
event = eventOrName;
|
|
451
653
|
opts = propertiesOrOptions || {};
|
|
452
654
|
}
|
|
655
|
+
// Filter properties if whitelist is enabled
|
|
656
|
+
if (this.config.allowedProperties && event.properties) {
|
|
657
|
+
const filtered = {};
|
|
658
|
+
for (const key of this.config.allowedProperties) {
|
|
659
|
+
if (key in event.properties) {
|
|
660
|
+
filtered[key] = event.properties[key];
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
event.properties = filtered;
|
|
664
|
+
}
|
|
453
665
|
const formattedEvent = this.formatEvent(event);
|
|
666
|
+
// Check consent before tracking
|
|
667
|
+
if (this.consentManager.shouldWaitForConsent() && this.config.waitForConsent) {
|
|
668
|
+
// Queue event until consent is granted
|
|
669
|
+
this.waitingForConsentQueue.push(formattedEvent);
|
|
670
|
+
this.log(`Event waiting for consent: ${event.eventName}`, event.properties);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
if (!this.consentManager.hasConsent('analytics')) {
|
|
674
|
+
this.log(`Event blocked by consent: ${event.eventName}`);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
454
677
|
this.eventQueue.push(formattedEvent);
|
|
678
|
+
this.eventCountSinceLastHeartbeat++;
|
|
455
679
|
this.log(`Queued event: ${event.eventName}`, event.properties);
|
|
456
680
|
// Check if we should flush immediately
|
|
457
681
|
if (opts.flush || this.eventQueue.length >= this.config.batchSize) {
|
|
@@ -463,6 +687,22 @@ export class GrainAnalytics {
|
|
|
463
687
|
this.logError(formattedError);
|
|
464
688
|
}
|
|
465
689
|
}
|
|
690
|
+
/**
|
|
691
|
+
* Flush events that were waiting for consent
|
|
692
|
+
*/
|
|
693
|
+
flushWaitingForConsentQueue() {
|
|
694
|
+
if (this.waitingForConsentQueue.length === 0)
|
|
695
|
+
return;
|
|
696
|
+
this.log(`Flushing ${this.waitingForConsentQueue.length} events waiting for consent`);
|
|
697
|
+
// Move waiting events to main queue
|
|
698
|
+
this.eventQueue.push(...this.waitingForConsentQueue);
|
|
699
|
+
this.waitingForConsentQueue = [];
|
|
700
|
+
// Flush immediately
|
|
701
|
+
this.flush().catch((error) => {
|
|
702
|
+
const formattedError = this.formatError(error, 'flush waiting for consent queue');
|
|
703
|
+
this.logError(formattedError);
|
|
704
|
+
});
|
|
705
|
+
}
|
|
466
706
|
/**
|
|
467
707
|
* Identify a user (sets userId for subsequent events)
|
|
468
708
|
*/
|
|
@@ -509,7 +749,7 @@ export class GrainAnalytics {
|
|
|
509
749
|
* Get current effective user ID (global userId or persistent anonymous ID)
|
|
510
750
|
*/
|
|
511
751
|
getEffectiveUserIdPublic() {
|
|
512
|
-
return this.
|
|
752
|
+
return this.getEffectiveUserIdInternal();
|
|
513
753
|
}
|
|
514
754
|
/**
|
|
515
755
|
* Login with auth token or userId on the fly
|
|
@@ -559,7 +799,7 @@ export class GrainAnalytics {
|
|
|
559
799
|
this.log(`Login: Setting auth strategy to ${options.authStrategy}`);
|
|
560
800
|
this.config.authStrategy = options.authStrategy;
|
|
561
801
|
}
|
|
562
|
-
this.log(`Login successful. Effective user ID: ${this.
|
|
802
|
+
this.log(`Login successful. Effective user ID: ${this.getEffectiveUserIdInternal()}`);
|
|
563
803
|
}
|
|
564
804
|
catch (error) {
|
|
565
805
|
const formattedError = this.formatError(error, 'login');
|
|
@@ -604,7 +844,7 @@ export class GrainAnalytics {
|
|
|
604
844
|
}
|
|
605
845
|
}
|
|
606
846
|
}
|
|
607
|
-
this.log(`Logout successful. Effective user ID: ${this.
|
|
847
|
+
this.log(`Logout successful. Effective user ID: ${this.getEffectiveUserIdInternal()}`);
|
|
608
848
|
}
|
|
609
849
|
catch (error) {
|
|
610
850
|
const formattedError = this.formatError(error, 'logout');
|
|
@@ -622,7 +862,7 @@ export class GrainAnalytics {
|
|
|
622
862
|
this.logError(formattedError);
|
|
623
863
|
return;
|
|
624
864
|
}
|
|
625
|
-
const userId = options?.userId || this.
|
|
865
|
+
const userId = options?.userId || this.getEffectiveUserIdInternal();
|
|
626
866
|
// Validate property count (max 4 properties)
|
|
627
867
|
const propertyKeys = Object.keys(properties);
|
|
628
868
|
if (propertyKeys.length > 4) {
|
|
@@ -902,7 +1142,7 @@ export class GrainAnalytics {
|
|
|
902
1142
|
this.logError(formattedError);
|
|
903
1143
|
return null;
|
|
904
1144
|
}
|
|
905
|
-
const userId = options.userId || this.
|
|
1145
|
+
const userId = options.userId || this.getEffectiveUserIdInternal();
|
|
906
1146
|
const immediateKeys = options.immediateKeys || [];
|
|
907
1147
|
const properties = options.properties || {};
|
|
908
1148
|
const request = {
|
|
@@ -1104,7 +1344,7 @@ export class GrainAnalytics {
|
|
|
1104
1344
|
async preloadConfig(immediateKeys = [], properties) {
|
|
1105
1345
|
try {
|
|
1106
1346
|
// Use effective userId (will be generated if not set)
|
|
1107
|
-
const effectiveUserId = this.
|
|
1347
|
+
const effectiveUserId = this.getEffectiveUserIdInternal();
|
|
1108
1348
|
this.log(`Preloading config for user: ${effectiveUserId}`);
|
|
1109
1349
|
const response = await this.fetchConfig({ immediateKeys, properties });
|
|
1110
1350
|
if (response) {
|
|
@@ -1126,6 +1366,63 @@ export class GrainAnalytics {
|
|
|
1126
1366
|
}
|
|
1127
1367
|
return chunks;
|
|
1128
1368
|
}
|
|
1369
|
+
// Privacy & Consent Methods
|
|
1370
|
+
/**
|
|
1371
|
+
* Grant consent for tracking
|
|
1372
|
+
* @param categories - Optional array of consent categories (e.g., ['analytics', 'functional'])
|
|
1373
|
+
*/
|
|
1374
|
+
grantConsent(categories) {
|
|
1375
|
+
try {
|
|
1376
|
+
this.consentManager.grantConsent(categories);
|
|
1377
|
+
this.log('Consent granted', categories);
|
|
1378
|
+
}
|
|
1379
|
+
catch (error) {
|
|
1380
|
+
const formattedError = this.formatError(error, 'grantConsent');
|
|
1381
|
+
this.logError(formattedError);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Revoke consent for tracking (opt-out)
|
|
1386
|
+
* @param categories - Optional array of categories to revoke (if not provided, revokes all)
|
|
1387
|
+
*/
|
|
1388
|
+
revokeConsent(categories) {
|
|
1389
|
+
try {
|
|
1390
|
+
this.consentManager.revokeConsent(categories);
|
|
1391
|
+
this.log('Consent revoked', categories);
|
|
1392
|
+
// Clear queued events when consent is revoked
|
|
1393
|
+
this.eventQueue = [];
|
|
1394
|
+
this.waitingForConsentQueue = [];
|
|
1395
|
+
}
|
|
1396
|
+
catch (error) {
|
|
1397
|
+
const formattedError = this.formatError(error, 'revokeConsent');
|
|
1398
|
+
this.logError(formattedError);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Get current consent state
|
|
1403
|
+
*/
|
|
1404
|
+
getConsentState() {
|
|
1405
|
+
return this.consentManager.getConsentState();
|
|
1406
|
+
}
|
|
1407
|
+
/**
|
|
1408
|
+
* Check if user has granted consent
|
|
1409
|
+
* @param category - Optional category to check (if not provided, checks general consent)
|
|
1410
|
+
*/
|
|
1411
|
+
hasConsent(category) {
|
|
1412
|
+
return this.consentManager.hasConsent(category);
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Add listener for consent state changes
|
|
1416
|
+
*/
|
|
1417
|
+
onConsentChange(listener) {
|
|
1418
|
+
this.consentManager.addListener(listener);
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Remove consent change listener
|
|
1422
|
+
*/
|
|
1423
|
+
offConsentChange(listener) {
|
|
1424
|
+
this.consentManager.removeListener(listener);
|
|
1425
|
+
}
|
|
1129
1426
|
/**
|
|
1130
1427
|
* Destroy the client and clean up resources
|
|
1131
1428
|
*/
|
|
@@ -1139,6 +1436,19 @@ export class GrainAnalytics {
|
|
|
1139
1436
|
this.stopConfigRefreshTimer();
|
|
1140
1437
|
// Clear config change listeners
|
|
1141
1438
|
this.configChangeListeners = [];
|
|
1439
|
+
// Destroy automatic tracking managers
|
|
1440
|
+
if (this.heartbeatManager) {
|
|
1441
|
+
this.heartbeatManager.destroy();
|
|
1442
|
+
this.heartbeatManager = null;
|
|
1443
|
+
}
|
|
1444
|
+
if (this.pageTrackingManager) {
|
|
1445
|
+
this.pageTrackingManager.destroy();
|
|
1446
|
+
this.pageTrackingManager = null;
|
|
1447
|
+
}
|
|
1448
|
+
if (this.activityDetector) {
|
|
1449
|
+
this.activityDetector.destroy();
|
|
1450
|
+
this.activityDetector = null;
|
|
1451
|
+
}
|
|
1142
1452
|
// Send any remaining events (in chunks if necessary)
|
|
1143
1453
|
if (this.eventQueue.length > 0) {
|
|
1144
1454
|
const eventsToSend = [...this.eventQueue];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page Tracking for Grain Analytics
|
|
3
|
+
* Automatically tracks page views with consent-aware behavior
|
|
4
|
+
*/
|
|
5
|
+
export interface PageTrackingConfig {
|
|
6
|
+
stripQueryParams: boolean;
|
|
7
|
+
debug?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface PageTracker {
|
|
10
|
+
trackSystemEvent(eventName: string, properties: Record<string, unknown>): void;
|
|
11
|
+
hasConsent(category?: string): boolean;
|
|
12
|
+
getEffectiveUserId(): string;
|
|
13
|
+
getEphemeralSessionId(): string;
|
|
14
|
+
}
|
|
15
|
+
export declare class PageTrackingManager {
|
|
16
|
+
private config;
|
|
17
|
+
private tracker;
|
|
18
|
+
private isDestroyed;
|
|
19
|
+
private currentPath;
|
|
20
|
+
private originalPushState;
|
|
21
|
+
private originalReplaceState;
|
|
22
|
+
constructor(tracker: PageTracker, config: PageTrackingConfig);
|
|
23
|
+
/**
|
|
24
|
+
* Setup History API listeners (pushState, replaceState, popstate)
|
|
25
|
+
*/
|
|
26
|
+
private setupHistoryListeners;
|
|
27
|
+
/**
|
|
28
|
+
* Setup hash change listener
|
|
29
|
+
*/
|
|
30
|
+
private setupHashChangeListener;
|
|
31
|
+
/**
|
|
32
|
+
* Handle popstate event (back/forward navigation)
|
|
33
|
+
*/
|
|
34
|
+
private handlePopState;
|
|
35
|
+
/**
|
|
36
|
+
* Handle hash change event
|
|
37
|
+
*/
|
|
38
|
+
private handleHashChange;
|
|
39
|
+
/**
|
|
40
|
+
* Track the current page
|
|
41
|
+
*/
|
|
42
|
+
private trackCurrentPage;
|
|
43
|
+
/**
|
|
44
|
+
* Extract path from URL, optionally stripping query parameters
|
|
45
|
+
*/
|
|
46
|
+
private extractPath;
|
|
47
|
+
/**
|
|
48
|
+
* Get the current page path
|
|
49
|
+
*/
|
|
50
|
+
getCurrentPage(): string | null;
|
|
51
|
+
/**
|
|
52
|
+
* Manually track a page view (for custom navigation)
|
|
53
|
+
*/
|
|
54
|
+
trackPage(page: string, properties?: Record<string, unknown>): void;
|
|
55
|
+
/**
|
|
56
|
+
* Destroy the page tracker
|
|
57
|
+
*/
|
|
58
|
+
destroy(): void;
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=page-tracking.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-tracking.d.ts","sourceRoot":"","sources":["../src/page-tracking.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,kBAAkB;IACjC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;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;CACjC;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;gBAE5D,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;IAmCxB;;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;IAgCnE;;OAEG;IACH,OAAO,IAAI,IAAI;CAyBhB"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity Detection for Grain Analytics
|
|
3
|
+
* Tracks user activity (mouse, keyboard, touch, scroll) to determine if user is active
|
|
4
|
+
*/
|
|
5
|
+
export declare class ActivityDetector {
|
|
6
|
+
private lastActivityTime;
|
|
7
|
+
private activityThreshold;
|
|
8
|
+
private listeners;
|
|
9
|
+
private boundActivityHandler;
|
|
10
|
+
private isDestroyed;
|
|
11
|
+
private readonly activityEvents;
|
|
12
|
+
constructor();
|
|
13
|
+
/**
|
|
14
|
+
* Setup event listeners for activity detection
|
|
15
|
+
*/
|
|
16
|
+
private setupListeners;
|
|
17
|
+
/**
|
|
18
|
+
* Handle activity event
|
|
19
|
+
*/
|
|
20
|
+
private handleActivity;
|
|
21
|
+
/**
|
|
22
|
+
* Debounce function to limit how often activity handler is called
|
|
23
|
+
*/
|
|
24
|
+
private debounce;
|
|
25
|
+
/**
|
|
26
|
+
* Check if user is currently active
|
|
27
|
+
* @param threshold Time in ms to consider user inactive (default: 30s)
|
|
28
|
+
*/
|
|
29
|
+
isActive(threshold?: number): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Get time since last activity in milliseconds
|
|
32
|
+
*/
|
|
33
|
+
getTimeSinceLastActivity(): number;
|
|
34
|
+
/**
|
|
35
|
+
* Get last activity timestamp
|
|
36
|
+
*/
|
|
37
|
+
getLastActivityTime(): number;
|
|
38
|
+
/**
|
|
39
|
+
* Set activity threshold
|
|
40
|
+
*/
|
|
41
|
+
setActivityThreshold(threshold: number): void;
|
|
42
|
+
/**
|
|
43
|
+
* Add listener for activity changes
|
|
44
|
+
*/
|
|
45
|
+
addListener(listener: () => void): void;
|
|
46
|
+
/**
|
|
47
|
+
* Remove listener
|
|
48
|
+
*/
|
|
49
|
+
removeListener(listener: () => void): void;
|
|
50
|
+
/**
|
|
51
|
+
* Notify all listeners
|
|
52
|
+
*/
|
|
53
|
+
private notifyListeners;
|
|
54
|
+
/**
|
|
55
|
+
* Cleanup and remove listeners
|
|
56
|
+
*/
|
|
57
|
+
destroy(): void;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=activity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"activity.d.ts","sourceRoot":"","sources":["../../src/activity.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,iBAAiB,CAAiB;IAC1C,OAAO,CAAC,SAAS,CAAyB;IAC1C,OAAO,CAAC,oBAAoB,CAAa;IACzC,OAAO,CAAC,WAAW,CAAS;IAG5B,OAAO,CAAC,QAAQ,CAAC,cAAc,CAOpB;;IAQX;;OAEG;IACH,OAAO,CAAC,cAAc;IAQtB;;OAEG;IACH,OAAO,CAAC,cAAc;IAMtB;;OAEG;IACH,OAAO,CAAC,QAAQ;IAehB;;;OAGG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO;IAMrC;;OAEG;IACH,wBAAwB,IAAI,MAAM;IAIlC;;OAEG;IACH,mBAAmB,IAAI,MAAM;IAI7B;;OAEG;IACH,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAI7C;;OAEG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI;IAIvC;;OAEG;IACH,cAAc,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,IAAI;IAO1C;;OAEG;IACH,OAAO,CAAC,eAAe;IAUvB;;OAEG;IACH,OAAO,IAAI,IAAI;CAYhB"}
|