UNPKG

thrivestack-analytics

Version:

ThriveStack Analytics - Privacy-first web analytics for Node.js and browsers

912 lines (911 loc) 38.6 kB
"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;