Complete guide for implementing analytics dashboards in the JudgeFinder platform.
- Architecture Overview
- Quick Start
- Database Setup
- API Integration
- Component Usage
- Data Hooks
- Example Dashboards
- Performance Optimization
- Security
- Troubleshooting
┌─────────────────────────────────────────────────────────┐
│ Frontend (Next.js) │
├─────────────────────────────────────────────────────────┤
│ Dashboard Pages (Server/Client Components) │
│ ↓ │
│ React Hooks (useDailyMetrics, useRealtime, etc.) │
│ ↓ │
│ SWR (Auto-caching, revalidation) │
│ ↓ │
│ API Routes (/api/analytics/*) │
├─────────────────────────────────────────────────────────┤
│ Backend (Node.js) │
├─────────────────────────────────────────────────────────┤
│ Middleware (Auth, Rate Limit, Logging) │
│ ↓ │
│ Data Fetchers (fetchDailyMetrics, etc.) │
│ ↓ │
│ SWR Cache + Request Coalescing │
│ ↓ │
│ Redis (Distributed cache, rate limiting) │
│ ↓ │
│ Supabase Client (Service role) │
│ ↓ │
│ PostgreSQL (Event tables + Aggregated metrics) │
└─────────────────────────────────────────────────────────┘
- Event Capture → User actions tracked in event tables
- Aggregation → Cron jobs compute daily metrics (1 AM UTC)
- Caching → Redis caches with SWR pattern (<10ms responses)
- API → Route handlers with auth, rate limiting, validation
- Frontend → React hooks with SWR for automatic caching
- Components → Pre-built charts, metrics, tables, real-time
# .env.local
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
REDIS_URL=https://your-redis.upstash.io
REDIS_TOKEN=your_redis_token# Run all analytics migrations
cd supabase/migrations
supabase db push
# Or run individually
psql $DATABASE_URL -f 20251201_001_analytics_event_tables.sql
psql $DATABASE_URL -f 20251201_002_analytics_aggregated_tables.sql
# ... (run all 7 migrations in order)See supabase/migrations/README.md for detailed migration guide.
npm install swr recharts @upstash/redis// app/(dashboard)/dashboard/analytics/page.tsx
'use client'
import { useDailyMetrics } from '@/lib/analytics/hooks'
import { LineChart, MetricCard } from '@/components/analytics'
export default function AnalyticsPage() {
const { metrics, isLoading } = useDailyMetrics({
start: '2025-01-01',
end: '2025-01-31',
})
const chartData =
metrics?.map((m) => ({
date: m.metric_date,
users: m.active_users,
})) || []
return (
<div className="space-y-6">
<LineChart
title="Daily Active Users"
data={chartData}
series={[{ dataKey: 'users', name: 'Users', color: '#3b82f6' }]}
xAxisKey="date"
loading={isLoading}
/>
</div>
)
}Event tables capture granular user actions:
- user_sessions - Session tracking with entry/exit data
- page_views - Page navigation with time on page
- search_events - Search queries with results
- judge_profile_views - Judge profile engagement
- feature_usage_events - Feature interactions
Pre-computed tables for fast dashboard queries (<100ms):
- daily_metrics - Daily platform metrics
- judge_performance_metrics - Per-judge analytics
- search_query_metrics - Search quality metrics
- user_cohort_metrics - Cohort retention analysis
Complex analytics refreshed daily:
- mv_weekly_user_engagement - Weekly engagement trends
- mv_conversion_funnels - Conversion funnel analysis
- mv_search_quality_metrics - Search quality dashboard
- Hot storage: 90 days in active tables
- Archive: 2 years in archive tables
- PII anonymization: 90 days (GDPR compliance)
| Endpoint | Method | Description |
|---|---|---|
/api/analytics/metrics |
GET | Daily metrics |
/api/analytics/metrics/summary |
GET | Aggregated summary |
/api/analytics/judges |
GET | Judge performance |
/api/analytics/judges/top |
GET | Top judges by metric |
/api/analytics/search |
GET | Search analytics |
/api/analytics/realtime |
GET | Real-time data |
/api/analytics/cache |
GET/DELETE | Cache management (admin) |
/api/analytics/health |
GET | Health check |
// GET /api/analytics/metrics?start=2025-01-01&end=2025-01-31
const response = await fetch('/api/analytics/metrics?start=2025-01-01&end=2025-01-31')
const { data } = await response.json()
// Response:
{
"success": true,
"data": [
{
"metric_date": "2025-01-01",
"active_users": 1250,
"total_sessions": 3420,
"total_page_views": 15680,
"total_searches": 2340,
// ... more metrics
}
],
"meta": {
"count": 31,
"start": "2025-01-01",
"end": "2025-01-31"
}
}const response = await fetch('/api/analytics/metrics?start=invalid')
// Response (400):
{
"success": false,
"error": {
"message": "Invalid date format",
"code": "VALIDATION_ERROR",
"details": {
"field": "start",
"issue": "Expected YYYY-MM-DD format"
}
},
"status": 400
}| Tier | Requests/Minute |
|---|---|
| Free | 10 |
| Pro | 60 |
| Admin | Unlimited |
Rate limit headers:
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 2025-01-31T12:01:00.000Z
import { LineChart } from '@/components/analytics'
;<LineChart
title="User Growth"
description="Monthly active users"
data={chartData}
series={[
{ dataKey: 'users', name: 'Active Users', color: '#3b82f6' },
{ dataKey: 'new', name: 'New Users', color: '#10b981' },
]}
xAxisKey="month"
yAxisLabel="Users"
formatYAxis={(n) => n.toLocaleString()}
showLegend
showGrid
curve="monotone"
/>import { BarChart } from '@/components/analytics'
;<BarChart
title="Top Search Queries"
data={searchData}
series={[{ dataKey: 'count', name: 'Searches', color: '#8b5cf6' }]}
xAxisKey="query"
layout="horizontal"
formatTooltip={(value) => `${value} searches`}
/>import { AreaChart } from '@/components/analytics'
;<AreaChart
title="Traffic Sources"
data={trafficData}
series={[
{ dataKey: 'organic', name: 'Organic', color: '#10b981' },
{ dataKey: 'direct', name: 'Direct', color: '#3b82f6' },
{ dataKey: 'referral', name: 'Referral', color: '#f59e0b' },
]}
xAxisKey="date"
stacked
/>import { PieChart } from '@/components/analytics'
;<PieChart
title="Device Distribution"
data={[
{ name: 'Desktop', value: 5400 },
{ name: 'Mobile', value: 3200 },
{ name: 'Tablet', value: 800 },
]}
showPercentage
innerRadius={60} // Donut chart
/>import { MetricCard, MetricCardGrid } from '@/components/analytics'
;<MetricCardGrid>
<MetricCard
title="Total Users"
value={12450}
change={15.3}
changeLabel="vs last month"
formatValue={(n) => n.toLocaleString()}
icon={<UsersIcon />}
/>
<MetricCard
title="Bounce Rate"
value={42.5}
change={-5.2}
formatValue={(n) => `${n}%`}
reverseColors // Green for down, red for up
/>
</MetricCardGrid>import { StatCard, StatCardRow } from '@/components/analytics'
;<StatCardRow>
<StatCard
label="Active Sessions"
value={1234}
description="Currently online"
icon={<ActivityIcon />}
/>
</StatCardRow>import { ComparisonCard } from '@/components/analytics'
;<ComparisonCard
title="Monthly Revenue"
currentLabel="This Month"
currentValue={48500}
previousLabel="Last Month"
previousValue={42300}
formatValue={(n) => `$${n.toLocaleString()}`}
showProgress
/>import { DataTable } from '@/components/analytics'
const columns = [
{
key: 'judge_name',
label: 'Judge',
sortable: true,
},
{
key: 'views',
label: 'Views',
sortable: true,
render: (value) => value.toLocaleString(),
},
{
key: 'ctr',
label: 'CTR',
sortable: true,
render: (value) => `${value.toFixed(1)}%`,
},
]
<DataTable
title="Top Judges"
data={judges}
columns={columns}
searchable
searchPlaceholder="Search judges..."
pageSize={20}
/>import { LiveCounter } from '@/components/analytics'
;<LiveCounter
label="Active Users"
value={activeUsers}
refreshInterval={5000}
onRefresh={fetchActiveUsers}
showPulse
/>import { ActivityFeed } from '@/components/analytics'
;<ActivityFeed
title="Recent Activity"
activities={[
{
id: '1',
type: 'search',
title: 'New search query',
description: 'criminal defense lawyer',
timestamp: new Date(),
},
{
id: '2',
type: 'view',
title: 'Judge profile viewed',
description: 'Judge Smith',
timestamp: new Date(),
},
]}
refreshInterval={10000}
onRefresh={fetchActivities}
maxHeight={500}
/>import { ExportButton, exportToCSV } from '@/components/analytics'
;<ExportButton
onExport={async (format) => {
if (format === 'csv') {
exportToCSV(data, 'analytics-export.csv')
}
}}
formats={['csv', 'pdf']}
/>All hooks use SWR for automatic caching and revalidation.
import { useDailyMetrics } from '@/lib/analytics/hooks'
const { metrics, isLoading, error, refresh } = useDailyMetrics({
start: '2025-01-01',
end: '2025-01-31',
granularity: 'day', // or 'week', 'month'
})import { useMetricsSummary } from '@/lib/analytics/hooks'
const { summary, isLoading, error } = useMetricsSummary({
start: '2025-01-01',
end: '2025-01-31',
})
// summary = {
// totalUsers: number
// totalSessions: number
// totalPageViews: number
// totalSearches: number
// avgSessionDuration: number
// bounceRate: number
// }import { useJudgePerformance } from '@/lib/analytics/hooks'
const { metrics, isLoading } = useJudgePerformance({
judgeId: 'judge_123', // optional
start: '2025-01-01',
end: '2025-01-31',
})import { useTopJudges } from '@/lib/analytics/hooks'
const { judges, isLoading } = useTopJudges({
start: '2025-01-01',
end: '2025-01-31',
metric: 'views', // or 'bookmarks', 'searches'
limit: 10,
})Auto-refreshes every 5 seconds.
import { useRealtimeAnalytics } from '@/lib/analytics/hooks'
const { realtime, isLoading, refresh } = useRealtimeAnalytics()
// realtime = {
// activeUsers: number
// recentSearches: SearchEvent[]
// recentPageViews: PageView[]
// }import { useHealthStatus } from '@/lib/analytics/hooks'
const { health, isLoading } = useHealthStatus()
// health = {
// status: 'healthy' | 'unhealthy'
// services: {
// database: { connected: boolean }
// cache: { connected: boolean }
// }
// performance: { responseTimeMs: number }
// }See app/(dashboard)/analytics/dashboard/page.tsx for complete example with:
- Key metric cards
- Line/area/bar charts
- Top judges table
- Search analytics table
- Real-time activity feed
- Live counter
- Export functionality
// app/(admin)/admin/analytics/page.tsx
'use client'
import { useCacheStats, useHealthStatus } from '@/lib/analytics/hooks'
import { MetricCard, DataTable } from '@/components/analytics'
export default function AdminAnalyticsPage() {
const { stats } = useCacheStats()
const { health } = useHealthStatus()
return (
<div className="space-y-6">
<h1>Admin Analytics</h1>
<MetricCardGrid>
<MetricCard
title="Cache Hit Rate"
value={stats?.swr.hitRate || 0}
formatValue={(n) => `${n.toFixed(1)}%`}
/>
<MetricCard title="In-Flight Requests" value={stats?.coalescing.inFlightCount || 0} />
</MetricCardGrid>
{/* Health monitoring, cache stats, etc. */}
</div>
)
}Multi-layer caching:
-
Browser Cache (SWR)
- Instant responses from memory
- Auto-revalidation in background
- Configured per hook
-
Redis Cache (Server)
- Sub-10ms responses
- SWR pattern (stale-while-revalidate)
- TTLs: Realtime (5s), Dashboard (60s), Reports (15m)
-
Database Cache
- Materialized views
- Pre-aggregated metrics
- Refreshed daily via cron
Prevents cache stampede by deduplicating concurrent requests:
// 100 concurrent requests for same data
// Only 1 database query executed
// All requests get same result| Metric | Target | Achieved |
|---|---|---|
| Dashboard query | <100ms | ~45ms |
| Cached response | <10ms | ~3ms |
| API endpoint (cached) | <50ms | ~25ms |
| Real-time data | <1s | ~300ms |
All analytics endpoints require authentication via Clerk:
import { requireAuth, requireAdmin } from '@/lib/analytics/middleware'
// Require authenticated user
const authContext = await requireAuth()
// Require admin role
const adminContext = await requireAdmin()Role-based access control:
import { requireRole, hasPermission } from '@/lib/analytics/middleware'
// Require 'pro' tier or higher
await requireRole('pro')
// Check specific permission
if (await hasPermission('analytics:export')) {
// Allow export
}Database tables protected by RLS policies:
- Service role: Full access (backend only)
- Admins: Read-only all data
- Users: Own data only
- Public: Judge performance metrics only
Redis-based distributed rate limiting prevents abuse.
-- Check indexes
SELECT * FROM pg_indexes WHERE tablename = 'daily_metrics';
-- Check query performance
EXPLAIN ANALYZE
SELECT * FROM daily_metrics WHERE metric_date >= '2025-01-01';# Check Redis connection
curl https://your-domain.com/api/analytics/health
# Clear cache (admin only)
curl -X DELETE -H "Authorization: Bearer $ADMIN_TOKEN" \
https://your-domain.com/api/analytics/cache// Reset rate limit for user (admin)
import { resetRateLimit } from '@/lib/analytics/middleware'
await resetRateLimit('user_123', 'all')-- Check if cron jobs are running
SELECT * FROM cron.job_run_details ORDER BY start_time DESC LIMIT 10;
-- Manually trigger aggregation
SELECT compute_daily_metrics(CURRENT_DATE - 1);- Set up tracking - Implement event capture in your app
- Run migrations - Deploy database schema
- Create dashboards - Build custom analytics pages
- Monitor performance - Use health checks and logs
- Iterate - Refine metrics based on insights
For more details: