// 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): 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 = { 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 = {}; 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)); }