All files / src/app/api/analytics/session route.ts

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

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                                                                                                                                                                                                                               
/**
 * Analytics Session API Route
 *
 * Handles session creation and heartbeat updates.
 * POST - Create new session
 * PATCH - Update session (heartbeat, user identification)
 */

import { NextRequest, NextResponse } from 'next/server';
import { z } from "zod";
import { prisma } from "@/lib/prisma";
import { parseUserAgent, getDeviceType } from "@/lib/analytics/utils";
import {
  withErrorHandling,
  successResponse,
  createdResponse,
  ApiError,
  ApiSuccessResponse,
  ApiErrorResponse } from "@/lib/api";

// Schema for creating a new session
const createSessionSchema = z.object({
  visitorId: z.string().min(1).max(50),
  userId: z.number().optional(),
  landingPage: z.string().max(500),
  referrer: z.string().max(500).optional(),
  source: z.string().max(100).optional(),
  medium: z.string().max(100).optional(),
  campaign: z.string().max(200).optional()});

// Schema for session heartbeat/update
const updateSessionSchema = z.object({
  sessionId: z.string().min(1),
  userId: z.number().optional()});

/**
 * POST - Create a new session
 */
async function handlePost(
  request: NextRequest
): Promise<NextResponse<ApiSuccessResponse<{ sessionId: string }> | ApiErrorResponse>> {
  const body = await request.json();
  const data = createSessionSchema.parse(body);

  const userAgent = request.headers.get("user-agent") || "";
  const { browser, os } = parseUserAgent(userAgent);
  const deviceType = getDeviceType(userAgent);

  const session = await prisma.analyticsSession.create({
    data: {
      visitorId: data.visitorId,
      userId: data.userId,
      landingPage: data.landingPage,
      referrer: data.referrer,
      source: data.source,
      medium: data.medium,
      campaign: data.campaign,
      browser,
      os,
      deviceType,
      pageViews: 0, // Will be incremented on first pageview
    }});

  return createdResponse({ sessionId: session.id });
}

export const POST = withErrorHandling(handlePost);

/**
 * PATCH - Update session (heartbeat or user identification)
 */
async function handlePatch(
  request: NextRequest
): Promise<NextResponse<ApiSuccessResponse<Record<string, never>> | ApiErrorResponse>> {
  const body = await request.json();
  const data = updateSessionSchema.parse(body);

  // Build update data
  const updateData: {
    lastSeenAt: Date;
    duration?: { increment: number };
    userId?: number;
  } = {
    lastSeenAt: new Date(),
    duration: { increment: 30 }, // Heartbeat every 30 seconds
  };

  // If userId provided, update it (for when user logs in)
  if (data.userId) {
    updateData.userId = data.userId;
  }

  try {
    await prisma.analyticsSession.update({
      where: { id: data.sessionId },
      data: updateData});
  } catch (error) {
    // Session not found is not an error we need to log
    if (
      error instanceof Error &&
      error.message.includes("Record to update not found")
    ) {
      throw ApiError.notFound("Session");
    }
    throw error;
  }

  return successResponse({});
}

export const PATCH = withErrorHandling(handlePatch);