/** * Serla Analytics - TypeScript/Node.js SDK * * Usage: * import { Serla } from './lib/sdk/serla'; * const serla = new Serla('sk_live_YOUR_API_KEY'); * await serla.send('user_signup', { plan: 'pro' }); */ interface SerlaConfig { apiKey: string; endpoint?: string; debug?: boolean; } interface EventMetadata { [key: string]: any; } export class Serla { private apiKey: string; private endpoint: string; private pageviewEndpoint: string; private debug: boolean; private userId: string | null = null; private sessionId: string | null = null; private visitorId: string | null = null; constructor(apiKey: string, options?: Partial) { if (!apiKey) { throw new Error('Serla: API key is required'); } this.apiKey = apiKey; this.endpoint = options?.endpoint || 'https://serla.dev/api/events/ingest'; this.pageviewEndpoint = 'https://serla.dev/api/pageviews/ingest'; this.debug = options?.debug || false; // Generate or retrieve visitor ID from localStorage (browser only) this.visitorId = this.getOrCreateVisitorId(); this.log('Initialized with endpoint:', this.endpoint); this.log('Visitor ID:', this.visitorId); } /** * Generate or retrieve a unique visitor ID * Stored in localStorage for persistence across sessions */ private getOrCreateVisitorId(): string | null { // Only works in browser environment if (typeof window === 'undefined' || typeof localStorage === 'undefined') { return null; } const STORAGE_KEY = 'serla_visitor_id'; let visitorId = localStorage.getItem(STORAGE_KEY); if (!visitorId) { // Generate a unique visitor ID visitorId = this.generateVisitorId(); localStorage.setItem(STORAGE_KEY, visitorId); this.log('Generated new visitor ID:', visitorId); } return visitorId; } /** * Generate a unique visitor ID using timestamp + random string */ private generateVisitorId(): string { const timestamp = Date.now().toString(36); const randomStr = Math.random().toString(36).substring(2, 15); return `${timestamp}-${randomStr}`; } /** * Send an event to Serla * @param eventName - Name of the event (e.g., 'user_signup', 'purchase') * @param metadata - Optional metadata object * @param userId - Optional userId for this event (overrides identify()) * @param sessionId - Optional sessionId for this event (overrides setSession()) */ async send( eventName: string, metadata?: EventMetadata, userId?: string | null, sessionId?: string | null ): Promise { if (!eventName || typeof eventName !== 'string') { throw new Error('Serla: Event name is required and must be a string'); } const payload: any = { eventName, metadata: metadata || {}, }; // Use parameter if provided, otherwise fall back to instance variable if (userId !== undefined) { if (userId !== null) payload.userId = userId; } else if (this.userId) { payload.userId = this.userId; } if (sessionId !== undefined) { if (sessionId !== null) payload.sessionId = sessionId; } else if (this.sessionId) { payload.sessionId = this.sessionId; } this.log('Sending event:', eventName, metadata); try { const response = await fetch(this.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}`, }, body: JSON.stringify(payload), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Serla API error (${response.status}): ${errorText}`); } const data = await response.json(); this.log('Event sent successfully:', data); return data; } catch (error) { this.log('Error sending event:', error); throw error; } } /** * Track a pageview * @param path - Page path (defaults to window.location.pathname in browser) * @param referrer - Referrer URL (defaults to document.referrer in browser) */ async pageview(path?: string, referrer?: string): Promise { // Ensure we have a session ID if (!this.sessionId) { this.sessionId = this.generateVisitorId(); // Reuse same generator for session } // Construct URL - required field let url: string; if (typeof window !== 'undefined') { // Browser environment - use window.location if (path) { url = `${window.location.origin}${path}`; } else { url = window.location.href; } } else { // Server environment - construct from path url = path || '/'; } const payload: any = { url, sessionId: this.sessionId, }; // Priority: userId > visitorId (authenticated users trump anonymous visitors) if (this.userId) { payload.userId = this.userId; } else if (this.visitorId) { payload.visitorId = this.visitorId; } if (referrer) { payload.referrer = referrer; } else if (typeof document !== 'undefined' && document.referrer) { payload.referrer = document.referrer; } this.log('Tracking pageview:', url); try { const response = await fetch(this.pageviewEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}`, }, body: JSON.stringify(payload), }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Serla API error (${response.status}): ${errorText}`); } const data = await response.json(); this.log('Pageview tracked successfully:', data); return data; } catch (error) { this.log('Error tracking pageview:', error); throw error; } } /** * Identify a user for all subsequent events * @param userId - User identifier */ identify(userId: string | null): void { this.userId = userId; this.log('User identified:', userId); } /** * Set a session ID for all subsequent events * @param sessionId - Session identifier */ setSession(sessionId: string | null): void { this.sessionId = sessionId; this.log('Session set:', sessionId); } /** * Get the current user ID */ getUserId(): string | null { return this.userId; } /** * Get the current session ID */ getSessionId(): string | null { return this.sessionId; } /** * Log debug messages if debug mode is enabled */ private log(...args: any[]): void { if (this.debug) { console.log('[Serla]', ...args); } } } // Default export export default Serla; // Simple factory function export function createSerla(apiKey: string, options?: Partial): Serla { return new Serla(apiKey, options); }