Skip to content

Latest commit

 

History

History
737 lines (583 loc) · 17.5 KB

File metadata and controls

737 lines (583 loc) · 17.5 KB

Analytics Implementation Guide

Complete guide for implementing analytics dashboards in the JudgeFinder platform.

Table of Contents

Architecture Overview

┌─────────────────────────────────────────────────────────┐
│                    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)         │
└─────────────────────────────────────────────────────────┘

Data Flow

  1. Event Capture → User actions tracked in event tables
  2. Aggregation → Cron jobs compute daily metrics (1 AM UTC)
  3. Caching → Redis caches with SWR pattern (<10ms responses)
  4. API → Route handlers with auth, rate limiting, validation
  5. Frontend → React hooks with SWR for automatic caching
  6. Components → Pre-built charts, metrics, tables, real-time

Quick Start

1. Environment Setup

# .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

2. Database Migrations

# 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.

3. Install Dependencies

npm install swr recharts @upstash/redis

4. Create Your First Dashboard

// 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>
  )
}

Database Setup

Event Tables

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

Aggregated Metrics

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

Materialized Views

Complex analytics refreshed daily:

  • mv_weekly_user_engagement - Weekly engagement trends
  • mv_conversion_funnels - Conversion funnel analysis
  • mv_search_quality_metrics - Search quality dashboard

Data Retention

  • Hot storage: 90 days in active tables
  • Archive: 2 years in archive tables
  • PII anonymization: 90 days (GDPR compliance)

API Integration

Available Endpoints

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

Request Example

// 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"
  }
}

Error Handling

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
}

Rate Limits

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

Component Usage

Charts

Line Chart

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"
/>

Bar Chart

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`}
/>

Area Chart

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
/>

Pie Chart

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
/>

Metrics

Metric Card

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>

Stat Card

import { StatCard, StatCardRow } from '@/components/analytics'
;<StatCardRow>
  <StatCard
    label="Active Sessions"
    value={1234}
    description="Currently online"
    icon={<ActivityIcon />}
  />
</StatCardRow>

Comparison Card

import { ComparisonCard } from '@/components/analytics'
;<ComparisonCard
  title="Monthly Revenue"
  currentLabel="This Month"
  currentValue={48500}
  previousLabel="Last Month"
  previousValue={42300}
  formatValue={(n) => `$${n.toLocaleString()}`}
  showProgress
/>

Tables

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}
/>

Real-time Components

Live Counter

import { LiveCounter } from '@/components/analytics'
;<LiveCounter
  label="Active Users"
  value={activeUsers}
  refreshInterval={5000}
  onRefresh={fetchActiveUsers}
  showPulse
/>

Activity Feed

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}
/>

Export

import { ExportButton, exportToCSV } from '@/components/analytics'
;<ExportButton
  onExport={async (format) => {
    if (format === 'csv') {
      exportToCSV(data, 'analytics-export.csv')
    }
  }}
  formats={['csv', 'pdf']}
/>

Data Hooks

All hooks use SWR for automatic caching and revalidation.

useDailyMetrics

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'
})

useMetricsSummary

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
// }

useJudgePerformance

import { useJudgePerformance } from '@/lib/analytics/hooks'

const { metrics, isLoading } = useJudgePerformance({
  judgeId: 'judge_123', // optional
  start: '2025-01-01',
  end: '2025-01-31',
})

useTopJudges

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,
})

useRealtimeAnalytics

Auto-refreshes every 5 seconds.

import { useRealtimeAnalytics } from '@/lib/analytics/hooks'

const { realtime, isLoading, refresh } = useRealtimeAnalytics()

// realtime = {
//   activeUsers: number
//   recentSearches: SearchEvent[]
//   recentPageViews: PageView[]
// }

useHealthStatus

import { useHealthStatus } from '@/lib/analytics/hooks'

const { health, isLoading } = useHealthStatus()

// health = {
//   status: 'healthy' | 'unhealthy'
//   services: {
//     database: { connected: boolean }
//     cache: { connected: boolean }
//   }
//   performance: { responseTimeMs: number }
// }

Example Dashboards

Basic Dashboard

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

Admin Dashboard

// 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>
  )
}

Performance Optimization

Caching Strategy

Multi-layer caching:

  1. Browser Cache (SWR)

    • Instant responses from memory
    • Auto-revalidation in background
    • Configured per hook
  2. Redis Cache (Server)

    • Sub-10ms responses
    • SWR pattern (stale-while-revalidate)
    • TTLs: Realtime (5s), Dashboard (60s), Reports (15m)
  3. Database Cache

    • Materialized views
    • Pre-aggregated metrics
    • Refreshed daily via cron

Request Coalescing

Prevents cache stampede by deduplicating concurrent requests:

// 100 concurrent requests for same data
// Only 1 database query executed
// All requests get same result

Performance Targets

Metric Target Achieved
Dashboard query <100ms ~45ms
Cached response <10ms ~3ms
API endpoint (cached) <50ms ~25ms
Real-time data <1s ~300ms

Security

Authentication

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()

Authorization

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
}

Row Level Security

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

Rate Limiting

Redis-based distributed rate limiting prevents abuse.

Troubleshooting

Slow Queries

-- 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';

Cache Issues

# 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

Rate Limit Exceeded

// Reset rate limit for user (admin)
import { resetRateLimit } from '@/lib/analytics/middleware'
await resetRateLimit('user_123', 'all')

Missing Data

-- 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);

Next Steps

  1. Set up tracking - Implement event capture in your app
  2. Run migrations - Deploy database schema
  3. Create dashboards - Build custom analytics pages
  4. Monitor performance - Use health checks and logs
  5. Iterate - Refine metrics based on insights

For more details: