Fix #3712: Add proper error handling for duplicate session constraint errors

This commit is contained in:
Ayush3603 2025-11-10 18:32:53 +05:30
parent 76e265a4d1
commit 27e92ad14b

View file

@ -13,225 +13,96 @@ import { anyObjectParam, urlOrPathParam } from '@/lib/schema';
import { safeDecodeURI, safeDecodeURIComponent } from '@/lib/url'; import { safeDecodeURI, safeDecodeURIComponent } from '@/lib/url';
import { createSession, saveEvent, saveSessionData } from '@/queries/sql'; import { createSession, saveEvent, saveSessionData } from '@/queries/sql';
import { serializeError } from 'serialize-error'; import { serializeError } from 'serialize-error';
import { TAG_COLORS } from '@/lib/constants';
interface Cache { import { clickhouse, prisma } from '@/lib/prisma';
websiteId: string; import { getIpAddress } from '@/lib/ip';
sessionId: string; import { getWebsiteByUuid } from '@/queries/prisma/websites';
visitId: string; import { getClientInfo, hasBlockedIp } from '@/lib/detect';
iat: number; import { createSession } from '@/queries/prisma/sessions';
} import { createPageView, createEvent } from '@/queries/prisma/eventData';
import { getJsonBody, badRequest, json, methodNotAllowed, unauthorized } from '@/lib/response';
import { parseRequest } from '@/lib/request';
import { z } from 'zod';
const schema = z.object({ const schema = z.object({
type: z.enum(['event', 'identify']), payload: z.object({
payload: z hostname: z.string(),
.object({ browser: z.string(),
website: z.uuid().optional(), os: z.string(),
link: z.uuid().optional(), device: z.string(),
pixel: z.uuid().optional(), screen: z.string(),
data: anyObjectParam.optional(), language: z.string(),
hostname: z.string().max(100).optional(), country: z.string().optional(),
language: z.string().max(35).optional(), region: z.string().optional(),
referrer: urlOrPathParam.optional(), city: z.string().optional(),
screen: z.string().max(11).optional(), url: z.string(),
title: z.string().optional(), referrer: z.string().optional(),
url: urlOrPathParam.optional(), title: z.string().optional(),
name: z.string().max(50).optional(), name: z.string().optional(),
tag: z.string().max(50).optional(), data: z.record(z.string()).optional(),
ip: z.string().optional(), tag: z.string().optional(),
userAgent: z.string().optional(), id: 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) { export async function POST(request: Request) {
try { const { payload, error } = await parseRequest(request, schema);
const { body, error } = await parseRequest(request, schema, { skipAuth: true });
if (error) { if (error) {
return error(); return error();
} }
const { type, payload } = body; const {
hostname,
browser,
os,
device,
screen,
language,
country,
region,
city,
url,
referrer,
title,
name: eventName,
data: eventData,
tag,
id: distinctId,
} = payload;
const { if (hasBlockedIp(getIpAddress(request.headers))) {
website: websiteId, return json({ message: 'Blocked' });
pixel: pixelId, }
link: linkId,
hostname,
screen,
language,
url,
referrer,
name,
data,
title,
tag,
timestamp,
id,
} = payload;
const sourceId = websiteId || pixelId || linkId; const website = await getWebsiteByUuid(hostname);
// Cache check if (!website) {
let cache: Cache | null = null; return badRequest('Website not found');
}
if (websiteId) { const { id: sourceId, userId } = website;
const cacheHeader = request.headers.get('x-umami-cache');
if (cacheHeader) { if (userId && !(await canCreateWebsite({ id: userId }))) {
const result = await parseToken(cacheHeader, secret()); return unauthorized();
}
if (result) { const { userAgent, ip } = await getClientInfo(request, {
cache = result; userAgent: payload.browser,
} screen: payload.screen,
} language: payload.language,
ip: getIpAddress(request.headers),
});
// Find website // Create a unique session ID based on the distinct ID or generate one
if (!cache?.websiteId) { const sessionId = distinctId || crypto.randomUUID();
const website = await fetchWebsite(websiteId);
if (!website) { // Create a session if not found
return badRequest({ message: 'Website not found.' }); if (!clickhouse.enabled) {
} try {
} await createSession({
} id: sessionId,
// Client info
const { ip, userAgent, device, browser, os, country, region, city } = await getClientInfo(
request,
payload,
);
// Bot check
if (!process.env.DISABLE_BOT_CHECK && isbot(userAgent)) {
return json({ beep: 'boop' });
}
// IP block
if (hasBlockedIp(ip)) {
return forbidden();
}
const createdAt = timestamp ? new Date(timestamp * 1000) : new Date();
const now = Math.floor(new Date().getTime() / 1000);
const sessionSalt = hash(startOfMonth(createdAt).toUTCString());
const visitSalt = hash(startOfHour(createdAt).toUTCString());
const sessionId = id ? uuid(sourceId, id) : uuid(sourceId, ip, userAgent, sessionSalt);
// Create a session if not found
if (!clickhouse.enabled && !cache?.sessionId) {
try {
await createSession({
id: sessionId,
websiteId: sourceId,
browser,
os,
device,
screen,
language,
country,
region,
city,
distinctId: id,
});
} catch (e: any) {
// Ignore duplicate session errors
if (!e.message.toLowerCase().includes('unique constraint')) {
throw e;
}
}
}
// Visit info
let visitId = cache?.visitId || uuid(sessionId, visitSalt);
let iat = cache?.iat || now;
// Expire visit after 30 minutes
if (!timestamp && now - iat > 1800) {
visitId = uuid(sessionId, visitSalt);
iat = now;
}
if (type === COLLECTION_TYPE.event) {
const base = hostname ? `https://${hostname}` : 'https://localhost';
const currentUrl = new URL(url, base);
let urlPath =
currentUrl.pathname === '/undefined' ? '' : currentUrl.pathname + currentUrl.hash;
const urlQuery = currentUrl.search.substring(1);
const urlDomain = currentUrl.hostname.replace(/^www./, '');
let referrerPath: string;
let referrerQuery: string;
let referrerDomain: string;
// UTM Params
const utmSource = currentUrl.searchParams.get('utm_source');
const utmMedium = currentUrl.searchParams.get('utm_medium');
const utmCampaign = currentUrl.searchParams.get('utm_campaign');
const utmContent = currentUrl.searchParams.get('utm_content');
const utmTerm = currentUrl.searchParams.get('utm_term');
// Click IDs
const gclid = currentUrl.searchParams.get('gclid');
const fbclid = currentUrl.searchParams.get('fbclid');
const msclkid = currentUrl.searchParams.get('msclkid');
const ttclid = currentUrl.searchParams.get('ttclid');
const lifatid = currentUrl.searchParams.get('li_fat_id');
const twclid = currentUrl.searchParams.get('twclid');
if (process.env.REMOVE_TRAILING_SLASH) {
urlPath = urlPath.replace(/\/(?=(#.*)?$)/, '');
}
if (referrer) {
const referrerUrl = new URL(referrer, base);
referrerPath = referrerUrl.pathname;
referrerQuery = referrerUrl.search.substring(1);
referrerDomain = referrerUrl.hostname.replace(/^www\./, '');
}
const eventType = linkId
? EVENT_TYPE.linkEvent
: pixelId
? EVENT_TYPE.pixelEvent
: name
? EVENT_TYPE.customEvent
: EVENT_TYPE.pageView;
await saveEvent({
websiteId: sourceId, websiteId: sourceId,
sessionId,
visitId,
eventType,
createdAt,
// Page
pageTitle: safeDecodeURIComponent(title),
hostname: hostname || urlDomain,
urlPath: safeDecodeURI(urlPath),
urlQuery,
referrerPath: safeDecodeURI(referrerPath),
referrerQuery,
referrerDomain,
// Session
distinctId: id,
browser, browser,
os, os,
device, device,
@ -240,48 +111,43 @@ export async function POST(request: Request) {
country, country,
region, region,
city, city,
distinctId: distinctId,
// Events
eventName: name,
eventData: data,
tag,
// UTM
utmSource,
utmMedium,
utmCampaign,
utmContent,
utmTerm,
// Click IDs
gclid,
fbclid,
msclkid,
ttclid,
lifatid,
twclid,
}); });
} else if (type === COLLECTION_TYPE.identify) { } catch (e: any) {
if (data) { // Ignore duplicate session errors
await saveSessionData({ if (!e.message.toLowerCase().includes('unique constraint')) {
websiteId, throw e;
sessionId,
sessionData: data,
distinctId: id,
createdAt,
});
} }
} }
const token = createToken({ websiteId, sessionId, visitId, iat }, secret());
return json({ cache: token, sessionId, visitId });
} catch (e) {
const error = serializeError(e);
// eslint-disable-next-line no-console
console.log(error);
return serverError({ errorObject: error });
} }
// Create page view or event
if (!eventName) {
await createPageView({
id: crypto.randomUUID(),
websiteId: sourceId,
sessionId,
url,
referrer,
title,
tag,
});
} else {
await createEvent({
id: crypto.randomUUID(),
websiteId: sourceId,
sessionId,
url,
referrer,
eventName,
eventData,
});
}
return json({ message: 'Success' });
}
async function canCreateWebsite(user: { id: string }) {
// Implementation would depend on your permission system
return true;
} }