Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions database/add_pipeline_analytics_view.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
-- Pipeline Analytics: Materialized view for efficient latency aggregation
-- Run this in Supabase SQL Editor after setup-supabase.sql
--
-- This view pre-aggregates per-turn pipeline latency metrics (STT, LLM TTFT,
-- TTS TTFB, EOU delay) from soundflare_metrics_logs so the analytics dashboard
-- can query summaries without scanning the full table every time.

-- 1. Create materialized view
CREATE MATERIALIZED VIEW IF NOT EXISTS public.pipeline_analytics_daily AS
SELECT
ml.session_id,
cl.agent_id,
DATE(cl.call_started_at) AS call_date,
EXTRACT(HOUR FROM cl.call_started_at) AS call_hour,

-- STT duration (seconds)
AVG((ml.stt_metrics->>'duration')::numeric)
FILTER (WHERE ml.stt_metrics->>'duration' IS NOT NULL)
AS avg_stt_duration,

-- LLM time-to-first-token (seconds)
AVG((ml.llm_metrics->>'ttft')::numeric)
FILTER (WHERE ml.llm_metrics->>'ttft' IS NOT NULL)
AS avg_llm_ttft,

-- TTS time-to-first-byte (seconds)
AVG((ml.tts_metrics->>'ttfb')::numeric)
FILTER (WHERE ml.tts_metrics->>'ttfb' IS NOT NULL)
AS avg_tts_ttfb,

-- End-of-utterance delay (seconds)
AVG((ml.eou_metrics->>'end_of_utterance_delay')::numeric)
FILTER (WHERE ml.eou_metrics->>'end_of_utterance_delay' IS NOT NULL)
AS avg_eou_delay,

-- Turn count for weighting
COUNT(*) AS turn_count

FROM public.soundflare_metrics_logs ml
JOIN public.soundflare_call_logs cl
ON ml.session_id = cl.id
WHERE cl.call_started_at IS NOT NULL
GROUP BY ml.session_id, cl.agent_id, DATE(cl.call_started_at), EXTRACT(HOUR FROM cl.call_started_at);

-- 2. Unique index (required for CONCURRENTLY refresh)
CREATE UNIQUE INDEX IF NOT EXISTS idx_pipeline_analytics_unique
ON public.pipeline_analytics_daily (session_id, call_date, call_hour);

-- 3. Lookup index for fast agent + date queries
CREATE INDEX IF NOT EXISTS idx_pipeline_analytics_agent_date
ON public.pipeline_analytics_daily (agent_id, call_date);

-- 3. Refresh function (call from cron or API)
CREATE OR REPLACE FUNCTION public.refresh_pipeline_analytics()
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
REFRESH MATERIALIZED VIEW CONCURRENTLY public.pipeline_analytics_daily;
END;
$$;

-- 4. P95 helper — returns the 95th-percentile latency for a date range
CREATE OR REPLACE FUNCTION public.pipeline_p95_latency(
p_agent_id uuid,
p_from date,
p_to date
)
RETURNS numeric
LANGUAGE sql
STABLE
AS $$
SELECT PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY cl.avg_latency)
FROM public.soundflare_call_logs cl
WHERE cl.agent_id = p_agent_id
AND cl.call_started_at >= p_from
AND cl.call_started_at < (p_to + INTERVAL '1 day')
AND cl.avg_latency IS NOT NULL;
$$;

-- 5. RLS policy (mirrors existing call_logs policy)
ALTER MATERIALIZED VIEW public.pipeline_analytics_daily OWNER TO postgres;
82 changes: 82 additions & 0 deletions src/app/[projectid]/analytics/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// src/app/[projectid]/analytics/page.tsx
'use client'

import { useParams, useSearchParams } from 'next/navigation'
import { Suspense } from 'react'
import { PipelineAnalytics } from '@/components/analytics/PipelineAnalytics'
import { Skeleton } from '@/components/ui/skeleton'
import { Card, CardContent } from '@/components/ui/card'

function AnalyticsSkeleton() {
return (
<div className="p-6 max-w-7xl mx-auto space-y-6">
<div className="space-y-2">
<Skeleton className="h-8 w-56" />
<Skeleton className="h-4 w-80" />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{[1, 2, 3, 4].map((i) => (
<Card key={i}>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<Skeleton className="h-10 w-10 rounded-lg" />
<div className="space-y-2">
<Skeleton className="h-3 w-20" />
<Skeleton className="h-7 w-14" />
</div>
</div>
</CardContent>
</Card>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<Skeleton className="h-[280px] rounded-lg" />
<Skeleton className="h-[280px] rounded-lg" />
</div>
</div>
)
}

function AnalyticsContent() {
const params = useParams()
const searchParams = useSearchParams()

const projectId = Array.isArray(params?.projectid)
? params.projectid[0]
: (params?.projectid as string)

// Agent can be specified via query param: ?agent_id=xxx
const agentId = searchParams.get('agent_id') || ''

if (!agentId) {
return (
<div className="p-6 max-w-7xl mx-auto">
<div className="text-center py-20">
<h2 className="text-lg font-semibold mb-2">Select an Agent</h2>
<p className="text-sm text-muted-foreground">
Navigate to an agent&apos;s page and click &quot;Pipeline Analytics&quot; to view
latency and performance metrics.
</p>
<p className="text-xs text-muted-foreground mt-4">
Or append <code className="bg-muted px-1.5 py-0.5 rounded">?agent_id=YOUR_AGENT_ID</code> to
this URL.
</p>
</div>
</div>
)
}

return (
<div className="p-6 max-w-7xl mx-auto">
<PipelineAnalytics agentId={agentId} />
</div>
)
}

export default function AnalyticsPage() {
return (
<Suspense fallback={<AnalyticsSkeleton />}>
<AnalyticsContent />
</Suspense>
)
}
Loading