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 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 38x 38x 38x 38x 36x 36x 38x 12x 12x 38x 22x 22x 24x 2x 2x 38x 1x 1x 1x 1x 1x 1x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 12x 11x 11x 11x 11x 11x 11x 11x 11x 12x 10x 10x 10x 12x 11x 11x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 10x 1x 1x 1x 1x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 22x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 21x 22x 22x 22x | /**
* Analytics Event Tracking API Route
*
* Handles batched event tracking for custom events.
* POST - Track one or more events
*/
import { NextRequest, NextResponse } from 'next/server';
import { z } from "zod";
import { prisma } from "@/lib/prisma";
import { getClientIP, hashIP, sanitizeEventProperties } from "@/lib/analytics/utils";
import { logger } from "@/lib/logging";
import { Prisma } from "@prisma/client";
import {
withErrorHandling,
successResponse,
ApiError,
ApiSuccessResponse,
ApiErrorResponse } from "@/lib/api";
// Schema for a single event
const eventSchema = z.object({
eventType: z.string().max(50),
eventName: z.string().max(100),
sessionId: z.string().min(1),
visitorId: z.string().min(1).max(50),
path: z.string().max(500),
referrer: z.string().max(500).optional(),
properties: z.record(z.string(), z.unknown()).optional()});
// Schema for batch event tracking
const batchTrackSchema = z.object({
events: z.array(eventSchema).min(1).max(50), // Max 50 events per batch
});
// Legacy schema for backwards compatibility
const legacyEventSchema = z.object({
category: z.string(),
action: z.string(),
label: z.string().optional(),
value: z.number().optional(),
timestamp: z.string().optional(),
userAgent: z.string().optional(),
url: z.string().optional()});
/**
* POST - Track events (supports batching and legacy format)
*/
async function handlePost(
request: NextRequest
): Promise<NextResponse<ApiSuccessResponse<unknown> | ApiErrorResponse>> {
const body = await request.json();
// Check if this is the new batch format or legacy format
if ("events" in body && Array.isArray(body.events)) {
// New batch format
return await handleBatchEvents(request, body);
} else if ("category" in body && "action" in body) {
// Legacy format - convert to new format
return await handleLegacyEvent(request, body);
} else {
throw ApiError.badRequest("Invalid event format");
}
}
export const POST = withErrorHandling(handlePost);
/**
* Handle new batch event format
*/
async function handleBatchEvents(
request: NextRequest,
body: unknown
): Promise<NextResponse<ApiSuccessResponse<{ eventsTracked: number }> | ApiErrorResponse>> {
const { events } = batchTrackSchema.parse(body);
const userAgent = request.headers.get("user-agent") || undefined;
const ip = getClientIP(request.headers);
const ipHash = hashIP(ip);
// Batch insert events
await prisma.analyticsEvent.createMany({
data: events.map((event) => ({
eventType: event.eventType,
eventName: event.eventName,
sessionId: event.sessionId,
visitorId: event.visitorId,
path: event.path,
referrer: event.referrer,
userAgent,
ipHash,
properties: sanitizeEventProperties(event.properties) as Prisma.InputJsonValue | undefined}))});
// Update session event counts
const sessionCounts = new Map<string, number>();
for (const event of events) {
sessionCounts.set(event.sessionId, (sessionCounts.get(event.sessionId) || 0) + 1);
}
// Update each session's event count
const updatePromises = Array.from(sessionCounts.entries()).map(([sessionId, count]) =>
prisma.analyticsSession
.update({
where: { id: sessionId },
data: {
eventCount: { increment: count },
lastSeenAt: new Date()}})
.catch((error) => {
// Session might not exist, log but don't fail
logger.warn(`Failed to update session ${sessionId} event count`, { category: "API", error: error instanceof Error ? error.message : String(error) });
})
);
await Promise.all(updatePromises);
logger.info(`Tracked ${events.length} events`, { category: "API" });
return successResponse({ eventsTracked: events.length });
}
/**
* Handle legacy event format for backwards compatibility
*/
async function handleLegacyEvent(
request: NextRequest,
body: unknown
): Promise<NextResponse<ApiSuccessResponse<Record<string, never>> | ApiErrorResponse>> {
const data = legacyEventSchema.parse(body);
const { category, action, label, value, timestamp, userAgent, url } = data;
// Log the event (legacy behavior)
logger.info(`Event tracked: ${category}.${action}`, {
category: "API",
eventCategory: category,
action,
label,
value,
timestamp,
userAgent,
url});
// Also store in database if we have enough info
// This provides a migration path from legacy to new format
if (url) {
try {
const ipHash = hashIP(getClientIP(request.headers));
await prisma.analyticsEvent.create({
data: {
eventType: category,
eventName: action,
sessionId: null, // Legacy events don't have session tracking
visitorId: "legacy",
path: new URL(url).pathname,
userAgent: userAgent || request.headers.get("user-agent") || undefined,
ipHash,
properties: {
label,
value,
timestamp,
legacy: true}}});
} catch (error) {
// Don't fail the request if DB insert fails for legacy events
logger.warn("Failed to store legacy event in database", { category: "API", error: error instanceof Error ? error.message : String(error) });
}
}
return successResponse({});
}
|