chore(analytics): 📈 Add event tracking methods for analytics integration

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Lilith 2026-01-29 08:20:58 -08:00
parent 541728c002
commit a0d6b6fb11
2 changed files with 0 additions and 199 deletions

View file

@ -1,198 +0,0 @@
import { Injectable, Inject, OnModuleDestroy } from '@nestjs/common';
import { ANALYTICS_OPTIONS, type AnalyticsModuleOptions } from '../module';
import type { AnalyticsEvent } from '../types';
import type { TrackEventPayload } from '../../types';
@Injectable()
export class AnalyticsService implements OnModuleDestroy {
private eventQueue: AnalyticsEvent[] = [];
private flushTimer: ReturnType<typeof setInterval> | null = null;
constructor(
@Inject(ANALYTICS_OPTIONS) private readonly options: AnalyticsModuleOptions
) {
this.startFlushTimer();
}
async onModuleDestroy(): Promise<void> {
if (this.flushTimer) {
clearInterval(this.flushTimer);
}
await this.flush();
}
/**
* Track an analytics event
*/
async track(event: AnalyticsEvent): Promise<void> {
if (!this.options.enabled) {
return;
}
const fullEvent: AnalyticsEvent = {
...event,
timestamp: event.timestamp || new Date().toISOString(),
sessionId: event.sessionId || 'server',
};
if (this.options.debug) {
console.log('[Analytics] Track:', fullEvent);
}
this.eventQueue.push(fullEvent);
const maxSize = this.options.batch?.maxSize ?? 100;
if (this.eventQueue.length >= maxSize) {
await this.flush();
}
}
/**
* Track a page view event
*/
async trackPageView(
url: string,
options: { title?: string; referrer?: string; userId?: string } = {}
): Promise<void> {
const parsedUrl = new URL(url, 'http://localhost');
const queryParams: Record<string, string> = {};
parsedUrl.searchParams.forEach((value, key) => {
queryParams[key] = value;
});
await this.track({
type: 'page_view',
userId: options.userId,
properties: {
url,
title: options.title,
referrer: options.referrer,
path: parsedUrl.pathname,
queryParams,
},
});
}
/**
* Track a conversion event
*/
async trackConversion(
goalId: string,
goalName: string,
options: { value?: number; currency?: string; userId?: string } = {}
): Promise<void> {
await this.track({
type: 'conversion',
userId: options.userId,
properties: {
goalId,
goalName,
value: options.value,
currency: options.currency,
},
});
}
/**
* Track a revenue event
*/
async trackRevenue(
transactionId: string,
amount: number,
currency: string,
options: { productIds?: string[]; category?: string; userId?: string } = {}
): Promise<void> {
await this.track({
type: 'revenue',
userId: options.userId,
properties: {
transactionId,
amount,
currency,
productIds: options.productIds,
category: options.category,
},
});
}
/**
* Track a custom event
*/
async trackCustom(
name: string,
properties: Record<string, unknown> = {},
options: { category?: string; userId?: string } = {}
): Promise<void> {
await this.track({
type: 'custom',
userId: options.userId,
properties: {
name,
category: options.category,
...properties,
},
});
}
/**
* Flush queued events to the collector
*/
async flush(): Promise<void> {
if (this.eventQueue.length === 0) {
return;
}
const eventsToSend = [...this.eventQueue];
this.eventQueue = [];
try {
const payload: TrackEventPayload = {
// Cast events to the expected type - they match the interface
events: eventsToSend as unknown as TrackEventPayload['events'],
};
const response = await fetch(this.options.collector.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(this.options.collector.apiKey && {
Authorization: `Bearer ${this.options.collector.apiKey}`,
}),
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(this.options.collector.timeout ?? 5000),
});
if (!response.ok) {
throw new Error(`Collector responded with ${response.status}`);
}
if (this.options.debug) {
console.log(`[Analytics] Flushed ${eventsToSend.length} events`);
}
} catch (error) {
// Re-queue events on failure
this.eventQueue = [...eventsToSend, ...this.eventQueue];
if (this.options.debug) {
console.error('[Analytics] Failed to flush events:', error);
}
// Optionally retry based on config
const retries = this.options.collector.retries ?? 0;
if (retries > 0) {
// Implement retry logic if needed
}
}
}
private startFlushTimer(): void {
const maxWait = this.options.batch?.maxWait ?? 10000;
if (maxWait > 0) {
this.flushTimer = setInterval(() => {
this.flush().catch(() => {
// Errors are logged in flush()
});
}, maxWait);
}
}
}

View file

@ -1 +0,0 @@
export { AnalyticsService } from './analytics';