-- ===================================================== -- USER LOGS MIGRATION -- Run this in: Supabase Dashboard → SQL Editor → New query -- ===================================================== -- Table 1: user_logs -- Records every chat message sent through the AI. CREATE TABLE IF NOT EXISTS public.user_logs ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, user_id UUID REFERENCES auth.users(id) NOT NULL, query TEXT, route TEXT, -- 'sql_query' | 'rag_retrieval' | 'tavily_search' sql_query TEXT, -- generated SQL (only for sql_query route) answer TEXT, -- truncated bot reply (up to 8 000 chars) answer_length INTEGER, -- character count of the full bot reply total_tokens INTEGER, prompt_tokens INTEGER, completion_tokens INTEGER, has_error BOOLEAN DEFAULT FALSE, duration_ms INTEGER, -- total end-to-end request duration (ms) -- Observability & eval fields trace_id UUID, -- unique ID per request for distributed tracing session_id TEXT, -- client session UUID for conversation continuity routing_confidence NUMERIC(5,3), -- 0–1 keyword-density score that drove agent selection eval_score NUMERIC(5,3), -- 0–1 answer quality score from AnswerEvalHook created_at TIMESTAMPTZ DEFAULT NOW() ); ALTER TABLE public.user_logs ENABLE ROW LEVEL SECURITY; -- Users can read their own chat logs (e.g. history view in the dashboard). -- The backend uses the service role key and bypasses RLS on INSERT. CREATE POLICY "Users can view their own logs" ON public.user_logs FOR SELECT USING (auth.uid() = user_id); -- Index for fast per-user analytics queries CREATE INDEX IF NOT EXISTS idx_user_logs_user_id ON public.user_logs (user_id); CREATE INDEX IF NOT EXISTS idx_user_logs_created_at ON public.user_logs (created_at DESC); -- trace_id identifies exactly one chat turn; unique so view joins stay 1:1. DROP INDEX IF EXISTS idx_user_logs_trace_id; CREATE UNIQUE INDEX IF NOT EXISTS uq_user_logs_trace_id ON public.user_logs (trace_id) WHERE trace_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_user_logs_session_id ON public.user_logs (session_id); -- ===================================================== -- ALTER scripts: add new columns to an existing table -- Safe to run even if the table already exists. -- ===================================================== ALTER TABLE public.user_logs ADD COLUMN IF NOT EXISTS answer TEXT; ALTER TABLE public.user_logs ADD COLUMN IF NOT EXISTS trace_id UUID; ALTER TABLE public.user_logs ADD COLUMN IF NOT EXISTS session_id TEXT; ALTER TABLE public.user_logs ADD COLUMN IF NOT EXISTS routing_confidence NUMERIC(5,3); ALTER TABLE public.user_logs ADD COLUMN IF NOT EXISTS eval_score NUMERIC(5,3); -- ===================================================== -- Table 2: user_login_logs -- Records every time a user signs in from the frontend. -- ===================================================== CREATE TABLE IF NOT EXISTS public.user_login_logs ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, user_id UUID REFERENCES auth.users(id) NOT NULL, ip_address TEXT, user_agent TEXT, login_method TEXT, login_at TIMESTAMPTZ DEFAULT NOW() ); ALTER TABLE public.user_login_logs ENABLE ROW LEVEL SECURITY; CREATE POLICY "Users can view their own login logs" ON public.user_login_logs FOR SELECT USING (auth.uid() = user_id); CREATE INDEX IF NOT EXISTS idx_user_login_logs_user_id ON public.user_login_logs (user_id); -- Backfill columns that may have been added after table creation ALTER TABLE public.user_login_logs ADD COLUMN IF NOT EXISTS ip_address TEXT; ALTER TABLE public.user_login_logs ADD COLUMN IF NOT EXISTS user_agent TEXT; ALTER TABLE public.user_login_logs ADD COLUMN IF NOT EXISTS login_method TEXT; -- ===================================================== -- Table 3: feedback_logs -- Records thumbs-up / thumbs-down ratings per chat turn. -- Joined to user_logs via trace_id for eval analysis. -- ===================================================== CREATE TABLE IF NOT EXISTS public.feedback_logs ( id UUID DEFAULT gen_random_uuid() PRIMARY KEY, user_id UUID REFERENCES auth.users(id) NOT NULL, trace_id UUID NOT NULL, rating SMALLINT NOT NULL CHECK (rating IN (-1, 1)), comment TEXT CHECK (char_length(comment) <= 500), created_at TIMESTAMPTZ DEFAULT NOW() ); ALTER TABLE public.feedback_logs ENABLE ROW LEVEL SECURITY; -- Users can read their own feedback; backend inserts via service role. CREATE POLICY "Users can view their own feedback" ON public.feedback_logs FOR SELECT USING (auth.uid() = user_id); CREATE INDEX IF NOT EXISTS idx_feedback_logs_trace_id ON public.feedback_logs (trace_id); CREATE INDEX IF NOT EXISTS idx_feedback_logs_created_at ON public.feedback_logs (created_at DESC); -- One rating per user per chat turn; the backend upserts on re-rating. -- Also serves as the per-user lookup index. CREATE UNIQUE INDEX IF NOT EXISTS uq_feedback_logs_user_trace ON public.feedback_logs (user_id, trace_id); -- ===================================================== -- Convenience view: eval summary per route -- Useful for a quick health-check dashboard. -- ===================================================== -- security_invoker: run with the caller's permissions so RLS on the -- underlying tables applies (otherwise the view bypasses RLS entirely). CREATE OR REPLACE VIEW public.v_eval_summary WITH (security_invoker = true) AS SELECT route, COUNT(*) AS total_requests, ROUND(AVG(eval_score)::NUMERIC, 3) AS avg_eval_score, ROUND(AVG(routing_confidence)::NUMERIC,3) AS avg_routing_confidence, ROUND(AVG(duration_ms)) AS avg_duration_ms, SUM(CASE WHEN has_error THEN 1 ELSE 0 END) AS error_count, SUM(total_tokens) AS total_tokens_used, DATE_TRUNC('day', created_at) AS day FROM public.user_logs GROUP BY route, DATE_TRUNC('day', created_at); -- ===================================================== -- Convenience view: feedback join with eval scores -- ===================================================== -- DROP first: CREATE OR REPLACE cannot reorder/insert view columns. DROP VIEW IF EXISTS public.v_feedback_with_eval; CREATE VIEW public.v_feedback_with_eval WITH (security_invoker = true) AS SELECT f.id, f.user_id, f.trace_id, f.rating, f.comment, f.created_at AS rated_at, l.route, l.query, l.session_id, l.has_error, l.eval_score, l.routing_confidence, l.duration_ms, l.total_tokens, f.created_at - l.created_at AS time_to_feedback, -- Rows where the human rating and the automated eval disagree. (f.rating = -1 AND l.eval_score >= 0.8) OR (f.rating = 1 AND l.eval_score <= 0.3) AS eval_disagreement FROM public.feedback_logs f LEFT JOIN public.user_logs l ON l.trace_id = f.trace_id;