/** * Classe para tracking de telemetria no marketplace * Gerencia eventos client-side com debouncing e batch sending */ type TelemetryEventType = | "ad_view" | "ad_click" | "ad_exit" | "search" | "filter" | "favorite" | "unfavorite" | "purchase" | "purchase_intention_created" | "purchase_intention_viewed" | "bid_placed" | "bid_viewed" | "seller_profile_viewed" | "inquiry" | "page_view" | "price_change"; interface TelemetryEvent { event_type: TelemetryEventType; ad_id?: number | null; session_id?: string; metadata?: Record; } interface PendingEvent extends TelemetryEvent { timestamp: number; } export class MarketTelemetry { private sessionId: string; private userId?: number; private eventQueue: PendingEvent[] = []; private viewStartTimes: Map = new Map(); private batchInterval: NodeJS.Timeout | null = null; private readonly BATCH_INTERVAL_MS = 30000; // 30 segundos private readonly MAX_BATCH_SIZE = 100; constructor(userId?: number) { this.userId = userId; this.sessionId = this.generateSessionId(); this.startBatchInterval(); this.setupPageUnloadHandler(); } /** * Gera um ID de sessão único */ private generateSessionId(): string { if (typeof window === "undefined") return ""; // Tentar recuperar da sessionStorage let sessionId = sessionStorage.getItem("telemetry_session_id"); if (sessionId) return sessionId; // Gerar novo ID sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; sessionStorage.setItem("telemetry_session_id", sessionId); return sessionId; } /** * Inicia tracking de visualização de anúncio */ trackAdView(adId: number): void { if (typeof window === "undefined") return; const startTime = Date.now(); this.viewStartTimes.set(adId, startTime); this.queueEvent({ event_type: "ad_view", ad_id: adId, metadata: { timestamp: startTime, }, }); } /** * Finaliza tracking de visualização com duração */ trackAdExit(adId: number): void { if (typeof window === "undefined") return; const startTime = this.viewStartTimes.get(adId); if (!startTime) return; const duration = (Date.now() - startTime) / 1000; // em segundos this.viewStartTimes.delete(adId); this.queueEvent({ event_type: "ad_exit", ad_id: adId, metadata: { duration, timestamp: Date.now(), }, }); } /** * Registra clique em anúncio */ trackAdClick(adId: number, position?: number): void { this.queueEvent({ event_type: "ad_click", ad_id: adId, metadata: { position, timestamp: Date.now(), }, }); } /** * Registra busca */ trackSearch(query: string, filters?: Record): void { this.queueEvent({ event_type: "search", metadata: { query, filters, timestamp: Date.now(), }, }); } /** * Registra aplicação de filtros */ trackFilterApplied(filters: Record): void { this.queueEvent({ event_type: "filter", metadata: { filters, timestamp: Date.now(), }, }); } /** * Registra favoritar/desfavoritar */ trackFavorite(adId: number, action: "favorite" | "unfavorite"): void { this.queueEvent({ event_type: action, ad_id: adId, metadata: { timestamp: Date.now(), }, }); } /** * Registra compra */ trackPurchase(adId: number, price: number): void { this.queueEvent({ event_type: "purchase", ad_id: adId, metadata: { price, timestamp: Date.now(), }, }); // Enviar imediatamente para eventos críticos this.flushQueue(); } /** * Registra inquiry (mensagem ao vendedor) */ trackInquiry(adId: number): void { this.queueEvent({ event_type: "inquiry", ad_id: adId, metadata: { timestamp: Date.now(), }, }); } /** * Registra visualização de página */ trackPageView(page: string): void { this.queueEvent({ event_type: "page_view", metadata: { page, timestamp: Date.now(), }, }); } /** * Registra evento customizado (para eventos que não têm método específico) */ trackCustomEvent( eventType: TelemetryEventType | string, adId?: number, metadata?: Record, ): void { this.queueEvent({ event_type: eventType as TelemetryEventType, ad_id: adId, metadata: { ...metadata, timestamp: Date.now(), }, }); } /** * Adiciona evento à fila */ private queueEvent(event: TelemetryEvent): void { if (typeof window === "undefined") return; this.eventQueue.push({ ...event, session_id: this.sessionId, timestamp: Date.now(), }); // Enviar imediatamente se a fila estiver cheia if (this.eventQueue.length >= this.MAX_BATCH_SIZE) { this.flushQueue(); } } /** * Envia eventos em batch para o servidor */ async flushQueue(): Promise { if (this.eventQueue.length === 0) return; const eventsToSend = [...this.eventQueue]; this.eventQueue = []; try { const response = await fetch("/api/telemetry", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ events: eventsToSend.map(({ timestamp, ...event }) => ({ ...event, session_id: this.sessionId, })), }), }); if (!response.ok) { // Re-adicionar eventos à fila se falhar (mas não logar erro para não poluir console) this.eventQueue.unshift(...eventsToSend); // Silenciar erro para não gerar mensagens no console } } catch (_error) { // Re-adicionar eventos à fila se falhar (mas não logar erro) this.eventQueue.unshift(...eventsToSend); // Silenciar erro para não gerar mensagens no console } } /** * Inicia intervalo de envio automático */ private startBatchInterval(): void { if (typeof window === "undefined") return; this.batchInterval = setInterval(() => { this.flushQueue(); }, this.BATCH_INTERVAL_MS); } /** * Para o intervalo de envio automático */ stop(): void { if (this.batchInterval) { clearInterval(this.batchInterval); this.batchInterval = null; } // Enviar eventos pendentes antes de parar this.flushQueue(); } /** * Configura handler para enviar eventos antes de sair da página */ private setupPageUnloadHandler(): void { if (typeof window === "undefined") return; // Navigator.sendBeacon é mais confiável para page unload window.addEventListener("beforeunload", () => { if (this.eventQueue.length === 0) return; try { const eventsToSend = this.eventQueue.map( ({ timestamp, ...event }) => event, ); // Usar sendBeacon para garantir envio const eventsWithSession = eventsToSend.map((event) => ({ ...event, session_id: this.sessionId, })); // Usar Blob para evitar problemas de message channel const blob = new Blob([JSON.stringify({ events: eventsWithSession })], { type: "application/json", }); if (navigator.sendBeacon) { navigator.sendBeacon("/api/telemetry", blob); } } catch (_error) { // Silenciar erro para não gerar mensagens no console } }); // Fallback para visibilitychange document.addEventListener("visibilitychange", () => { if (document.visibilityState === "hidden") { this.flushQueue(); } }); } /** * Atualiza user_id (útil após login) */ setUserId(userId: number | undefined): void { this.userId = userId; } } // Instância singleton let telemetryInstance: MarketTelemetry | null = null; /** * Obtém ou cria instância singleton de telemetria */ export function getTelemetry(userId?: number): MarketTelemetry { if (!telemetryInstance) { telemetryInstance = new MarketTelemetry(userId); } else if (userId !== undefined) { telemetryInstance.setUserId(userId); } return telemetryInstance; }