All files / app/api/analytics/track route.ts

98.24% Statements 56/57
88.88% Branches 24/27
100% Functions 3/3
98.24% Lines 56/57

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 831x     1x     1x 1x 1x   123x 123x 123x   123x 3x 3x 3x   123x 1x 1x   119x 119x 119x   21x 21x 21x   123x 123x   123x 123x 1x       123x 1x 1x     122x 23x   123x 1x 1x   22x     123x 123x     123x 21x 21x 21x 123x 123x 123x 123x 123x   123x 1x   1x 1x   20x 123x 100x   100x 100x 123x  
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { trackEventSchema } from '@/lib/validations/analytics';
import { createHash } from 'crypto';
 
// Rate limit: 100 requests per minute per IP
const rateLimitMap = new Map<string, { count: number; timestamp: number }>();
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
const RATE_LIMIT_MAX = 100;
 
function isRateLimited(ip: string): boolean {
  const now = Date.now();
  const record = rateLimitMap.get(ip);
 
  if (!record || now - record.timestamp > RATE_LIMIT_WINDOW) {
    rateLimitMap.set(ip, { count: 1, timestamp: now });
    return false;
  }
 
  if (record.count >= RATE_LIMIT_MAX) {
    return true;
  }
 
  record.count++;
  return false;
}
 
function hashIP(ip: string): string {
  return createHash('sha256').update(ip).digest('hex').slice(0, 16);
}
 
export async function POST(request: NextRequest) {
  try {
    // Get IP for rate limiting and hashing
    const ip =
      request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
      request.headers.get('x-real-ip') ||
      'unknown';
 
    // Rate limit check
    if (isRateLimited(ip)) {
      return NextResponse.json({ success: false, error: 'Rate limit exceeded' }, { status: 429 });
    }
 
    // Parse and validate body
    const body = await request.json();
    const validation = trackEventSchema.safeParse(body);
 
    if (!validation.success) {
      return NextResponse.json({ success: false, error: 'Invalid request body' }, { status: 400 });
    }
 
    const { profile_id, event_type, meta } = validation.data;
 
    // Get user agent and referrer
    const userAgent = request.headers.get('user-agent') || null;
    const referrer = (meta?.referrer as string) || request.headers.get('referer') || null;
 
    // Insert analytics event
    const supabase = await createClient();
    const { error } = await supabase.from('analytics').insert({
      profile_id,
      event_type,
      meta: meta || {},
      ip_hash: hashIP(ip),
      user_agent: userAgent,
      referrer: referrer,
    });
 
    if (error) {
      console.error('[Analytics] Insert error:', error);
      // Don't expose internal errors
      return NextResponse.json({ success: true });
    }
 
    return NextResponse.json({ success: true });
  } catch (error) {
    console.error('[Analytics] Error:', error);
    // Always return success to not break the client
    return NextResponse.json({ success: true });
  }
}