@grainql/analytics-web 2.1.0 → 2.1.1

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.
@@ -0,0 +1,131 @@
1
+ "use strict";
2
+ /**
3
+ * Activity Detection for Grain Analytics
4
+ * Tracks user activity (mouse, keyboard, touch, scroll) to determine if user is active
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.ActivityDetector = void 0;
8
+ class ActivityDetector {
9
+ constructor() {
10
+ this.activityThreshold = 30000; // 30 seconds
11
+ this.listeners = [];
12
+ this.isDestroyed = false;
13
+ // Events that indicate user activity
14
+ this.activityEvents = [
15
+ 'mousemove',
16
+ 'mousedown',
17
+ 'keydown',
18
+ 'scroll',
19
+ 'touchstart',
20
+ 'click',
21
+ ];
22
+ this.lastActivityTime = Date.now();
23
+ this.boundActivityHandler = this.debounce(this.handleActivity.bind(this), 500);
24
+ this.setupListeners();
25
+ }
26
+ /**
27
+ * Setup event listeners for activity detection
28
+ */
29
+ setupListeners() {
30
+ if (typeof window === 'undefined')
31
+ return;
32
+ for (const event of this.activityEvents) {
33
+ window.addEventListener(event, this.boundActivityHandler, { passive: true });
34
+ }
35
+ }
36
+ /**
37
+ * Handle activity event
38
+ */
39
+ handleActivity() {
40
+ if (this.isDestroyed)
41
+ return;
42
+ this.lastActivityTime = Date.now();
43
+ this.notifyListeners();
44
+ }
45
+ /**
46
+ * Debounce function to limit how often activity handler is called
47
+ */
48
+ debounce(func, wait) {
49
+ let timeout = null;
50
+ return () => {
51
+ if (timeout !== null) {
52
+ clearTimeout(timeout);
53
+ }
54
+ timeout = window.setTimeout(() => {
55
+ func();
56
+ timeout = null;
57
+ }, wait);
58
+ };
59
+ }
60
+ /**
61
+ * Check if user is currently active
62
+ * @param threshold Time in ms to consider user inactive (default: 30s)
63
+ */
64
+ isActive(threshold) {
65
+ const thresholdToUse = threshold ?? this.activityThreshold;
66
+ const now = Date.now();
67
+ return (now - this.lastActivityTime) < thresholdToUse;
68
+ }
69
+ /**
70
+ * Get time since last activity in milliseconds
71
+ */
72
+ getTimeSinceLastActivity() {
73
+ return Date.now() - this.lastActivityTime;
74
+ }
75
+ /**
76
+ * Get last activity timestamp
77
+ */
78
+ getLastActivityTime() {
79
+ return this.lastActivityTime;
80
+ }
81
+ /**
82
+ * Set activity threshold
83
+ */
84
+ setActivityThreshold(threshold) {
85
+ this.activityThreshold = threshold;
86
+ }
87
+ /**
88
+ * Add listener for activity changes
89
+ */
90
+ addListener(listener) {
91
+ this.listeners.push(listener);
92
+ }
93
+ /**
94
+ * Remove listener
95
+ */
96
+ removeListener(listener) {
97
+ const index = this.listeners.indexOf(listener);
98
+ if (index > -1) {
99
+ this.listeners.splice(index, 1);
100
+ }
101
+ }
102
+ /**
103
+ * Notify all listeners
104
+ */
105
+ notifyListeners() {
106
+ for (const listener of this.listeners) {
107
+ try {
108
+ listener();
109
+ }
110
+ catch (error) {
111
+ console.error('[Activity Detector] Listener error:', error);
112
+ }
113
+ }
114
+ }
115
+ /**
116
+ * Cleanup and remove listeners
117
+ */
118
+ destroy() {
119
+ if (this.isDestroyed)
120
+ return;
121
+ if (typeof window !== 'undefined') {
122
+ for (const event of this.activityEvents) {
123
+ window.removeEventListener(event, this.boundActivityHandler);
124
+ }
125
+ }
126
+ this.listeners = [];
127
+ this.isDestroyed = true;
128
+ }
129
+ }
130
+ exports.ActivityDetector = ActivityDetector;
131
+ //# sourceMappingURL=activity.js.map
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ /**
3
+ * Consent management for Grain Analytics
4
+ * Handles GDPR-compliant consent tracking and state management
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.ConsentManager = exports.CONSENT_VERSION = exports.DEFAULT_CONSENT_CATEGORIES = void 0;
8
+ exports.DEFAULT_CONSENT_CATEGORIES = ['necessary', 'analytics', 'functional'];
9
+ exports.CONSENT_VERSION = '1.0.0';
10
+ /**
11
+ * Consent manager for handling user consent state
12
+ */
13
+ class ConsentManager {
14
+ constructor(tenantId, consentMode = 'opt-out') {
15
+ this.consentState = null;
16
+ this.listeners = [];
17
+ this.consentMode = consentMode;
18
+ this.storageKey = `grain_consent_${tenantId}`;
19
+ this.loadConsentState();
20
+ }
21
+ /**
22
+ * Load consent state from localStorage
23
+ */
24
+ loadConsentState() {
25
+ if (typeof window === 'undefined')
26
+ return;
27
+ try {
28
+ const stored = localStorage.getItem(this.storageKey);
29
+ if (stored) {
30
+ const parsed = JSON.parse(stored);
31
+ this.consentState = {
32
+ ...parsed,
33
+ timestamp: new Date(parsed.timestamp),
34
+ };
35
+ }
36
+ else if (this.consentMode === 'opt-out' || this.consentMode === 'disabled') {
37
+ // Auto-grant consent for opt-out and disabled modes
38
+ this.consentState = {
39
+ granted: true,
40
+ categories: exports.DEFAULT_CONSENT_CATEGORIES,
41
+ timestamp: new Date(),
42
+ version: exports.CONSENT_VERSION,
43
+ };
44
+ this.saveConsentState();
45
+ }
46
+ }
47
+ catch (error) {
48
+ console.error('[Grain Consent] Failed to load consent state:', error);
49
+ }
50
+ }
51
+ /**
52
+ * Save consent state to localStorage
53
+ */
54
+ saveConsentState() {
55
+ if (typeof window === 'undefined' || !this.consentState)
56
+ return;
57
+ try {
58
+ localStorage.setItem(this.storageKey, JSON.stringify(this.consentState));
59
+ }
60
+ catch (error) {
61
+ console.error('[Grain Consent] Failed to save consent state:', error);
62
+ }
63
+ }
64
+ /**
65
+ * Grant consent with optional categories
66
+ */
67
+ grantConsent(categories) {
68
+ const grantedCategories = categories || exports.DEFAULT_CONSENT_CATEGORIES;
69
+ this.consentState = {
70
+ granted: true,
71
+ categories: grantedCategories,
72
+ timestamp: new Date(),
73
+ version: exports.CONSENT_VERSION,
74
+ };
75
+ this.saveConsentState();
76
+ this.notifyListeners();
77
+ }
78
+ /**
79
+ * Revoke consent (opt-out)
80
+ */
81
+ revokeConsent(categories) {
82
+ if (!this.consentState) {
83
+ this.consentState = {
84
+ granted: false,
85
+ categories: [],
86
+ timestamp: new Date(),
87
+ version: exports.CONSENT_VERSION,
88
+ };
89
+ }
90
+ else if (categories) {
91
+ // Remove specific categories
92
+ this.consentState = {
93
+ ...this.consentState,
94
+ categories: this.consentState.categories.filter(cat => !categories.includes(cat)),
95
+ granted: this.consentState.categories.length > 0,
96
+ timestamp: new Date(),
97
+ };
98
+ }
99
+ else {
100
+ // Revoke all consent
101
+ this.consentState = {
102
+ granted: false,
103
+ categories: [],
104
+ timestamp: new Date(),
105
+ version: exports.CONSENT_VERSION,
106
+ };
107
+ }
108
+ this.saveConsentState();
109
+ this.notifyListeners();
110
+ }
111
+ /**
112
+ * Get current consent state
113
+ */
114
+ getConsentState() {
115
+ return this.consentState ? { ...this.consentState } : null;
116
+ }
117
+ /**
118
+ * Check if user has granted consent
119
+ */
120
+ hasConsent(category) {
121
+ // Disabled mode always returns true (no consent required)
122
+ if (this.consentMode === 'disabled') {
123
+ return true;
124
+ }
125
+ // No consent state in opt-in mode means no consent
126
+ if (this.consentMode === 'opt-in' && !this.consentState) {
127
+ return false;
128
+ }
129
+ // Check consent state
130
+ if (!this.consentState?.granted) {
131
+ return false;
132
+ }
133
+ // Check specific category if provided
134
+ if (category) {
135
+ return this.consentState.categories.includes(category);
136
+ }
137
+ return true;
138
+ }
139
+ /**
140
+ * Check if we should wait for consent before tracking
141
+ */
142
+ shouldWaitForConsent() {
143
+ return this.consentMode === 'opt-in' && !this.consentState?.granted;
144
+ }
145
+ /**
146
+ * Add consent change listener
147
+ */
148
+ addListener(listener) {
149
+ this.listeners.push(listener);
150
+ }
151
+ /**
152
+ * Remove consent change listener
153
+ */
154
+ removeListener(listener) {
155
+ const index = this.listeners.indexOf(listener);
156
+ if (index > -1) {
157
+ this.listeners.splice(index, 1);
158
+ }
159
+ }
160
+ /**
161
+ * Notify all listeners of consent state change
162
+ */
163
+ notifyListeners() {
164
+ if (!this.consentState)
165
+ return;
166
+ this.listeners.forEach(listener => {
167
+ try {
168
+ listener(this.consentState);
169
+ }
170
+ catch (error) {
171
+ console.error('[Grain Consent] Listener error:', error);
172
+ }
173
+ });
174
+ }
175
+ /**
176
+ * Clear all consent data
177
+ */
178
+ clearConsent() {
179
+ if (typeof window === 'undefined')
180
+ return;
181
+ try {
182
+ localStorage.removeItem(this.storageKey);
183
+ this.consentState = null;
184
+ }
185
+ catch (error) {
186
+ console.error('[Grain Consent] Failed to clear consent:', error);
187
+ }
188
+ }
189
+ }
190
+ exports.ConsentManager = ConsentManager;
191
+ //# sourceMappingURL=consent.js.map
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ /**
3
+ * Cookie utilities for Grain Analytics
4
+ * Provides GDPR-compliant cookie management with configurable options
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.setCookie = setCookie;
8
+ exports.getCookie = getCookie;
9
+ exports.deleteCookie = deleteCookie;
10
+ exports.areCookiesEnabled = areCookiesEnabled;
11
+ /**
12
+ * Set a cookie with configurable options
13
+ */
14
+ function setCookie(name, value, config) {
15
+ if (typeof document === 'undefined')
16
+ return;
17
+ const parts = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];
18
+ if (config?.maxAge !== undefined) {
19
+ parts.push(`max-age=${config.maxAge}`);
20
+ }
21
+ if (config?.domain) {
22
+ parts.push(`domain=${config.domain}`);
23
+ }
24
+ if (config?.path) {
25
+ parts.push(`path=${config.path}`);
26
+ }
27
+ else {
28
+ parts.push('path=/');
29
+ }
30
+ if (config?.sameSite) {
31
+ parts.push(`samesite=${config.sameSite}`);
32
+ }
33
+ if (config?.secure) {
34
+ parts.push('secure');
35
+ }
36
+ document.cookie = parts.join('; ');
37
+ }
38
+ /**
39
+ * Get a cookie value by name
40
+ */
41
+ function getCookie(name) {
42
+ if (typeof document === 'undefined')
43
+ return null;
44
+ const nameEQ = encodeURIComponent(name) + '=';
45
+ const cookies = document.cookie.split(';');
46
+ for (let i = 0; i < cookies.length; i++) {
47
+ let cookie = cookies[i];
48
+ while (cookie.charAt(0) === ' ') {
49
+ cookie = cookie.substring(1);
50
+ }
51
+ if (cookie.indexOf(nameEQ) === 0) {
52
+ return decodeURIComponent(cookie.substring(nameEQ.length));
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+ /**
58
+ * Delete a cookie by name
59
+ */
60
+ function deleteCookie(name, config) {
61
+ if (typeof document === 'undefined')
62
+ return;
63
+ const parts = [
64
+ `${encodeURIComponent(name)}=`,
65
+ 'max-age=0',
66
+ ];
67
+ if (config?.domain) {
68
+ parts.push(`domain=${config.domain}`);
69
+ }
70
+ if (config?.path) {
71
+ parts.push(`path=${config.path}`);
72
+ }
73
+ else {
74
+ parts.push('path=/');
75
+ }
76
+ document.cookie = parts.join('; ');
77
+ }
78
+ /**
79
+ * Check if cookies are available and working
80
+ */
81
+ function areCookiesEnabled() {
82
+ if (typeof document === 'undefined')
83
+ return false;
84
+ try {
85
+ const testCookie = '_grain_cookie_test';
86
+ setCookie(testCookie, 'test', { maxAge: 1 });
87
+ const result = getCookie(testCookie) === 'test';
88
+ deleteCookie(testCookie);
89
+ return result;
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ }
95
+ //# sourceMappingURL=cookies.js.map
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ /**
3
+ * Heartbeat Manager for Grain Analytics
4
+ * Tracks session activity with consent-aware behavior
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.HeartbeatManager = void 0;
8
+ class HeartbeatManager {
9
+ constructor(tracker, activityDetector, config) {
10
+ this.heartbeatTimer = null;
11
+ this.isDestroyed = false;
12
+ this.tracker = tracker;
13
+ this.activityDetector = activityDetector;
14
+ this.config = config;
15
+ this.lastHeartbeatTime = Date.now();
16
+ this.currentInterval = config.activeInterval;
17
+ // Start heartbeat tracking
18
+ this.scheduleNextHeartbeat();
19
+ }
20
+ /**
21
+ * Schedule the next heartbeat based on current activity
22
+ */
23
+ scheduleNextHeartbeat() {
24
+ if (this.isDestroyed)
25
+ return;
26
+ // Clear existing timer
27
+ if (this.heartbeatTimer !== null) {
28
+ clearTimeout(this.heartbeatTimer);
29
+ }
30
+ // Determine interval based on activity
31
+ const isActive = this.activityDetector.isActive(60000); // 1 minute threshold
32
+ this.currentInterval = isActive ? this.config.activeInterval : this.config.inactiveInterval;
33
+ // Schedule next heartbeat
34
+ this.heartbeatTimer = window.setTimeout(() => {
35
+ this.sendHeartbeat();
36
+ this.scheduleNextHeartbeat();
37
+ }, this.currentInterval);
38
+ if (this.config.debug) {
39
+ console.log(`[Heartbeat] Scheduled next heartbeat in ${this.currentInterval / 1000}s (${isActive ? 'active' : 'inactive'})`);
40
+ }
41
+ }
42
+ /**
43
+ * Send heartbeat event
44
+ */
45
+ sendHeartbeat() {
46
+ if (this.isDestroyed)
47
+ return;
48
+ const now = Date.now();
49
+ const isActive = this.activityDetector.isActive(60000); // 1 minute threshold
50
+ const hasConsent = this.tracker.hasConsent('analytics');
51
+ // Base properties (always included)
52
+ const properties = {
53
+ type: 'heartbeat',
54
+ status: isActive ? 'active' : 'inactive',
55
+ timestamp: now,
56
+ };
57
+ // Enhanced properties when consent is granted
58
+ if (hasConsent) {
59
+ const page = this.tracker.getCurrentPage();
60
+ if (page) {
61
+ properties.page = page;
62
+ }
63
+ properties.duration = now - this.lastHeartbeatTime;
64
+ properties.event_count = this.tracker.getEventCountSinceLastHeartbeat();
65
+ // Reset event count
66
+ this.tracker.resetEventCountSinceLastHeartbeat();
67
+ }
68
+ // Track the heartbeat event
69
+ this.tracker.trackSystemEvent('_grain_heartbeat', properties);
70
+ this.lastHeartbeatTime = now;
71
+ if (this.config.debug) {
72
+ console.log('[Heartbeat] Sent heartbeat:', properties);
73
+ }
74
+ }
75
+ /**
76
+ * Destroy the heartbeat manager
77
+ */
78
+ destroy() {
79
+ if (this.isDestroyed)
80
+ return;
81
+ if (this.heartbeatTimer !== null) {
82
+ clearTimeout(this.heartbeatTimer);
83
+ this.heartbeatTimer = null;
84
+ }
85
+ this.isDestroyed = true;
86
+ if (this.config.debug) {
87
+ console.log('[Heartbeat] Destroyed');
88
+ }
89
+ }
90
+ }
91
+ exports.HeartbeatManager = HeartbeatManager;
92
+ //# sourceMappingURL=heartbeat.js.map
@@ -1,4 +1,4 @@
1
- /* Grain Analytics Web SDK v2.1.0 | MIT License | Development Build */
1
+ /* Grain Analytics Web SDK v2.1.1 | MIT License | Development Build */
2
2
  "use strict";
3
3
  var Grain = (() => {
4
4
  var __defProp = Object.defineProperty;
@@ -1,3 +1,3 @@
1
- /* Grain Analytics Web SDK v2.1.0 | MIT License */
1
+ /* Grain Analytics Web SDK v2.1.1 | MIT License */
2
2
  "use strict";var Grain=(()=>{var C=Object.defineProperty;var R=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var T=Object.prototype.hasOwnProperty;var x=(c,e)=>{for(var t in e)C(c,t,{get:e[t],enumerable:!0})},U=(c,e,t,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of A(e))!T.call(c,n)&&n!==t&&C(c,n,{get:()=>e[n],enumerable:!(r=R(e,n))||r.enumerable});return c};var D=c=>U(C({},"__esModule",{value:!0}),c);var $={};x($,{GrainAnalytics:()=>u,createGrainAnalytics:()=>P,default:()=>O});var S=["necessary","analytics","functional"],f="1.0.0",p=class{constructor(e,t="opt-out"){this.consentState=null;this.listeners=[];this.consentMode=t,this.storageKey=`grain_consent_${e}`,this.loadConsentState()}loadConsentState(){if(!(typeof window>"u"))try{let e=localStorage.getItem(this.storageKey);if(e){let t=JSON.parse(e);this.consentState={...t,timestamp:new Date(t.timestamp)}}else(this.consentMode==="opt-out"||this.consentMode==="disabled")&&(this.consentState={granted:!0,categories:S,timestamp:new Date,version:f},this.saveConsentState())}catch(e){console.error("[Grain Consent] Failed to load consent state:",e)}}saveConsentState(){if(!(typeof window>"u"||!this.consentState))try{localStorage.setItem(this.storageKey,JSON.stringify(this.consentState))}catch(e){console.error("[Grain Consent] Failed to save consent state:",e)}}grantConsent(e){let t=e||S;this.consentState={granted:!0,categories:t,timestamp:new Date,version:f},this.saveConsentState(),this.notifyListeners()}revokeConsent(e){this.consentState?e?this.consentState={...this.consentState,categories:this.consentState.categories.filter(t=>!e.includes(t)),granted:this.consentState.categories.length>0,timestamp:new Date}:this.consentState={granted:!1,categories:[],timestamp:new Date,version:f}:this.consentState={granted:!1,categories:[],timestamp:new Date,version:f},this.saveConsentState(),this.notifyListeners()}getConsentState(){return this.consentState?{...this.consentState}:null}hasConsent(e){return this.consentMode==="disabled"?!0:this.consentMode==="opt-in"&&!this.consentState||!this.consentState?.granted?!1:e?this.consentState.categories.includes(e):!0}shouldWaitForConsent(){return this.consentMode==="opt-in"&&!this.consentState?.granted}addListener(e){this.listeners.push(e)}removeListener(e){let t=this.listeners.indexOf(e);t>-1&&this.listeners.splice(t,1)}notifyListeners(){this.consentState&&this.listeners.forEach(e=>{try{e(this.consentState)}catch(t){console.error("[Grain Consent] Listener error:",t)}})}clearConsent(){if(!(typeof window>"u"))try{localStorage.removeItem(this.storageKey),this.consentState=null}catch(e){console.error("[Grain Consent] Failed to clear consent:",e)}}};function b(c,e,t){if(typeof document>"u")return;let r=[`${encodeURIComponent(c)}=${encodeURIComponent(e)}`];t?.maxAge!==void 0&&r.push(`max-age=${t.maxAge}`),t?.domain&&r.push(`domain=${t.domain}`),t?.path?r.push(`path=${t.path}`):r.push("path=/"),t?.sameSite&&r.push(`samesite=${t.sameSite}`),t?.secure&&r.push("secure"),document.cookie=r.join("; ")}function w(c){if(typeof document>"u")return null;let e=encodeURIComponent(c)+"=",t=document.cookie.split(";");for(let r=0;r<t.length;r++){let n=t[r];for(;n.charAt(0)===" ";)n=n.substring(1);if(n.indexOf(e)===0)return decodeURIComponent(n.substring(e.length))}return null}function L(c,e){if(typeof document>"u")return;let t=[`${encodeURIComponent(c)}=`,"max-age=0"];e?.domain&&t.push(`domain=${e.domain}`),e?.path?t.push(`path=${e.path}`):t.push("path=/"),document.cookie=t.join("; ")}function k(){if(typeof document>"u")return!1;try{let c="_grain_cookie_test";b(c,"test",{maxAge:1});let e=w(c)==="test";return L(c),e}catch{return!1}}var m=class{constructor(){this.activityThreshold=3e4;this.listeners=[];this.isDestroyed=!1;this.activityEvents=["mousemove","mousedown","keydown","scroll","touchstart","click"];this.lastActivityTime=Date.now(),this.boundActivityHandler=this.debounce(this.handleActivity.bind(this),500),this.setupListeners()}setupListeners(){if(!(typeof window>"u"))for(let e of this.activityEvents)window.addEventListener(e,this.boundActivityHandler,{passive:!0})}handleActivity(){this.isDestroyed||(this.lastActivityTime=Date.now(),this.notifyListeners())}debounce(e,t){let r=null;return()=>{r!==null&&clearTimeout(r),r=window.setTimeout(()=>{e(),r=null},t)}}isActive(e){let t=e??this.activityThreshold;return Date.now()-this.lastActivityTime<t}getTimeSinceLastActivity(){return Date.now()-this.lastActivityTime}getLastActivityTime(){return this.lastActivityTime}setActivityThreshold(e){this.activityThreshold=e}addListener(e){this.listeners.push(e)}removeListener(e){let t=this.listeners.indexOf(e);t>-1&&this.listeners.splice(t,1)}notifyListeners(){for(let e of this.listeners)try{e()}catch(t){console.error("[Activity Detector] Listener error:",t)}}destroy(){if(!this.isDestroyed){if(typeof window<"u")for(let e of this.activityEvents)window.removeEventListener(e,this.boundActivityHandler);this.listeners=[],this.isDestroyed=!0}}};var v=class{constructor(e,t,r){this.heartbeatTimer=null;this.isDestroyed=!1;this.tracker=e,this.activityDetector=t,this.config=r,this.lastHeartbeatTime=Date.now(),this.currentInterval=r.activeInterval,this.scheduleNextHeartbeat()}scheduleNextHeartbeat(){if(this.isDestroyed)return;this.heartbeatTimer!==null&&clearTimeout(this.heartbeatTimer);let e=this.activityDetector.isActive(6e4);this.currentInterval=e?this.config.activeInterval:this.config.inactiveInterval,this.heartbeatTimer=window.setTimeout(()=>{this.sendHeartbeat(),this.scheduleNextHeartbeat()},this.currentInterval),this.config.debug&&console.log(`[Heartbeat] Scheduled next heartbeat in ${this.currentInterval/1e3}s (${e?"active":"inactive"})`)}sendHeartbeat(){if(this.isDestroyed)return;let e=Date.now(),t=this.activityDetector.isActive(6e4),r=this.tracker.hasConsent("analytics"),n={type:"heartbeat",status:t?"active":"inactive",timestamp:e};if(r){let s=this.tracker.getCurrentPage();s&&(n.page=s),n.duration=e-this.lastHeartbeatTime,n.event_count=this.tracker.getEventCountSinceLastHeartbeat(),this.tracker.resetEventCountSinceLastHeartbeat()}this.tracker.trackSystemEvent("_grain_heartbeat",n),this.lastHeartbeatTime=e,this.config.debug&&console.log("[Heartbeat] Sent heartbeat:",n)}destroy(){this.isDestroyed||(this.heartbeatTimer!==null&&(clearTimeout(this.heartbeatTimer),this.heartbeatTimer=null),this.isDestroyed=!0,this.config.debug&&console.log("[Heartbeat] Destroyed"))}};var y=class{constructor(e,t){this.isDestroyed=!1;this.currentPath=null;this.originalPushState=null;this.originalReplaceState=null;this.handlePopState=()=>{this.isDestroyed||this.trackCurrentPage()};this.handleHashChange=()=>{this.isDestroyed||this.trackCurrentPage()};this.tracker=e,this.config=t,this.trackCurrentPage(),this.setupHistoryListeners(),this.setupHashChangeListener()}setupHistoryListeners(){typeof window>"u"||typeof history>"u"||(this.originalPushState=history.pushState,history.pushState=(e,t,r)=>{this.originalPushState?.call(history,e,t,r),this.trackCurrentPage()},this.originalReplaceState=history.replaceState,history.replaceState=(e,t,r)=>{this.originalReplaceState?.call(history,e,t,r),this.trackCurrentPage()},window.addEventListener("popstate",this.handlePopState))}setupHashChangeListener(){typeof window>"u"||window.addEventListener("hashchange",this.handleHashChange)}trackCurrentPage(){if(this.isDestroyed||typeof window>"u")return;let e=this.extractPath(window.location.href);if(e===this.currentPath)return;this.currentPath=e;let t=this.tracker.hasConsent("analytics"),r={page:e,timestamp:Date.now()};t&&(r.referrer=document.referrer||"",r.title=document.title||"",r.full_url=window.location.href),this.tracker.trackSystemEvent("page_view",r),this.config.debug&&console.log("[Page Tracking] Tracked page view:",r)}extractPath(e){try{let t=new URL(e),r=t.pathname+t.hash;return!this.config.stripQueryParams&&t.search&&(r+=t.search),r}catch(t){return this.config.debug&&console.warn("[Page Tracking] Failed to parse URL:",e,t),e}}getCurrentPage(){return this.currentPath}trackPage(e,t){if(this.isDestroyed)return;let r=this.tracker.hasConsent("analytics"),n={page:e,timestamp:Date.now(),...t};r&&typeof document<"u"&&(n.referrer||(n.referrer=document.referrer||""),n.title||(n.title=document.title||""),!n.full_url&&typeof window<"u"&&(n.full_url=window.location.href)),this.tracker.trackSystemEvent("page_view",n),this.config.debug&&console.log("[Page Tracking] Manually tracked page:",n)}destroy(){this.isDestroyed||(typeof history<"u"&&(this.originalPushState&&(history.pushState=this.originalPushState),this.originalReplaceState&&(history.replaceState=this.originalReplaceState)),typeof window<"u"&&(window.removeEventListener("popstate",this.handlePopState),window.removeEventListener("hashchange",this.handleHashChange)),this.isDestroyed=!0,this.config.debug&&console.log("[Page Tracking] Destroyed"))}};var u=class{constructor(e){this.eventQueue=[];this.waitingForConsentQueue=[];this.flushTimer=null;this.isDestroyed=!1;this.globalUserId=null;this.persistentAnonymousUserId=null;this.configCache=null;this.configRefreshTimer=null;this.configChangeListeners=[];this.configFetchPromise=null;this.cookiesEnabled=!1;this.activityDetector=null;this.heartbeatManager=null;this.pageTrackingManager=null;this.ephemeralSessionId=null;this.eventCountSinceLastHeartbeat=0;this.config={apiUrl:"https://api.grainql.com",authStrategy:"NONE",batchSize:50,flushInterval:5e3,retryAttempts:3,retryDelay:1e3,maxEventsPerRequest:160,debug:!1,defaultConfigurations:{},configCacheKey:"grain_config",configRefreshInterval:3e5,enableConfigCache:!0,consentMode:"opt-out",waitForConsent:!1,enableCookies:!1,anonymizeIP:!1,disableAutoProperties:!1,enableHeartbeat:!0,heartbeatActiveInterval:12e4,heartbeatInactiveInterval:3e5,enableAutoPageView:!0,stripQueryParams:!0,...e,tenantId:e.tenantId},this.consentManager=new p(this.config.tenantId,this.config.consentMode),this.config.enableCookies&&(this.cookiesEnabled=k(),!this.cookiesEnabled&&this.config.debug&&console.warn("[Grain Analytics] Cookies are not available, falling back to localStorage")),e.userId&&(this.globalUserId=e.userId),this.validateConfig(),this.initializePersistentAnonymousUserId(),this.setupBeforeUnload(),this.startFlushTimer(),this.initializeConfigCache(),this.ephemeralSessionId=this.generateUUID(),typeof window<"u"&&this.initializeAutomaticTracking(),this.consentManager.addListener(t=>{t.granted&&this.handleConsentGranted()})}validateConfig(){if(!this.config.tenantId)throw new Error("Grain Analytics: tenantId is required");if(this.config.authStrategy==="SERVER_SIDE"&&!this.config.secretKey)throw new Error("Grain Analytics: secretKey is required for SERVER_SIDE auth strategy");if(this.config.authStrategy==="JWT"&&!this.config.authProvider)throw new Error("Grain Analytics: authProvider is required for JWT auth strategy")}generateUUID(){return typeof crypto<"u"&&crypto.randomUUID?crypto.randomUUID():"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(e){let t=Math.random()*16|0;return(e==="x"?t:t&3|8).toString(16)})}generateAnonymousUserId(){return this.generateUUID()}initializePersistentAnonymousUserId(){if(typeof window>"u")return;let e=`grain_anonymous_user_id_${this.config.tenantId}`,t="_grain_uid";try{if(this.cookiesEnabled){let n=w(t);if(n){this.persistentAnonymousUserId=n,this.log("Loaded persistent anonymous user ID from cookie:",this.persistentAnonymousUserId);return}}let r=localStorage.getItem(e);r?(this.persistentAnonymousUserId=r,this.log("Loaded persistent anonymous user ID from localStorage:",this.persistentAnonymousUserId),this.cookiesEnabled&&this.savePersistentAnonymousUserId(r)):(this.persistentAnonymousUserId=this.generateAnonymousUserId(),this.savePersistentAnonymousUserId(this.persistentAnonymousUserId),this.log("Generated new persistent anonymous user ID:",this.persistentAnonymousUserId))}catch(r){this.log("Failed to initialize persistent anonymous user ID:",r),this.persistentAnonymousUserId=this.generateAnonymousUserId()}}savePersistentAnonymousUserId(e){if(typeof window>"u")return;let t=`grain_anonymous_user_id_${this.config.tenantId}`,r="_grain_uid";try{if(this.cookiesEnabled){let n={maxAge:31536e3,sameSite:"lax",secure:window.location.protocol==="https:",...this.config.cookieOptions};b(r,e,n)}localStorage.setItem(t,e)}catch(n){this.log("Failed to save persistent anonymous user ID:",n)}}getEffectiveUserIdInternal(){if(this.globalUserId)return this.globalUserId;if(this.persistentAnonymousUserId)return this.persistentAnonymousUserId;if(this.persistentAnonymousUserId=this.generateAnonymousUserId(),typeof window<"u")try{let e=`grain_anonymous_user_id_${this.config.tenantId}`;localStorage.setItem(e,this.persistentAnonymousUserId)}catch(e){this.log("Failed to persist generated anonymous user ID:",e)}return this.persistentAnonymousUserId}log(...e){this.config.debug&&console.log("[Grain Analytics]",...e)}createErrorDigest(e){let t=[...new Set(e.map(i=>i.eventName))],r=[...new Set(e.map(i=>i.userId))],n=0,s=0;return e.forEach(i=>{let o=i.properties||{};n+=Object.keys(o).length,s+=JSON.stringify(i).length}),{eventCount:e.length,totalProperties:n,totalSize:s,eventNames:t,userIds:r}}formatError(e,t,r){let n=r?this.createErrorDigest(r):{eventCount:0,totalProperties:0,totalSize:0,eventNames:[],userIds:[]},s="UNKNOWN_ERROR",i="An unknown error occurred";if(e instanceof Error)i=e.message,i.includes("fetch failed")||i.includes("network error")?s="NETWORK_ERROR":i.includes("timeout")?s="TIMEOUT_ERROR":i.includes("HTTP 4")?s="CLIENT_ERROR":i.includes("HTTP 5")?s="SERVER_ERROR":i.includes("JSON")?s="PARSE_ERROR":i.includes("auth")||i.includes("unauthorized")?s="AUTH_ERROR":i.includes("rate limit")||i.includes("429")?s="RATE_LIMIT_ERROR":s="GENERAL_ERROR";else if(typeof e=="string")i=e,s="STRING_ERROR";else if(e&&typeof e=="object"&&"status"in e){let o=e.status;s=`HTTP_${o}`,i=`HTTP ${o} error`}return{code:s,message:i,digest:n,timestamp:new Date().toISOString(),context:t,originalError:e}}logError(e){let{code:t,message:r,digest:n,timestamp:s,context:i}=e,o={"\u{1F6A8} Grain Analytics Error":{"Error Code":t,Message:r,Context:i,Timestamp:s,"Event Digest":{Events:n.eventCount,Properties:n.totalProperties,"Size (bytes)":n.totalSize,"Event Names":n.eventNames.length>0?n.eventNames.join(", "):"None","User IDs":n.userIds.length>0?n.userIds.slice(0,3).join(", ")+(n.userIds.length>3?"...":""):"None"}}};console.error("\u{1F6A8} Grain Analytics Error:",o),this.config.debug&&console.error(`[Grain Analytics] ${t}: ${r} (${i}) - Events: ${n.eventCount}, Props: ${n.totalProperties}, Size: ${n.totalSize}B`)}async safeExecute(e,t,r){try{return await e()}catch(n){let s=this.formatError(n,t,r);return this.logError(s),null}}formatEvent(e){return{eventName:e.eventName,userId:e.userId||this.getEffectiveUserIdInternal(),properties:e.properties||{}}}async getAuthHeaders(){let e={"Content-Type":"application/json"};switch(this.config.authStrategy){case"NONE":break;case"SERVER_SIDE":e.Authorization=`Chase ${this.config.secretKey}`;break;case"JWT":if(this.config.authProvider){let t=await this.config.authProvider.getToken();e.Authorization=`Bearer ${t}`}break}return e}async delay(e){return new Promise(t=>setTimeout(t,e))}isRetriableError(e){if(e instanceof Error){let t=e.message.toLowerCase();if(t.includes("fetch failed")||t==="network error"||t.includes("timeout")||t.includes("connection"))return!0}if(typeof e=="object"&&e!==null&&"status"in e){let t=e.status;return t>=500||t===429}return!1}async sendEvents(e){if(e.length===0)return;let t;for(let r=0;r<=this.config.retryAttempts;r++)try{let n=await this.getAuthHeaders(),s=`${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/multi`;this.log(`Sending ${e.length} events to ${s} (attempt ${r+1})`);let i=await fetch(s,{method:"POST",headers:n,body:JSON.stringify(e)});if(!i.ok){let o=`HTTP ${i.status}`;try{let h=await i.json();h?.message&&(o=h.message)}catch{let h=await i.text();h&&(o=h)}let a=new Error(`Failed to send events: ${o}`);throw a.status=i.status,a}this.log(`Successfully sent ${e.length} events`);return}catch(n){if(t=n,r===this.config.retryAttempts){let i=this.formatError(n,`sendEvents (attempt ${r+1}/${this.config.retryAttempts+1})`,e);this.logError(i);return}if(!this.isRetriableError(n)){let i=this.formatError(n,"sendEvents (non-retriable error)",e);this.logError(i);return}let s=this.config.retryDelay*Math.pow(2,r);this.log(`Retrying in ${s}ms after error:`,n),await this.delay(s)}}async sendEventsWithBeacon(e){if(e.length!==0)try{let t=await this.getAuthHeaders(),r=`${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/multi`,n=JSON.stringify({events:e});if(typeof navigator<"u"&&"sendBeacon"in navigator){let s=new Blob([n],{type:"application/json"});if(navigator.sendBeacon(r,s)){this.log(`Successfully sent ${e.length} events via beacon`);return}}await fetch(r,{method:"POST",headers:t,body:n,keepalive:!0}),this.log(`Successfully sent ${e.length} events via fetch (keepalive)`)}catch(t){let r=this.formatError(t,"sendEventsWithBeacon",e);this.logError(r)}}startFlushTimer(){this.flushTimer&&clearInterval(this.flushTimer),this.flushTimer=window.setInterval(()=>{this.eventQueue.length>0&&this.flush().catch(e=>{let t=this.formatError(e,"auto-flush");this.logError(t)})},this.config.flushInterval)}setupBeforeUnload(){if(typeof window>"u")return;let e=()=>{if(this.eventQueue.length>0){let t=[...this.eventQueue];this.eventQueue=[];let r=this.chunkEvents(t,this.config.maxEventsPerRequest);r.length>0&&this.sendEventsWithBeacon(r[0]).catch(()=>{})}};window.addEventListener("beforeunload",e),window.addEventListener("pagehide",e),document.addEventListener("visibilitychange",()=>{if(document.visibilityState==="hidden"&&this.eventQueue.length>0){let t=[...this.eventQueue];this.eventQueue=[];let r=this.chunkEvents(t,this.config.maxEventsPerRequest);r.length>0&&this.sendEventsWithBeacon(r[0]).catch(()=>{})}})}initializeAutomaticTracking(){if(this.config.enableHeartbeat)try{this.activityDetector=new m,this.heartbeatManager=new v(this,this.activityDetector,{activeInterval:this.config.heartbeatActiveInterval,inactiveInterval:this.config.heartbeatInactiveInterval,debug:this.config.debug}),this.log("Heartbeat tracking initialized")}catch(e){this.log("Failed to initialize heartbeat tracking:",e)}if(this.config.enableAutoPageView)try{this.pageTrackingManager=new y(this,{stripQueryParams:this.config.stripQueryParams,debug:this.config.debug}),this.log("Auto page view tracking initialized")}catch(e){this.log("Failed to initialize page view tracking:",e)}}handleConsentGranted(){this.flushWaitingForConsentQueue(),this.ephemeralSessionId&&this.trackSystemEvent("_grain_consent_granted",{previous_session_id:this.ephemeralSessionId,new_user_id:this.getEffectiveUserId(),timestamp:Date.now()})}trackSystemEvent(e,t){if(this.isDestroyed)return;let r=this.consentManager.hasConsent("analytics"),n={eventName:e,userId:r?this.getEffectiveUserId():this.getEphemeralSessionId(),properties:{...t,_minimal:!r,_consent_status:r?"granted":"pending"}};this.eventQueue.push(n),this.eventCountSinceLastHeartbeat++,this.log(`Queued system event: ${e}`,t),this.eventQueue.length>=this.config.batchSize&&this.flush().catch(s=>{let i=this.formatError(s,"flush system event");this.logError(i)})}getEphemeralSessionId(){return this.ephemeralSessionId||(this.ephemeralSessionId=this.generateUUID()),this.ephemeralSessionId}getCurrentPage(){return this.pageTrackingManager?.getCurrentPage()||null}getEventCountSinceLastHeartbeat(){return this.eventCountSinceLastHeartbeat}resetEventCountSinceLastHeartbeat(){this.eventCountSinceLastHeartbeat=0}getEffectiveUserId(){return this.getEffectiveUserIdInternal()}getSessionId(){return this.consentManager.hasConsent("analytics")?this.getEffectiveUserId():this.getEphemeralSessionId()}async track(e,t,r){try{if(this.isDestroyed){let o=new Error("Grain Analytics: Client has been destroyed"),a=this.formatError(o,"track (client destroyed)");this.logError(a);return}let n,s={};if(typeof e=="string"?(n={eventName:e,properties:t},s=r||{}):(n=e,s=t||{}),this.config.allowedProperties&&n.properties){let o={};for(let a of this.config.allowedProperties)a in n.properties&&(o[a]=n.properties[a]);n.properties=o}let i=this.formatEvent(n);if(this.consentManager.shouldWaitForConsent()&&this.config.waitForConsent){this.waitingForConsentQueue.push(i),this.log(`Event waiting for consent: ${n.eventName}`,n.properties);return}if(!this.consentManager.hasConsent("analytics")){this.log(`Event blocked by consent: ${n.eventName}`);return}this.eventQueue.push(i),this.eventCountSinceLastHeartbeat++,this.log(`Queued event: ${n.eventName}`,n.properties),(s.flush||this.eventQueue.length>=this.config.batchSize)&&await this.flush()}catch(n){let s=this.formatError(n,"track");this.logError(s)}}flushWaitingForConsentQueue(){this.waitingForConsentQueue.length!==0&&(this.log(`Flushing ${this.waitingForConsentQueue.length} events waiting for consent`),this.eventQueue.push(...this.waitingForConsentQueue),this.waitingForConsentQueue=[],this.flush().catch(e=>{let t=this.formatError(e,"flush waiting for consent queue");this.logError(t)}))}identify(e){this.log(`Identified user: ${e}`),this.globalUserId=e,this.persistentAnonymousUserId=null}setUserId(e){if(this.log(`Set global user ID: ${e}`),this.globalUserId=e,e)this.persistentAnonymousUserId=null;else if(!this.persistentAnonymousUserId&&(this.persistentAnonymousUserId=this.generateAnonymousUserId(),typeof window<"u"))try{let t=`grain_anonymous_user_id_${this.config.tenantId}`;localStorage.setItem(t,this.persistentAnonymousUserId)}catch(t){this.log("Failed to persist new anonymous user ID:",t)}}getUserId(){return this.globalUserId}getEffectiveUserIdPublic(){return this.getEffectiveUserIdInternal()}login(e){try{if(this.isDestroyed){let t=new Error("Grain Analytics: Client has been destroyed"),r=this.formatError(t,"login (client destroyed)");this.logError(r);return}e.userId&&(this.log(`Login: Setting user ID to ${e.userId}`),this.globalUserId=e.userId,this.persistentAnonymousUserId=null),e.authToken&&(this.log("Login: Setting auth token"),this.config.authStrategy==="NONE"&&(this.config.authStrategy="JWT"),this.config.authProvider={getToken:()=>e.authToken}),e.authStrategy&&(this.log(`Login: Setting auth strategy to ${e.authStrategy}`),this.config.authStrategy=e.authStrategy),this.log(`Login successful. Effective user ID: ${this.getEffectiveUserIdInternal()}`)}catch(t){let r=this.formatError(t,"login");this.logError(r)}}logout(){try{if(this.isDestroyed){let e=new Error("Grain Analytics: Client has been destroyed"),t=this.formatError(e,"logout (client destroyed)");this.logError(t);return}if(this.log("Logout: Clearing user session"),this.globalUserId=null,this.config.authStrategy="NONE",this.config.authProvider=void 0,!this.persistentAnonymousUserId&&(this.persistentAnonymousUserId=this.generateAnonymousUserId(),typeof window<"u"))try{let e=`grain_anonymous_user_id_${this.config.tenantId}`;localStorage.setItem(e,this.persistentAnonymousUserId)}catch(e){this.log("Failed to persist new anonymous user ID after logout:",e)}this.log(`Logout successful. Effective user ID: ${this.getEffectiveUserIdInternal()}`)}catch(e){let t=this.formatError(e,"logout");this.logError(t)}}async setProperty(e,t){try{if(this.isDestroyed){let o=new Error("Grain Analytics: Client has been destroyed"),a=this.formatError(o,"setProperty (client destroyed)");this.logError(a);return}let r=t?.userId||this.getEffectiveUserIdInternal(),n=Object.keys(e);if(n.length>4){let o=new Error("Grain Analytics: Maximum 4 properties allowed per request"),a=this.formatError(o,"setProperty (validation)");this.logError(a);return}if(n.length===0){let o=new Error("Grain Analytics: At least one property is required"),a=this.formatError(o,"setProperty (validation)");this.logError(a);return}let s={};for(let[o,a]of Object.entries(e))a==null?s[o]="":typeof a=="string"?s[o]=a:s[o]=JSON.stringify(a);let i={userId:r,...s};await this.sendProperties(i)}catch(r){let n=this.formatError(r,"setProperty");this.logError(n)}}async sendProperties(e){let t;for(let r=0;r<=this.config.retryAttempts;r++)try{let n=await this.getAuthHeaders(),s=`${this.config.apiUrl}/v1/events/${encodeURIComponent(this.config.tenantId)}/properties`;this.log(`Setting properties for user ${e.userId} (attempt ${r+1})`);let i=await fetch(s,{method:"POST",headers:n,body:JSON.stringify(e)});if(!i.ok){let o=`HTTP ${i.status}`;try{let h=await i.json();h?.message&&(o=h.message)}catch{let h=await i.text();h&&(o=h)}let a=new Error(`Failed to set properties: ${o}`);throw a.status=i.status,a}this.log(`Successfully set properties for user ${e.userId}`);return}catch(n){if(t=n,r===this.config.retryAttempts){let i=this.formatError(n,`sendProperties (attempt ${r+1}/${this.config.retryAttempts+1})`);this.logError(i);return}if(!this.isRetriableError(n)){let i=this.formatError(n,"sendProperties (non-retriable error)");this.logError(i);return}let s=this.config.retryDelay*Math.pow(2,r);this.log(`Retrying in ${s}ms after error:`,n),await this.delay(s)}}async trackLogin(e,t){try{return await this.track("login",e,t)}catch(r){let n=this.formatError(r,"trackLogin");this.logError(n)}}async trackSignup(e,t){try{return await this.track("signup",e,t)}catch(r){let n=this.formatError(r,"trackSignup");this.logError(n)}}async trackCheckout(e,t){try{return await this.track("checkout",e,t)}catch(r){let n=this.formatError(r,"trackCheckout");this.logError(n)}}async trackPageView(e,t){try{return await this.track("page_view",e,t)}catch(r){let n=this.formatError(r,"trackPageView");this.logError(n)}}async trackPurchase(e,t){try{return await this.track("purchase",e,t)}catch(r){let n=this.formatError(r,"trackPurchase");this.logError(n)}}async trackSearch(e,t){try{return await this.track("search",e,t)}catch(r){let n=this.formatError(r,"trackSearch");this.logError(n)}}async trackAddToCart(e,t){try{return await this.track("add_to_cart",e,t)}catch(r){let n=this.formatError(r,"trackAddToCart");this.logError(n)}}async trackRemoveFromCart(e,t){try{return await this.track("remove_from_cart",e,t)}catch(r){let n=this.formatError(r,"trackRemoveFromCart");this.logError(n)}}async flush(){try{if(this.eventQueue.length===0)return;let e=[...this.eventQueue];this.eventQueue=[];let t=this.chunkEvents(e,this.config.maxEventsPerRequest);for(let r of t)await this.sendEvents(r)}catch(e){let t=this.formatError(e,"flush");this.logError(t)}}initializeConfigCache(){if(!(!this.config.enableConfigCache||typeof window>"u"))try{let e=localStorage.getItem(this.config.configCacheKey);e&&(this.configCache=JSON.parse(e),this.log("Loaded configuration from cache:",this.configCache))}catch(e){this.log("Failed to load configuration cache:",e)}}saveConfigCache(e){if(!(!this.config.enableConfigCache||typeof window>"u"))try{localStorage.setItem(this.config.configCacheKey,JSON.stringify(e)),this.log("Saved configuration to cache:",e)}catch(t){this.log("Failed to save configuration cache:",t)}}getConfig(e){if(this.configCache?.configurations?.[e])return this.configCache.configurations[e];if(this.config.defaultConfigurations?.[e])return this.config.defaultConfigurations[e]}getAllConfigs(){let e={...this.config.defaultConfigurations};return this.configCache?.configurations&&Object.assign(e,this.configCache.configurations),e}async fetchConfig(e={}){try{if(this.isDestroyed){let o=new Error("Grain Analytics: Client has been destroyed"),a=this.formatError(o,"fetchConfig (client destroyed)");return this.logError(a),null}let t=e.userId||this.getEffectiveUserIdInternal(),r=e.immediateKeys||[],n=e.properties||{},s={userId:t,immediateKeys:r,properties:n},i;for(let o=0;o<=this.config.retryAttempts;o++)try{let a=await this.getAuthHeaders(),h=`${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;this.log(`Fetching configurations for user ${t} (attempt ${o+1})`);let g=await fetch(h,{method:"POST",headers:a,body:JSON.stringify(s)});if(!g.ok){let E=`HTTP ${g.status}`;try{let l=await g.json();l?.message&&(E=l.message)}catch{let l=await g.text();l&&(E=l)}let I=new Error(`Failed to fetch configurations: ${E}`);throw I.status=g.status,I}let d=await g.json();return d.configurations&&this.updateConfigCache(d,t),this.log(`Successfully fetched configurations for user ${t}:`,d),d}catch(a){if(i=a,o===this.config.retryAttempts){let g=this.formatError(a,`fetchConfig (attempt ${o+1}/${this.config.retryAttempts+1})`);return this.logError(g),null}if(!this.isRetriableError(a)){let g=this.formatError(a,"fetchConfig (non-retriable error)");return this.logError(g),null}let h=this.config.retryDelay*Math.pow(2,o);this.log(`Retrying config fetch in ${h}ms after error:`,a),await this.delay(h)}return null}catch(t){let r=this.formatError(t,"fetchConfig");return this.logError(r),null}}async getConfigAsync(e,t={}){try{if(!t.forceRefresh&&this.configCache?.configurations?.[e])return this.configCache.configurations[e];if(!t.forceRefresh&&this.config.defaultConfigurations?.[e])return this.config.defaultConfigurations[e];let r=await this.fetchConfig(t);return r?r.configurations[e]:this.config.defaultConfigurations?.[e]}catch(r){let n=this.formatError(r,"getConfigAsync");return this.logError(n),this.config.defaultConfigurations?.[e]}}async getAllConfigsAsync(e={}){try{if(!e.forceRefresh&&this.configCache?.configurations)return{...this.config.defaultConfigurations,...this.configCache.configurations};let t=await this.fetchConfig(e);return t?{...this.config.defaultConfigurations,...t.configurations}:{...this.config.defaultConfigurations}}catch(t){let r=this.formatError(t,"getAllConfigsAsync");return this.logError(r),{...this.config.defaultConfigurations}}}updateConfigCache(e,t){let r={configurations:e.configurations,snapshotId:e.snapshotId,timestamp:e.timestamp,userId:t},n=this.configCache?.configurations||{};this.configCache=r,this.saveConfigCache(r),JSON.stringify(n)!==JSON.stringify(e.configurations)&&this.notifyConfigChangeListeners(e.configurations)}addConfigChangeListener(e){this.configChangeListeners.push(e)}removeConfigChangeListener(e){let t=this.configChangeListeners.indexOf(e);t>-1&&this.configChangeListeners.splice(t,1)}notifyConfigChangeListeners(e){this.configChangeListeners.forEach(t=>{try{t(e)}catch(r){console.error("[Grain Analytics] Config change listener error:",r)}})}startConfigRefreshTimer(){this.configRefreshTimer&&clearInterval(this.configRefreshTimer),this.configRefreshTimer=window.setInterval(()=>{this.isDestroyed||this.fetchConfig().catch(e=>{let t=this.formatError(e,"auto-config refresh");this.logError(t)})},this.config.configRefreshInterval)}stopConfigRefreshTimer(){this.configRefreshTimer&&(clearInterval(this.configRefreshTimer),this.configRefreshTimer=null)}async preloadConfig(e=[],t){try{let r=this.getEffectiveUserIdInternal();this.log(`Preloading config for user: ${r}`),await this.fetchConfig({immediateKeys:e,properties:t})&&this.startConfigRefreshTimer()}catch(r){let n=this.formatError(r,"preloadConfig");this.logError(n)}}chunkEvents(e,t){let r=[];for(let n=0;n<e.length;n+=t)r.push(e.slice(n,n+t));return r}grantConsent(e){try{this.consentManager.grantConsent(e),this.log("Consent granted",e)}catch(t){let r=this.formatError(t,"grantConsent");this.logError(r)}}revokeConsent(e){try{this.consentManager.revokeConsent(e),this.log("Consent revoked",e),this.eventQueue=[],this.waitingForConsentQueue=[]}catch(t){let r=this.formatError(t,"revokeConsent");this.logError(r)}}getConsentState(){return this.consentManager.getConsentState()}hasConsent(e){return this.consentManager.hasConsent(e)}onConsentChange(e){this.consentManager.addListener(e)}offConsentChange(e){this.consentManager.removeListener(e)}destroy(){if(this.isDestroyed=!0,this.flushTimer&&(clearInterval(this.flushTimer),this.flushTimer=null),this.stopConfigRefreshTimer(),this.configChangeListeners=[],this.heartbeatManager&&(this.heartbeatManager.destroy(),this.heartbeatManager=null),this.pageTrackingManager&&(this.pageTrackingManager.destroy(),this.pageTrackingManager=null),this.activityDetector&&(this.activityDetector.destroy(),this.activityDetector=null),this.eventQueue.length>0){let e=[...this.eventQueue];this.eventQueue=[];let t=this.chunkEvents(e,this.config.maxEventsPerRequest);if(t.length>0){this.sendEventsWithBeacon(t[0]).catch(()=>{});for(let r=1;r<t.length;r++)this.sendEventsWithBeacon(t[r]).catch(()=>{})}}}};function P(c){return new u(c)}var O=u;typeof window<"u"&&(window.Grain={GrainAnalytics:u,createGrainAnalytics:P});return D($);})();
3
3
  //# sourceMappingURL=index.global.js.map
package/dist/index.mjs CHANGED
@@ -2,11 +2,11 @@
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
+ import { ConsentManager } from './consent.js';
6
+ import { setCookie, getCookie, areCookiesEnabled } from './cookies.js';
7
+ import { ActivityDetector } from './activity.js';
8
+ import { HeartbeatManager } from './heartbeat.js';
9
+ import { PageTrackingManager } from './page-tracking.js';
10
10
  export class GrainAnalytics {
11
11
  constructor(config) {
12
12
  this.eventQueue = [];
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ /**
3
+ * Page Tracking for Grain Analytics
4
+ * Automatically tracks page views with consent-aware behavior
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.PageTrackingManager = void 0;
8
+ class PageTrackingManager {
9
+ constructor(tracker, config) {
10
+ this.isDestroyed = false;
11
+ this.currentPath = null;
12
+ this.originalPushState = null;
13
+ this.originalReplaceState = null;
14
+ /**
15
+ * Handle popstate event (back/forward navigation)
16
+ */
17
+ this.handlePopState = () => {
18
+ if (this.isDestroyed)
19
+ return;
20
+ this.trackCurrentPage();
21
+ };
22
+ /**
23
+ * Handle hash change event
24
+ */
25
+ this.handleHashChange = () => {
26
+ if (this.isDestroyed)
27
+ return;
28
+ this.trackCurrentPage();
29
+ };
30
+ this.tracker = tracker;
31
+ this.config = config;
32
+ // Track initial page load
33
+ this.trackCurrentPage();
34
+ // Setup listeners
35
+ this.setupHistoryListeners();
36
+ this.setupHashChangeListener();
37
+ }
38
+ /**
39
+ * Setup History API listeners (pushState, replaceState, popstate)
40
+ */
41
+ setupHistoryListeners() {
42
+ if (typeof window === 'undefined' || typeof history === 'undefined')
43
+ return;
44
+ // Wrap pushState
45
+ this.originalPushState = history.pushState;
46
+ history.pushState = (state, title, url) => {
47
+ this.originalPushState?.call(history, state, title, url);
48
+ this.trackCurrentPage();
49
+ };
50
+ // Wrap replaceState
51
+ this.originalReplaceState = history.replaceState;
52
+ history.replaceState = (state, title, url) => {
53
+ this.originalReplaceState?.call(history, state, title, url);
54
+ this.trackCurrentPage();
55
+ };
56
+ // Listen to popstate (back/forward buttons)
57
+ window.addEventListener('popstate', this.handlePopState);
58
+ }
59
+ /**
60
+ * Setup hash change listener
61
+ */
62
+ setupHashChangeListener() {
63
+ if (typeof window === 'undefined')
64
+ return;
65
+ window.addEventListener('hashchange', this.handleHashChange);
66
+ }
67
+ /**
68
+ * Track the current page
69
+ */
70
+ trackCurrentPage() {
71
+ if (this.isDestroyed || typeof window === 'undefined')
72
+ return;
73
+ const page = this.extractPath(window.location.href);
74
+ // Don't track if it's the same page
75
+ if (page === this.currentPath) {
76
+ return;
77
+ }
78
+ this.currentPath = page;
79
+ const hasConsent = this.tracker.hasConsent('analytics');
80
+ // Base properties (always included)
81
+ const properties = {
82
+ page,
83
+ timestamp: Date.now(),
84
+ };
85
+ // Enhanced properties when consent is granted
86
+ if (hasConsent) {
87
+ properties.referrer = document.referrer || '';
88
+ properties.title = document.title || '';
89
+ properties.full_url = window.location.href;
90
+ }
91
+ // Track the page view event
92
+ this.tracker.trackSystemEvent('page_view', properties);
93
+ if (this.config.debug) {
94
+ console.log('[Page Tracking] Tracked page view:', properties);
95
+ }
96
+ }
97
+ /**
98
+ * Extract path from URL, optionally stripping query parameters
99
+ */
100
+ extractPath(url) {
101
+ try {
102
+ const urlObj = new URL(url);
103
+ let path = urlObj.pathname + urlObj.hash;
104
+ if (!this.config.stripQueryParams && urlObj.search) {
105
+ path += urlObj.search;
106
+ }
107
+ return path;
108
+ }
109
+ catch (error) {
110
+ // If URL parsing fails, return the raw string
111
+ if (this.config.debug) {
112
+ console.warn('[Page Tracking] Failed to parse URL:', url, error);
113
+ }
114
+ return url;
115
+ }
116
+ }
117
+ /**
118
+ * Get the current page path
119
+ */
120
+ getCurrentPage() {
121
+ return this.currentPath;
122
+ }
123
+ /**
124
+ * Manually track a page view (for custom navigation)
125
+ */
126
+ trackPage(page, properties) {
127
+ if (this.isDestroyed)
128
+ return;
129
+ const hasConsent = this.tracker.hasConsent('analytics');
130
+ // Base properties
131
+ const baseProperties = {
132
+ page,
133
+ timestamp: Date.now(),
134
+ ...properties,
135
+ };
136
+ // Enhanced properties when consent is granted
137
+ if (hasConsent && typeof document !== 'undefined') {
138
+ if (!baseProperties.referrer) {
139
+ baseProperties.referrer = document.referrer || '';
140
+ }
141
+ if (!baseProperties.title) {
142
+ baseProperties.title = document.title || '';
143
+ }
144
+ if (!baseProperties.full_url && typeof window !== 'undefined') {
145
+ baseProperties.full_url = window.location.href;
146
+ }
147
+ }
148
+ this.tracker.trackSystemEvent('page_view', baseProperties);
149
+ if (this.config.debug) {
150
+ console.log('[Page Tracking] Manually tracked page:', baseProperties);
151
+ }
152
+ }
153
+ /**
154
+ * Destroy the page tracker
155
+ */
156
+ destroy() {
157
+ if (this.isDestroyed)
158
+ return;
159
+ // Restore original history methods
160
+ if (typeof history !== 'undefined') {
161
+ if (this.originalPushState) {
162
+ history.pushState = this.originalPushState;
163
+ }
164
+ if (this.originalReplaceState) {
165
+ history.replaceState = this.originalReplaceState;
166
+ }
167
+ }
168
+ // Remove event listeners
169
+ if (typeof window !== 'undefined') {
170
+ window.removeEventListener('popstate', this.handlePopState);
171
+ window.removeEventListener('hashchange', this.handleHashChange);
172
+ }
173
+ this.isDestroyed = true;
174
+ if (this.config.debug) {
175
+ console.log('[Page Tracking] Destroyed');
176
+ }
177
+ }
178
+ }
179
+ exports.PageTrackingManager = PageTrackingManager;
180
+ //# sourceMappingURL=page-tracking.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grainql/analytics-web",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "Lightweight TypeScript SDK for sending analytics events and managing remote configurations via Grain's REST API",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -31,8 +31,8 @@
31
31
  "scripts": {
32
32
  "build": "npm run build:types && npm run build:esm && npm run build:cjs && npm run build:react && npm run build:iife",
33
33
  "build:types": "tsc --declaration --emitDeclarationOnly --outDir dist",
34
- "build:esm": "tsc --module esnext --target es2020 --outDir dist/esm && mv dist/esm/index.js dist/index.mjs",
35
- "build:cjs": "tsc --module commonjs --target es2020 --outDir dist/cjs && mv dist/cjs/index.js dist/index.js",
34
+ "build:esm": "tsc --module esnext --target es2020 --outDir dist/esm && mv dist/esm/index.js dist/index.mjs && cp dist/esm/*.js dist/ && node scripts/fix-esm-imports.js",
35
+ "build:cjs": "tsc --module commonjs --target es2020 --outDir dist/cjs && mv dist/cjs/index.js dist/index.js && cp dist/cjs/*.js dist/",
36
36
  "build:react": "node scripts/build-react.js",
37
37
  "build:iife": "node scripts/build-iife.js",
38
38
  "clean": "rm -rf dist",