All files / src/app/api/rum/collect route.ts

0% Statements 0/145
100% Branches 0/0
0% Functions 0/1
0% Lines 0/145

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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146                                                                                                                                                                                                                                                                                                   
/**
 * RUM Collection API
 *
 * Receives Real User Monitoring data from the client and stores it for analysis.
 */

import { NextRequest, NextResponse } from 'next/server';
import { prisma } from '@/lib/prisma';
import { logger } from '@/lib/logging';

export const runtime = 'nodejs';

// Rate limiting: simple in-memory tracking
const rateLimitMap = new Map<string, { count: number; timestamp: number }>();
const RATE_LIMIT_WINDOW_MS = 60000; // 1 minute
const RATE_LIMIT_MAX_REQUESTS = 100; // 100 requests per minute per session

/**
 * POST /api/rum/collect
 *
 * Collect RUM events from the client
 */
export async function POST(request: NextRequest): Promise<NextResponse> {
  try {
    const body = await request.json();
    const events = body.events;

    if (!Array.isArray(events) || events.length === 0) {
      return NextResponse.json(
        { success: false, error: 'No events provided' },
        { status: 400 }
      );
    }

    // Get session ID for rate limiting
    const sessionId = events[0]?.sessionId;
    if (!sessionId) {
      return NextResponse.json(
        { success: false, error: 'Session ID required' },
        { status: 400 }
      );
    }

    // Check rate limit
    if (!checkRateLimit(sessionId)) {
      return NextResponse.json(
        { success: false, error: 'Rate limit exceeded' },
        { status: 429 }
      );
    }

    // Store events
    const storedCount = await storeEvents(events);

    return NextResponse.json({
      success: true,
      stored: storedCount,
    });
  } catch (error) {
    logger.error('RUM collection error', error as Error);

    return NextResponse.json(
      { success: false, error: 'Failed to store events' },
      { status: 500 }
    );
  }
}

/**
 * Check rate limit for a session
 */
function checkRateLimit(sessionId: string): boolean {
  const now = Date.now();
  const existing = rateLimitMap.get(sessionId);

  if (!existing || now - existing.timestamp > RATE_LIMIT_WINDOW_MS) {
    // New window or expired
    rateLimitMap.set(sessionId, { count: 1, timestamp: now });
    return true;
  }

  if (existing.count >= RATE_LIMIT_MAX_REQUESTS) {
    return false;
  }

  existing.count++;
  return true;
}

/**
 * Store RUM events in the database
 */
async function storeEvents(events: Array<{
  type: string;
  sessionId: string;
  pageLoadId: string;
  timestamp: number;
  url: string;
  userAgent: string;
  [key: string]: unknown;
}>): Promise<number> {
  // Skip database in mock mode (E2E tests)
  if (process.env.USE_MOCK_DATA === 'true') {
    logger.debug('RUM events skipped (mock mode)', {
      eventCount: events.length,
      types: events.map((e) => e.type),
    });
    return events.length;
  }

  // Try to use the RumEvent model if it exists
  try {
    await prisma.rumEvent.createMany({
      data: events.map((event) => ({
        type: event.type,
        sessionId: event.sessionId,
        pageLoadId: event.pageLoadId,
        url: event.url,
        userAgent: event.userAgent,
        data: JSON.parse(JSON.stringify(event)),
        timestamp: new Date(event.timestamp),
      })),
    });

    return events.length;
  } catch {
    // Model might not exist yet - log to console instead
    logger.debug('RUM events received (model not available)', {
      eventCount: events.length,
      types: events.map((e) => e.type),
    });

    return 0;
  }
}

// Clean up old rate limit entries periodically
setInterval(() => {
  const now = Date.now();
  for (const [key, value] of rateLimitMap.entries()) {
    if (now - value.timestamp > RATE_LIMIT_WINDOW_MS * 2) {
      rateLimitMap.delete(key);
    }
  }
}, RATE_LIMIT_WINDOW_MS);