@grainql/analytics-web 2.8.0 → 3.0.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.global.dev.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Grain Analytics Web SDK
|
|
1
|
+
/* Grain Analytics Web SDK v3.0.0 | MIT License | Development Build */
|
|
2
2
|
"use strict";
|
|
3
3
|
var Grain = (() => {
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -2700,7 +2700,7 @@ var Grain = (() => {
|
|
|
2700
2700
|
var DEFAULT_CONSENT_CATEGORIES = ["necessary", "analytics", "functional"];
|
|
2701
2701
|
var CONSENT_VERSION = "1.0.0";
|
|
2702
2702
|
var ConsentManager = class {
|
|
2703
|
-
constructor(tenantId, consentMode = "
|
|
2703
|
+
constructor(tenantId, consentMode = "cookieless") {
|
|
2704
2704
|
this.consentState = null;
|
|
2705
2705
|
this.listeners = [];
|
|
2706
2706
|
this.consentMode = consentMode;
|
|
@@ -2710,9 +2710,8 @@ var Grain = (() => {
|
|
|
2710
2710
|
/**
|
|
2711
2711
|
* Load consent state from localStorage
|
|
2712
2712
|
*
|
|
2713
|
-
* GDPR Compliance:
|
|
2714
|
-
*
|
|
2715
|
-
* The consent preference itself is not tracking data.
|
|
2713
|
+
* GDPR Compliance: localStorage only used for storing consent preferences
|
|
2714
|
+
* (not for tracking), which is a legitimate interest for compliance.
|
|
2716
2715
|
*/
|
|
2717
2716
|
loadConsentState() {
|
|
2718
2717
|
if (typeof window === "undefined")
|
|
@@ -2725,7 +2724,7 @@ var Grain = (() => {
|
|
|
2725
2724
|
...parsed,
|
|
2726
2725
|
timestamp: new Date(parsed.timestamp)
|
|
2727
2726
|
};
|
|
2728
|
-
} else if (this.consentMode === "opt-out"
|
|
2727
|
+
} else if (this.consentMode === "gdpr-opt-out") {
|
|
2729
2728
|
this.consentState = {
|
|
2730
2729
|
granted: true,
|
|
2731
2730
|
categories: DEFAULT_CONSENT_CATEGORIES,
|
|
@@ -2798,28 +2797,67 @@ var Grain = (() => {
|
|
|
2798
2797
|
return this.consentState ? { ...this.consentState } : null;
|
|
2799
2798
|
}
|
|
2800
2799
|
/**
|
|
2801
|
-
* Check if user has granted consent
|
|
2800
|
+
* Check if user has granted consent for permanent IDs
|
|
2802
2801
|
*/
|
|
2803
2802
|
hasConsent(category) {
|
|
2804
|
-
if (this.consentMode === "
|
|
2805
|
-
return true;
|
|
2806
|
-
}
|
|
2807
|
-
if (this.consentMode === "opt-in" && !this.consentState) {
|
|
2803
|
+
if (this.consentMode === "cookieless") {
|
|
2808
2804
|
return false;
|
|
2809
2805
|
}
|
|
2810
|
-
if (
|
|
2811
|
-
|
|
2806
|
+
if (this.consentMode === "gdpr-strict") {
|
|
2807
|
+
if (!this.consentState?.granted) {
|
|
2808
|
+
return false;
|
|
2809
|
+
}
|
|
2812
2810
|
}
|
|
2813
|
-
if (
|
|
2811
|
+
if (this.consentMode === "gdpr-opt-out") {
|
|
2812
|
+
if (!this.consentState) {
|
|
2813
|
+
return true;
|
|
2814
|
+
}
|
|
2815
|
+
if (!this.consentState.granted) {
|
|
2816
|
+
return false;
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
if (category && this.consentState) {
|
|
2814
2820
|
return this.consentState.categories.includes(category);
|
|
2815
2821
|
}
|
|
2822
|
+
return this.consentState?.granted ?? this.consentMode === "gdpr-opt-out";
|
|
2823
|
+
}
|
|
2824
|
+
/**
|
|
2825
|
+
* Check if permanent IDs are allowed
|
|
2826
|
+
*/
|
|
2827
|
+
shouldUsePermanentId() {
|
|
2828
|
+
return this.hasConsent();
|
|
2829
|
+
}
|
|
2830
|
+
/**
|
|
2831
|
+
* Check if we should strip query parameters from URLs
|
|
2832
|
+
* Query params stripped unless:
|
|
2833
|
+
* - Mode is gdpr-opt-out, OR
|
|
2834
|
+
* - Mode is gdpr-strict AND consent given
|
|
2835
|
+
*/
|
|
2836
|
+
shouldStripQueryParams() {
|
|
2837
|
+
if (this.consentMode === "cookieless") {
|
|
2838
|
+
return true;
|
|
2839
|
+
}
|
|
2840
|
+
if (this.consentMode === "gdpr-strict") {
|
|
2841
|
+
return !this.hasConsent();
|
|
2842
|
+
}
|
|
2843
|
+
if (this.consentMode === "gdpr-opt-out") {
|
|
2844
|
+
return false;
|
|
2845
|
+
}
|
|
2846
|
+
return true;
|
|
2847
|
+
}
|
|
2848
|
+
/**
|
|
2849
|
+
* Check if we can track events (always true in v2.0)
|
|
2850
|
+
* Even cookieless mode allows basic analytics with daily IDs
|
|
2851
|
+
*/
|
|
2852
|
+
canTrack() {
|
|
2816
2853
|
return true;
|
|
2817
2854
|
}
|
|
2818
2855
|
/**
|
|
2819
2856
|
* Check if we should wait for consent before tracking
|
|
2857
|
+
* Only relevant for GDPR Strict mode
|
|
2820
2858
|
*/
|
|
2821
2859
|
shouldWaitForConsent() {
|
|
2822
|
-
return this.consentMode === "
|
|
2860
|
+
return this.consentMode === "gdpr-strict" && !this.consentState?.granted;
|
|
2823
2861
|
}
|
|
2824
2862
|
/**
|
|
2825
2863
|
* Add consent change listener
|
|
@@ -2861,6 +2899,19 @@ var Grain = (() => {
|
|
|
2861
2899
|
} catch (error) {
|
|
2862
2900
|
}
|
|
2863
2901
|
}
|
|
2902
|
+
/**
|
|
2903
|
+
* Get current consent mode
|
|
2904
|
+
*/
|
|
2905
|
+
getConsentMode() {
|
|
2906
|
+
return this.consentMode;
|
|
2907
|
+
}
|
|
2908
|
+
/**
|
|
2909
|
+
* Get ID mode based on consent state
|
|
2910
|
+
* Returns 'cookieless' or 'permanent'
|
|
2911
|
+
*/
|
|
2912
|
+
getIdMode() {
|
|
2913
|
+
return this.shouldUsePermanentId() ? "permanent" : "cookieless";
|
|
2914
|
+
}
|
|
2864
2915
|
};
|
|
2865
2916
|
|
|
2866
2917
|
// src/cookies.ts
|
|
@@ -2903,36 +2954,6 @@ var Grain = (() => {
|
|
|
2903
2954
|
}
|
|
2904
2955
|
return null;
|
|
2905
2956
|
}
|
|
2906
|
-
function deleteCookie(name, config) {
|
|
2907
|
-
if (typeof document === "undefined")
|
|
2908
|
-
return;
|
|
2909
|
-
const parts = [
|
|
2910
|
-
`${encodeURIComponent(name)}=`,
|
|
2911
|
-
"max-age=0"
|
|
2912
|
-
];
|
|
2913
|
-
if (config?.domain) {
|
|
2914
|
-
parts.push(`domain=${config.domain}`);
|
|
2915
|
-
}
|
|
2916
|
-
if (config?.path) {
|
|
2917
|
-
parts.push(`path=${config.path}`);
|
|
2918
|
-
} else {
|
|
2919
|
-
parts.push("path=/");
|
|
2920
|
-
}
|
|
2921
|
-
document.cookie = parts.join("; ");
|
|
2922
|
-
}
|
|
2923
|
-
function areCookiesEnabled() {
|
|
2924
|
-
if (typeof document === "undefined")
|
|
2925
|
-
return false;
|
|
2926
|
-
try {
|
|
2927
|
-
const testCookie = "_grain_cookie_test";
|
|
2928
|
-
setCookie(testCookie, "test", { maxAge: 1 });
|
|
2929
|
-
const result = getCookie(testCookie) === "test";
|
|
2930
|
-
deleteCookie(testCookie);
|
|
2931
|
-
return result;
|
|
2932
|
-
} catch {
|
|
2933
|
-
return false;
|
|
2934
|
-
}
|
|
2935
|
-
}
|
|
2936
2957
|
|
|
2937
2958
|
// src/activity.ts
|
|
2938
2959
|
var ActivityDetector = class {
|
|
@@ -6312,7 +6333,7 @@ var Grain = (() => {
|
|
|
6312
6333
|
};
|
|
6313
6334
|
if (hasConsent) {
|
|
6314
6335
|
properties.title = document.title || "";
|
|
6315
|
-
properties.full_url = currentUrl;
|
|
6336
|
+
properties.full_url = this.cleanUrl(currentUrl);
|
|
6316
6337
|
properties.session_id = this.tracker.getSessionId();
|
|
6317
6338
|
if (referrer) {
|
|
6318
6339
|
properties.referrer = referrer;
|
|
@@ -6417,14 +6438,18 @@ var Grain = (() => {
|
|
|
6417
6438
|
}
|
|
6418
6439
|
/**
|
|
6419
6440
|
* Extract path from URL, optionally stripping query parameters
|
|
6441
|
+
* Privacy-first: strips query params by default
|
|
6420
6442
|
*/
|
|
6421
6443
|
extractPath(url) {
|
|
6422
6444
|
try {
|
|
6423
6445
|
const urlObj = new URL(url);
|
|
6424
|
-
let path = urlObj.pathname
|
|
6446
|
+
let path = urlObj.pathname;
|
|
6425
6447
|
if (!this.config.stripQueryParams && urlObj.search) {
|
|
6426
6448
|
path += urlObj.search;
|
|
6427
6449
|
}
|
|
6450
|
+
if (!this.config.stripHash && urlObj.hash) {
|
|
6451
|
+
path += urlObj.hash;
|
|
6452
|
+
}
|
|
6428
6453
|
return path;
|
|
6429
6454
|
} catch (error) {
|
|
6430
6455
|
if (this.config.debug) {
|
|
@@ -6433,6 +6458,20 @@ var Grain = (() => {
|
|
|
6433
6458
|
return url;
|
|
6434
6459
|
}
|
|
6435
6460
|
}
|
|
6461
|
+
/**
|
|
6462
|
+
* Clean URL for privacy (strip query params based on config)
|
|
6463
|
+
*/
|
|
6464
|
+
cleanUrl(url) {
|
|
6465
|
+
if (!this.config.stripQueryParams) {
|
|
6466
|
+
return url;
|
|
6467
|
+
}
|
|
6468
|
+
try {
|
|
6469
|
+
const urlObj = new URL(url);
|
|
6470
|
+
return `${urlObj.origin}${urlObj.pathname}${this.config.stripHash ? "" : urlObj.hash}`;
|
|
6471
|
+
} catch (error) {
|
|
6472
|
+
return url;
|
|
6473
|
+
}
|
|
6474
|
+
}
|
|
6436
6475
|
/**
|
|
6437
6476
|
* Get the current page path
|
|
6438
6477
|
*/
|
|
@@ -6501,6 +6540,172 @@ var Grain = (() => {
|
|
|
6501
6540
|
}
|
|
6502
6541
|
};
|
|
6503
6542
|
|
|
6543
|
+
// src/id-manager.ts
|
|
6544
|
+
function simpleHash(str) {
|
|
6545
|
+
let hash = 0;
|
|
6546
|
+
for (let i = 0; i < str.length; i++) {
|
|
6547
|
+
const char = str.charCodeAt(i);
|
|
6548
|
+
hash = (hash << 5) - hash + char;
|
|
6549
|
+
hash = hash & hash;
|
|
6550
|
+
}
|
|
6551
|
+
return Math.abs(hash).toString(36);
|
|
6552
|
+
}
|
|
6553
|
+
function generateUUID() {
|
|
6554
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
6555
|
+
return crypto.randomUUID();
|
|
6556
|
+
}
|
|
6557
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
6558
|
+
const r = Math.random() * 16 | 0;
|
|
6559
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
6560
|
+
return v.toString(16);
|
|
6561
|
+
});
|
|
6562
|
+
}
|
|
6563
|
+
function getBrowserFingerprint() {
|
|
6564
|
+
if (typeof window === "undefined")
|
|
6565
|
+
return "server";
|
|
6566
|
+
const components = [
|
|
6567
|
+
screen.width?.toString() || "",
|
|
6568
|
+
screen.height?.toString() || "",
|
|
6569
|
+
navigator.language || "",
|
|
6570
|
+
Intl.DateTimeFormat().resolvedOptions().timeZone || ""
|
|
6571
|
+
];
|
|
6572
|
+
return simpleHash(components.join("|"));
|
|
6573
|
+
}
|
|
6574
|
+
function getLocalDateString() {
|
|
6575
|
+
const now = /* @__PURE__ */ new Date();
|
|
6576
|
+
const year = now.getFullYear();
|
|
6577
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
6578
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
6579
|
+
return `${year}-${month}-${day}`;
|
|
6580
|
+
}
|
|
6581
|
+
var IdManager = class {
|
|
6582
|
+
constructor(config) {
|
|
6583
|
+
this.cachedDailyId = null;
|
|
6584
|
+
this.dailyIdDate = null;
|
|
6585
|
+
this.permanentId = null;
|
|
6586
|
+
this.config = config;
|
|
6587
|
+
if (config.mode === "permanent" && config.useLocalStorage) {
|
|
6588
|
+
this.loadPermanentId();
|
|
6589
|
+
}
|
|
6590
|
+
}
|
|
6591
|
+
/**
|
|
6592
|
+
* Generate a daily rotating ID
|
|
6593
|
+
* Rotates at midnight in user's local timezone
|
|
6594
|
+
* Provides same-day continuity without persistent tracking
|
|
6595
|
+
*/
|
|
6596
|
+
generateDailyRotatingId() {
|
|
6597
|
+
const currentDate = getLocalDateString();
|
|
6598
|
+
if (this.cachedDailyId && this.dailyIdDate === currentDate) {
|
|
6599
|
+
return this.cachedDailyId;
|
|
6600
|
+
}
|
|
6601
|
+
const fingerprint = getBrowserFingerprint();
|
|
6602
|
+
const seed = `${this.config.tenantId}|${currentDate}|${fingerprint}`;
|
|
6603
|
+
const dailyId = `daily_${simpleHash(seed)}_${simpleHash(Date.now().toString())}`;
|
|
6604
|
+
this.cachedDailyId = dailyId;
|
|
6605
|
+
this.dailyIdDate = currentDate;
|
|
6606
|
+
return dailyId;
|
|
6607
|
+
}
|
|
6608
|
+
/**
|
|
6609
|
+
* Generate or retrieve permanent user ID
|
|
6610
|
+
* Only used when consent is given
|
|
6611
|
+
*/
|
|
6612
|
+
generatePermanentId() {
|
|
6613
|
+
if (this.permanentId) {
|
|
6614
|
+
return this.permanentId;
|
|
6615
|
+
}
|
|
6616
|
+
if (this.config.useLocalStorage) {
|
|
6617
|
+
const stored = this.loadPermanentId();
|
|
6618
|
+
if (stored) {
|
|
6619
|
+
return stored;
|
|
6620
|
+
}
|
|
6621
|
+
}
|
|
6622
|
+
const newId = generateUUID();
|
|
6623
|
+
this.permanentId = newId;
|
|
6624
|
+
if (this.config.useLocalStorage) {
|
|
6625
|
+
this.savePermanentId(newId);
|
|
6626
|
+
}
|
|
6627
|
+
return newId;
|
|
6628
|
+
}
|
|
6629
|
+
/**
|
|
6630
|
+
* Get the current user ID based on mode
|
|
6631
|
+
*/
|
|
6632
|
+
getCurrentUserId() {
|
|
6633
|
+
if (this.config.mode === "cookieless") {
|
|
6634
|
+
return this.generateDailyRotatingId();
|
|
6635
|
+
} else {
|
|
6636
|
+
return this.generatePermanentId();
|
|
6637
|
+
}
|
|
6638
|
+
}
|
|
6639
|
+
/**
|
|
6640
|
+
* Switch ID mode (e.g., when consent is granted/revoked)
|
|
6641
|
+
*/
|
|
6642
|
+
setMode(mode) {
|
|
6643
|
+
this.config.mode = mode;
|
|
6644
|
+
if (mode === "permanent") {
|
|
6645
|
+
this.cachedDailyId = null;
|
|
6646
|
+
this.dailyIdDate = null;
|
|
6647
|
+
}
|
|
6648
|
+
if (mode === "cookieless") {
|
|
6649
|
+
this.permanentId = null;
|
|
6650
|
+
if (this.config.useLocalStorage) {
|
|
6651
|
+
this.clearPermanentId();
|
|
6652
|
+
}
|
|
6653
|
+
}
|
|
6654
|
+
}
|
|
6655
|
+
/**
|
|
6656
|
+
* Load permanent ID from localStorage
|
|
6657
|
+
*/
|
|
6658
|
+
loadPermanentId() {
|
|
6659
|
+
if (typeof window === "undefined")
|
|
6660
|
+
return null;
|
|
6661
|
+
try {
|
|
6662
|
+
const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
|
|
6663
|
+
const stored = localStorage.getItem(storageKey);
|
|
6664
|
+
if (stored) {
|
|
6665
|
+
this.permanentId = stored;
|
|
6666
|
+
return stored;
|
|
6667
|
+
}
|
|
6668
|
+
} catch (error) {
|
|
6669
|
+
}
|
|
6670
|
+
return null;
|
|
6671
|
+
}
|
|
6672
|
+
/**
|
|
6673
|
+
* Save permanent ID to localStorage
|
|
6674
|
+
*/
|
|
6675
|
+
savePermanentId(id) {
|
|
6676
|
+
if (typeof window === "undefined")
|
|
6677
|
+
return;
|
|
6678
|
+
try {
|
|
6679
|
+
const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
|
|
6680
|
+
localStorage.setItem(storageKey, id);
|
|
6681
|
+
} catch (error) {
|
|
6682
|
+
}
|
|
6683
|
+
}
|
|
6684
|
+
/**
|
|
6685
|
+
* Clear permanent ID from localStorage
|
|
6686
|
+
*/
|
|
6687
|
+
clearPermanentId() {
|
|
6688
|
+
if (typeof window === "undefined")
|
|
6689
|
+
return;
|
|
6690
|
+
try {
|
|
6691
|
+
const storageKey = `grain_anonymous_user_id_${this.config.tenantId}`;
|
|
6692
|
+
localStorage.removeItem(storageKey);
|
|
6693
|
+
} catch (error) {
|
|
6694
|
+
}
|
|
6695
|
+
}
|
|
6696
|
+
/**
|
|
6697
|
+
* Get info about current ID for debugging
|
|
6698
|
+
*/
|
|
6699
|
+
getIdInfo() {
|
|
6700
|
+
const id = this.getCurrentUserId();
|
|
6701
|
+
return {
|
|
6702
|
+
mode: this.config.mode,
|
|
6703
|
+
id,
|
|
6704
|
+
isDailyRotating: id.startsWith("daily_")
|
|
6705
|
+
};
|
|
6706
|
+
}
|
|
6707
|
+
};
|
|
6708
|
+
|
|
6504
6709
|
// src/index.ts
|
|
6505
6710
|
var GrainAnalytics = class {
|
|
6506
6711
|
constructor(config) {
|
|
@@ -6510,12 +6715,14 @@ var Grain = (() => {
|
|
|
6510
6715
|
this.isDestroyed = false;
|
|
6511
6716
|
this.globalUserId = null;
|
|
6512
6717
|
this.persistentAnonymousUserId = null;
|
|
6718
|
+
// Deprecated: use idManager instead
|
|
6513
6719
|
// Remote Config properties
|
|
6514
6720
|
this.configCache = null;
|
|
6515
6721
|
this.configRefreshTimer = null;
|
|
6516
6722
|
this.configChangeListeners = [];
|
|
6517
6723
|
this.configFetchPromise = null;
|
|
6518
6724
|
this.cookiesEnabled = false;
|
|
6725
|
+
// Deprecated: cookies no longer used for IDs
|
|
6519
6726
|
// Automatic Tracking properties
|
|
6520
6727
|
this.activityDetector = null;
|
|
6521
6728
|
this.heartbeatManager = null;
|
|
@@ -6550,11 +6757,10 @@ var Grain = (() => {
|
|
|
6550
6757
|
configRefreshInterval: 3e5,
|
|
6551
6758
|
// 5 minutes
|
|
6552
6759
|
enableConfigCache: true,
|
|
6553
|
-
// Privacy defaults
|
|
6554
|
-
consentMode: "
|
|
6760
|
+
// Privacy defaults (v2.0)
|
|
6761
|
+
consentMode: "cookieless",
|
|
6762
|
+
// Default: privacy-first, no permanent tracking
|
|
6555
6763
|
waitForConsent: false,
|
|
6556
|
-
enableCookies: false,
|
|
6557
|
-
anonymizeIP: false,
|
|
6558
6764
|
disableAutoProperties: false,
|
|
6559
6765
|
// Automatic Tracking defaults
|
|
6560
6766
|
enableHeartbeat: true,
|
|
@@ -6564,23 +6770,25 @@ var Grain = (() => {
|
|
|
6564
6770
|
// 5 minutes
|
|
6565
6771
|
enableAutoPageView: true,
|
|
6566
6772
|
stripQueryParams: true,
|
|
6773
|
+
// Privacy-first: strip by default
|
|
6774
|
+
stripHash: false,
|
|
6567
6775
|
// Heatmap Tracking defaults
|
|
6568
6776
|
enableHeatmapTracking: true,
|
|
6569
6777
|
...config,
|
|
6570
6778
|
tenantId: config.tenantId
|
|
6571
6779
|
};
|
|
6572
6780
|
this.consentManager = new ConsentManager(this.config.tenantId, this.config.consentMode);
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6781
|
+
const idMode = this.consentManager.getIdMode();
|
|
6782
|
+
this.idManager = new IdManager({
|
|
6783
|
+
mode: idMode,
|
|
6784
|
+
tenantId: this.config.tenantId,
|
|
6785
|
+
useLocalStorage: true
|
|
6786
|
+
// For permanent IDs when consented
|
|
6787
|
+
});
|
|
6579
6788
|
if (config.userId) {
|
|
6580
6789
|
this.globalUserId = config.userId;
|
|
6581
6790
|
}
|
|
6582
6791
|
this.validateConfig();
|
|
6583
|
-
this.initializePersistentAnonymousUserId();
|
|
6584
6792
|
this.setupBeforeUnload();
|
|
6585
6793
|
this.startFlushTimer();
|
|
6586
6794
|
this.initializeConfigCache();
|
|
@@ -6594,6 +6802,8 @@ var Grain = (() => {
|
|
|
6594
6802
|
}
|
|
6595
6803
|
}
|
|
6596
6804
|
this.consentManager.addListener((state) => {
|
|
6805
|
+
const idMode2 = this.consentManager.getIdMode();
|
|
6806
|
+
this.idManager.setMode(idMode2);
|
|
6597
6807
|
if (state.granted) {
|
|
6598
6808
|
this.handleConsentGranted();
|
|
6599
6809
|
}
|
|
@@ -6634,10 +6844,12 @@ var Grain = (() => {
|
|
|
6634
6844
|
*/
|
|
6635
6845
|
shouldAllowPersistentStorage() {
|
|
6636
6846
|
const hasConsent = this.consentManager.hasConsent("analytics");
|
|
6637
|
-
const
|
|
6847
|
+
const isCookieless = this.config.consentMode === "cookieless";
|
|
6638
6848
|
const userExplicitlyIdentified = !!this.globalUserId;
|
|
6639
6849
|
const isJWTAuth = this.config.authStrategy === "JWT";
|
|
6640
|
-
|
|
6850
|
+
if (isCookieless)
|
|
6851
|
+
return false;
|
|
6852
|
+
return hasConsent || userExplicitlyIdentified || isJWTAuth;
|
|
6641
6853
|
}
|
|
6642
6854
|
/**
|
|
6643
6855
|
* Generate a proper UUIDv4 identifier for anonymous user ID
|
|
@@ -6720,21 +6932,19 @@ var Grain = (() => {
|
|
|
6720
6932
|
}
|
|
6721
6933
|
}
|
|
6722
6934
|
/**
|
|
6723
|
-
* Get the effective user ID (
|
|
6935
|
+
* Get the effective user ID (v2.0)
|
|
6724
6936
|
*
|
|
6725
|
-
*
|
|
6726
|
-
*
|
|
6937
|
+
* Privacy-first implementation:
|
|
6938
|
+
* - Returns global userId if explicitly set (via identify/login)
|
|
6939
|
+
* - Otherwise uses IdManager to generate:
|
|
6940
|
+
* - Daily rotating ID (cookieless mode)
|
|
6941
|
+
* - Permanent ID (with consent)
|
|
6727
6942
|
*/
|
|
6728
6943
|
getEffectiveUserIdInternal() {
|
|
6729
6944
|
if (this.globalUserId) {
|
|
6730
6945
|
return this.globalUserId;
|
|
6731
6946
|
}
|
|
6732
|
-
|
|
6733
|
-
return this.persistentAnonymousUserId;
|
|
6734
|
-
}
|
|
6735
|
-
this.persistentAnonymousUserId = this.generateAnonymousUserId();
|
|
6736
|
-
this.savePersistentAnonymousUserId(this.persistentAnonymousUserId);
|
|
6737
|
-
return this.persistentAnonymousUserId;
|
|
6947
|
+
return this.idManager.getCurrentUserId();
|
|
6738
6948
|
}
|
|
6739
6949
|
log(...args) {
|
|
6740
6950
|
if (this.config.debug) {
|
|
@@ -7072,6 +7282,7 @@ var Grain = (() => {
|
|
|
7072
7282
|
this,
|
|
7073
7283
|
{
|
|
7074
7284
|
stripQueryParams: this.config.stripQueryParams,
|
|
7285
|
+
stripHash: this.config.stripHash,
|
|
7075
7286
|
debug: this.config.debug,
|
|
7076
7287
|
tenantId: this.config.tenantId
|
|
7077
7288
|
}
|
|
@@ -7365,11 +7576,12 @@ var Grain = (() => {
|
|
|
7365
7576
|
const hasConsent = this.consentManager.hasConsent("analytics");
|
|
7366
7577
|
const event = {
|
|
7367
7578
|
eventName,
|
|
7368
|
-
userId:
|
|
7579
|
+
userId: this.getEffectiveUserId(),
|
|
7580
|
+
// IdManager handles daily vs permanent based on consent
|
|
7369
7581
|
properties: {
|
|
7370
7582
|
...properties,
|
|
7371
7583
|
_minimal: !hasConsent,
|
|
7372
|
-
// Flag to indicate minimal tracking
|
|
7584
|
+
// Flag to indicate minimal tracking (daily rotating ID)
|
|
7373
7585
|
_consent_status: hasConsent ? "granted" : "pending"
|
|
7374
7586
|
}
|
|
7375
7587
|
};
|
|
@@ -7467,10 +7679,13 @@ var Grain = (() => {
|
|
|
7467
7679
|
this.log(`Event waiting for consent: ${event.eventName}`, event.properties);
|
|
7468
7680
|
return;
|
|
7469
7681
|
}
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7682
|
+
const hasConsent = this.consentManager.hasConsent("analytics");
|
|
7683
|
+
formattedEvent.properties = {
|
|
7684
|
+
...formattedEvent.properties,
|
|
7685
|
+
_minimal: !hasConsent,
|
|
7686
|
+
// Flag: true = daily rotating ID, false = permanent ID
|
|
7687
|
+
_consent_status: hasConsent ? "granted" : "pending"
|
|
7688
|
+
};
|
|
7474
7689
|
this.eventQueue.push(formattedEvent);
|
|
7475
7690
|
this.eventCountSinceLastHeartbeat++;
|
|
7476
7691
|
this.sessionEventCount++;
|
|
@@ -8090,28 +8305,42 @@ var Grain = (() => {
|
|
|
8090
8305
|
}
|
|
8091
8306
|
// Privacy & Consent Methods
|
|
8092
8307
|
/**
|
|
8093
|
-
* Grant consent for tracking
|
|
8308
|
+
* Grant consent for tracking (v2.0)
|
|
8309
|
+
* Switches from cookie-less mode to permanent IDs
|
|
8094
8310
|
* @param categories - Optional array of consent categories (e.g., ['analytics', 'functional'])
|
|
8095
8311
|
*/
|
|
8096
8312
|
grantConsent(categories) {
|
|
8097
8313
|
try {
|
|
8098
8314
|
this.consentManager.grantConsent(categories);
|
|
8099
|
-
this.
|
|
8315
|
+
const idMode = this.consentManager.getIdMode();
|
|
8316
|
+
this.idManager.setMode(idMode);
|
|
8317
|
+
this.log("Consent granted, switched to permanent IDs", categories);
|
|
8318
|
+
if (this.waitingForConsentQueue.length > 0) {
|
|
8319
|
+
this.log(`Processing ${this.waitingForConsentQueue.length} queued events`);
|
|
8320
|
+
this.eventQueue.push(...this.waitingForConsentQueue);
|
|
8321
|
+
this.waitingForConsentQueue = [];
|
|
8322
|
+
this.flush();
|
|
8323
|
+
}
|
|
8100
8324
|
} catch (error) {
|
|
8101
8325
|
const formattedError = this.formatError(error, "grantConsent");
|
|
8102
8326
|
this.logError(formattedError);
|
|
8103
8327
|
}
|
|
8104
8328
|
}
|
|
8105
8329
|
/**
|
|
8106
|
-
* Revoke consent for tracking (
|
|
8330
|
+
* Revoke consent for tracking (v2.0)
|
|
8331
|
+
* Switches from permanent IDs to cookie-less mode
|
|
8107
8332
|
* @param categories - Optional array of categories to revoke (if not provided, revokes all)
|
|
8108
8333
|
*/
|
|
8109
8334
|
revokeConsent(categories) {
|
|
8110
8335
|
try {
|
|
8111
8336
|
this.consentManager.revokeConsent(categories);
|
|
8112
|
-
this.
|
|
8113
|
-
this.
|
|
8114
|
-
this.
|
|
8337
|
+
const idMode = this.consentManager.getIdMode();
|
|
8338
|
+
this.idManager.setMode(idMode);
|
|
8339
|
+
this.log("Consent revoked, switched to cookie-less mode", categories);
|
|
8340
|
+
if (!this.consentManager.hasConsent()) {
|
|
8341
|
+
this.eventQueue = [];
|
|
8342
|
+
this.waitingForConsentQueue = [];
|
|
8343
|
+
}
|
|
8115
8344
|
} catch (error) {
|
|
8116
8345
|
const formattedError = this.formatError(error, "revokeConsent");
|
|
8117
8346
|
this.logError(formattedError);
|