@cruxstack/browser-sdk
Version:
A lightweight, privacy-focused JavaScript SDK for web analytics and event tracking. Built with TypeScript, featuring automatic event capture, event-time environment snapshots, intelligent queuing, and robust error handling.
235 lines (234 loc) • 8.63 kB
JavaScript
import { setDebugLog, debug } from "./utils/log";
import { SessionManager } from "./session";
import { EventQueue } from "./queue";
import { ApiClient } from "./apiClient";
import { EventTracker } from "./tracker";
import { setupAutocapture, } from "../features/autocapture";
let sessionManager;
let eventQueue;
let apiClient;
let eventTracker;
let autocaptureCleanup = null;
let globalConfig = null;
// Store event listener references for proper cleanup
let unloadHandler = null;
let onlineHandler = null;
// Generate plain UUID for event IDs
function generateEventId() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
export function init(config) {
try {
// Prevent multiple initializations
if (eventTracker && globalConfig) {
if (config.debugLog) {
console.warn("Cruxstack: SDK already initialized. Skipping re-initialization.");
}
return;
}
// Validate config
if (!config.clientId) {
throw new Error("Cruxstack: clientId is required. Please provide a valid client identifier.");
}
if (typeof config.clientId !== "string" || config.clientId.trim() === "") {
throw new Error("Cruxstack: clientId must be a non-empty string.");
}
// Store config globally
globalConfig = config;
// Initialize core modules
setDebugLog(config.debugLog || false);
sessionManager = new SessionManager(config);
eventQueue = new EventQueue();
apiClient = new ApiClient(config.clientId, config.customerId, config.customerName, config.debugLog || false);
eventTracker = new EventTracker(apiClient, eventQueue, sessionManager, config.clientId, config.customerId, config.customerName);
// Setup autocapture if enabled
if (config.autoCapture !== false) {
const autocaptureTrackers = {
trackClick: (data) => {
eventTracker.track({
type: "click",
data,
id: generateEventId(),
clientId: config.clientId,
customerId: config.customerId,
customerName: config.customerName,
});
},
trackForm: (data) => {
eventTracker.track({
type: `form_${data.eventType}`,
data,
id: generateEventId(),
clientId: config.clientId,
customerId: config.customerId,
customerName: config.customerName,
});
},
trackPageView: (data) => {
eventTracker.track({
type: "page_view",
data,
id: generateEventId(),
clientId: config.clientId,
customerId: config.customerId,
customerName: config.customerName,
});
},
};
autocaptureCleanup = setupAutocapture(autocaptureTrackers, config);
}
// Setup unload handler for queue flushing
unloadHandler = () => {
if (!eventTracker)
return;
const status = eventTracker.getQueueStatus();
if (status.length > 0) {
if (config.debugLog) {
console.log("Cruxstack: Attempting unload-safe delivery via sendBeacon for queued events");
}
// Best-effort: do a one-shot flush; sendEvent will use sendBeacon in unload scenario
// Do not await here to avoid blocking unload
eventTracker.flushQueue();
}
};
// Setup online handler for retry
onlineHandler = () => {
if (eventTracker && eventTracker.getQueueStatus().length > 0) {
if (config.debugLog) {
console.log("Cruxstack: Connection restored, retrying queued events");
}
eventTracker.flushQueue();
}
};
// Add event listeners
window.addEventListener("beforeunload", unloadHandler);
window.addEventListener("online", onlineHandler);
// Attempt to flush any queued events immediately on init (ensures delivery of persisted events)
if (eventTracker.getQueueStatus().length > 0) {
eventTracker.flushQueue();
}
debug("SDK initialized successfully", {
clientId: config.clientId,
customerId: config.customerId,
autoCapture: config.autoCapture !== false,
userId: config.userId,
});
}
catch (error) {
console.error("Cruxstack: Initialization failed", error);
throw error;
}
}
export function trackEvent(eventData) {
if (!eventTracker || !globalConfig) {
throw new Error("Cruxstack: SDK not initialized. Please call init() with your configuration before tracking events.");
}
if (!eventData.type || typeof eventData.type !== "string") {
throw new Error("Cruxstack: Event type is required and must be a string.");
}
if (eventData.type.trim() === "") {
throw new Error("Cruxstack: Event type cannot be empty or contain only whitespace.");
}
// Validate data structure
if (eventData.data !== undefined &&
(typeof eventData.data !== "object" || eventData.data === null)) {
throw new Error("Cruxstack: Event data must be an object or undefined.");
}
eventTracker.track({
id: generateEventId(),
type: eventData.type,
data: eventData.data || {},
clientId: globalConfig.clientId,
customerId: globalConfig.customerId,
customerName: globalConfig.customerName,
});
}
// Public API methods for data fetching
export async function getUserTraits(customerId) {
if (!apiClient || !globalConfig) {
throw new Error("Cruxstack: SDK not initialized. Please call init() with your configuration before using API methods.");
}
if (typeof customerId !== "string" || customerId.trim() === "") {
throw new Error("Cruxstack: customerId must be a non-empty string.");
}
try {
return await apiClient.getUserTraits(customerId);
}
catch (error) {
if (globalConfig.debugLog) {
console.error("Cruxstack: Failed to fetch user traits", error);
}
throw error;
}
}
export function getSessionInfo() {
if (!sessionManager) {
return {
sessionId: null,
userId: null,
isInitialized: false,
};
}
return {
sessionId: sessionManager.getSessionId(),
userId: sessionManager.getUserId(),
isInitialized: true,
};
}
export function flushEvents() {
if (!eventTracker) {
throw new Error("Cruxstack: SDK not initialized. Please call init() with your configuration.");
}
return eventTracker.flushQueue();
}
export function getQueueStatus() {
if (!eventTracker) {
return {
length: 0,
events: [],
};
}
return eventTracker.getQueueStatus();
}
export function clearQueue() {
if (!eventTracker) {
throw new Error("Cruxstack: SDK not initialized. Please call init() with your configuration.");
}
eventTracker.clearQueue();
}
export function resetSession() {
if (!sessionManager) {
throw new Error("Cruxstack: SDK not initialized. Please call init() with your configuration.");
}
sessionManager.resetSession();
}
export function isInitialized() {
return !!(eventTracker && globalConfig);
}
export function cleanup() {
// Remove event listeners
if (unloadHandler) {
window.removeEventListener("beforeunload", unloadHandler);
unloadHandler = null;
}
if (onlineHandler) {
window.removeEventListener("online", onlineHandler);
onlineHandler = null;
}
// Cleanup autocapture
if (autocaptureCleanup) {
autocaptureCleanup();
autocaptureCleanup = null;
}
// Reset all modules
sessionManager = null;
eventQueue = null;
apiClient = null;
eventTracker = null;
globalConfig = null;
debug("SDK cleaned up successfully");
}