thrivestack-analytics
Version:
ThriveStack Analytics - Privacy-first web analytics for Node.js and browsers
912 lines (911 loc) • 38.6 kB
JavaScript
"use strict";
// Browser-compatible ThriveStack implementation
// No Node.js dependencies - uses native browser APIs
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ThriveStack = void 0;
class ThriveStack {
constructor(options) {
// IP and location information storage
this.ipAddress = null;
this.locationInfo = null;
// Event batching
this.eventQueue = [];
this.queueTimer = null;
// Analytics state
this.interactionHistory = [];
this.maxHistoryLength = 20;
// User and group management
this.userId = '';
this.groupId = '';
// Device ID management
this.deviceId = null;
this.deviceIdReady = false;
this.sessionUpdateTimer = null;
// Debug mode
this.debugMode = false;
// Handle string API key for backward compatibility
if (typeof options === 'string') {
options = {
apiKey: options
};
}
// Core settings
this.apiKey = options.apiKey;
this.apiEndpoint = options.apiEndpoint || "https://api.app.thrivestack.ai/api";
this.respectDoNotTrack = options.respectDoNotTrack !== false;
this.trackClicks = options.trackClicks === true;
this.trackForms = options.trackForms === true;
this.enableConsent = options.enableConsent === true;
this.source = options.source || "";
// Geo IP service URL
this.geoIpServiceUrl = options.geoIpServiceUrl || "https://ipinfo.io/json";
// Event batching
this.batchSize = options.batchSize || 10;
this.batchInterval = options.batchInterval || 2000;
// Session configuration
this.sessionTimeout = options.sessionTimeout || (30 * 60 * 1000);
this.debounceDelay = options.debounceDelay || 2000;
// Consent settings
this.consentCategories = {
functional: true,
analytics: options.defaultConsent === true,
marketing: options.defaultConsent === true
};
// Initialize device ID
this.initializeDeviceId();
// Fetch IP and location data
this.fetchIpAndLocationInfo();
// Setup session tracking
this.setupSessionTracking();
// Initialize automatically if tracking is allowed
if (this.shouldTrack()) {
this.autoCapturePageVisit();
if (this.trackClicks) {
this.autoCaptureClickEvents();
}
if (this.trackForms) {
this.autoCaptureFormEvents();
}
}
}
initializeDeviceId() {
return __awaiter(this, void 0, void 0, function* () {
try {
// Check if we already have a device ID in cookie
const existingDeviceId = this.getDeviceIdFromCookie();
if (existingDeviceId) {
this.deviceId = existingDeviceId;
this.deviceIdReady = true;
console.debug("Using existing device ID from cookie:", this.deviceId);
this.processQueueIfReady();
return;
}
// Generate new device ID
this.deviceId = this.generateRandomDeviceId();
this.deviceIdReady = true;
this.setDeviceIdCookie(this.deviceId);
console.debug("Generated device ID:", this.deviceId);
this.processQueueIfReady();
}
catch (error) {
console.warn("Failed to initialize device ID:", error);
this.deviceId = this.generateRandomDeviceId();
this.deviceIdReady = true;
this.setDeviceIdCookie(this.deviceId);
this.processQueueIfReady();
}
});
}
generateRandomDeviceId() {
return 'device_' + Math.random().toString(36).substring(2, 15) +
Math.random().toString(36).substring(2, 15);
}
setDeviceIdCookie(deviceId) {
if (!deviceId)
return;
const cookieName = 'thrivestack_device_id';
const expiryDate = new Date();
expiryDate.setTime(expiryDate.getTime() + (730 * 24 * 60 * 60 * 1000));
const cookieValue = `${cookieName}=${deviceId};expires=${expiryDate.toUTCString()};path=/;SameSite=Lax`;
try {
document.cookie = cookieValue;
}
catch (e) {
console.warn('Could not store device ID in cookie:', e);
}
}
getDeviceIdFromCookie() {
const cookieName = 'thrivestack_device_id';
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.indexOf(cookieName + '=') === 0) {
return cookie.substring(cookieName.length + 1);
}
}
return null;
}
fetchIpAndLocationInfo() {
return __awaiter(this, void 0, void 0, function* () {
try {
const cachedData = this.getLocationInfoFromCookie();
if (cachedData) {
this.ipAddress = cachedData.ip || null;
this.locationInfo = {
city: cachedData.city || null,
region: cachedData.region || null,
country: cachedData.country || null,
postal: cachedData.postal || null,
loc: cachedData.loc || null,
timezone: cachedData.timezone || null
};
console.debug("Using cached IP and location info from cookie");
return;
}
console.debug("Fetching IP and location info from API...");
const response = yield fetch(this.geoIpServiceUrl);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const data = yield response.json();
this.ipAddress = data.ip || null;
this.locationInfo = {
city: data.city || null,
region: data.region || null,
country: data.country || null,
postal: data.postal || null,
loc: data.loc || null,
timezone: data.timezone || null
};
this.setLocationInfoCookie(data);
console.debug("IP and location info fetched successfully");
}
catch (error) {
console.warn("Failed to fetch IP and location info:", error);
this.ipAddress = null;
this.locationInfo = null;
}
});
}
setLocationInfoCookie(locationData) {
if (!locationData)
return;
const cookieName = 'thrivestack_location_info';
try {
const encodedData = btoa(JSON.stringify(locationData));
const expiryDate = new Date();
expiryDate.setTime(expiryDate.getTime() + (24 * 60 * 60 * 1000));
const cookieValue = `${cookieName}=${encodedData};expires=${expiryDate.toUTCString()};path=/;SameSite=Lax`;
document.cookie = cookieValue;
console.debug("Location info cached in cookie");
}
catch (e) {
console.warn('Could not store location info in cookie:', e);
}
}
getLocationInfoFromCookie() {
const cookieName = 'thrivestack_location_info';
try {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.indexOf(cookieName + '=') === 0) {
const encodedValue = cookie.substring(cookieName.length + 1);
const decodedValue = atob(encodedValue);
return JSON.parse(decodedValue);
}
}
}
catch (e) {
console.warn('Could not read location info from cookie:', e);
}
return null;
}
init() {
return __awaiter(this, arguments, void 0, function* (userId = "", source = "") {
try {
if (userId) {
this.setUserId(userId);
}
if (source) {
this.source = source;
}
console.debug("ThriveStack initialized successfully");
}
catch (error) {
console.error("Failed to initialize ThriveStack:", error);
throw error;
}
});
}
shouldTrack() {
if (this.respectDoNotTrack && (navigator.doNotTrack === "1" ||
navigator.doNotTrack === "yes" ||
window.doNotTrack === "1")) {
console.warn("User has enabled Do Not Track. Tracking is disabled.");
return false;
}
return true;
}
isTrackingAllowed(category) {
if (!this.shouldTrack())
return false;
if (this.enableConsent) {
return this.consentCategories[category] === true;
}
return true;
}
setConsent(category, hasConsent) {
if (this.consentCategories.hasOwnProperty(category)) {
this.consentCategories[category] = hasConsent;
}
}
setUserId(userId) {
this.userId = userId;
this.setUserIdCookie(userId);
}
setGroupId(groupId) {
this.groupId = groupId;
this.setGroupIdCookie(groupId);
}
setSource(source) {
this.source = source;
}
setUserIdCookie(userId) {
if (!userId)
return;
const cookieName = 'thrivestack_user_id';
const expiryDate = new Date();
expiryDate.setTime(expiryDate.getTime() + (365 * 24 * 60 * 60 * 1000));
const cookieValue = `${cookieName}=${encodeURIComponent(userId)};expires=${expiryDate.toUTCString()};path=/;SameSite=Lax`;
try {
document.cookie = cookieValue;
}
catch (e) {
console.warn('Could not store user ID in cookie:', e);
}
}
getUserIdFromCookie() {
const cookieName = 'thrivestack_user_id';
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.indexOf(cookieName + '=') === 0) {
const value = cookie.substring(cookieName.length + 1);
return decodeURIComponent(value);
}
}
return null;
}
setGroupIdCookie(groupId) {
if (!groupId)
return;
const cookieName = 'thrivestack_group_id';
const expiryDate = new Date();
expiryDate.setTime(expiryDate.getTime() + (365 * 24 * 60 * 60 * 1000));
const cookieValue = `${cookieName}=${encodeURIComponent(groupId)};expires=${expiryDate.toUTCString()};path=/;SameSite=Lax`;
try {
document.cookie = cookieValue;
}
catch (e) {
console.warn('Could not store group ID in cookie:', e);
}
}
getGroupIdFromCookie() {
const cookieName = 'thrivestack_group_id';
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.indexOf(cookieName + '=') === 0) {
const value = cookie.substring(cookieName.length + 1);
return decodeURIComponent(value);
}
}
return null;
}
queueEvent(events) {
if (!Array.isArray(events)) {
events = [events];
}
this.eventQueue.push(...events);
if (this.deviceIdReady) {
this.processQueueIfReady();
}
else {
console.debug("Device ID not ready, keeping events in queue");
}
}
processQueueIfReady() {
if (!this.deviceIdReady || this.eventQueue.length === 0) {
return;
}
if (this.eventQueue.length >= this.batchSize) {
this.processQueue();
}
else if (!this.queueTimer) {
this.queueTimer = window.setTimeout(() => this.processQueue(), this.batchInterval);
}
}
processQueue() {
return __awaiter(this, void 0, void 0, function* () {
if (this.eventQueue.length === 0 || !this.deviceIdReady)
return;
const events = [...this.eventQueue];
const updatedEvents = events.map(event => (Object.assign(Object.assign({}, event), { context: Object.assign(Object.assign({}, event.context), { device_id: this.deviceId }) })));
this.eventQueue = [];
if (this.queueTimer) {
clearTimeout(this.queueTimer);
this.queueTimer = null;
}
try {
yield this.track(updatedEvents);
}
catch (error) {
console.error("Failed to send batch events:", error);
this.eventQueue.unshift(...events);
}
});
}
track(events) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.apiKey) {
throw new Error("Initialize the ThriveStack instance before sending telemetry data.");
}
const cleanedEvents = events.map(event => this.cleanPIIFromEventData(event));
let retries = 3;
while (retries > 0) {
try {
const response = yield fetch(`${this.apiEndpoint}/track`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": `${this.apiKey}`
},
body: JSON.stringify(cleanedEvents)
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}: ${yield response.text()}`);
}
const data = yield response.json();
return data;
}
catch (error) {
retries--;
if (retries === 0) {
console.error("Failed to send telemetry after multiple attempts:", error);
throw error;
}
yield new Promise(resolve => setTimeout(resolve, 1000 * (3 - retries)));
}
}
});
}
identify(data) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.apiKey) {
throw new Error("Initialize the ThriveStack instance before sending telemetry data.");
}
try {
let userId = "";
if (Array.isArray(data) && data.length > 0) {
const lastElement = data[data.length - 1];
userId = lastElement.user_id || "";
}
else {
userId = data.user_id || "";
}
if (userId) {
this.setUserId(userId);
}
const response = yield fetch(`${this.apiEndpoint}/identify`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": `${this.apiKey}`
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}: ${yield response.text()}`);
}
const result = yield response.json();
return result;
}
catch (error) {
console.error("Failed to send identification data:", error);
throw error;
}
});
}
group(data) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.apiKey) {
throw new Error("Initialize the ThriveStack instance before sending telemetry data.");
}
try {
let groupId = "";
if (Array.isArray(data) && data.length > 0) {
const lastElement = data[data.length - 1];
groupId = lastElement.group_id || "";
}
else {
groupId = data.group_id || "";
}
if (groupId) {
this.setGroupId(groupId);
}
const response = yield fetch(`${this.apiEndpoint}/group`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": `${this.apiKey}`
},
body: JSON.stringify(data)
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}: ${yield response.text()}`);
}
const result = yield response.json();
return result;
}
catch (error) {
console.error("Failed to send group data:", error);
throw error;
}
});
}
getDeviceId() {
if (this.deviceIdReady && this.deviceId) {
return this.deviceId;
}
return null;
}
getSessionId() {
const sessionCookieName = 'thrivestack_session';
try {
const cookies = document.cookie.split(';');
let sessionCookieValue = null;
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.indexOf(sessionCookieName + '=') === 0) {
sessionCookieValue = cookie.substring(sessionCookieName.length + 1);
break;
}
}
if (sessionCookieValue) {
try {
const sessionData = JSON.parse(atob(sessionCookieValue));
if (sessionData.sessionId && sessionData.lastActivity) {
const lastActivity = new Date(sessionData.lastActivity);
const now = new Date();
const timeSinceLastActivity = now.getTime() - lastActivity.getTime();
if (timeSinceLastActivity < this.sessionTimeout) {
return sessionData.sessionId;
}
else {
console.debug("Session expired, creating new session");
return this.createNewSession();
}
}
else {
return this.createNewSession();
}
}
catch (parseError) {
console.debug("Migrating old session format to new format");
return this.migrateOldSession(sessionCookieValue);
}
}
else {
return this.createNewSession();
}
}
catch (error) {
console.warn('Error getting session ID:', error);
return this.createNewSession();
}
}
createNewSession() {
const sessionId = 'session_' + Math.random().toString(36).substring(2, 15);
const now = new Date().toISOString();
const sessionData = {
sessionId: sessionId,
startTime: now,
lastActivity: now
};
this.setSessionCookie(sessionData);
return sessionId;
}
migrateOldSession(oldSessionId) {
const now = new Date().toISOString();
const sessionData = {
sessionId: oldSessionId,
startTime: now,
lastActivity: now
};
this.setSessionCookie(sessionData);
return oldSessionId;
}
setSessionCookie(sessionData) {
const sessionCookieName = 'thrivestack_session';
try {
const encodedData = btoa(JSON.stringify(sessionData));
const cookieValue = `${sessionCookieName}=${encodedData};path=/;SameSite=Lax`;
document.cookie = cookieValue;
}
catch (error) {
console.warn('Could not store session in cookie:', error);
}
}
updateSessionActivity() {
if (this.sessionUpdateTimer) {
clearTimeout(this.sessionUpdateTimer);
}
this.sessionUpdateTimer = window.setTimeout(() => {
this.updateSessionActivityImmediate();
}, this.debounceDelay);
}
updateSessionActivityImmediate() {
const sessionCookieName = 'thrivestack_session';
try {
const cookies = document.cookie.split(';');
let sessionCookieValue = null;
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.indexOf(sessionCookieName + '=') === 0) {
sessionCookieValue = cookie.substring(sessionCookieName.length + 1);
break;
}
}
if (sessionCookieValue) {
try {
const sessionData = JSON.parse(atob(sessionCookieValue));
sessionData.lastActivity = new Date().toISOString();
this.setSessionCookie(sessionData);
}
catch (parseError) {
this.createNewSession();
}
}
else {
this.createNewSession();
}
}
catch (error) {
console.warn('Could not update session activity:', error);
}
}
setupSessionTracking() {
// Session tracking is handled by capture functions
}
capturePageVisit() {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e, _f;
if (!this.isTrackingAllowed('functional')) {
return;
}
const deviceId = this.getDeviceId();
if (!deviceId) {
console.debug("Device ID not ready, page visit will be queued");
}
const utmParams = this.isTrackingAllowed('marketing') ?
this.getUtmParameters() : {};
const sessionId = this.getSessionId();
this.updateSessionActivity();
const currentUserId = this.userId || this.getUserIdFromCookie() || "";
const currentGroupId = this.groupId || this.getGroupIdFromCookie() || "";
const events = [{
event_name: "page_visit",
properties: Object.assign({ page_title: document.title, page_url: window.location.href, page_path: window.location.pathname, page_referrer: document.referrer || null, language: navigator.language || null, ip_address: this.ipAddress, city: ((_a = this.locationInfo) === null || _a === void 0 ? void 0 : _a.city) || null, region: ((_b = this.locationInfo) === null || _b === void 0 ? void 0 : _b.region) || null, country: ((_c = this.locationInfo) === null || _c === void 0 ? void 0 : _c.country) || null, postal: ((_d = this.locationInfo) === null || _d === void 0 ? void 0 : _d.postal) || null, loc: ((_e = this.locationInfo) === null || _e === void 0 ? void 0 : _e.loc) || null, timezone: ((_f = this.locationInfo) === null || _f === void 0 ? void 0 : _f.timezone) || null }, utmParams),
user_id: currentUserId,
context: {
group_id: currentGroupId,
device_id: deviceId,
session_id: sessionId,
source: this.source
},
timestamp: new Date().toISOString(),
}];
this.queueEvent(events);
this.addToInteractionHistory('page_visit', events[0].properties);
});
}
captureEvent(eventName_1) {
return __awaiter(this, arguments, void 0, function* (eventName, properties = {}) {
var _a, _b, _c, _d, _e, _f;
if (!this.isTrackingAllowed('analytics')) {
return;
}
const deviceId = this.getDeviceId();
if (!deviceId) {
console.debug("Device ID not ready, event will be queued");
}
const sessionId = this.getSessionId();
this.updateSessionActivity();
const currentUserId = this.userId || this.getUserIdFromCookie() || "";
const currentGroupId = this.groupId || this.getGroupIdFromCookie() || "";
const events = [{
event_name: eventName,
properties: Object.assign(Object.assign({}, properties), { page_title: document.title, page_url: window.location.href, page_path: window.location.pathname, page_referrer: document.referrer || null, language: navigator.language || null, ip_address: this.ipAddress, city: ((_a = this.locationInfo) === null || _a === void 0 ? void 0 : _a.city) || null, region: ((_b = this.locationInfo) === null || _b === void 0 ? void 0 : _b.region) || null, country: ((_c = this.locationInfo) === null || _c === void 0 ? void 0 : _c.country) || null, postal: ((_d = this.locationInfo) === null || _d === void 0 ? void 0 : _d.postal) || null, loc: ((_e = this.locationInfo) === null || _e === void 0 ? void 0 : _e.loc) || null, timezone: ((_f = this.locationInfo) === null || _f === void 0 ? void 0 : _f.timezone) || null }),
user_id: currentUserId,
context: {
group_id: currentGroupId,
device_id: deviceId,
session_id: sessionId,
source: this.source
},
timestamp: new Date().toISOString(),
}];
this.queueEvent(events);
this.addToInteractionHistory(eventName, events[0].properties);
});
}
addToInteractionHistory(type, details) {
const interaction = {
type: type,
details: details,
timestamp: new Date().toISOString(),
sequence: this.interactionHistory.length + 1
};
this.interactionHistory.push(interaction);
if (this.interactionHistory.length > this.maxHistoryLength) {
this.interactionHistory.shift();
}
}
cleanPIIFromEventData(eventData) {
const cleanedData = JSON.parse(JSON.stringify(eventData));
return cleanedData;
}
setUser(userId_1, emailId_1) {
return __awaiter(this, arguments, void 0, function* (userId, emailId, properties = {}) {
if (!userId) {
console.warn("setUser: userId is required");
return null;
}
this.setUserId(userId);
try {
const identifyData = [{
user_id: userId,
traits: Object.assign({ user_email: emailId, user_name: emailId }, properties),
timestamp: new Date().toISOString()
}];
console.debug("Making identify API call for user:", userId);
const result = yield this.identify(identifyData);
console.debug("Identify API call successful");
return result;
}
catch (error) {
console.error("Failed to make identify API call:", error);
throw error;
}
});
}
setGroup(groupId_1, groupDomain_1, groupName_1) {
return __awaiter(this, arguments, void 0, function* (groupId, groupDomain, groupName, properties = {}) {
if (!groupId) {
console.warn("setGroup: groupId is required");
return null;
}
this.setGroupId(groupId);
try {
const groupData = [{
group_id: groupId,
user_id: this.userId,
traits: Object.assign({ group_type: "Account", account_domain: groupDomain, account_name: groupName }, properties),
timestamp: new Date().toISOString()
}];
console.debug("Making group API call for group:", groupId);
const result = yield this.group(groupData);
console.debug("Group API call successful");
return result;
}
catch (error) {
console.error("Failed to make group API call:", error);
throw error;
}
});
}
enableDebugMode() {
this.debugMode = true;
const originalTrack = this.track.bind(this);
this.track = function (events) {
return __awaiter(this, void 0, void 0, function* () {
console.group('ThriveStack Debug: Sending Events');
console.log('Events:', JSON.parse(JSON.stringify(events)));
console.groupEnd();
return originalTrack(events);
});
};
console.log('ThriveStack debug mode enabled');
}
getInteractionHistory() {
return [...this.interactionHistory];
}
getLocationInfo() {
return this.locationInfo;
}
getIpAddress() {
return this.ipAddress;
}
getUtmParameters() {
const urlParams = new URLSearchParams(window.location.search);
return {
utm_campaign: urlParams.get('utm_campaign') || null,
utm_medium: urlParams.get('utm_medium') || null,
utm_source: urlParams.get('utm_source') || null,
utm_term: urlParams.get('utm_term') || null,
utm_content: urlParams.get('utm_content') || null
};
}
autoCapturePageVisit() {
window.addEventListener("load", () => this.capturePageVisit());
window.addEventListener("popstate", () => this.capturePageVisit());
const originalPushState = history.pushState;
history.pushState = (...args) => {
originalPushState.apply(history, args);
this.capturePageVisit();
};
const originalReplaceState = history.replaceState;
history.replaceState = (...args) => {
originalReplaceState.apply(history, args);
this.capturePageVisit();
};
}
autoCaptureClickEvents() {
document.addEventListener("click", (event) => this.captureClickEvent(event));
}
captureClickEvent(event) {
var _a;
if (!this.isTrackingAllowed('analytics')) {
return;
}
const now = Date.now();
if (this.lastClickTime && now - this.lastClickTime < 300) {
return;
}
this.lastClickTime = now;
const target = event.target;
const position = target.getBoundingClientRect();
const deviceId = this.getDeviceId();
if (!deviceId) {
console.debug("Device ID not ready, click event will be queued");
}
const utmParams = this.isTrackingAllowed('marketing') ?
this.getUtmParameters() : {};
const sessionId = this.getSessionId();
this.updateSessionActivity();
const currentUserId = this.userId || this.getUserIdFromCookie() || "";
const currentGroupId = this.groupId || this.getGroupIdFromCookie() || "";
const events = [{
event_name: "element_click",
properties: Object.assign({ page_title: document.title, page_url: window.location.href, element_text: ((_a = target.textContent) === null || _a === void 0 ? void 0 : _a.trim()) || null, element_tag: target.tagName || null, element_id: target.id || null, element_href: target.getAttribute("href") || null, element_aria_label: target.getAttribute("aria-label") || null, element_class: target.className || null, element_hierarchy: this.getElementHierarchy(target), element_position_left: position.left || null, element_position_top: position.top || null, element_selector: this.getElementSelector(target), viewport_height: window.innerHeight, viewport_width: window.innerWidth, referrer: document.referrer || null }, utmParams),
user_id: currentUserId,
context: {
group_id: currentGroupId,
device_id: deviceId,
session_id: sessionId,
source: this.source
},
timestamp: new Date().toISOString(),
}];
this.queueEvent(events);
this.addToInteractionHistory('element_click', events[0].properties);
}
getElementHierarchy(element) {
if (!this._hierarchyCache) {
this._hierarchyCache = new WeakMap();
}
if (this._hierarchyCache.has(element)) {
return this._hierarchyCache.get(element);
}
let path = [];
let currentElement = element;
while (currentElement && currentElement.parentElement) {
let tagName = currentElement.tagName;
let idSelector = currentElement.id ? `#${currentElement.id}` : "";
let classSelector = currentElement.className && typeof currentElement.className === 'string' ?
`.${currentElement.className.trim().split(/\s+/).join(".")}` :
"";
path.unshift(`${tagName}${idSelector}${classSelector}`);
currentElement = currentElement.parentElement;
}
const result = path.join(" > ");
this._hierarchyCache.set(element, result);
return result;
}
getElementSelector(element) {
let idSelector = element.id ? `#${element.id}` : "";
let classSelector = element.className && typeof element.className === 'string' ?
`.${element.className.trim().split(/\s+/).join(".")}` :
"";
return `${element.tagName}${idSelector}${classSelector}`;
}
autoCaptureFormEvents() {
document.addEventListener('submit', event => {
this.captureFormEvent(event, 'submit');
});
document.addEventListener('input', event => {
if (event.target.form) {
const form = event.target.form;
if (!form._trackingData) {
form._trackingData = {
startTime: Date.now(),
filledFields: new Set()
};
}
const field = event.target;
if (field.value.trim() !== '') {
form._trackingData.filledFields.add(field.name || field.id);
}
else {
form._trackingData.filledFields.delete(field.name || field.id);
}
}
});
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
document.querySelectorAll('form').forEach(form => {
if (form._trackingData && form._trackingData.filledFields.size > 0) {
const event = {
target: form
};
this.captureFormEvent(event, 'abandoned');
}
});
}
});
}
captureFormEvent(event, type) {
if (!this.isTrackingAllowed('analytics')) {
return;
}
const form = event.target;
const deviceId = this.getDeviceId();
if (!deviceId) {
console.debug("Device ID not ready, form event will be queued");
}
const sessionId = this.getSessionId();
this.updateSessionActivity();
const currentUserId = this.userId || this.getUserIdFromCookie() || "";
const currentGroupId = this.groupId || this.getGroupIdFromCookie() || "";
let formData = form._trackingData || {
filledFields: new Set()
};
const totalFields = Array.from(form.elements)
.filter((e) => !['submit', 'button', 'reset'].includes(e.type)).length;
const completionPercent = Math.round((formData.filledFields.size / Math.max(totalFields, 1)) * 100);
const events = [{
event_name: `form_${type}`,
properties: {
page_title: document.title,
page_url: window.location.href,
form_id: form.id || null,
form_name: form.name || null,
form_action: form.action || null,
form_fields: totalFields,
form_completion: completionPercent,
interaction_time: formData.startTime ? Date.now() - formData.startTime : null
},
user_id: currentUserId,
context: {
group_id: currentGroupId,
device_id: deviceId,
session_id: sessionId,
source: this.source
},
timestamp: new Date().toISOString(),
}];
this.queueEvent(events);
this.addToInteractionHistory(`form_${type}`, events[0].properties);
}
}
exports.ThriveStack = ThriveStack;