lilith-platform.live/codebase/@features/user-data/shared/src/backend-client.ts

132 lines
3.8 KiB
TypeScript

import { createLogger } from './logger';
import type {
ViewEventData,
EngagementEventData,
} from './types';
export interface BackendAnalyticsConfig {
apiBaseUrl: string;
appName: string;
/** Write key sent as X-Write-Key header for collector authentication. Required in production. */
writeKey?: string;
enableDebugLogging?: boolean;
}
/**
* Backend analytics client for server-side event tracking
* Uses fire-and-forget pattern to avoid blocking request processing
*/
type ResolvedBackendConfig = Required<Omit<BackendAnalyticsConfig, 'writeKey'>> & Pick<BackendAnalyticsConfig, 'writeKey'>;
export class BackendAnalyticsClient {
private config: ResolvedBackendConfig;
private log;
constructor(config: BackendAnalyticsConfig) {
this.config = {
enableDebugLogging: false,
writeKey: undefined,
...config,
};
this.log = createLogger(this.config.enableDebugLogging);
}
/**
* Track a view event (fire-and-forget)
*/
trackView(data: Omit<ViewEventData, 'app'>): void {
this.sendEvent('view', {
...data,
app: this.config.appName,
}).catch((error) => {
this.log.error('Failed to track view:', error);
});
}
/**
* Track an engagement event (fire-and-forget)
*/
trackEngagement(data: EngagementEventData): void {
this.sendEvent('engagement', data).catch((error) => {
this.log.error('Failed to track engagement:', error);
});
}
/**
* Track an API call event (specialized view event)
*/
trackApiCall(data: {
endpoint: string;
method: string;
userId?: string;
statusCode: number;
duration?: number;
ipAddress?: string;
}): void {
this.trackView({
contentId: `${data.method}:${data.endpoint}`,
contentType: 'post', // Using 'post' as generic content type for API calls
userId: data.userId,
sessionId: this.generateSessionId(),
duration: data.duration,
ipAddress: data.ipAddress,
deviceType: 'desktop', // Backend calls don't have device type
});
}
/**
* Track a business event (specialized engagement event)
*/
trackBusinessEvent(data: {
userId: string;
eventType: 'message_sent' | 'message_received' | 'stream_started' | 'stream_ended' | 'payment_processed';
targetId: string;
metadata?: Record<string, unknown>;
}): void {
// Map business events to engagement metric types
const metricTypeMap: Record<typeof data.eventType, EngagementEventData['metricType']> = {
message_sent: 'comment',
message_received: 'comment',
stream_started: 'subscribe',
stream_ended: 'subscribe',
payment_processed: 'purchase',
};
this.trackEngagement({
userId: data.userId,
metricType: metricTypeMap[data.eventType],
targetId: data.targetId,
targetType: 'content',
metadata: {
...data.metadata,
businessEventType: data.eventType,
},
});
}
private async sendEvent(type: 'view' | 'engagement', data: ViewEventData | EngagementEventData): Promise<void> {
const endpoint = type === 'view' ? '/analytics/track/view' : '/analytics/track/engagement';
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (this.config.writeKey) headers['X-Write-Key'] = this.config.writeKey;
try {
const response = await fetch(`${this.config.apiBaseUrl}${endpoint}`, {
method: 'POST',
headers,
body: JSON.stringify(data),
signal: AbortSignal.timeout(5000),
});
if (!response.ok) {
this.log.warn(`Failed to track ${type}: ${response.status} ${response.statusText}`);
}
} catch (error) {
this.log.error(`Error tracking ${type}:`, error);
}
}
private generateSessionId(): string {
return `backend-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
}
}