thrivestack-analytics
Version:
ThriveStack Analytics - Privacy-first web analytics for Node.js and browsers
914 lines (913 loc) • 36.8 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
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());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ThriveStack = void 0;
const node_fetch_1 = __importDefault(require("node-fetch"));
//import { v4 as uuidv4 } from 'uuid';
const os = __importStar(require("os"));
const crypto = __importStar(require("crypto"));
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 - defaults to free ipinfo.io service
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); // 30 minutes
this.debounceDelay = options.debounceDelay || 2000; // 2 seconds
// Consent settings (default to functional only)
this.consentCategories = {
functional: true, // Always needed
analytics: options.defaultConsent === true,
marketing: options.defaultConsent === true
};
// Initialize device ID
this.initializeDeviceId();
// Fetch IP and location data on initialization
this.fetchIpAndLocationInfo();
// Setup session tracking (browser only)
this.setupSessionTracking();
// Initialize automatically if tracking is allowed and in browser
if (this.shouldTrack() && typeof window !== 'undefined') {
this.autoCapturePageVisit();
if (this.trackClicks) {
this.autoCaptureClickEvents();
}
if (this.trackForms) {
this.autoCaptureFormEvents();
}
}
}
/**
* Initialize device ID with proper fallback logic
*/
initializeDeviceId() {
return __awaiter(this, void 0, void 0, function* () {
try {
// Generate a unique device ID for Node.js environment
this.deviceId = this.generateRandomDeviceId();
this.deviceIdReady = true;
console.debug("Generated device ID:", this.deviceId);
// Process any queued events now that device ID is ready
this.processQueueIfReady();
}
catch (error) {
console.warn("Failed to initialize device ID:", error);
// Fallback to random device ID
this.deviceId = this.generateRandomDeviceId();
this.deviceIdReady = true;
console.debug("Using fallback random device ID:", this.deviceId);
// Process any queued events now that device ID is ready
this.processQueueIfReady();
}
});
}
/**
* Generate a random device ID as fallback
*/
generateRandomDeviceId() {
return 'device_' + crypto.randomBytes(16).toString('hex');
}
/**
* Fetch IP address and location information
*/
fetchIpAndLocationInfo() {
return __awaiter(this, void 0, void 0, function* () {
try {
console.debug("Fetching IP and location info from API...");
const response = yield (0, node_fetch_1.default)(this.geoIpServiceUrl);
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const data = yield response.json();
// Store IP and location data
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
};
console.debug("IP and location info fetched successfully");
}
catch (error) {
console.warn("Failed to fetch IP and location info:", error);
// Set fallback values
this.ipAddress = null;
this.locationInfo = null;
}
});
}
/**
* Initialize ThriveStack and start tracking
*/
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;
}
});
}
/**
* Check if tracking is allowed based on DNT and consent settings
*/
shouldTrack() {
// Check Do Not Track setting if enabled and in browser
if (this.respectDoNotTrack && typeof navigator !== 'undefined' && (navigator.doNotTrack === "1" ||
navigator.doNotTrack === "yes" ||
(typeof window !== 'undefined' && window.doNotTrack === "1"))) {
console.warn("User has enabled Do Not Track. Tracking is disabled.");
return false;
}
return true;
}
/**
* Check if specific tracking category is allowed
*/
isTrackingAllowed(category) {
if (!this.shouldTrack())
return false;
if (this.enableConsent) {
return this.consentCategories[category] === true;
}
return true;
}
/**
* Update consent settings for a tracking category
*/
setConsent(category, hasConsent) {
if (this.consentCategories.hasOwnProperty(category)) {
this.consentCategories[category] = hasConsent;
}
}
/**
* Set user ID for tracking
*/
setUserId(userId) {
this.userId = userId;
}
/**
* Set group ID for tracking
*/
setGroupId(groupId) {
this.groupId = groupId;
}
/**
* Get userId from cookie (browser only)
*/
getUserIdFromCookie() {
if (typeof document === 'undefined')
return null;
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;
}
/**
* Get groupId from cookie (browser only)
*/
getGroupIdFromCookie() {
if (typeof document === 'undefined')
return null;
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;
}
/**
* Set source for tracking
*/
setSource(source) {
this.source = source;
}
/**
* Queue an event for batched sending
*/
queueEvent(events) {
// Handle both single events and arrays
if (!Array.isArray(events)) {
events = [events];
}
// Add events to queue
this.eventQueue.push(...events);
// Only process queue if device ID is ready
if (this.deviceIdReady) {
this.processQueueIfReady();
}
else {
console.debug("Device ID not ready, keeping events in queue");
}
}
/**
* Process queue only if device ID is ready
*/
processQueueIfReady() {
if (!this.deviceIdReady || this.eventQueue.length === 0) {
return;
}
// Process queue if we've reached the batch size
if (this.eventQueue.length >= this.batchSize) {
this.processQueue();
}
else if (!this.queueTimer) {
// Start timer to process queue after delay
this.queueTimer = setTimeout(() => this.processQueue(), this.batchInterval);
}
}
/**
* Process and send queued events
*/
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);
// Add events back to front of queue for retry
this.eventQueue.unshift(...events);
}
});
}
/**
* Track events by sending to ThriveStack API
*/
track(events) {
return __awaiter(this, void 0, void 0, function* () {
if (!this.apiKey) {
throw new Error("Initialize the ThriveStack instance before sending telemetry data.");
}
// Clean events of PII before sending
const cleanedEvents = events.map(event => this.cleanPIIFromEventData(event));
// Add retry logic for network errors
let retries = 3;
while (retries > 0) {
try {
const response = yield (0, node_fetch_1.default)(`${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;
}
// Wait before retrying (exponential backoff)
yield new Promise(resolve => setTimeout(resolve, 1000 * (3 - retries)));
}
}
});
}
/**
* Send user identification data
*/
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 || "";
}
// Set userId in instance
if (userId) {
this.setUserId(userId);
}
// Send data to API
const response = yield (0, node_fetch_1.default)(`${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;
}
});
}
/**
* Send group data
*/
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 {
// Extract groupId from data
let groupId = "";
if (Array.isArray(data) && data.length > 0) {
const lastElement = data[data.length - 1];
groupId = lastElement.group_id || "";
}
else {
groupId = data.group_id || "";
}
// Store groupId in instance
if (groupId) {
this.setGroupId(groupId);
}
// Send data to API
const response = yield (0, node_fetch_1.default)(`${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;
}
});
}
/**
* Get device ID
*/
getDeviceId() {
if (this.deviceIdReady && this.deviceId) {
return this.deviceId;
}
return null;
}
/**
* Get session ID
*/
getSessionId() {
const sessionId = 'session_' + crypto.randomBytes(16).toString('hex');
return sessionId;
}
/**
* Update session activity with debouncing
*/
updateSessionActivity() {
// Clear existing timer
if (this.sessionUpdateTimer) {
clearTimeout(this.sessionUpdateTimer);
}
// Set new debounced timer
this.sessionUpdateTimer = setTimeout(() => {
this.updateSessionActivityImmediate();
}, this.debounceDelay);
}
/**
* Immediate session activity update
*/
updateSessionActivityImmediate() {
// In Node.js, we don't persist sessions the same way as browsers
// This is a simplified implementation
console.debug("Session activity updated");
}
/**
* Capture page visit event (works in both Node.js and browser)
*/
capturePageVisit(pageInfo) {
return __awaiter(this, void 0, void 0, function* () {
var _a, _b, _c, _d, _e, _f;
if (!this.isTrackingAllowed('functional')) {
return;
}
// Get device ID - return early if not ready
const deviceId = this.getDeviceId();
if (!deviceId) {
console.debug("Device ID not ready, page visit will be queued");
}
// Validate session first, then update activity
const sessionId = this.getSessionId();
this.updateSessionActivity();
// Get user and group IDs
const currentUserId = this.userId || "";
const currentGroupId = this.groupId || "";
// Get UTM parameters if marketing consent is given
const utmParams = this.isTrackingAllowed('marketing') ?
this.getUtmParameters() : {};
// Build event with IP and location info
const events = [{
event_name: "page_visit",
properties: Object.assign({ page_title: (pageInfo === null || pageInfo === void 0 ? void 0 : pageInfo.title) || (typeof document !== 'undefined' ? document.title : "Node.js Application"), page_url: (pageInfo === null || pageInfo === void 0 ? void 0 : pageInfo.url) || (typeof window !== 'undefined' ? window.location.href : "http://localhost"), page_path: (pageInfo === null || pageInfo === void 0 ? void 0 : pageInfo.path) || (typeof window !== 'undefined' ? window.location.pathname : "/"), page_referrer: (pageInfo === null || pageInfo === void 0 ? void 0 : pageInfo.referrer) || (typeof document !== 'undefined' ? document.referrer : null), language: typeof navigator !== 'undefined' ? navigator.language : "en-US", 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, platform: typeof os !== 'undefined' ? os.platform() : (typeof navigator !== 'undefined' ? navigator.platform : 'unknown'), arch: typeof os !== 'undefined' ? os.arch() : 'unknown', node_version: typeof process !== 'undefined' ? process.version : null }, utmParams),
user_id: currentUserId,
context: {
group_id: currentGroupId,
device_id: deviceId,
session_id: sessionId,
source: this.source
},
timestamp: new Date().toISOString(),
}];
// Queue event instead of sending immediately
this.queueEvent(events);
// Add to interaction history
this.addToInteractionHistory('page_visit', events[0].properties);
});
}
/**
* Capture custom event
*/
captureEvent(eventName_1) {
return __awaiter(this, arguments, void 0, function* (eventName, properties = {}) {
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 || "";
const currentGroupId = this.groupId || "";
const events = [{
event_name: eventName,
properties: Object.assign(Object.assign({}, properties), { platform: os.platform(), arch: os.arch(), node_version: process.version }),
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);
});
}
/**
* Add event to interaction history
*/
addToInteractionHistory(type, details) {
const interaction = {
type: type,
details: details,
timestamp: new Date().toISOString(),
sequence: this.interactionHistory.length + 1
};
this.interactionHistory.push(interaction);
// Trim if necessary
if (this.interactionHistory.length > this.maxHistoryLength) {
this.interactionHistory.shift();
}
}
/**
* Automatically detect and clean PII from event data
*/
cleanPIIFromEventData(eventData) {
// Deep clone to avoid modifying original
const cleanedData = JSON.parse(JSON.stringify(eventData));
return cleanedData;
}
/**
* Set user information and optionally make identify API call
*/
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;
}
// Always update local state
this.setUserId(userId);
try {
// Prepare identify payload
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;
}
});
}
/**
* Set group information and optionally make group API call
*/
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;
}
// Always update local state
this.setGroupId(groupId);
try {
// Prepare group payload
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;
}
});
}
/**
* Enable debug mode for troubleshooting
*/
enableDebugMode() {
this.debugMode = true;
// Override track method to log events
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');
}
/**
* Get interaction history
*/
getInteractionHistory() {
return [...this.interactionHistory];
}
/**
* Get current location info
*/
getLocationInfo() {
return this.locationInfo;
}
/**
* Get current IP address
*/
getIpAddress() {
return this.ipAddress;
}
/**
* Get UTM parameters from URL (browser only)
*/
getUtmParameters() {
if (typeof window === 'undefined') {
return {};
}
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
};
}
/**
* Auto-capture page visits (browser only)
*/
autoCapturePageVisit() {
if (typeof window === 'undefined')
return;
// Track initial page load
window.addEventListener("load", () => this.capturePageVisit());
// Track navigation events
window.addEventListener("popstate", () => this.capturePageVisit());
// Track history API calls for SPA support
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();
};
}
/**
* Auto-capture click events (browser only)
*/
autoCaptureClickEvents() {
if (typeof document === 'undefined')
return;
document.addEventListener("click", (event) => this.captureClickEvent(event));
}
/**
* Capture click event (browser only)
*/
captureClickEvent(event) {
var _a;
if (typeof document === 'undefined' || !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);
}
/**
* Get element hierarchy for DOM traversal (browser only)
*/
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;
}
/**
* Get CSS selector for element (browser only)
*/
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}`;
}
/**
* Auto-capture form events (browser only)
*/
autoCaptureFormEvents() {
if (typeof document === 'undefined')
return;
// Track form submissions
document.addEventListener('submit', event => {
this.captureFormEvent(event, 'submit');
});
// Track form field interactions
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);
}
}
});
// Track form abandonment
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');
}
});
}
});
}
/**
* Capture form events (browser only)
*/
captureFormEvent(event, type) {
if (typeof document === 'undefined' || !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);
}
/**
* Setup session tracking (browser only)
*/
setupSessionTracking() {
// Session tracking is handled by capture functions
}
}
exports.ThriveStack = ThriveStack;