@grainql/analytics-web 2.3.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/countries.d.ts +13 -0
- package/dist/cjs/countries.d.ts.map +1 -0
- package/dist/cjs/countries.js +2907 -0
- package/dist/cjs/countries.js.map +1 -0
- package/dist/cjs/index.d.ts +29 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/interaction-tracking.d.ts +71 -0
- package/dist/cjs/interaction-tracking.d.ts.map +1 -0
- package/dist/cjs/interaction-tracking.js +270 -0
- package/dist/cjs/interaction-tracking.js.map +1 -0
- package/dist/cjs/page-tracking.d.ts.map +1 -1
- package/dist/cjs/page-tracking.js +5 -1
- package/dist/cjs/page-tracking.js.map +1 -1
- package/dist/cjs/section-tracking.d.ts +91 -0
- package/dist/cjs/section-tracking.d.ts.map +1 -0
- package/dist/cjs/section-tracking.js +373 -0
- package/dist/cjs/section-tracking.js.map +1 -0
- package/dist/cjs/types/auto-tracking.d.ts +55 -0
- package/dist/cjs/types/auto-tracking.d.ts.map +1 -0
- package/dist/cjs/types/auto-tracking.js +6 -0
- package/dist/cjs/types/auto-tracking.js.map +1 -0
- package/dist/countries.d.ts +13 -0
- package/dist/countries.d.ts.map +1 -0
- package/dist/countries.js +2907 -0
- package/dist/esm/countries.d.ts +13 -0
- package/dist/esm/countries.d.ts.map +1 -0
- package/dist/esm/countries.js +2902 -0
- package/dist/esm/countries.js.map +1 -0
- package/dist/esm/index.d.ts +29 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/interaction-tracking.d.ts +71 -0
- package/dist/esm/interaction-tracking.d.ts.map +1 -0
- package/dist/esm/interaction-tracking.js +266 -0
- package/dist/esm/interaction-tracking.js.map +1 -0
- package/dist/esm/page-tracking.d.ts.map +1 -1
- package/dist/esm/page-tracking.js +5 -1
- package/dist/esm/page-tracking.js.map +1 -1
- package/dist/esm/section-tracking.d.ts +91 -0
- package/dist/esm/section-tracking.d.ts.map +1 -0
- package/dist/esm/section-tracking.js +369 -0
- package/dist/esm/section-tracking.js.map +1 -0
- package/dist/esm/types/auto-tracking.d.ts +55 -0
- package/dist/esm/types/auto-tracking.d.ts.map +1 -0
- package/dist/esm/types/auto-tracking.js +5 -0
- package/dist/esm/types/auto-tracking.js.map +1 -0
- package/dist/index.d.ts +29 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.dev.js +3609 -2
- package/dist/index.global.dev.js.map +4 -4
- package/dist/index.global.js +2 -2
- package/dist/index.global.js.map +4 -4
- package/dist/index.js +130 -1
- package/dist/index.mjs +93 -0
- package/dist/interaction-tracking.d.ts +71 -0
- package/dist/interaction-tracking.d.ts.map +1 -0
- package/dist/interaction-tracking.js +270 -0
- package/dist/page-tracking.d.ts.map +1 -1
- package/dist/page-tracking.js +5 -1
- package/dist/section-tracking.d.ts +91 -0
- package/dist/section-tracking.d.ts.map +1 -0
- package/dist/section-tracking.js +373 -0
- package/dist/types/auto-tracking.d.ts +55 -0
- package/dist/types/auto-tracking.d.ts.map +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,8 +3,41 @@
|
|
|
3
3
|
* Grain Analytics Web SDK
|
|
4
4
|
* A lightweight, dependency-free TypeScript SDK for sending analytics events to Grain's REST API
|
|
5
5
|
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
6
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.GrainAnalytics = exports.parseUTMParameters = exports.categorizeReferrer = void 0;
|
|
40
|
+
exports.GrainAnalytics = exports.getState = exports.getCountryCodeFromTimezone = exports.getCountry = exports.parseUTMParameters = exports.categorizeReferrer = void 0;
|
|
8
41
|
exports.createGrainAnalytics = createGrainAnalytics;
|
|
9
42
|
const consent_1 = require("./consent");
|
|
10
43
|
const cookies_1 = require("./cookies");
|
|
@@ -14,6 +47,11 @@ const page_tracking_1 = require("./page-tracking");
|
|
|
14
47
|
const attribution_1 = require("./attribution");
|
|
15
48
|
Object.defineProperty(exports, "categorizeReferrer", { enumerable: true, get: function () { return attribution_1.categorizeReferrer; } });
|
|
16
49
|
Object.defineProperty(exports, "parseUTMParameters", { enumerable: true, get: function () { return attribution_1.parseUTMParameters; } });
|
|
50
|
+
// Re-export timezone-country utilities
|
|
51
|
+
var countries_1 = require("./countries");
|
|
52
|
+
Object.defineProperty(exports, "getCountry", { enumerable: true, get: function () { return countries_1.getCountry; } });
|
|
53
|
+
Object.defineProperty(exports, "getCountryCodeFromTimezone", { enumerable: true, get: function () { return countries_1.getCountryCodeFromTimezone; } });
|
|
54
|
+
Object.defineProperty(exports, "getState", { enumerable: true, get: function () { return countries_1.getState; } });
|
|
17
55
|
class GrainAnalytics {
|
|
18
56
|
constructor(config) {
|
|
19
57
|
this.eventQueue = [];
|
|
@@ -34,6 +72,9 @@ class GrainAnalytics {
|
|
|
34
72
|
this.pageTrackingManager = null;
|
|
35
73
|
this.ephemeralSessionId = null;
|
|
36
74
|
this.eventCountSinceLastHeartbeat = 0;
|
|
75
|
+
// Auto-tracking properties
|
|
76
|
+
this.interactionTrackingManager = null;
|
|
77
|
+
this.sectionTrackingManager = null;
|
|
37
78
|
// Session tracking
|
|
38
79
|
this.sessionStartTime = Date.now();
|
|
39
80
|
this.sessionEventCount = 0;
|
|
@@ -593,6 +634,85 @@ class GrainAnalytics {
|
|
|
593
634
|
this.log('Failed to initialize page view tracking:', error);
|
|
594
635
|
}
|
|
595
636
|
}
|
|
637
|
+
// Initialize auto-tracking when config is available
|
|
638
|
+
this.initializeAutoTracking();
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Initialize auto-tracking (interactions and sections)
|
|
642
|
+
*/
|
|
643
|
+
async initializeAutoTracking() {
|
|
644
|
+
try {
|
|
645
|
+
// Fetch remote config to get auto-tracking configuration
|
|
646
|
+
const userId = this.globalUserId || this.persistentAnonymousUserId || this.generateUUID();
|
|
647
|
+
const currentUrl = typeof window !== 'undefined' ? window.location.href : '';
|
|
648
|
+
// Fetch config with currentUrl
|
|
649
|
+
const request = {
|
|
650
|
+
userId,
|
|
651
|
+
immediateKeys: [],
|
|
652
|
+
properties: {},
|
|
653
|
+
currentUrl, // Add current URL to request
|
|
654
|
+
};
|
|
655
|
+
const headers = await this.getAuthHeaders();
|
|
656
|
+
const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
|
|
657
|
+
const response = await fetch(url, {
|
|
658
|
+
method: 'POST',
|
|
659
|
+
headers,
|
|
660
|
+
body: JSON.stringify(request),
|
|
661
|
+
});
|
|
662
|
+
if (response.ok) {
|
|
663
|
+
const configResponse = await response.json();
|
|
664
|
+
if (configResponse.autoTrackingConfig) {
|
|
665
|
+
this.setupAutoTrackingManagers(configResponse.autoTrackingConfig);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
this.log('Failed to initialize auto-tracking:', error);
|
|
671
|
+
// Fail silently - auto-tracking is optional
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Setup auto-tracking managers
|
|
676
|
+
*/
|
|
677
|
+
setupAutoTrackingManagers(config) {
|
|
678
|
+
// Lazy load the managers to avoid bundling them if not needed
|
|
679
|
+
Promise.resolve().then(() => __importStar(require('./interaction-tracking'))).then(({ InteractionTrackingManager }) => {
|
|
680
|
+
try {
|
|
681
|
+
if (config.interactions && config.interactions.length > 0) {
|
|
682
|
+
this.interactionTrackingManager = new InteractionTrackingManager(this, config.interactions, {
|
|
683
|
+
debug: this.config.debug,
|
|
684
|
+
enableMutationObserver: true,
|
|
685
|
+
mutationDebounceDelay: 500,
|
|
686
|
+
});
|
|
687
|
+
this.log('Interaction tracking initialized with', config.interactions.length, 'interactions');
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
catch (error) {
|
|
691
|
+
this.log('Failed to initialize interaction tracking:', error);
|
|
692
|
+
}
|
|
693
|
+
}).catch((error) => {
|
|
694
|
+
this.log('Failed to load interaction tracking module:', error);
|
|
695
|
+
});
|
|
696
|
+
Promise.resolve().then(() => __importStar(require('./section-tracking'))).then(({ SectionTrackingManager }) => {
|
|
697
|
+
try {
|
|
698
|
+
if (config.sections && config.sections.length > 0) {
|
|
699
|
+
this.sectionTrackingManager = new SectionTrackingManager(this, config.sections, {
|
|
700
|
+
minDwellTime: 1000,
|
|
701
|
+
scrollVelocityThreshold: 500,
|
|
702
|
+
intersectionThreshold: 0.1,
|
|
703
|
+
debounceDelay: 100,
|
|
704
|
+
batchDelay: 2000,
|
|
705
|
+
debug: this.config.debug,
|
|
706
|
+
});
|
|
707
|
+
this.log('Section tracking initialized with', config.sections.length, 'sections');
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
catch (error) {
|
|
711
|
+
this.log('Failed to initialize section tracking:', error);
|
|
712
|
+
}
|
|
713
|
+
}).catch((error) => {
|
|
714
|
+
this.log('Failed to load section tracking module:', error);
|
|
715
|
+
});
|
|
596
716
|
}
|
|
597
717
|
/**
|
|
598
718
|
* Track session start event
|
|
@@ -1639,6 +1759,15 @@ class GrainAnalytics {
|
|
|
1639
1759
|
this.activityDetector.destroy();
|
|
1640
1760
|
this.activityDetector = null;
|
|
1641
1761
|
}
|
|
1762
|
+
// Destroy auto-tracking managers
|
|
1763
|
+
if (this.interactionTrackingManager) {
|
|
1764
|
+
this.interactionTrackingManager.destroy();
|
|
1765
|
+
this.interactionTrackingManager = null;
|
|
1766
|
+
}
|
|
1767
|
+
if (this.sectionTrackingManager) {
|
|
1768
|
+
this.sectionTrackingManager.destroy();
|
|
1769
|
+
this.sectionTrackingManager = null;
|
|
1770
|
+
}
|
|
1642
1771
|
// Send any remaining events (in chunks if necessary)
|
|
1643
1772
|
if (this.eventQueue.length > 0) {
|
|
1644
1773
|
const eventsToSend = [...this.eventQueue];
|
package/dist/index.mjs
CHANGED
|
@@ -9,6 +9,8 @@ import { HeartbeatManager } from './heartbeat.js';
|
|
|
9
9
|
import { PageTrackingManager } from './page-tracking.js';
|
|
10
10
|
import { categorizeReferrer, parseUTMParameters, getOrCreateFirstTouchAttribution, getSessionUTMParameters, } from './attribution.js';
|
|
11
11
|
export { categorizeReferrer, parseUTMParameters };
|
|
12
|
+
// Re-export timezone-country utilities
|
|
13
|
+
export { getCountry, getCountryCodeFromTimezone, getState } from './countries.js';
|
|
12
14
|
export class GrainAnalytics {
|
|
13
15
|
constructor(config) {
|
|
14
16
|
this.eventQueue = [];
|
|
@@ -29,6 +31,9 @@ export class GrainAnalytics {
|
|
|
29
31
|
this.pageTrackingManager = null;
|
|
30
32
|
this.ephemeralSessionId = null;
|
|
31
33
|
this.eventCountSinceLastHeartbeat = 0;
|
|
34
|
+
// Auto-tracking properties
|
|
35
|
+
this.interactionTrackingManager = null;
|
|
36
|
+
this.sectionTrackingManager = null;
|
|
32
37
|
// Session tracking
|
|
33
38
|
this.sessionStartTime = Date.now();
|
|
34
39
|
this.sessionEventCount = 0;
|
|
@@ -588,6 +593,85 @@ export class GrainAnalytics {
|
|
|
588
593
|
this.log('Failed to initialize page view tracking:', error);
|
|
589
594
|
}
|
|
590
595
|
}
|
|
596
|
+
// Initialize auto-tracking when config is available
|
|
597
|
+
this.initializeAutoTracking();
|
|
598
|
+
}
|
|
599
|
+
/**
|
|
600
|
+
* Initialize auto-tracking (interactions and sections)
|
|
601
|
+
*/
|
|
602
|
+
async initializeAutoTracking() {
|
|
603
|
+
try {
|
|
604
|
+
// Fetch remote config to get auto-tracking configuration
|
|
605
|
+
const userId = this.globalUserId || this.persistentAnonymousUserId || this.generateUUID();
|
|
606
|
+
const currentUrl = typeof window !== 'undefined' ? window.location.href : '';
|
|
607
|
+
// Fetch config with currentUrl
|
|
608
|
+
const request = {
|
|
609
|
+
userId,
|
|
610
|
+
immediateKeys: [],
|
|
611
|
+
properties: {},
|
|
612
|
+
currentUrl, // Add current URL to request
|
|
613
|
+
};
|
|
614
|
+
const headers = await this.getAuthHeaders();
|
|
615
|
+
const url = `${this.config.apiUrl}/v1/client/${encodeURIComponent(this.config.tenantId)}/config/configurations`;
|
|
616
|
+
const response = await fetch(url, {
|
|
617
|
+
method: 'POST',
|
|
618
|
+
headers,
|
|
619
|
+
body: JSON.stringify(request),
|
|
620
|
+
});
|
|
621
|
+
if (response.ok) {
|
|
622
|
+
const configResponse = await response.json();
|
|
623
|
+
if (configResponse.autoTrackingConfig) {
|
|
624
|
+
this.setupAutoTrackingManagers(configResponse.autoTrackingConfig);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
this.log('Failed to initialize auto-tracking:', error);
|
|
630
|
+
// Fail silently - auto-tracking is optional
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Setup auto-tracking managers
|
|
635
|
+
*/
|
|
636
|
+
setupAutoTrackingManagers(config) {
|
|
637
|
+
// Lazy load the managers to avoid bundling them if not needed
|
|
638
|
+
import('./interaction-tracking').then(({ InteractionTrackingManager }) => {
|
|
639
|
+
try {
|
|
640
|
+
if (config.interactions && config.interactions.length > 0) {
|
|
641
|
+
this.interactionTrackingManager = new InteractionTrackingManager(this, config.interactions, {
|
|
642
|
+
debug: this.config.debug,
|
|
643
|
+
enableMutationObserver: true,
|
|
644
|
+
mutationDebounceDelay: 500,
|
|
645
|
+
});
|
|
646
|
+
this.log('Interaction tracking initialized with', config.interactions.length, 'interactions');
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
catch (error) {
|
|
650
|
+
this.log('Failed to initialize interaction tracking:', error);
|
|
651
|
+
}
|
|
652
|
+
}).catch((error) => {
|
|
653
|
+
this.log('Failed to load interaction tracking module:', error);
|
|
654
|
+
});
|
|
655
|
+
import('./section-tracking').then(({ SectionTrackingManager }) => {
|
|
656
|
+
try {
|
|
657
|
+
if (config.sections && config.sections.length > 0) {
|
|
658
|
+
this.sectionTrackingManager = new SectionTrackingManager(this, config.sections, {
|
|
659
|
+
minDwellTime: 1000,
|
|
660
|
+
scrollVelocityThreshold: 500,
|
|
661
|
+
intersectionThreshold: 0.1,
|
|
662
|
+
debounceDelay: 100,
|
|
663
|
+
batchDelay: 2000,
|
|
664
|
+
debug: this.config.debug,
|
|
665
|
+
});
|
|
666
|
+
this.log('Section tracking initialized with', config.sections.length, 'sections');
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
catch (error) {
|
|
670
|
+
this.log('Failed to initialize section tracking:', error);
|
|
671
|
+
}
|
|
672
|
+
}).catch((error) => {
|
|
673
|
+
this.log('Failed to load section tracking module:', error);
|
|
674
|
+
});
|
|
591
675
|
}
|
|
592
676
|
/**
|
|
593
677
|
* Track session start event
|
|
@@ -1634,6 +1718,15 @@ export class GrainAnalytics {
|
|
|
1634
1718
|
this.activityDetector.destroy();
|
|
1635
1719
|
this.activityDetector = null;
|
|
1636
1720
|
}
|
|
1721
|
+
// Destroy auto-tracking managers
|
|
1722
|
+
if (this.interactionTrackingManager) {
|
|
1723
|
+
this.interactionTrackingManager.destroy();
|
|
1724
|
+
this.interactionTrackingManager = null;
|
|
1725
|
+
}
|
|
1726
|
+
if (this.sectionTrackingManager) {
|
|
1727
|
+
this.sectionTrackingManager.destroy();
|
|
1728
|
+
this.sectionTrackingManager = null;
|
|
1729
|
+
}
|
|
1637
1730
|
// Send any remaining events (in chunks if necessary)
|
|
1638
1731
|
if (this.eventQueue.length > 0) {
|
|
1639
1732
|
const eventsToSend = [...this.eventQueue];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interaction Tracking Manager for Grain Analytics
|
|
3
|
+
* Automatically attaches click and focus listeners to detected interactive elements
|
|
4
|
+
*/
|
|
5
|
+
import type { InteractionConfig } from './types/auto-tracking';
|
|
6
|
+
export interface InteractionTracker {
|
|
7
|
+
track(eventName: string, properties?: Record<string, unknown>): void | Promise<void>;
|
|
8
|
+
hasConsent(category: 'analytics' | 'marketing' | 'functional'): boolean;
|
|
9
|
+
log(...args: unknown[]): void;
|
|
10
|
+
}
|
|
11
|
+
export interface InteractionTrackingConfig {
|
|
12
|
+
debug?: boolean;
|
|
13
|
+
enableMutationObserver?: boolean;
|
|
14
|
+
mutationDebounceDelay?: number;
|
|
15
|
+
}
|
|
16
|
+
export declare class InteractionTrackingManager {
|
|
17
|
+
private tracker;
|
|
18
|
+
private interactions;
|
|
19
|
+
private config;
|
|
20
|
+
private isDestroyed;
|
|
21
|
+
private attachedListeners;
|
|
22
|
+
private xpathCache;
|
|
23
|
+
private mutationObserver;
|
|
24
|
+
private mutationDebounceTimer;
|
|
25
|
+
constructor(tracker: InteractionTracker, interactions: InteractionConfig[], config?: InteractionTrackingConfig);
|
|
26
|
+
/**
|
|
27
|
+
* Attach listeners to all configured interactions
|
|
28
|
+
*/
|
|
29
|
+
private attachAllListeners;
|
|
30
|
+
/**
|
|
31
|
+
* Attach listener to a specific interaction
|
|
32
|
+
*/
|
|
33
|
+
private attachInteractionListener;
|
|
34
|
+
/**
|
|
35
|
+
* Handle click event on interaction
|
|
36
|
+
*/
|
|
37
|
+
private handleInteractionClick;
|
|
38
|
+
/**
|
|
39
|
+
* Handle focus event on interaction (for form fields)
|
|
40
|
+
*/
|
|
41
|
+
private handleInteractionFocus;
|
|
42
|
+
/**
|
|
43
|
+
* Find element by XPath selector
|
|
44
|
+
*/
|
|
45
|
+
private findElementByXPath;
|
|
46
|
+
/**
|
|
47
|
+
* Setup mutation observer to handle dynamic content
|
|
48
|
+
*/
|
|
49
|
+
private setupMutationObserver;
|
|
50
|
+
/**
|
|
51
|
+
* Handle DOM mutations
|
|
52
|
+
*/
|
|
53
|
+
private handleMutations;
|
|
54
|
+
/**
|
|
55
|
+
* Detach listeners from an element
|
|
56
|
+
*/
|
|
57
|
+
private detachListeners;
|
|
58
|
+
/**
|
|
59
|
+
* Log debug messages
|
|
60
|
+
*/
|
|
61
|
+
private log;
|
|
62
|
+
/**
|
|
63
|
+
* Update interactions configuration
|
|
64
|
+
*/
|
|
65
|
+
updateInteractions(interactions: InteractionConfig[]): void;
|
|
66
|
+
/**
|
|
67
|
+
* Cleanup and destroy
|
|
68
|
+
*/
|
|
69
|
+
destroy(): void;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=interaction-tracking.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interaction-tracking.d.ts","sourceRoot":"","sources":["../src/interaction-tracking.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE/D,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrF,UAAU,CAAC,QAAQ,EAAE,WAAW,GAAG,WAAW,GAAG,YAAY,GAAG,OAAO,CAAC;IACxE,GAAG,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,yBAAyB;IACxC,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,sBAAsB,CAAC,EAAE,OAAO,CAAC;IACjC,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED,qBAAa,0BAA0B;IACrC,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,YAAY,CAAsB;IAC1C,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,iBAAiB,CAA6E;IACtG,OAAO,CAAC,UAAU,CAA0C;IAC5D,OAAO,CAAC,gBAAgB,CAAiC;IACzD,OAAO,CAAC,qBAAqB,CAAuB;gBAGlD,OAAO,EAAE,kBAAkB,EAC3B,YAAY,EAAE,iBAAiB,EAAE,EACjC,MAAM,GAAE,yBAA8B;IA0BxC;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAU1B;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAkCjC;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAqB9B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAoB9B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAmC1B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IA0B7B;;OAEG;IACH,OAAO,CAAC,eAAe;IA+BvB;;OAEG;IACH,OAAO,CAAC,eAAe;IAYvB;;OAEG;IACH,OAAO,CAAC,GAAG;IAMX;;OAEG;IACH,kBAAkB,CAAC,YAAY,EAAE,iBAAiB,EAAE,GAAG,IAAI;IAoB3D;;OAEG;IACH,OAAO,IAAI,IAAI;CA4BhB"}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Interaction Tracking Manager for Grain Analytics
|
|
4
|
+
* Automatically attaches click and focus listeners to detected interactive elements
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.InteractionTrackingManager = void 0;
|
|
8
|
+
class InteractionTrackingManager {
|
|
9
|
+
constructor(tracker, interactions, config = {}) {
|
|
10
|
+
this.isDestroyed = false;
|
|
11
|
+
this.attachedListeners = new Map();
|
|
12
|
+
this.xpathCache = new Map();
|
|
13
|
+
this.mutationObserver = null;
|
|
14
|
+
this.mutationDebounceTimer = null;
|
|
15
|
+
this.tracker = tracker;
|
|
16
|
+
this.interactions = interactions;
|
|
17
|
+
this.config = {
|
|
18
|
+
debug: config.debug ?? false,
|
|
19
|
+
enableMutationObserver: config.enableMutationObserver ?? true,
|
|
20
|
+
mutationDebounceDelay: config.mutationDebounceDelay ?? 500,
|
|
21
|
+
};
|
|
22
|
+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
|
23
|
+
// Attach listeners after DOM is ready
|
|
24
|
+
if (document.readyState === 'loading') {
|
|
25
|
+
document.addEventListener('DOMContentLoaded', () => this.attachAllListeners());
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
// DOM already loaded
|
|
29
|
+
setTimeout(() => this.attachAllListeners(), 0);
|
|
30
|
+
}
|
|
31
|
+
// Setup mutation observer for dynamic content
|
|
32
|
+
if (this.config.enableMutationObserver) {
|
|
33
|
+
this.setupMutationObserver();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Attach listeners to all configured interactions
|
|
39
|
+
*/
|
|
40
|
+
attachAllListeners() {
|
|
41
|
+
if (this.isDestroyed)
|
|
42
|
+
return;
|
|
43
|
+
this.log('Attaching interaction listeners for', this.interactions.length, 'interactions');
|
|
44
|
+
for (const interaction of this.interactions) {
|
|
45
|
+
this.attachInteractionListener(interaction);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Attach listener to a specific interaction
|
|
50
|
+
*/
|
|
51
|
+
attachInteractionListener(interaction) {
|
|
52
|
+
if (this.isDestroyed)
|
|
53
|
+
return;
|
|
54
|
+
const element = this.findElementByXPath(interaction.selector);
|
|
55
|
+
if (!element) {
|
|
56
|
+
this.log('Element not found for interaction:', interaction.eventName, 'selector:', interaction.selector);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Check if we already attached listeners to this element for this interaction
|
|
60
|
+
if (this.attachedListeners.has(element)) {
|
|
61
|
+
this.log('Listeners already attached for element:', element);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const handlers = [];
|
|
65
|
+
// Click handler
|
|
66
|
+
const clickHandler = (event) => this.handleInteractionClick(interaction, event);
|
|
67
|
+
element.addEventListener('click', clickHandler, { passive: true });
|
|
68
|
+
handlers.push({ event: 'click', handler: clickHandler });
|
|
69
|
+
// Focus handler (for form inputs)
|
|
70
|
+
if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement || element instanceof HTMLSelectElement) {
|
|
71
|
+
const focusHandler = (event) => this.handleInteractionFocus(interaction, event);
|
|
72
|
+
element.addEventListener('focus', focusHandler, { passive: true });
|
|
73
|
+
handlers.push({ event: 'focus', handler: focusHandler });
|
|
74
|
+
}
|
|
75
|
+
this.attachedListeners.set(element, handlers);
|
|
76
|
+
this.log('Attached listeners to element for:', interaction.eventName);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Handle click event on interaction
|
|
80
|
+
*/
|
|
81
|
+
handleInteractionClick(interaction, event) {
|
|
82
|
+
if (this.isDestroyed)
|
|
83
|
+
return;
|
|
84
|
+
if (!this.tracker.hasConsent('analytics'))
|
|
85
|
+
return;
|
|
86
|
+
const element = event.target;
|
|
87
|
+
this.tracker.track(interaction.eventName, {
|
|
88
|
+
interaction_type: 'click',
|
|
89
|
+
interaction_label: interaction.label,
|
|
90
|
+
interaction_description: interaction.description,
|
|
91
|
+
interaction_priority: interaction.priority,
|
|
92
|
+
element_tag: element.tagName?.toLowerCase(),
|
|
93
|
+
element_text: element.textContent?.trim().substring(0, 100),
|
|
94
|
+
element_id: element.id || undefined,
|
|
95
|
+
element_class: element.className || undefined,
|
|
96
|
+
timestamp: Date.now(),
|
|
97
|
+
});
|
|
98
|
+
this.log('Tracked click interaction:', interaction.eventName);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Handle focus event on interaction (for form fields)
|
|
102
|
+
*/
|
|
103
|
+
handleInteractionFocus(interaction, event) {
|
|
104
|
+
if (this.isDestroyed)
|
|
105
|
+
return;
|
|
106
|
+
if (!this.tracker.hasConsent('analytics'))
|
|
107
|
+
return;
|
|
108
|
+
const element = event.target;
|
|
109
|
+
this.tracker.track(interaction.eventName, {
|
|
110
|
+
interaction_type: 'focus',
|
|
111
|
+
interaction_label: interaction.label,
|
|
112
|
+
interaction_description: interaction.description,
|
|
113
|
+
interaction_priority: interaction.priority,
|
|
114
|
+
element_tag: element.tagName?.toLowerCase(),
|
|
115
|
+
element_id: element.id || undefined,
|
|
116
|
+
element_class: element.className || undefined,
|
|
117
|
+
timestamp: Date.now(),
|
|
118
|
+
});
|
|
119
|
+
this.log('Tracked focus interaction:', interaction.eventName);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Find element by XPath selector
|
|
123
|
+
*/
|
|
124
|
+
findElementByXPath(xpath) {
|
|
125
|
+
// Check cache first
|
|
126
|
+
if (this.xpathCache.has(xpath)) {
|
|
127
|
+
const cached = this.xpathCache.get(xpath);
|
|
128
|
+
// Verify element is still in DOM
|
|
129
|
+
if (cached && document.contains(cached)) {
|
|
130
|
+
return cached;
|
|
131
|
+
}
|
|
132
|
+
// Clear invalid cache entry
|
|
133
|
+
this.xpathCache.delete(xpath);
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
137
|
+
const element = result.singleNodeValue;
|
|
138
|
+
// Cache the result
|
|
139
|
+
if (element) {
|
|
140
|
+
this.xpathCache.set(xpath, element);
|
|
141
|
+
}
|
|
142
|
+
return element;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
this.log('Error evaluating XPath:', xpath, error);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Setup mutation observer to handle dynamic content
|
|
151
|
+
*/
|
|
152
|
+
setupMutationObserver() {
|
|
153
|
+
if (typeof MutationObserver === 'undefined') {
|
|
154
|
+
this.log('MutationObserver not supported');
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
this.mutationObserver = new MutationObserver((mutations) => {
|
|
158
|
+
// Debounce the re-attachment
|
|
159
|
+
if (this.mutationDebounceTimer !== null) {
|
|
160
|
+
clearTimeout(this.mutationDebounceTimer);
|
|
161
|
+
}
|
|
162
|
+
this.mutationDebounceTimer = window.setTimeout(() => {
|
|
163
|
+
this.handleMutations(mutations);
|
|
164
|
+
this.mutationDebounceTimer = null;
|
|
165
|
+
}, this.config.mutationDebounceDelay);
|
|
166
|
+
});
|
|
167
|
+
this.mutationObserver.observe(document.body, {
|
|
168
|
+
childList: true,
|
|
169
|
+
subtree: true,
|
|
170
|
+
});
|
|
171
|
+
this.log('Mutation observer setup');
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Handle DOM mutations
|
|
175
|
+
*/
|
|
176
|
+
handleMutations(mutations) {
|
|
177
|
+
if (this.isDestroyed)
|
|
178
|
+
return;
|
|
179
|
+
// Clear XPath cache on mutations
|
|
180
|
+
this.xpathCache.clear();
|
|
181
|
+
// Check if any of our tracked elements were removed
|
|
182
|
+
const removedElements = new Set();
|
|
183
|
+
for (const mutation of mutations) {
|
|
184
|
+
mutation.removedNodes.forEach((node) => {
|
|
185
|
+
if (node instanceof Element) {
|
|
186
|
+
removedElements.add(node);
|
|
187
|
+
// Also check for child elements we were tracking
|
|
188
|
+
this.attachedListeners.forEach((handlers, element) => {
|
|
189
|
+
if (node.contains(element)) {
|
|
190
|
+
removedElements.add(element);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
// Clean up removed elements
|
|
197
|
+
removedElements.forEach((element) => {
|
|
198
|
+
this.detachListeners(element);
|
|
199
|
+
});
|
|
200
|
+
// Try to re-attach listeners for any interactions that might now be available
|
|
201
|
+
this.attachAllListeners();
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Detach listeners from an element
|
|
205
|
+
*/
|
|
206
|
+
detachListeners(element) {
|
|
207
|
+
const handlers = this.attachedListeners.get(element);
|
|
208
|
+
if (!handlers)
|
|
209
|
+
return;
|
|
210
|
+
handlers.forEach(({ event, handler }) => {
|
|
211
|
+
element.removeEventListener(event, handler);
|
|
212
|
+
});
|
|
213
|
+
this.attachedListeners.delete(element);
|
|
214
|
+
this.log('Detached listeners from element');
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Log debug messages
|
|
218
|
+
*/
|
|
219
|
+
log(...args) {
|
|
220
|
+
if (this.config.debug) {
|
|
221
|
+
console.log('[InteractionTracking]', ...args);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Update interactions configuration
|
|
226
|
+
*/
|
|
227
|
+
updateInteractions(interactions) {
|
|
228
|
+
if (this.isDestroyed)
|
|
229
|
+
return;
|
|
230
|
+
this.log('Updating interactions configuration');
|
|
231
|
+
// Detach all existing listeners
|
|
232
|
+
this.attachedListeners.forEach((handlers, element) => {
|
|
233
|
+
this.detachListeners(element);
|
|
234
|
+
});
|
|
235
|
+
// Clear cache
|
|
236
|
+
this.xpathCache.clear();
|
|
237
|
+
// Update configuration
|
|
238
|
+
this.interactions = interactions;
|
|
239
|
+
// Reattach listeners
|
|
240
|
+
this.attachAllListeners();
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Cleanup and destroy
|
|
244
|
+
*/
|
|
245
|
+
destroy() {
|
|
246
|
+
if (this.isDestroyed)
|
|
247
|
+
return;
|
|
248
|
+
this.log('Destroying interaction tracking manager');
|
|
249
|
+
this.isDestroyed = true;
|
|
250
|
+
// Clear debounce timer
|
|
251
|
+
if (this.mutationDebounceTimer !== null) {
|
|
252
|
+
clearTimeout(this.mutationDebounceTimer);
|
|
253
|
+
this.mutationDebounceTimer = null;
|
|
254
|
+
}
|
|
255
|
+
// Disconnect mutation observer
|
|
256
|
+
if (this.mutationObserver) {
|
|
257
|
+
this.mutationObserver.disconnect();
|
|
258
|
+
this.mutationObserver = null;
|
|
259
|
+
}
|
|
260
|
+
// Detach all listeners
|
|
261
|
+
this.attachedListeners.forEach((handlers, element) => {
|
|
262
|
+
this.detachListeners(element);
|
|
263
|
+
});
|
|
264
|
+
// Clear caches
|
|
265
|
+
this.attachedListeners.clear();
|
|
266
|
+
this.xpathCache.clear();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
exports.InteractionTrackingManager = InteractionTrackingManager;
|
|
270
|
+
//# sourceMappingURL=interaction-tracking.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"page-tracking.d.ts","sourceRoot":"","sources":["../src/page-tracking.ts"],"names":[],"mappings":"AAAA;;;GAGG;
|
|
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;IAgHxB;;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,WAAW;IAmBnB;;OAEG;IACH,cAAc,IAAI,MAAM,GAAG,IAAI;IAI/B;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAyCnE;;OAEG;IACH,gBAAgB,IAAI,MAAM;IAI1B;;OAEG;IACH,OAAO,IAAI,IAAI;CAyBhB"}
|
package/dist/page-tracking.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
7
|
exports.PageTrackingManager = void 0;
|
|
8
8
|
const attribution_1 = require("./attribution");
|
|
9
|
+
const countries_1 = require("./countries");
|
|
9
10
|
class PageTrackingManager {
|
|
10
11
|
constructor(tracker, config) {
|
|
11
12
|
this.isDestroyed = false;
|
|
@@ -149,7 +150,10 @@ class PageTrackingManager {
|
|
|
149
150
|
properties.browser = this.getBrowser();
|
|
150
151
|
properties.os = this.getOS();
|
|
151
152
|
properties.language = navigator.language || '';
|
|
152
|
-
|
|
153
|
+
// Timezone and country (privacy-friendly: derived from timezone, no IP tracking)
|
|
154
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
155
|
+
properties.timezone = timezone;
|
|
156
|
+
properties.country = (0, countries_1.getCountryCodeFromTimezone)();
|
|
153
157
|
properties.screen_resolution = `${screen.width}x${screen.height}`;
|
|
154
158
|
properties.viewport = `${window.innerWidth}x${window.innerHeight}`;
|
|
155
159
|
}
|