Pixel route.

This commit is contained in:
Mike Cao 2025-08-18 15:49:10 -07:00
parent 3c5c1e48e9
commit 0ac8bd41b6
10 changed files with 139 additions and 64 deletions

View file

@ -0,0 +1,45 @@
import { NextResponse } from 'next/server';
import { notFound } from '@/lib/response';
import { findPixel } from '@/queries';
import { POST } from '@/app/api/send/route';
const image = Buffer.from('R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw', 'base64');
export async function GET(request: Request, { params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const pixel = await findPixel({
where: {
slug,
},
});
if (!pixel) {
return notFound();
}
const payload = {
type: 'event',
payload: {
pixel: pixel.id,
url: request.url,
referrer: request.referrer,
},
};
const req = new Request(request.url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
const res = await POST(req);
return new NextResponse(image, {
headers: {
'Content-Type': 'image/gif',
'Content-Length': image.length.toString(),
'x-umami-collect': JSON.stringify(res),
},
});
}

View file

View file

@ -47,7 +47,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ lin
return Response.json(result);
} catch (e: any) {
if (e.message.includes('Unique constraint') && e.message.includes('slug')) {
if (e.message.toLowerCase().includes('unique constraint') && e.message.includes('slug')) {
return badRequest('That slug is already taken.');
}

View file

@ -46,7 +46,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ pix
return Response.json(pixel);
} catch (e: any) {
if (e.message.includes('Unique constraint') && e.message.includes('slug')) {
if (e.message.toLowerCase().includes('unique constraint') && e.message.includes('slug')) {
return badRequest('That slug is already taken.');
}

View file

@ -1,4 +1,5 @@
import { z } from 'zod';
import debug from 'debug';
import { isbot } from 'isbot';
import { startOfHour, startOfMonth } from 'date-fns';
import clickhouse from '@/lib/clickhouse';
@ -8,29 +9,52 @@ import { fetchWebsite } from '@/lib/load';
import { getClientInfo, hasBlockedIp } from '@/lib/detect';
import { createToken, parseToken } from '@/lib/jwt';
import { secret, uuid, hash } from '@/lib/crypto';
import { COLLECTION_TYPE } from '@/lib/constants';
import { COLLECTION_TYPE, EVENT_TYPE } from '@/lib/constants';
import { anyObjectParam, urlOrPathParam } from '@/lib/schema';
import { safeDecodeURI, safeDecodeURIComponent } from '@/lib/url';
import { createSession, saveEvent, saveSessionData } from '@/queries';
const log = debug('umami:send');
interface Cache {
websiteId: string;
sessionId: string;
visitId: string;
iat: number;
}
const schema = z.object({
type: z.enum(['event', 'identify']),
payload: z.object({
website: z.string().uuid(),
data: anyObjectParam.optional(),
hostname: z.string().max(100).optional(),
language: z.string().max(35).optional(),
referrer: urlOrPathParam.optional(),
screen: z.string().max(11).optional(),
title: z.string().optional(),
url: urlOrPathParam.optional(),
name: z.string().max(50).optional(),
tag: z.string().max(50).optional(),
ip: z.string().ip().optional(),
userAgent: z.string().optional(),
timestamp: z.coerce.number().int().optional(),
id: z.string().optional(),
}),
payload: z
.object({
website: z.string().uuid().optional(),
link: z.string().uuid().optional(),
pixel: z.string().uuid().optional(),
data: anyObjectParam.optional(),
hostname: z.string().max(100).optional(),
language: z.string().max(35).optional(),
referrer: urlOrPathParam.optional(),
screen: z.string().max(11).optional(),
title: z.string().optional(),
url: urlOrPathParam.optional(),
name: z.string().max(50).optional(),
tag: z.string().max(50).optional(),
ip: z.string().ip().optional(),
userAgent: z.string().optional(),
timestamp: z.coerce.number().int().optional(),
id: z.string().optional(),
})
.refine(
data => {
const keys = [data.website, data.link, data.pixel];
const count = keys.filter(Boolean).length;
return count === 1;
},
{
message: 'Exactly one of website, link, or pixel must be provided',
path: ['website'],
},
),
});
export async function POST(request: Request) {
@ -45,6 +69,8 @@ export async function POST(request: Request) {
const {
website: websiteId,
pixel: pixelId,
link: linkId,
hostname,
screen,
language,
@ -59,23 +85,26 @@ export async function POST(request: Request) {
} = payload;
// Cache check
let cache: { websiteId: string; sessionId: string; visitId: string; iat: number } | null = null;
const cacheHeader = request.headers.get('x-umami-cache');
let cache: Cache | null = null;
if (cacheHeader) {
const result = await parseToken(cacheHeader, secret());
if (websiteId) {
const cacheHeader = request.headers.get('x-umami-cache');
if (result) {
cache = result;
if (cacheHeader) {
const result = await parseToken(cacheHeader, secret());
if (result) {
cache = result;
}
}
}
// Find website
if (!cache?.websiteId) {
const website = await fetchWebsite(websiteId);
// Find website
if (!cache?.websiteId) {
const website = await fetchWebsite(websiteId);
if (!website) {
return badRequest('Website not found.');
if (!website) {
return badRequest('Website not found.');
}
}
}
@ -105,22 +134,19 @@ export async function POST(request: Request) {
// Create a session if not found
if (!clickhouse.enabled && !cache?.sessionId) {
await createSession(
{
id: sessionId,
websiteId,
browser,
os,
device,
screen,
language,
country,
region,
city,
distinctId: id,
},
{ skipDuplicates: true },
);
await createSession({
id: sessionId,
websiteId,
browser,
os,
device,
screen,
language,
country,
region,
city,
distinctId: id,
});
}
// Visit info
@ -176,10 +202,19 @@ export async function POST(request: Request) {
}
}
const eventType = linkId
? EVENT_TYPE.linkEvent
: pixelId
? EVENT_TYPE.pixelEvent
: name
? EVENT_TYPE.customEvent
: EVENT_TYPE.pageView;
await saveEvent({
websiteId,
websiteId: websiteId || linkId || pixelId,
sessionId,
visitId,
eventType,
createdAt,
// Page
@ -240,6 +275,7 @@ export async function POST(request: Request) {
return json({ cache: token, sessionId, visitId });
} catch (e) {
log.error(e);
return serverError(e);
}
}

View file

@ -54,7 +54,7 @@ export async function POST(
return Response.json(website);
} catch (e: any) {
if (e.message.includes('Unique constraint') && e.message.includes('share_id')) {
if (e.message.toLowerCase().includes('unique constraint') && e.message.includes('share_id')) {
return badRequest('That share ID is already taken.');
}