UNPKG

thrivestack-node-sdk

Version:

Official ThriveStack Analytics SDK for Node.js and Next.js server-side applications

682 lines 23.9 kB
"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 __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ThriveStack = void 0; const axios_1 = __importDefault(require("axios")); const uuid_1 = require("uuid"); const geoip = __importStar(require("geoip-lite")); const os_1 = require("os"); 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 IDs this.userId = ''; this.groupId = ''; // Device ID management this.deviceId = null; this.sessionUpdateTimer = null; // Session storage (in-memory for Node.js) this.sessionStorage = new Map(); // 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.enableConsent = options.enableConsent === true; this.source = options.source || ""; // Geo IP service URL this.geoIpServiceUrl = "https://ipinfo.io/json"; // Event batching this.batchSize = options.batchSize || 10; this.batchInterval = options.batchInterval || 2000; // Consent settings (default to functional only) this.consentCategories = { functional: true, // Always needed analytics: options.defaultConsent === true, marketing: options.defaultConsent === true }; // Session configuration this.sessionTimeout = options.sessionTimeout || (30 * 60 * 1000); // 30 minutes this.debounceDelay = options.debounceDelay || 2000; // 2 seconds // Initialize device ID this.initializeDeviceId(); // Fetch IP and location data on initialization this.fetchIpAndLocationInfo(); } /** * Initialize device ID */ initializeDeviceId() { try { // Generate a unique device ID for Node.js this.deviceId = `node_${(0, uuid_1.v4)()}`; console.debug("Generated device ID:", this.deviceId); } catch (error) { console.warn("Failed to generate device ID:", error); this.deviceId = `node_fallback_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`; } } /** * Fetch IP address and location information */ async fetchIpAndLocationInfo() { try { // Get local IP address this.ipAddress = this.getLocalIpAddress(); // Try to get location info from geoip-lite if (this.ipAddress) { const geo = geoip.lookup(this.ipAddress); if (geo) { this.locationInfo = { city: geo.city || null, region: geo.region || null, country: geo.country || null, postal: null, // geoip-lite doesn't provide postal loc: geo.ll ? `${geo.ll[0]},${geo.ll[1]}` : null, timezone: geo.timezone || null }; } } // If geoip-lite doesn't work, try external service if (!this.locationInfo) { console.debug("No local geo data found, fetching from external service..."); const response = await axios_1.default.get(this.geoIpServiceUrl); const data = response.data; this.ipAddress = data.ip || this.ipAddress; 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:", { ip: this.ipAddress, location: this.locationInfo }); } catch (error) { console.warn("Failed to fetch IP and location info:", error); this.ipAddress = this.getLocalIpAddress(); this.locationInfo = null; } } /** * Get local IP address */ getLocalIpAddress() { const interfaces = (0, os_1.networkInterfaces)(); for (const name of Object.keys(interfaces)) { const iface = interfaces[name]; if (iface) { for (const alias of iface) { if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) { return alias.address; } } } } return '127.0.0.1'; } /** * Initialize ThriveStack and start tracking */ async init(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 */ shouldTrack() { // In Node.js, we don't have browser DNT settings // But we can respect environment variables if (this.respectDoNotTrack && process.env.DO_NOT_TRACK === '1') { console.warn("DO_NOT_TRACK environment variable is set. 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; } /** * 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); // 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() { if (this.eventQueue.length === 0) return; const events = [...this.eventQueue]; const updatedEvents = events.map(event => ({ ...event, context: { ...event.context, device_id: this.deviceId } })); this.eventQueue = []; if (this.queueTimer) { clearTimeout(this.queueTimer); this.queueTimer = null; } 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 */ async track(events) { 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 = await axios_1.default.post(`${this.apiEndpoint}/track`, cleanedEvents, { headers: { "Content-Type": "application/json", "x-api-key": this.apiKey } }); return response.data; } catch (error) { retries--; if (retries === 0) { console.error("Failed to send telemetry after multiple attempts:", error); throw error; } // Wait before retrying (exponential backoff) await new Promise(resolve => setTimeout(resolve, 1000 * (3 - retries))); } } } /** * Send user identification data */ async identify(data) { 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 = await axios_1.default.post(`${this.apiEndpoint}/identify`, data, { headers: { "Content-Type": "application/json", "x-api-key": this.apiKey } }); return response.data; } catch (error) { console.error("Failed to send identification data:", error); throw error; } } /** * Send group data */ async group(data) { 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 = await axios_1.default.post(`${this.apiEndpoint}/group`, data, { headers: { "Content-Type": "application/json", "x-api-key": this.apiKey } }); return response.data; } catch (error) { console.error("Failed to send group data:", error); throw error; } } /** * Get device ID */ getDeviceId() { return this.deviceId; } /** * Get session ID */ getSessionId() { const sessionKey = 'thrivestack_session'; const sessionData = this.sessionStorage.get(sessionKey); if (sessionData) { const lastActivity = new Date(sessionData.lastActivity); const now = new Date(); const timeSinceLastActivity = now.getTime() - lastActivity.getTime(); // Check if session is still valid (within timeout) if (timeSinceLastActivity < this.sessionTimeout) { return sessionData.sessionId; } else { // Session expired, create new one console.debug("Session expired, creating new session"); return this.createNewSession(); } } else { // No existing session, create new one return this.createNewSession(); } } /** * Create a new session */ createNewSession() { const sessionId = `session_${(0, uuid_1.v4)()}`; const now = new Date().toISOString(); const sessionData = { sessionId: sessionId, startTime: now, lastActivity: now }; this.sessionStorage.set('thrivestack_session', sessionData); 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() { const sessionKey = 'thrivestack_session'; const sessionData = this.sessionStorage.get(sessionKey); if (sessionData) { // Update last activity sessionData.lastActivity = new Date().toISOString(); this.sessionStorage.set(sessionKey, sessionData); } else { // No session exists, create new one this.createNewSession(); } } /** * Capture page visit event (adapted for Node.js) */ async capturePageVisit(pageInfo = {}) { if (!this.isTrackingAllowed('functional')) { return; } // Validate session first, then update activity const sessionId = this.getSessionId(); this.updateSessionActivity(); // Build event with IP and location info const events = [{ event_name: "page_visit", properties: { page_title: pageInfo.title || "Node.js Application", page_url: pageInfo.url || "http://localhost", page_path: pageInfo.path || "/", page_referrer: pageInfo.referrer || null, language: process.env.LANG || "en-US", ip_address: this.ipAddress, city: this.locationInfo?.city || null, region: this.locationInfo?.region || null, country: this.locationInfo?.country || null, postal: this.locationInfo?.postal || null, loc: this.locationInfo?.loc || null, timezone: this.locationInfo?.timezone || null, platform: process.platform, node_version: process.version, environment: process.env.NODE_ENV || "development" }, user_id: this.userId, context: { group_id: this.groupId, device_id: this.deviceId, session_id: sessionId, source: this.source }, timestamp: new Date().toISOString(), }]; // Queue event this.queueEvent(events); // Add to interaction history this.addToInteractionHistory('page_visit', events[0].properties); } /** * Capture custom event */ async captureEvent(eventName, properties = {}) { if (!this.isTrackingAllowed('analytics')) { return; } // Validate session first, then update activity const sessionId = this.getSessionId(); this.updateSessionActivity(); const events = [{ event_name: eventName, properties: { ...properties, platform: process.platform, node_version: process.version, environment: process.env.NODE_ENV || "development" }, user_id: this.userId, context: { group_id: this.groupId, device_id: this.deviceId, session_id: sessionId, source: this.source }, timestamp: new Date().toISOString(), }]; // Queue event this.queueEvent(events); // Add to interaction history 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 }; // Add to history 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 */ async setUser(userId, emailId, properties = {}) { if (!userId) { console.warn("setUser: userId is required"); return null; } // Check if we need to make API call const shouldMakeApiCall = !this.userId || this.userId !== userId; // Always update local state this.setUserId(userId); if (shouldMakeApiCall) { try { // Prepare identify payload const identifyData = [{ user_id: userId, traits: { user_email: emailId, user_name: emailId, ...properties }, timestamp: new Date().toISOString() }]; console.debug("Making identify API call for user:", userId); const result = await this.identify(identifyData); console.debug("Identify API call successful"); return result; } catch (error) { console.error("Failed to make identify API call:", error); throw error; } } else { console.debug("Skipping identify API call - user already set:", userId); return null; } } /** * Set group information and optionally make group API call */ async setGroup(groupId, groupDomain, groupName, properties = {}) { if (!groupId) { console.warn("setGroup: groupId is required"); return null; } // Check if we need to make API call const shouldMakeApiCall = !this.groupId || this.groupId !== groupId; // Always update local state this.setGroupId(groupId); if (shouldMakeApiCall) { try { // Prepare group payload const groupData = [{ group_id: groupId, user_id: this.userId, traits: { 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 = await this.group(groupData); console.debug("Group API call successful"); return result; } catch (error) { console.error("Failed to make group API call:", error); throw error; } } else { console.debug("Skipping group API call - group already set:", groupId); return null; } } /** * Enable debug mode for troubleshooting */ enableDebugMode() { console.log('ThriveStack debug mode enabled'); // Override track method to log events const originalTrack = this.track.bind(this); this.track = async function (events) { console.group('ThriveStack Debug: Sending Events'); console.log('Events:', JSON.parse(JSON.stringify(events))); console.groupEnd(); return originalTrack(events); }; } /** * Get interaction history */ getInteractionHistory() { return [...this.interactionHistory]; } /** * Clear interaction history */ clearInteractionHistory() { this.interactionHistory = []; } /** * Get current configuration */ getConfig() { return { apiEndpoint: this.apiEndpoint, batchSize: this.batchSize, batchInterval: this.batchInterval, sessionTimeout: this.sessionTimeout, source: this.source, userId: this.userId, groupId: this.groupId, deviceId: this.deviceId }; } } exports.ThriveStack = ThriveStack; //# sourceMappingURL=ThriveStack.js.map