@grainql/analytics-web 2.8.0 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -3
- package/dist/cjs/consent.d.ts +38 -7
- package/dist/cjs/consent.d.ts.map +1 -1
- package/dist/cjs/consent.js +82 -23
- package/dist/cjs/consent.js.map +1 -1
- package/dist/cjs/id-manager.d.ts +66 -0
- package/dist/cjs/id-manager.d.ts.map +1 -0
- package/dist/cjs/id-manager.js +212 -0
- package/dist/cjs/id-manager.js.map +1 -0
- package/dist/cjs/index.d.ts +12 -8
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/page-tracking.d.ts +6 -0
- package/dist/cjs/page-tracking.d.ts.map +1 -1
- package/dist/cjs/page-tracking.js +23 -2
- package/dist/cjs/page-tracking.js.map +1 -1
- package/dist/cjs/react/hooks/useConsent.d.ts +18 -2
- package/dist/cjs/react/hooks/useConsent.d.ts.map +1 -1
- package/dist/cjs/react/hooks/useConsent.js +52 -1
- package/dist/cjs/react/hooks/useConsent.js.map +1 -1
- package/dist/consent.d.ts +38 -7
- package/dist/consent.d.ts.map +1 -1
- package/dist/consent.js +82 -23
- package/dist/esm/consent.d.ts +38 -7
- package/dist/esm/consent.d.ts.map +1 -1
- package/dist/esm/consent.js +82 -23
- package/dist/esm/consent.js.map +1 -1
- package/dist/esm/id-manager.d.ts +66 -0
- package/dist/esm/id-manager.d.ts.map +1 -0
- package/dist/esm/id-manager.js +208 -0
- package/dist/esm/id-manager.js.map +1 -0
- package/dist/esm/index.d.ts +12 -8
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/page-tracking.d.ts +6 -0
- package/dist/esm/page-tracking.d.ts.map +1 -1
- package/dist/esm/page-tracking.js +23 -2
- package/dist/esm/page-tracking.js.map +1 -1
- package/dist/esm/react/hooks/useConsent.d.ts +18 -2
- package/dist/esm/react/hooks/useConsent.d.ts.map +1 -1
- package/dist/esm/react/hooks/useConsent.js +49 -1
- package/dist/esm/react/hooks/useConsent.js.map +1 -1
- package/dist/id-manager.d.ts +66 -0
- package/dist/id-manager.d.ts.map +1 -0
- package/dist/id-manager.js +212 -0
- package/dist/index.d.ts +12 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.dev.js +310 -81
- package/dist/index.global.dev.js.map +4 -4
- package/dist/index.global.js +8 -8
- package/dist/index.global.js.map +4 -4
- package/dist/index.js +72 -44
- package/dist/index.mjs +73 -45
- package/dist/page-tracking.d.ts +6 -0
- package/dist/page-tracking.d.ts.map +1 -1
- package/dist/page-tracking.js +23 -2
- package/dist/react/hooks/useConsent.d.ts +18 -2
- package/dist/react/hooks/useConsent.d.ts.map +1 -1
- package/dist/react/hooks/useConsent.js +52 -1
- package/dist/react/hooks/useConsent.mjs +49 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -44,6 +44,7 @@ const cookies_1 = require("./cookies");
|
|
|
44
44
|
const activity_1 = require("./activity");
|
|
45
45
|
const heartbeat_1 = require("./heartbeat");
|
|
46
46
|
const page_tracking_1 = require("./page-tracking");
|
|
47
|
+
const id_manager_1 = require("./id-manager");
|
|
47
48
|
const attribution_1 = require("./attribution");
|
|
48
49
|
Object.defineProperty(exports, "categorizeReferrer", { enumerable: true, get: function () { return attribution_1.categorizeReferrer; } });
|
|
49
50
|
Object.defineProperty(exports, "parseUTMParameters", { enumerable: true, get: function () { return attribution_1.parseUTMParameters; } });
|
|
@@ -59,13 +60,13 @@ class GrainAnalytics {
|
|
|
59
60
|
this.flushTimer = null;
|
|
60
61
|
this.isDestroyed = false;
|
|
61
62
|
this.globalUserId = null;
|
|
62
|
-
this.persistentAnonymousUserId = null;
|
|
63
|
+
this.persistentAnonymousUserId = null; // Deprecated: use idManager instead
|
|
63
64
|
// Remote Config properties
|
|
64
65
|
this.configCache = null;
|
|
65
66
|
this.configRefreshTimer = null;
|
|
66
67
|
this.configChangeListeners = [];
|
|
67
68
|
this.configFetchPromise = null;
|
|
68
|
-
this.cookiesEnabled = false;
|
|
69
|
+
this.cookiesEnabled = false; // Deprecated: cookies no longer used for IDs
|
|
69
70
|
// Automatic Tracking properties
|
|
70
71
|
this.activityDetector = null;
|
|
71
72
|
this.heartbeatManager = null;
|
|
@@ -96,38 +97,37 @@ class GrainAnalytics {
|
|
|
96
97
|
configCacheKey: 'grain_config',
|
|
97
98
|
configRefreshInterval: 300000, // 5 minutes
|
|
98
99
|
enableConfigCache: true,
|
|
99
|
-
// Privacy defaults
|
|
100
|
-
consentMode: '
|
|
100
|
+
// Privacy defaults (v2.0)
|
|
101
|
+
consentMode: 'cookieless', // Default: privacy-first, no permanent tracking
|
|
101
102
|
waitForConsent: false,
|
|
102
|
-
enableCookies: false,
|
|
103
|
-
anonymizeIP: false,
|
|
104
103
|
disableAutoProperties: false,
|
|
105
104
|
// Automatic Tracking defaults
|
|
106
105
|
enableHeartbeat: true,
|
|
107
106
|
heartbeatActiveInterval: 120000, // 2 minutes
|
|
108
107
|
heartbeatInactiveInterval: 300000, // 5 minutes
|
|
109
108
|
enableAutoPageView: true,
|
|
110
|
-
stripQueryParams: true,
|
|
109
|
+
stripQueryParams: true, // Privacy-first: strip by default
|
|
110
|
+
stripHash: false,
|
|
111
111
|
// Heatmap Tracking defaults
|
|
112
112
|
enableHeatmapTracking: true,
|
|
113
113
|
...config,
|
|
114
114
|
tenantId: config.tenantId,
|
|
115
115
|
};
|
|
116
|
-
// Initialize consent manager
|
|
116
|
+
// Initialize consent manager (v2.0)
|
|
117
117
|
this.consentManager = new consent_1.ConsentManager(this.config.tenantId, this.config.consentMode);
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
}
|
|
118
|
+
// Initialize ID manager (v2.0)
|
|
119
|
+
const idMode = this.consentManager.getIdMode();
|
|
120
|
+
this.idManager = new id_manager_1.IdManager({
|
|
121
|
+
mode: idMode,
|
|
122
|
+
tenantId: this.config.tenantId,
|
|
123
|
+
useLocalStorage: true, // For permanent IDs when consented
|
|
124
|
+
});
|
|
125
125
|
// Set global userId if provided in config
|
|
126
126
|
if (config.userId) {
|
|
127
127
|
this.globalUserId = config.userId;
|
|
128
128
|
}
|
|
129
129
|
this.validateConfig();
|
|
130
|
-
|
|
130
|
+
// Deprecated: initializePersistentAnonymousUserId() - now handled by IdManager
|
|
131
131
|
this.setupBeforeUnload();
|
|
132
132
|
this.startFlushTimer();
|
|
133
133
|
this.initializeConfigCache();
|
|
@@ -145,8 +145,11 @@ class GrainAnalytics {
|
|
|
145
145
|
this.initializeHeatmapTracking();
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
-
// Set up consent change listener to flush waiting events
|
|
148
|
+
// Set up consent change listener to sync IdManager and flush waiting events (v2.0)
|
|
149
149
|
this.consentManager.addListener((state) => {
|
|
150
|
+
// Sync IdManager with consent state
|
|
151
|
+
const idMode = this.consentManager.getIdMode();
|
|
152
|
+
this.idManager.setMode(idMode);
|
|
150
153
|
if (state.granted) {
|
|
151
154
|
this.handleConsentGranted();
|
|
152
155
|
}
|
|
@@ -188,11 +191,14 @@ class GrainAnalytics {
|
|
|
188
191
|
*/
|
|
189
192
|
shouldAllowPersistentStorage() {
|
|
190
193
|
const hasConsent = this.consentManager.hasConsent('analytics');
|
|
191
|
-
const
|
|
194
|
+
const isCookieless = this.config.consentMode === 'cookieless';
|
|
192
195
|
const userExplicitlyIdentified = !!this.globalUserId;
|
|
193
196
|
const isJWTAuth = this.config.authStrategy === 'JWT';
|
|
197
|
+
// Never allow persistent storage in cookieless mode
|
|
198
|
+
if (isCookieless)
|
|
199
|
+
return false;
|
|
194
200
|
// Allow persistent storage if any of these conditions are met
|
|
195
|
-
return hasConsent ||
|
|
201
|
+
return hasConsent || userExplicitlyIdentified || isJWTAuth;
|
|
196
202
|
}
|
|
197
203
|
/**
|
|
198
204
|
* Generate a proper UUIDv4 identifier for anonymous user ID
|
|
@@ -286,23 +292,21 @@ class GrainAnalytics {
|
|
|
286
292
|
}
|
|
287
293
|
}
|
|
288
294
|
/**
|
|
289
|
-
* Get the effective user ID (
|
|
295
|
+
* Get the effective user ID (v2.0)
|
|
290
296
|
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
297
|
+
* Privacy-first implementation:
|
|
298
|
+
* - Returns global userId if explicitly set (via identify/login)
|
|
299
|
+
* - Otherwise uses IdManager to generate:
|
|
300
|
+
* - Daily rotating ID (cookieless mode)
|
|
301
|
+
* - Permanent ID (with consent)
|
|
293
302
|
*/
|
|
294
303
|
getEffectiveUserIdInternal() {
|
|
304
|
+
// Explicit user identification always takes precedence
|
|
295
305
|
if (this.globalUserId) {
|
|
296
306
|
return this.globalUserId;
|
|
297
307
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
// Generate a new UUIDv4 identifier as fallback
|
|
302
|
-
this.persistentAnonymousUserId = this.generateAnonymousUserId();
|
|
303
|
-
// Try to persist it (will be skipped in opt-in mode without consent)
|
|
304
|
-
this.savePersistentAnonymousUserId(this.persistentAnonymousUserId);
|
|
305
|
-
return this.persistentAnonymousUserId;
|
|
308
|
+
// Use IdManager to generate appropriate ID based on consent
|
|
309
|
+
return this.idManager.getCurrentUserId();
|
|
306
310
|
}
|
|
307
311
|
log(...args) {
|
|
308
312
|
if (this.config.debug) {
|
|
@@ -673,6 +677,7 @@ class GrainAnalytics {
|
|
|
673
677
|
try {
|
|
674
678
|
this.pageTrackingManager = new page_tracking_1.PageTrackingManager(this, {
|
|
675
679
|
stripQueryParams: this.config.stripQueryParams,
|
|
680
|
+
stripHash: this.config.stripHash,
|
|
676
681
|
debug: this.config.debug,
|
|
677
682
|
tenantId: this.config.tenantId,
|
|
678
683
|
});
|
|
@@ -970,12 +975,13 @@ class GrainAnalytics {
|
|
|
970
975
|
return;
|
|
971
976
|
const hasConsent = this.consentManager.hasConsent('analytics');
|
|
972
977
|
// Create event with appropriate user ID
|
|
978
|
+
// v2.0: Always use IdManager which returns daily rotating ID or permanent ID based on consent
|
|
973
979
|
const event = {
|
|
974
980
|
eventName,
|
|
975
|
-
userId:
|
|
981
|
+
userId: this.getEffectiveUserId(), // IdManager handles daily vs permanent based on consent
|
|
976
982
|
properties: {
|
|
977
983
|
...properties,
|
|
978
|
-
_minimal: !hasConsent, // Flag to indicate minimal tracking
|
|
984
|
+
_minimal: !hasConsent, // Flag to indicate minimal tracking (daily rotating ID)
|
|
979
985
|
_consent_status: hasConsent ? 'granted' : 'pending',
|
|
980
986
|
},
|
|
981
987
|
};
|
|
@@ -1072,17 +1078,22 @@ class GrainAnalytics {
|
|
|
1072
1078
|
event.properties = filtered;
|
|
1073
1079
|
}
|
|
1074
1080
|
const formattedEvent = this.formatEvent(event);
|
|
1075
|
-
// Check consent
|
|
1081
|
+
// Check if we should wait for consent (only if explicitly configured)
|
|
1076
1082
|
if (this.consentManager.shouldWaitForConsent() && this.config.waitForConsent) {
|
|
1077
1083
|
// Queue event until consent is granted
|
|
1078
1084
|
this.waitingForConsentQueue.push(formattedEvent);
|
|
1079
1085
|
this.log(`Event waiting for consent: ${event.eventName}`, event.properties);
|
|
1080
1086
|
return;
|
|
1081
1087
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1088
|
+
// v2.0: GDPR Strict falls back to cookie-less mode (daily rotating IDs)
|
|
1089
|
+
// Events are never blocked - IdManager already provides correct ID (daily or permanent)
|
|
1090
|
+
const hasConsent = this.consentManager.hasConsent('analytics');
|
|
1091
|
+
// Add tracking flags to indicate consent status
|
|
1092
|
+
formattedEvent.properties = {
|
|
1093
|
+
...formattedEvent.properties,
|
|
1094
|
+
_minimal: !hasConsent, // Flag: true = daily rotating ID, false = permanent ID
|
|
1095
|
+
_consent_status: hasConsent ? 'granted' : 'pending',
|
|
1096
|
+
};
|
|
1086
1097
|
this.eventQueue.push(formattedEvent);
|
|
1087
1098
|
this.eventCountSinceLastHeartbeat++;
|
|
1088
1099
|
this.sessionEventCount++;
|
|
@@ -1770,13 +1781,24 @@ class GrainAnalytics {
|
|
|
1770
1781
|
}
|
|
1771
1782
|
// Privacy & Consent Methods
|
|
1772
1783
|
/**
|
|
1773
|
-
* Grant consent for tracking
|
|
1784
|
+
* Grant consent for tracking (v2.0)
|
|
1785
|
+
* Switches from cookie-less mode to permanent IDs
|
|
1774
1786
|
* @param categories - Optional array of consent categories (e.g., ['analytics', 'functional'])
|
|
1775
1787
|
*/
|
|
1776
1788
|
grantConsent(categories) {
|
|
1777
1789
|
try {
|
|
1778
1790
|
this.consentManager.grantConsent(categories);
|
|
1779
|
-
|
|
1791
|
+
// Sync ID manager with consent state
|
|
1792
|
+
const idMode = this.consentManager.getIdMode();
|
|
1793
|
+
this.idManager.setMode(idMode);
|
|
1794
|
+
this.log('Consent granted, switched to permanent IDs', categories);
|
|
1795
|
+
// Process any queued events waiting for consent
|
|
1796
|
+
if (this.waitingForConsentQueue.length > 0) {
|
|
1797
|
+
this.log(`Processing ${this.waitingForConsentQueue.length} queued events`);
|
|
1798
|
+
this.eventQueue.push(...this.waitingForConsentQueue);
|
|
1799
|
+
this.waitingForConsentQueue = [];
|
|
1800
|
+
this.flush();
|
|
1801
|
+
}
|
|
1780
1802
|
}
|
|
1781
1803
|
catch (error) {
|
|
1782
1804
|
const formattedError = this.formatError(error, 'grantConsent');
|
|
@@ -1784,16 +1806,22 @@ class GrainAnalytics {
|
|
|
1784
1806
|
}
|
|
1785
1807
|
}
|
|
1786
1808
|
/**
|
|
1787
|
-
* Revoke consent for tracking (
|
|
1809
|
+
* Revoke consent for tracking (v2.0)
|
|
1810
|
+
* Switches from permanent IDs to cookie-less mode
|
|
1788
1811
|
* @param categories - Optional array of categories to revoke (if not provided, revokes all)
|
|
1789
1812
|
*/
|
|
1790
1813
|
revokeConsent(categories) {
|
|
1791
1814
|
try {
|
|
1792
1815
|
this.consentManager.revokeConsent(categories);
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
this.
|
|
1796
|
-
this.
|
|
1816
|
+
// Sync ID manager with consent state
|
|
1817
|
+
const idMode = this.consentManager.getIdMode();
|
|
1818
|
+
this.idManager.setMode(idMode);
|
|
1819
|
+
this.log('Consent revoked, switched to cookie-less mode', categories);
|
|
1820
|
+
// Clear queued events when consent is fully revoked
|
|
1821
|
+
if (!this.consentManager.hasConsent()) {
|
|
1822
|
+
this.eventQueue = [];
|
|
1823
|
+
this.waitingForConsentQueue = [];
|
|
1824
|
+
}
|
|
1797
1825
|
}
|
|
1798
1826
|
catch (error) {
|
|
1799
1827
|
const formattedError = this.formatError(error, 'revokeConsent');
|
package/dist/index.mjs
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
* A lightweight, dependency-free TypeScript SDK for sending analytics events to Grain's REST API
|
|
4
4
|
*/
|
|
5
5
|
import { ConsentManager } from './consent.js';
|
|
6
|
-
import { setCookie, getCookie
|
|
6
|
+
import { setCookie, getCookie } from './cookies.js';
|
|
7
7
|
import { ActivityDetector } from './activity.js';
|
|
8
8
|
import { HeartbeatManager } from './heartbeat.js';
|
|
9
9
|
import { PageTrackingManager } from './page-tracking.js';
|
|
10
|
+
import { IdManager } from './id-manager.js';
|
|
10
11
|
import { categorizeReferrer, parseUTMParameters, getOrCreateFirstTouchAttribution, getSessionUTMParameters, getFirstTouchAttribution, } from './attribution.js';
|
|
11
12
|
export { categorizeReferrer, parseUTMParameters };
|
|
12
13
|
// Re-export timezone-country utilities
|
|
@@ -18,13 +19,13 @@ export class GrainAnalytics {
|
|
|
18
19
|
this.flushTimer = null;
|
|
19
20
|
this.isDestroyed = false;
|
|
20
21
|
this.globalUserId = null;
|
|
21
|
-
this.persistentAnonymousUserId = null;
|
|
22
|
+
this.persistentAnonymousUserId = null; // Deprecated: use idManager instead
|
|
22
23
|
// Remote Config properties
|
|
23
24
|
this.configCache = null;
|
|
24
25
|
this.configRefreshTimer = null;
|
|
25
26
|
this.configChangeListeners = [];
|
|
26
27
|
this.configFetchPromise = null;
|
|
27
|
-
this.cookiesEnabled = false;
|
|
28
|
+
this.cookiesEnabled = false; // Deprecated: cookies no longer used for IDs
|
|
28
29
|
// Automatic Tracking properties
|
|
29
30
|
this.activityDetector = null;
|
|
30
31
|
this.heartbeatManager = null;
|
|
@@ -55,38 +56,37 @@ export class GrainAnalytics {
|
|
|
55
56
|
configCacheKey: 'grain_config',
|
|
56
57
|
configRefreshInterval: 300000, // 5 minutes
|
|
57
58
|
enableConfigCache: true,
|
|
58
|
-
// Privacy defaults
|
|
59
|
-
consentMode: '
|
|
59
|
+
// Privacy defaults (v2.0)
|
|
60
|
+
consentMode: 'cookieless', // Default: privacy-first, no permanent tracking
|
|
60
61
|
waitForConsent: false,
|
|
61
|
-
enableCookies: false,
|
|
62
|
-
anonymizeIP: false,
|
|
63
62
|
disableAutoProperties: false,
|
|
64
63
|
// Automatic Tracking defaults
|
|
65
64
|
enableHeartbeat: true,
|
|
66
65
|
heartbeatActiveInterval: 120000, // 2 minutes
|
|
67
66
|
heartbeatInactiveInterval: 300000, // 5 minutes
|
|
68
67
|
enableAutoPageView: true,
|
|
69
|
-
stripQueryParams: true,
|
|
68
|
+
stripQueryParams: true, // Privacy-first: strip by default
|
|
69
|
+
stripHash: false,
|
|
70
70
|
// Heatmap Tracking defaults
|
|
71
71
|
enableHeatmapTracking: true,
|
|
72
72
|
...config,
|
|
73
73
|
tenantId: config.tenantId,
|
|
74
74
|
};
|
|
75
|
-
// Initialize consent manager
|
|
75
|
+
// Initialize consent manager (v2.0)
|
|
76
76
|
this.consentManager = new ConsentManager(this.config.tenantId, this.config.consentMode);
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
77
|
+
// Initialize ID manager (v2.0)
|
|
78
|
+
const idMode = this.consentManager.getIdMode();
|
|
79
|
+
this.idManager = new IdManager({
|
|
80
|
+
mode: idMode,
|
|
81
|
+
tenantId: this.config.tenantId,
|
|
82
|
+
useLocalStorage: true, // For permanent IDs when consented
|
|
83
|
+
});
|
|
84
84
|
// Set global userId if provided in config
|
|
85
85
|
if (config.userId) {
|
|
86
86
|
this.globalUserId = config.userId;
|
|
87
87
|
}
|
|
88
88
|
this.validateConfig();
|
|
89
|
-
|
|
89
|
+
// Deprecated: initializePersistentAnonymousUserId() - now handled by IdManager
|
|
90
90
|
this.setupBeforeUnload();
|
|
91
91
|
this.startFlushTimer();
|
|
92
92
|
this.initializeConfigCache();
|
|
@@ -104,8 +104,11 @@ export class GrainAnalytics {
|
|
|
104
104
|
this.initializeHeatmapTracking();
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
-
// Set up consent change listener to flush waiting events
|
|
107
|
+
// Set up consent change listener to sync IdManager and flush waiting events (v2.0)
|
|
108
108
|
this.consentManager.addListener((state) => {
|
|
109
|
+
// Sync IdManager with consent state
|
|
110
|
+
const idMode = this.consentManager.getIdMode();
|
|
111
|
+
this.idManager.setMode(idMode);
|
|
109
112
|
if (state.granted) {
|
|
110
113
|
this.handleConsentGranted();
|
|
111
114
|
}
|
|
@@ -147,11 +150,14 @@ export class GrainAnalytics {
|
|
|
147
150
|
*/
|
|
148
151
|
shouldAllowPersistentStorage() {
|
|
149
152
|
const hasConsent = this.consentManager.hasConsent('analytics');
|
|
150
|
-
const
|
|
153
|
+
const isCookieless = this.config.consentMode === 'cookieless';
|
|
151
154
|
const userExplicitlyIdentified = !!this.globalUserId;
|
|
152
155
|
const isJWTAuth = this.config.authStrategy === 'JWT';
|
|
156
|
+
// Never allow persistent storage in cookieless mode
|
|
157
|
+
if (isCookieless)
|
|
158
|
+
return false;
|
|
153
159
|
// Allow persistent storage if any of these conditions are met
|
|
154
|
-
return hasConsent ||
|
|
160
|
+
return hasConsent || userExplicitlyIdentified || isJWTAuth;
|
|
155
161
|
}
|
|
156
162
|
/**
|
|
157
163
|
* Generate a proper UUIDv4 identifier for anonymous user ID
|
|
@@ -245,23 +251,21 @@ export class GrainAnalytics {
|
|
|
245
251
|
}
|
|
246
252
|
}
|
|
247
253
|
/**
|
|
248
|
-
* Get the effective user ID (
|
|
254
|
+
* Get the effective user ID (v2.0)
|
|
249
255
|
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
256
|
+
* Privacy-first implementation:
|
|
257
|
+
* - Returns global userId if explicitly set (via identify/login)
|
|
258
|
+
* - Otherwise uses IdManager to generate:
|
|
259
|
+
* - Daily rotating ID (cookieless mode)
|
|
260
|
+
* - Permanent ID (with consent)
|
|
252
261
|
*/
|
|
253
262
|
getEffectiveUserIdInternal() {
|
|
263
|
+
// Explicit user identification always takes precedence
|
|
254
264
|
if (this.globalUserId) {
|
|
255
265
|
return this.globalUserId;
|
|
256
266
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
// Generate a new UUIDv4 identifier as fallback
|
|
261
|
-
this.persistentAnonymousUserId = this.generateAnonymousUserId();
|
|
262
|
-
// Try to persist it (will be skipped in opt-in mode without consent)
|
|
263
|
-
this.savePersistentAnonymousUserId(this.persistentAnonymousUserId);
|
|
264
|
-
return this.persistentAnonymousUserId;
|
|
267
|
+
// Use IdManager to generate appropriate ID based on consent
|
|
268
|
+
return this.idManager.getCurrentUserId();
|
|
265
269
|
}
|
|
266
270
|
log(...args) {
|
|
267
271
|
if (this.config.debug) {
|
|
@@ -632,6 +636,7 @@ export class GrainAnalytics {
|
|
|
632
636
|
try {
|
|
633
637
|
this.pageTrackingManager = new PageTrackingManager(this, {
|
|
634
638
|
stripQueryParams: this.config.stripQueryParams,
|
|
639
|
+
stripHash: this.config.stripHash,
|
|
635
640
|
debug: this.config.debug,
|
|
636
641
|
tenantId: this.config.tenantId,
|
|
637
642
|
});
|
|
@@ -929,12 +934,13 @@ export class GrainAnalytics {
|
|
|
929
934
|
return;
|
|
930
935
|
const hasConsent = this.consentManager.hasConsent('analytics');
|
|
931
936
|
// Create event with appropriate user ID
|
|
937
|
+
// v2.0: Always use IdManager which returns daily rotating ID or permanent ID based on consent
|
|
932
938
|
const event = {
|
|
933
939
|
eventName,
|
|
934
|
-
userId:
|
|
940
|
+
userId: this.getEffectiveUserId(), // IdManager handles daily vs permanent based on consent
|
|
935
941
|
properties: {
|
|
936
942
|
...properties,
|
|
937
|
-
_minimal: !hasConsent, // Flag to indicate minimal tracking
|
|
943
|
+
_minimal: !hasConsent, // Flag to indicate minimal tracking (daily rotating ID)
|
|
938
944
|
_consent_status: hasConsent ? 'granted' : 'pending',
|
|
939
945
|
},
|
|
940
946
|
};
|
|
@@ -1031,17 +1037,22 @@ export class GrainAnalytics {
|
|
|
1031
1037
|
event.properties = filtered;
|
|
1032
1038
|
}
|
|
1033
1039
|
const formattedEvent = this.formatEvent(event);
|
|
1034
|
-
// Check consent
|
|
1040
|
+
// Check if we should wait for consent (only if explicitly configured)
|
|
1035
1041
|
if (this.consentManager.shouldWaitForConsent() && this.config.waitForConsent) {
|
|
1036
1042
|
// Queue event until consent is granted
|
|
1037
1043
|
this.waitingForConsentQueue.push(formattedEvent);
|
|
1038
1044
|
this.log(`Event waiting for consent: ${event.eventName}`, event.properties);
|
|
1039
1045
|
return;
|
|
1040
1046
|
}
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1047
|
+
// v2.0: GDPR Strict falls back to cookie-less mode (daily rotating IDs)
|
|
1048
|
+
// Events are never blocked - IdManager already provides correct ID (daily or permanent)
|
|
1049
|
+
const hasConsent = this.consentManager.hasConsent('analytics');
|
|
1050
|
+
// Add tracking flags to indicate consent status
|
|
1051
|
+
formattedEvent.properties = {
|
|
1052
|
+
...formattedEvent.properties,
|
|
1053
|
+
_minimal: !hasConsent, // Flag: true = daily rotating ID, false = permanent ID
|
|
1054
|
+
_consent_status: hasConsent ? 'granted' : 'pending',
|
|
1055
|
+
};
|
|
1045
1056
|
this.eventQueue.push(formattedEvent);
|
|
1046
1057
|
this.eventCountSinceLastHeartbeat++;
|
|
1047
1058
|
this.sessionEventCount++;
|
|
@@ -1729,13 +1740,24 @@ export class GrainAnalytics {
|
|
|
1729
1740
|
}
|
|
1730
1741
|
// Privacy & Consent Methods
|
|
1731
1742
|
/**
|
|
1732
|
-
* Grant consent for tracking
|
|
1743
|
+
* Grant consent for tracking (v2.0)
|
|
1744
|
+
* Switches from cookie-less mode to permanent IDs
|
|
1733
1745
|
* @param categories - Optional array of consent categories (e.g., ['analytics', 'functional'])
|
|
1734
1746
|
*/
|
|
1735
1747
|
grantConsent(categories) {
|
|
1736
1748
|
try {
|
|
1737
1749
|
this.consentManager.grantConsent(categories);
|
|
1738
|
-
|
|
1750
|
+
// Sync ID manager with consent state
|
|
1751
|
+
const idMode = this.consentManager.getIdMode();
|
|
1752
|
+
this.idManager.setMode(idMode);
|
|
1753
|
+
this.log('Consent granted, switched to permanent IDs', categories);
|
|
1754
|
+
// Process any queued events waiting for consent
|
|
1755
|
+
if (this.waitingForConsentQueue.length > 0) {
|
|
1756
|
+
this.log(`Processing ${this.waitingForConsentQueue.length} queued events`);
|
|
1757
|
+
this.eventQueue.push(...this.waitingForConsentQueue);
|
|
1758
|
+
this.waitingForConsentQueue = [];
|
|
1759
|
+
this.flush();
|
|
1760
|
+
}
|
|
1739
1761
|
}
|
|
1740
1762
|
catch (error) {
|
|
1741
1763
|
const formattedError = this.formatError(error, 'grantConsent');
|
|
@@ -1743,16 +1765,22 @@ export class GrainAnalytics {
|
|
|
1743
1765
|
}
|
|
1744
1766
|
}
|
|
1745
1767
|
/**
|
|
1746
|
-
* Revoke consent for tracking (
|
|
1768
|
+
* Revoke consent for tracking (v2.0)
|
|
1769
|
+
* Switches from permanent IDs to cookie-less mode
|
|
1747
1770
|
* @param categories - Optional array of categories to revoke (if not provided, revokes all)
|
|
1748
1771
|
*/
|
|
1749
1772
|
revokeConsent(categories) {
|
|
1750
1773
|
try {
|
|
1751
1774
|
this.consentManager.revokeConsent(categories);
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
this.
|
|
1755
|
-
this.
|
|
1775
|
+
// Sync ID manager with consent state
|
|
1776
|
+
const idMode = this.consentManager.getIdMode();
|
|
1777
|
+
this.idManager.setMode(idMode);
|
|
1778
|
+
this.log('Consent revoked, switched to cookie-less mode', categories);
|
|
1779
|
+
// Clear queued events when consent is fully revoked
|
|
1780
|
+
if (!this.consentManager.hasConsent()) {
|
|
1781
|
+
this.eventQueue = [];
|
|
1782
|
+
this.waitingForConsentQueue = [];
|
|
1783
|
+
}
|
|
1756
1784
|
}
|
|
1757
1785
|
catch (error) {
|
|
1758
1786
|
const formattedError = this.formatError(error, 'revokeConsent');
|
package/dist/page-tracking.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export interface PageTrackingConfig {
|
|
6
6
|
stripQueryParams: boolean;
|
|
7
|
+
stripHash?: boolean;
|
|
7
8
|
debug?: boolean;
|
|
8
9
|
tenantId: string;
|
|
9
10
|
}
|
|
@@ -63,8 +64,13 @@ export declare class PageTrackingManager {
|
|
|
63
64
|
private getDeviceType;
|
|
64
65
|
/**
|
|
65
66
|
* Extract path from URL, optionally stripping query parameters
|
|
67
|
+
* Privacy-first: strips query params by default
|
|
66
68
|
*/
|
|
67
69
|
private extractPath;
|
|
70
|
+
/**
|
|
71
|
+
* Clean URL for privacy (strip query params based on config)
|
|
72
|
+
*/
|
|
73
|
+
private cleanUrl;
|
|
68
74
|
/**
|
|
69
75
|
* Get the current page path
|
|
70
76
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page-tracking.d.ts","sourceRoot":"","sources":["../src/page-tracking.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAaH,MAAM,WAAW,kBAAkB;IACjC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC/E,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACvC,kBAAkB,IAAI,MAAM,CAAC;IAC7B,qBAAqB,IAAI,MAAM,CAAC;IAChC,YAAY,IAAI,MAAM,CAAC;CACxB;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,iBAAiB,CAAyC;IAClE,OAAO,CAAC,oBAAoB,CAA4C;IACxE,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,aAAa,CAAK;gBAEd,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,kBAAkB;IAY5D;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAqB7B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAK/B;;OAEG;IACH,OAAO,CAAC,cAAc,CAGpB;IAEF;;OAEG;IACH,OAAO,CAAC,gBAAgB,CAGtB;IAEF;;OAEG;IACH,OAAO,CAAC,gBAAgB;IA4GxB;;OAEG;IACH,OAAO,CAAC,aAAa;IASrB;;OAEG;IACH,OAAO,CAAC,UAAU;IAUlB;;OAEG;IACH,OAAO,CAAC,KAAK;IAUb;;OAEG;IACH,OAAO,CAAC,aAAa;IAwBrB;;OAEG;IACH,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"page-tracking.d.ts","sourceRoot":"","sources":["../src/page-tracking.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAaH,MAAM,WAAW,kBAAkB;IACjC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC/E,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACvC,kBAAkB,IAAI,MAAM,CAAC;IAC7B,qBAAqB,IAAI,MAAM,CAAC;IAChC,YAAY,IAAI,MAAM,CAAC;CACxB;AAED,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,OAAO,CAAc;IAC7B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,iBAAiB,CAAyC;IAClE,OAAO,CAAC,oBAAoB,CAA4C;IACxE,OAAO,CAAC,YAAY,CAAuB;IAC3C,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,aAAa,CAAK;gBAEd,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,kBAAkB;IAY5D;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAqB7B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAK/B;;OAEG;IACH,OAAO,CAAC,cAAc,CAGpB;IAEF;;OAEG;IACH,OAAO,CAAC,gBAAgB,CAGtB;IAEF;;OAEG;IACH,OAAO,CAAC,gBAAgB;IA4GxB;;OAEG;IACH,OAAO,CAAC,aAAa;IASrB;;OAEG;IACH,OAAO,CAAC,UAAU;IAUlB;;OAEG;IACH,OAAO,CAAC,KAAK;IAUb;;OAEG;IACH,OAAO,CAAC,aAAa;IAwBrB;;;OAGG;IACH,OAAO,CAAC,WAAW;IAyBnB;;OAEG;IACH,OAAO,CAAC,QAAQ;IAahB;;OAEG;IACH,cAAc,IAAI,MAAM,GAAG,IAAI;IAI/B;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAqCnE;;OAEG;IACH,gBAAgB,IAAI,MAAM;IAI1B;;OAEG;IACH,OAAO,IAAI,IAAI;CAqBhB"}
|
package/dist/page-tracking.js
CHANGED
|
@@ -113,7 +113,7 @@ class PageTrackingManager {
|
|
|
113
113
|
// Enhanced properties when consent is granted
|
|
114
114
|
if (hasConsent) {
|
|
115
115
|
properties.title = document.title || '';
|
|
116
|
-
properties.full_url = currentUrl;
|
|
116
|
+
properties.full_url = this.cleanUrl(currentUrl); // Clean URL based on privacy settings
|
|
117
117
|
properties.session_id = this.tracker.getSessionId();
|
|
118
118
|
// Add referrer info
|
|
119
119
|
if (referrer) {
|
|
@@ -231,14 +231,20 @@ class PageTrackingManager {
|
|
|
231
231
|
}
|
|
232
232
|
/**
|
|
233
233
|
* Extract path from URL, optionally stripping query parameters
|
|
234
|
+
* Privacy-first: strips query params by default
|
|
234
235
|
*/
|
|
235
236
|
extractPath(url) {
|
|
236
237
|
try {
|
|
237
238
|
const urlObj = new URL(url);
|
|
238
|
-
let path = urlObj.pathname
|
|
239
|
+
let path = urlObj.pathname;
|
|
240
|
+
// Include query params only if not stripping
|
|
239
241
|
if (!this.config.stripQueryParams && urlObj.search) {
|
|
240
242
|
path += urlObj.search;
|
|
241
243
|
}
|
|
244
|
+
// Include hash only if not stripping
|
|
245
|
+
if (!this.config.stripHash && urlObj.hash) {
|
|
246
|
+
path += urlObj.hash;
|
|
247
|
+
}
|
|
242
248
|
return path;
|
|
243
249
|
}
|
|
244
250
|
catch (error) {
|
|
@@ -249,6 +255,21 @@ class PageTrackingManager {
|
|
|
249
255
|
return url;
|
|
250
256
|
}
|
|
251
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Clean URL for privacy (strip query params based on config)
|
|
260
|
+
*/
|
|
261
|
+
cleanUrl(url) {
|
|
262
|
+
if (!this.config.stripQueryParams) {
|
|
263
|
+
return url;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const urlObj = new URL(url);
|
|
267
|
+
return `${urlObj.origin}${urlObj.pathname}${this.config.stripHash ? '' : urlObj.hash}`;
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
return url;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
252
273
|
/**
|
|
253
274
|
* Get the current page path
|
|
254
275
|
*/
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* useConsent - Hook for managing user consent
|
|
2
|
+
* useConsent - Hook for managing user consent (v2.0)
|
|
3
|
+
* Updated for new consent modes: cookieless, gdpr-strict, gdpr-opt-out
|
|
3
4
|
*/
|
|
4
|
-
import type { ConsentState } from '../../consent';
|
|
5
|
+
import type { ConsentState, ConsentMode } from '../../consent';
|
|
5
6
|
export declare function useConsent(): {
|
|
6
7
|
consentState: ConsentState;
|
|
7
8
|
grantConsent: (categories?: string[]) => void;
|
|
@@ -10,4 +11,19 @@ export declare function useConsent(): {
|
|
|
10
11
|
isGranted: boolean;
|
|
11
12
|
categories: string[];
|
|
12
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* useConsentMode - Hook to get current consent mode
|
|
16
|
+
* v2.0: Returns 'cookieless' | 'gdpr-strict' | 'gdpr-opt-out'
|
|
17
|
+
*/
|
|
18
|
+
export declare function useConsentMode(): ConsentMode | null;
|
|
19
|
+
/**
|
|
20
|
+
* useTrackingId - Hook to get current tracking ID
|
|
21
|
+
* v2.0: Returns daily rotating ID or permanent ID based on consent
|
|
22
|
+
*/
|
|
23
|
+
export declare function useTrackingId(): string | null;
|
|
24
|
+
/**
|
|
25
|
+
* useCanTrack - Hook to check if tracking is allowed
|
|
26
|
+
* v2.0: Always returns true (even cookieless mode allows basic tracking)
|
|
27
|
+
*/
|
|
28
|
+
export declare function useCanTrack(): boolean;
|
|
13
29
|
//# sourceMappingURL=useConsent.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useConsent.d.ts","sourceRoot":"","sources":["../../../../src/react/hooks/useConsent.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"useConsent.d.ts","sourceRoot":"","sources":["../../../../src/react/hooks/useConsent.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE/D,wBAAgB,UAAU;;gCAwBR,MAAM,EAAE;iCASR,MAAM,EAAE;4BASV,MAAM;;;EAerB;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,WAAW,GAAG,IAAI,CAanD;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,MAAM,GAAG,IAAI,CAiB7C;AAED;;;GAGG;AACH,wBAAgB,WAAW,IAAI,OAAO,CAKrC"}
|