mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 12:47:13 +01:00
Pixel route.
This commit is contained in:
parent
3c5c1e48e9
commit
0ac8bd41b6
10 changed files with 139 additions and 64 deletions
45
src/app/(collect)/p/[slug]/route.ts
Normal file
45
src/app/(collect)/p/[slug]/route.ts
Normal 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),
|
||||
},
|
||||
});
|
||||
}
|
||||
0
src/app/(collect)/q/[slug]/route.ts
Normal file
0
src/app/(collect)/q/[slug]/route.ts
Normal 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.');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue