"use client"; import { useEffect, type RefObject } from "react"; export type AgentStyleSection = | "brand" | "style" | "mode" | "materials" | "generate" | "references" | "results" | "unknown"; export type AgentStylePerformanceEvent = { eventType: string; durationMs: number; blockingDurationMs?: number; section: AgentStyleSection; action?: string; domNodeCount?: number; visibleImageCount?: number; loadedImageCount?: number; viewport?: { width: number; height: number; dpr: number; }; browser?: string; path?: string; projectId?: string | null; chatId?: string | null; timestamp: string; }; type UseAgentStylePerformanceMonitorParams = { rootRef: RefObject; chatId?: string | null; projectId?: string | null; }; const DEFAULT_SAMPLE_RATE = 0.2; const SLOW_FRAME_THRESHOLD_MS = 120; const MAX_EVENTS_PER_MINUTE = 20; function getBooleanEnvFlag(value: string | undefined, fallback: boolean) { if (value === undefined || value === "") return fallback; return !["0", "false", "off", "no"].includes(value.toLowerCase()); } function getSampleRate() { const raw = Number(process.env.NEXT_PUBLIC_AGENT_STYLE_PERF_SAMPLE_RATE); if (!Number.isFinite(raw)) return DEFAULT_SAMPLE_RATE; return Math.min(1, Math.max(0, raw)); } function getBrowserLabel() { const ua = navigator.userAgent || ""; if (/Edg\//.test(ua)) return "edge"; if (/Chrome\//.test(ua)) return "chrome"; if (/Safari\//.test(ua) && !/Chrome\//.test(ua)) return "safari"; if (/Firefox\//.test(ua)) return "firefox"; return "unknown"; } function resolveSection(target: EventTarget | null): AgentStyleSection { if (!(target instanceof Element)) return "unknown"; const element = target.closest("[data-agent-style-section]"); const value = element?.dataset.agentStyleSection || "unknown"; if ( value === "brand" || value === "style" || value === "mode" || value === "materials" || value === "generate" || value === "references" || value === "results" ) { return value; } return "unknown"; } function resolveAction(target: EventTarget | null, fallback: string) { if (!(target instanceof Element)) return fallback; const explicit = target.closest("[data-agent-style-action]")?.dataset .agentStyleAction; if (explicit) return explicit.slice(0, 64); const element = target.closest("button,input,textarea,select,[role='button']"); if (!element) return fallback; if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) { return `${fallback}:${element.tagName.toLowerCase()}`; } return `${fallback}:${String(element.textContent || element.ariaLabel || element.tagName) .trim() .slice(0, 32)}`; } function collectCounts(root: HTMLElement) { const domNodeCount = root.querySelectorAll("*").length; const images = Array.from(root.querySelectorAll("img")); let visibleImageCount = 0; let loadedImageCount = 0; for (const image of images) { const rect = image.getBoundingClientRect(); const visible = rect.bottom >= 0 && rect.right >= 0 && rect.top <= window.innerHeight && rect.left <= window.innerWidth; if (visible) visibleImageCount += 1; if (image.complete && image.naturalWidth > 0) loadedImageCount += 1; } return { domNodeCount, visibleImageCount, loadedImageCount, }; } function postEvent(event: AgentStylePerformanceEvent) { const body = JSON.stringify(event); if (navigator.sendBeacon) { const blob = new Blob([body], { type: "application/json" }); if (navigator.sendBeacon("/api/client-performance/agent-style", blob)) { return; } } void fetch("/api/client-performance/agent-style", { method: "POST", headers: { "Content-Type": "application/json" }, body, keepalive: true, }).catch(() => {}); } export function useAgentStylePerformanceMonitor({ rootRef, chatId, projectId, }: UseAgentStylePerformanceMonitorParams) { useEffect(() => { if ( !getBooleanEnvFlag(process.env.NEXT_PUBLIC_AGENT_STYLE_PERF_MONITOR, true) || Math.random() > getSampleRate() ) { return; } const root = rootRef.current; if (!root || typeof PerformanceObserver === "undefined") return; let eventCount = 0; let minuteWindowStart = Date.now(); let lastSection: AgentStyleSection = "unknown"; let lastAction = "idle"; const browser = getBrowserLabel(); const markAction = (event: Event) => { lastSection = resolveSection(event.target); lastAction = resolveAction(event.target, event.type); }; root.addEventListener("click", markAction, true); root.addEventListener("input", markAction, true); root.addEventListener("change", markAction, true); root.addEventListener("keydown", markAction, true); root.addEventListener("scroll", markAction, true); const trySend = (entry: PerformanceEntry, eventType: string) => { const now = Date.now(); if (now - minuteWindowStart >= 60_000) { minuteWindowStart = now; eventCount = 0; } if (eventCount >= MAX_EVENTS_PER_MINUTE || entry.duration < SLOW_FRAME_THRESHOLD_MS) { return; } eventCount += 1; const anyEntry = entry as PerformanceEntry & { blockingDuration?: number; renderStart?: number; styleAndLayoutStart?: number; }; const counts = collectCounts(root); postEvent({ eventType, durationMs: Math.round(entry.duration), blockingDurationMs: Math.round(anyEntry.blockingDuration || 0), section: lastSection, action: lastAction, ...counts, viewport: { width: window.innerWidth, height: window.innerHeight, dpr: window.devicePixelRatio || 1, }, browser, path: window.location.pathname, projectId: projectId || null, chatId: chatId || null, timestamp: new Date().toISOString(), }); }; const observers: PerformanceObserver[] = []; const supported = PerformanceObserver.supportedEntryTypes || []; if (supported.includes("long-animation-frame")) { const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { trySend(entry, "long-animation-frame"); } }); observer.observe({ type: "long-animation-frame", buffered: true }); observers.push(observer); } if (supported.includes("longtask")) { const observer = new PerformanceObserver((list) => { for (const entry of list.getEntries()) { trySend(entry, "longtask"); } }); observer.observe({ type: "longtask", buffered: true }); observers.push(observer); } return () => { for (const observer of observers) { observer.disconnect(); } root.removeEventListener("click", markAction, true); root.removeEventListener("input", markAction, true); root.removeEventListener("change", markAction, true); root.removeEventListener("keydown", markAction, true); root.removeEventListener("scroll", markAction, true); }; }, [chatId, projectId, rootRef]); }