244 lines
6.2 KiB
TypeScript
244 lines
6.2 KiB
TypeScript
import { type NextRequest, NextResponse } from "next/server";
|
|
import { optionalAuth } from "@/lib/auth/middleware";
|
|
import {
|
|
getAdAnalytics,
|
|
getMarketplaceStats,
|
|
trackEvent,
|
|
trackEventsBatch,
|
|
} from "@/lib/prisma/queries/telemetry";
|
|
import {
|
|
checkRateLimit,
|
|
createRateLimitResponse,
|
|
getClientIP,
|
|
} from "@/lib/rate-limiter";
|
|
import { secureLogger } from "@/lib/secure-logger";
|
|
import {
|
|
telemetryBatchSchema,
|
|
telemetryEventSchema,
|
|
} from "@/lib/validation-schemas";
|
|
|
|
/**
|
|
* POST /api/telemetry/event
|
|
* Registra um único evento de telemetria
|
|
*/
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
// Rate limiting
|
|
const ip = getClientIP(request);
|
|
const rateLimit = checkRateLimit(ip, "/api/telemetry/event");
|
|
|
|
if (!rateLimit.allowed) {
|
|
return createRateLimitResponse(rateLimit.resetAt);
|
|
}
|
|
|
|
// Autenticação opcional (usuários anônimos podem gerar eventos)
|
|
const authResult = await optionalAuth(request);
|
|
const userId =
|
|
authResult instanceof NextResponse ? undefined : authResult?.userId;
|
|
|
|
const body = await request.json();
|
|
|
|
// Verificar se é batch ou evento único
|
|
const isBatch =
|
|
body.events && Array.isArray(body.events) && body.events.length > 0;
|
|
|
|
if (isBatch) {
|
|
// Batch de eventos
|
|
// Garantir que todos os eventos tenham metadata
|
|
const normalizedBody = {
|
|
...body,
|
|
events: body.events.map((event: any) => ({
|
|
...event,
|
|
metadata: event.metadata || {},
|
|
})),
|
|
};
|
|
|
|
const validationResult = telemetryBatchSchema.safeParse(normalizedBody);
|
|
|
|
if (!validationResult.success) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "Dados de entrada inválidos",
|
|
details: validationResult.error.issues,
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const { events, session_id: batchSessionId } = validationResult.data;
|
|
|
|
// Adicionar user_id e session_id aos eventos
|
|
const eventsWithAuth = events.map((event) => ({
|
|
...event,
|
|
user_id: userId,
|
|
session_id: batchSessionId || event.session_id || undefined,
|
|
ad_id: event.ad_id ?? undefined, // Converter null para undefined
|
|
metadata: event.metadata || {},
|
|
}));
|
|
|
|
const trackedEvents = await trackEventsBatch(eventsWithAuth);
|
|
|
|
secureLogger.debug("📊 Batch de eventos registrado", {
|
|
count: trackedEvents.length,
|
|
userId,
|
|
ip,
|
|
});
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: true,
|
|
events: trackedEvents,
|
|
},
|
|
{ status: 201 },
|
|
);
|
|
} else {
|
|
// Evento único
|
|
// Garantir que o evento tenha metadata
|
|
const normalizedBody = {
|
|
...body,
|
|
metadata: body.metadata || {},
|
|
};
|
|
|
|
const validationResult = telemetryEventSchema.safeParse(normalizedBody);
|
|
|
|
if (!validationResult.success) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "Dados de entrada inválidos",
|
|
details: validationResult.error.issues,
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const eventData = validationResult.data;
|
|
|
|
// Adicionar user_id e session_id
|
|
const trackedEvent = await trackEvent({
|
|
...eventData,
|
|
user_id: userId,
|
|
session_id: body.session_id || undefined,
|
|
ad_id: eventData.ad_id ?? undefined, // Converter null para undefined
|
|
metadata: eventData.metadata || {},
|
|
});
|
|
|
|
secureLogger.debug("📊 Evento registrado", {
|
|
eventType: eventData.event_type,
|
|
adId: eventData.ad_id,
|
|
userId,
|
|
ip,
|
|
});
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: true,
|
|
event: trackedEvent,
|
|
},
|
|
{ status: 201 },
|
|
);
|
|
}
|
|
} catch (error) {
|
|
secureLogger.error("❌ Erro ao registrar evento de telemetria", {
|
|
error: error instanceof Error ? error.message : "Erro desconhecido",
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "Erro ao registrar evento",
|
|
},
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/telemetry/analytics/:adId
|
|
* Busca analytics de um anúncio específico
|
|
*/
|
|
export async function GET(request: NextRequest) {
|
|
try {
|
|
const { searchParams } = new URL(request.url);
|
|
const adIdParam = searchParams.get("adId");
|
|
const dashboard = searchParams.get("dashboard");
|
|
|
|
// Dashboard agregado (admin)
|
|
if (dashboard === "true") {
|
|
const startDateParam = searchParams.get("startDate");
|
|
const endDateParam = searchParams.get("endDate");
|
|
|
|
const startDate = startDateParam ? new Date(startDateParam) : undefined;
|
|
const endDate = endDateParam ? new Date(endDateParam) : undefined;
|
|
|
|
const stats = await getMarketplaceStats(startDate, endDate);
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: true,
|
|
stats,
|
|
},
|
|
{ status: 200 },
|
|
);
|
|
}
|
|
|
|
// Analytics de um anúncio específico
|
|
if (adIdParam) {
|
|
const adId = parseInt(adIdParam, 10);
|
|
|
|
if (Number.isNaN(adId)) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "ID do anúncio inválido",
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const analytics = await getAdAnalytics(adId);
|
|
|
|
if (!analytics) {
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "Analytics não encontrado para este anúncio",
|
|
},
|
|
{ status: 404 },
|
|
);
|
|
}
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: true,
|
|
analytics,
|
|
},
|
|
{ status: 200 },
|
|
);
|
|
}
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "Parâmetro 'adId' ou 'dashboard' é obrigatório",
|
|
},
|
|
{ status: 400 },
|
|
);
|
|
} catch (error) {
|
|
secureLogger.error("❌ Erro ao buscar analytics", {
|
|
error: error instanceof Error ? error.message : "Erro desconhecido",
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
});
|
|
|
|
return NextResponse.json(
|
|
{
|
|
success: false,
|
|
error: "Erro ao buscar analytics",
|
|
},
|
|
{ status: 500 },
|
|
);
|
|
}
|
|
}
|