@grainql/analytics-web 2.4.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/index.d.ts +28 -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/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/esm/index.d.ts +28 -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/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 +28 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.global.dev.js +711 -1
- 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 +124 -0
- package/dist/index.mjs +91 -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/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,6 +3,39 @@
|
|
|
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
40
|
exports.GrainAnalytics = exports.getState = exports.getCountryCodeFromTimezone = exports.getCountry = exports.parseUTMParameters = exports.categorizeReferrer = void 0;
|
|
8
41
|
exports.createGrainAnalytics = createGrainAnalytics;
|
|
@@ -39,6 +72,9 @@ class GrainAnalytics {
|
|
|
39
72
|
this.pageTrackingManager = null;
|
|
40
73
|
this.ephemeralSessionId = null;
|
|
41
74
|
this.eventCountSinceLastHeartbeat = 0;
|
|
75
|
+
// Auto-tracking properties
|
|
76
|
+
this.interactionTrackingManager = null;
|
|
77
|
+
this.sectionTrackingManager = null;
|
|
42
78
|
// Session tracking
|
|
43
79
|
this.sessionStartTime = Date.now();
|
|
44
80
|
this.sessionEventCount = 0;
|
|
@@ -598,6 +634,85 @@ class GrainAnalytics {
|
|
|
598
634
|
this.log('Failed to initialize page view tracking:', error);
|
|
599
635
|
}
|
|
600
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
|
+
});
|
|
601
716
|
}
|
|
602
717
|
/**
|
|
603
718
|
* Track session start event
|
|
@@ -1644,6 +1759,15 @@ class GrainAnalytics {
|
|
|
1644
1759
|
this.activityDetector.destroy();
|
|
1645
1760
|
this.activityDetector = null;
|
|
1646
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
|
+
}
|
|
1647
1771
|
// Send any remaining events (in chunks if necessary)
|
|
1648
1772
|
if (this.eventQueue.length > 0) {
|
|
1649
1773
|
const eventsToSend = [...this.eventQueue];
|
package/dist/index.mjs
CHANGED
|
@@ -31,6 +31,9 @@ export class GrainAnalytics {
|
|
|
31
31
|
this.pageTrackingManager = null;
|
|
32
32
|
this.ephemeralSessionId = null;
|
|
33
33
|
this.eventCountSinceLastHeartbeat = 0;
|
|
34
|
+
// Auto-tracking properties
|
|
35
|
+
this.interactionTrackingManager = null;
|
|
36
|
+
this.sectionTrackingManager = null;
|
|
34
37
|
// Session tracking
|
|
35
38
|
this.sessionStartTime = Date.now();
|
|
36
39
|
this.sessionEventCount = 0;
|
|
@@ -590,6 +593,85 @@ export class GrainAnalytics {
|
|
|
590
593
|
this.log('Failed to initialize page view tracking:', error);
|
|
591
594
|
}
|
|
592
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
|
+
});
|
|
593
675
|
}
|
|
594
676
|
/**
|
|
595
677
|
* Track session start event
|
|
@@ -1636,6 +1718,15 @@ export class GrainAnalytics {
|
|
|
1636
1718
|
this.activityDetector.destroy();
|
|
1637
1719
|
this.activityDetector = null;
|
|
1638
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
|
+
}
|
|
1639
1730
|
// Send any remaining events (in chunks if necessary)
|
|
1640
1731
|
if (this.eventQueue.length > 0) {
|
|
1641
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
|