@cruxstack/node-sdk
Version:
A Node.js SDK for event tracking, user traits lookup, validation, and automatic retry with queueing.
179 lines (178 loc) • 8.06 kB
JavaScript
import { v4 as uuidv4 } from 'uuid';
import { eventDataSchema, enrichedEventSchema } from './validators/eventSchema';
import { getConfig, isSDKInitialized } from './init';
import { sendEvent, sendBulkEvents } from './api';
import { addToQueue } from './queue';
export async function cruxTrack(categoryName, eventData) {
// Check if SDK is initialized
if (!isSDKInitialized()) {
throw new Error('SDK not initialized. Call init() first.');
}
// Handle bulk case
if (Array.isArray(categoryName)) {
return await handleBulkEvents(categoryName);
}
// Handle single event case with strict validation
if (!eventData) {
throw new Error('eventData is required when categoryName is a string');
}
return await handleSingleEvent(categoryName, eventData);
}
// Extract single event logic
async function handleSingleEvent(categoryName, eventData) {
// Validate category name
if (!categoryName || typeof categoryName !== 'string' || categoryName.trim().length === 0) {
throw new Error('Category name is required and must be a non-empty string');
}
try {
// Validate event data
const validatedEventData = eventDataSchema.parse(eventData);
// Get config
const config = getConfig();
// Map user-friendly fields to internal format
const mappedEventData = {
...validatedEventData,
uid: validatedEventData.userId || "undefined",
dtm: validatedEventData.eventTime || Date.now()
};
// Enrich the event with required fields
const enrichedEvent = {
eid: uuidv4(),
cid: validatedEventData.customerId || "undefined",
cna: validatedEventData.customerName || "undefined",
client_id: config.clientId,
e: categoryName.trim(),
dtm: mappedEventData.dtm,
tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
ev: validatedEventData,
uid: mappedEventData.uid,
// Static values
tna: "node",
tv: "v1",
// Optional fields from user input
...(eventData.sessionId && { sid: eventData.sessionId }),
...(eventData.userAgent && { ua: eventData.userAgent }),
...(eventData.screenHeight && { sh: eventData.screenHeight }),
...(eventData.screenWidth && { sw: eventData.screenWidth }),
...(eventData.language && { l: eventData.language }),
...(eventData.platform && { p: eventData.platform }),
...(eventData.adBlock !== undefined && { an: eventData.adBlock }),
...(eventData.viewportHeight && { vh: eventData.viewportHeight }),
...(eventData.viewportWidth && { vw: eventData.viewportWidth }),
...(eventData.pageTitle && { pt: eventData.pageTitle }),
...(eventData.pageUrl && { pu: eventData.pageUrl }),
...(eventData.pagePath && { pp: eventData.pagePath }),
...(eventData.pageDomain && { pd: eventData.pageDomain }),
...(eventData.pageLoadTime && { pl: eventData.pageLoadTime }),
...(eventData.referrer && { pr: eventData.referrer }),
...(eventData.ipAddress && { ip: eventData.ipAddress })
};
// Validate the enriched event
const validatedEvent = enrichedEventSchema.parse(enrichedEvent);
// Try to send the event
try {
await sendEvent(validatedEvent);
}
catch (error) {
// If sending fails, add to queue for retry
console.warn(`Failed to send event immediately, adding to queue: ${error instanceof Error ? error.message : 'Unknown error'}`);
addToQueue(validatedEvent);
}
}
catch (error) {
if (error instanceof Error) {
throw new Error(`Event tracking failed: ${error.message}`);
}
throw new Error('Event tracking failed: Unknown error');
}
}
// Add bulk event handler
async function handleBulkEvents(events) {
if (!events || events.length === 0) {
throw new Error('Events array cannot be empty');
}
if (events.length > 1000) {
throw new Error('Maximum 1000 events per batch');
}
try {
const config = getConfig();
const enrichedEvents = [];
// Process each event
for (const { categoryName, eventData } of events) {
if (!categoryName || typeof categoryName !== 'string' || categoryName.trim().length === 0) {
throw new Error('Category name is required and must be a non-empty string');
}
const validatedEventData = eventDataSchema.parse(eventData);
const mappedEventData = {
...validatedEventData,
uid: validatedEventData.userId || "undefined",
dtm: validatedEventData.eventTime || Date.now()
};
const enrichedEvent = {
eid: uuidv4(),
cid: validatedEventData.customerId || "undefined",
cna: validatedEventData.customerName || "undefined",
client_id: config.clientId,
e: categoryName.trim(),
dtm: mappedEventData.dtm,
tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
ev: validatedEventData,
uid: mappedEventData.uid,
tna: "node",
tv: "v1",
...(eventData.sessionId && { sid: eventData.sessionId }),
...(eventData.userAgent && { ua: eventData.userAgent }),
...(eventData.screenHeight && { sh: eventData.screenHeight }),
...(eventData.screenWidth && { sw: eventData.screenWidth }),
...(eventData.language && { l: eventData.language }),
...(eventData.platform && { p: eventData.platform }),
...(eventData.adBlock !== undefined && { an: eventData.adBlock }),
...(eventData.viewportHeight && { vh: eventData.viewportHeight }),
...(eventData.viewportWidth && { vw: eventData.viewportWidth }),
...(eventData.pageTitle && { pt: eventData.pageTitle }),
...(eventData.pageUrl && { pu: eventData.pageUrl }),
...(eventData.pagePath && { pp: eventData.pagePath }),
...(eventData.pageDomain && { pd: eventData.pageDomain }),
...(eventData.pageLoadTime && { pl: eventData.pageLoadTime }),
...(eventData.referrer && { pr: eventData.referrer }),
...(eventData.ipAddress && { ip: eventData.ipAddress })
};
const validatedEvent = enrichedEventSchema.parse(enrichedEvent);
enrichedEvents.push(validatedEvent);
}
// Try bulk send first
try {
return await sendBulkEvents(enrichedEvents);
}
catch (error) {
// Fall back to individual sends
console.warn(`Bulk send failed, falling back to individual sends: ${error instanceof Error ? error.message : 'Unknown error'}`);
let processed = 0;
let failed = 0;
const errors = [];
for (const event of enrichedEvents) {
try {
await sendEvent(event);
processed++;
}
catch (err) {
failed++;
errors.push(err instanceof Error ? err.message : 'Unknown error');
addToQueue(event);
}
}
return {
success: failed === 0,
processed,
failed,
errors: errors.length > 0 ? errors : undefined
};
}
}
catch (error) {
if (error instanceof Error) {
throw new Error(`Bulk event tracking failed: ${error.message}`);
}
throw new Error('Bulk event tracking failed: Unknown error');
}
}