Files
eventify_command_center/src/lib/ads/tracking.ts

195 lines
6.2 KiB
TypeScript

// Ad Tracking Engine — Impression/Click recording, deduplication, rollups
import type { AdTrackingEvent, PlacementDailyStats } from '@/lib/types/ads';
import type { SurfaceKey } from '@/lib/types/ad-control';
const TRACKING_KEY = 'ad_tracking_events';
const DAILY_STATS_KEY = 'ad_daily_stats';
const DEDUPE_WINDOW_MS = 30_000; // 30 seconds
// --- Persistence ---
function getTrackingStore(): AdTrackingEvent[] {
if (typeof window === 'undefined') return [];
try { return JSON.parse(localStorage.getItem(TRACKING_KEY) || '[]'); }
catch { return []; }
}
function saveTrackingStore(events: AdTrackingEvent[]) {
if (typeof window === 'undefined') return;
// Cap at 5000 to avoid localStorage limits
localStorage.setItem(TRACKING_KEY, JSON.stringify(events.slice(-5000)));
}
function getDailyStatsStore(): PlacementDailyStats[] {
if (typeof window === 'undefined') return [];
try { return JSON.parse(localStorage.getItem(DAILY_STATS_KEY) || '[]'); }
catch { return []; }
}
function saveDailyStatsStore(stats: PlacementDailyStats[]) {
if (typeof window === 'undefined') return;
localStorage.setItem(DAILY_STATS_KEY, JSON.stringify(stats));
}
// --- Deduplication ---
function isDuplicate(events: AdTrackingEvent[], newEvent: Omit<AdTrackingEvent, 'id'>): boolean {
const cutoff = new Date(new Date(newEvent.timestamp).getTime() - DEDUPE_WINDOW_MS).toISOString();
return events.some(e =>
e.type === newEvent.type &&
e.placementId === newEvent.placementId &&
e.anonId === newEvent.anonId &&
e.timestamp > cutoff
);
}
// --- Record Events ---
export function recordImpression(data: {
placementId: string;
campaignId: string;
surfaceKey: SurfaceKey;
eventId: string;
userId?: string | null;
anonId: string;
sessionId: string;
device?: string;
cityId?: string;
}): { success: boolean; deduplicated?: boolean } {
const store = getTrackingStore();
const now = new Date().toISOString();
const event: Omit<AdTrackingEvent, 'id'> = {
type: 'IMPRESSION',
placementId: data.placementId,
campaignId: data.campaignId,
surfaceKey: data.surfaceKey,
eventId: data.eventId,
userId: data.userId || null,
anonId: data.anonId,
sessionId: data.sessionId,
timestamp: now,
device: data.device || 'unknown',
cityId: data.cityId || 'unknown',
};
if (isDuplicate(store, event)) {
return { success: true, deduplicated: true };
}
const fullEvent: AdTrackingEvent = {
...event,
id: `te-imp-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
};
store.push(fullEvent);
saveTrackingStore(store);
return { success: true };
}
export function recordClick(data: {
placementId: string;
campaignId: string;
surfaceKey: SurfaceKey;
eventId: string;
userId?: string | null;
anonId: string;
sessionId: string;
device?: string;
cityId?: string;
}): { success: boolean } {
const store = getTrackingStore();
const now = new Date().toISOString();
const fullEvent: AdTrackingEvent = {
id: `te-clk-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
type: 'CLICK',
placementId: data.placementId,
campaignId: data.campaignId,
surfaceKey: data.surfaceKey,
eventId: data.eventId,
userId: data.userId || null,
anonId: data.anonId,
sessionId: data.sessionId,
timestamp: now,
device: data.device || 'unknown',
cityId: data.cityId || 'unknown',
};
store.push(fullEvent);
saveTrackingStore(store);
return { success: true };
}
// --- Queries ---
export function getTrackingEvents(campaignId?: string, type?: 'IMPRESSION' | 'CLICK'): AdTrackingEvent[] {
let events = getTrackingStore();
if (campaignId) events = events.filter(e => e.campaignId === campaignId);
if (type) events = events.filter(e => e.type === type);
return events;
}
export function getUserImpressionCount(anonId: string, campaignId: string, date: string): number {
const events = getTrackingStore();
return events.filter(e =>
e.type === 'IMPRESSION' &&
e.anonId === anonId &&
e.campaignId === campaignId &&
e.timestamp.startsWith(date)
).length;
}
export function getTodaySpend(campaignId: string, cpmRate: number): number {
const today = new Date().toISOString().slice(0, 10);
const impressions = getTrackingStore().filter(e =>
e.type === 'IMPRESSION' &&
e.campaignId === campaignId &&
e.timestamp.startsWith(today)
).length;
return Number(((impressions / 1000) * cpmRate).toFixed(2));
}
// --- Daily Stats Rollup ---
export function computeDailyStats(campaignId: string, billingModel: string, bid: number): PlacementDailyStats[] {
const events = getTrackingStore().filter(e => e.campaignId === campaignId);
// Group by date + surfaceKey
const buckets: Record<string, { impressions: number; clicks: number; surfaceKey: SurfaceKey; placementId: string }> = {};
for (const e of events) {
const date = e.timestamp.slice(0, 10);
const key = `${date}|${e.surfaceKey}`;
if (!buckets[key]) {
buckets[key] = { impressions: 0, clicks: 0, surfaceKey: e.surfaceKey, placementId: e.placementId };
}
if (e.type === 'IMPRESSION') buckets[key].impressions++;
else buckets[key].clicks++;
}
return Object.entries(buckets).map(([key, data]) => {
const [date] = key.split('|');
let spend = 0;
if (billingModel === 'CPM') spend = (data.impressions / 1000) * bid;
else if (billingModel === 'CPC') spend = data.clicks * bid;
// FIXED: spread evenly (simplified)
else spend = bid / 30; // assume 30-day campaign
const ctr = data.impressions > 0 ? data.clicks / data.impressions : 0;
return {
id: `ds-${key}`,
campaignId,
placementId: data.placementId,
surfaceKey: data.surfaceKey,
date,
impressions: data.impressions,
clicks: data.clicks,
ctr: Number(ctr.toFixed(4)),
spend: Number(spend.toFixed(2)),
};
}).sort((a, b) => a.date.localeCompare(b.date));
}