thrivestack-node-sdk
Version:
Official ThriveStack Analytics SDK for Node.js and Next.js server-side applications
682 lines • 23.9 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 __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