@trynova-ai/shadow-js
Version:
A TypeScript library for user session tracking.
156 lines (137 loc) • 4.85 kB
text/typescript
import { v4 as uuidv4 } from 'uuid';
import { BrowserPlugin } from './plugins/browser';
interface Plugin {
setup: (shadow: Shadow) => void;
}
interface BufferConfig {
enabled: boolean;
maxEvents?: number; // The maximum number of events to buffer before flushing
flushInterval?: number; // The interval in milliseconds to flush events
}
interface ShadowOptions {
url: string;
headers?: Record<string, string>;
sessionProvider?: () => string;
plugins?: Plugin[];
buffer?: BufferConfig;
sampleRate?: number; // The sample rate of clients that should be shadowed. Default is 0.1 (10%)
}
export class Shadow {
private url: string;
private headers: Record<string, string>;
private sessionId: string;
private plugins: Plugin[];
private bufferConfig: BufferConfig;
private eventBuffer: any[] = [];
private flushTimeout: any;
private isClientEnabled: boolean;
constructor(options: ShadowOptions) {
this.url = options.url;
this.headers = options.headers || {};
this.sessionId = options.sessionProvider ? options.sessionProvider() : this.getStoredSessionId();
this.plugins = options.plugins || [new BrowserPlugin()];
this.bufferConfig = options.buffer || { enabled: false };
const sampleRate = options.sampleRate ?? 0.1; // Default to 10% if not provided
this.isClientEnabled = Math.random() < sampleRate;
if (this.isClientEnabled && this.bufferConfig.enabled) {
try {
this.startFlushInterval();
} catch (error) {
console.error('Error starting flush interval:', error);
}
}
if (this.isClientEnabled) {
try {
this.setupPlugins();
} catch (error) {
console.error('Error setting up plugins:', error);
}
}
}
public static init(options: ShadowOptions): Shadow {
try {
return new Shadow(options);
} catch (error) {
console.error('Error initializing Shadow:', error);
throw error; // re-throw if needed
}
}
private getStoredSessionId(): string {
try {
let sessionId = localStorage.getItem('session-id');
if (!sessionId) {
sessionId = uuidv4();
localStorage.setItem('session-id', sessionId);
}
return sessionId;
} catch (error) {
console.error('Error getting or storing session ID:', error);
return uuidv4(); // fallback to a new UUID if there's an issue
}
}
private setupPlugins() {
this.plugins.forEach(plugin => plugin.setup(this));
}
public capture(event: any) {
if (!this.isClientEnabled) return;
try {
const payload = {
...event,
sessionId: this.sessionId,
};
if (this.bufferConfig.enabled) {
this.eventBuffer.push(payload);
if (this.eventBuffer.length >= (this.bufferConfig.maxEvents || 10)) {
this.flushEvents();
}
} else {
this.sendEvent(this.url + '/session', payload);
}
} catch (error) {
console.error('Error capturing event:', error);
}
}
private flushEvents() {
try {
if (this.eventBuffer.length > 0) {
const bulkPayload = [...this.eventBuffer];
this.eventBuffer = [];
this.sendEvent(this.url + '/sessions', bulkPayload);
}
} catch (error) {
console.error('Error flushing events:', error);
}
}
private startFlushInterval() {
try {
this.flushTimeout = setInterval(() => {
this.flushEvents();
}, this.bufferConfig.flushInterval || 5000);
} catch (error) {
console.error('Error starting flush interval:', error);
}
}
private sendEvent(endpoint: string, payload: any) {
try {
const headers = {
...this.headers,
'Content-Type': 'application/json',
};
fetch(endpoint, {
method: 'POST',
headers: headers,
body: JSON.stringify(payload),
keepalive: true
}).catch(error => console.error('Error sending session events:', error));
} catch (error) {
console.error('Error in sendEvent:', error);
}
}
public stopFlushInterval() {
try {
clearInterval(this.flushTimeout);
} catch (error) {
console.error('Error stopping flush interval:', error);
}
}
}