diff --git a/next-env.d.ts b/next-env.d.ts index 40c3d680..1b3be084 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/src/app/api/send/route.ts b/src/app/api/send/route.ts index 4189d7d9..b9556ddd 100644 --- a/src/app/api/send/route.ts +++ b/src/app/api/send/route.ts @@ -7,16 +7,16 @@ import { badRequest, json, forbidden, serverError } from '@/lib/response'; import { fetchSession, fetchWebsite } from '@/lib/load'; import { getClientInfo, hasBlockedIp } from '@/lib/detect'; import { secret, uuid, visitSalt } from '@/lib/crypto'; -import { COLLECTION_TYPE, DOMAIN_REGEX } from '@/lib/constants'; +import { COLLECTION_TYPE } from '@/lib/constants'; +import { anyObjectParam, urlOrPathParam } from '@/lib/schema'; import { createSession, saveEvent, saveSessionData } from '@/queries'; -import { urlOrPathParam } from '@/lib/schema'; const schema = z.object({ type: z.enum(['event', 'identify']), payload: z.object({ website: z.string().uuid(), - data: z.object({}).passthrough().optional(), - hostname: z.string().regex(DOMAIN_REGEX).max(100).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(), @@ -26,7 +26,7 @@ const schema = z.object({ tag: z.string().max(50).optional(), ip: z.string().ip().optional(), userAgent: z.string().optional(), - createdAt: yup.number().optional(), + timestamp: z.coerce.number().int().optional(), }), }); @@ -56,7 +56,7 @@ export async function POST(request: Request) { data, title, tag, - reqCreatedAt, + timestamp, } = payload; // Cache check @@ -90,6 +90,7 @@ export async function POST(request: Request) { } const sessionId = uuid(websiteId, ip, userAgent); + const createdAt = timestamp ? new Date(timestamp * 1000) : new Date(); // Find session if (!clickhouse.enabled && !cache?.sessionId) { @@ -181,7 +182,7 @@ export async function POST(request: Request) { subdivision2, city, tag, - createdAt, + createdAt, }); } @@ -194,6 +195,7 @@ export async function POST(request: Request) { websiteId, sessionId, sessionData: data, + createdAt, }); } diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 8df7be9f..4e2b3e4a 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -36,6 +36,8 @@ export const unitParam = z.string().refine(value => UNIT_TYPES.includes(value), export const roleParam = z.enum(['team-member', 'team-view-only', 'team-manager']); +export const anyObjectParam = z.object({}).passthrough(); + export const urlOrPathParam = z.string().refine( value => { try { diff --git a/src/queries/sql/events/saveEvent.ts b/src/queries/sql/events/saveEvent.ts index 3b3f3a99..5df276e1 100644 --- a/src/queries/sql/events/saveEvent.ts +++ b/src/queries/sql/events/saveEvent.ts @@ -85,6 +85,7 @@ async function relationalQuery(data: { eventName: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null, createdAt, tag, + createdAt, }, }); @@ -96,6 +97,7 @@ async function relationalQuery(data: { urlPath: urlPath?.substring(0, URL_LENGTH), eventName: eventName?.substring(0, EVENT_NAME_LENGTH), eventData, + createdAt, }); } @@ -150,7 +152,6 @@ async function clickhouseQuery(data: { const { insert, getUTCString } = clickhouse; const { sendMessage } = kafka; const eventId = uuid(); - const createdAtUTC = getUTCString(createdAt); const message = { ...args, @@ -176,7 +177,7 @@ async function clickhouseQuery(data: { event_type: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView, event_name: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null, tag: tag, - created_at: createdAtUTC, + created_at: getUTCString(createdAt), }; if (kafka.enabled) { diff --git a/src/queries/sql/events/saveEventData.ts b/src/queries/sql/events/saveEventData.ts index 7c158da4..16a5cab1 100644 --- a/src/queries/sql/events/saveEventData.ts +++ b/src/queries/sql/events/saveEventData.ts @@ -15,7 +15,7 @@ export async function saveEventData(data: { urlPath?: string; eventName?: string; eventData: DynamicData; - createdAt?: string; + createdAt?: Date; }) { return runQuery({ [PRISMA]: () => relationalQuery(data), @@ -27,8 +27,9 @@ async function relationalQuery(data: { websiteId: string; eventId: string; eventData: DynamicData; + createdAt?: Date; }): Promise { - const { websiteId, eventId, eventData } = data; + const { websiteId, eventId, eventData, createdAt } = data; const jsonKeys = flattenJSON(eventData); @@ -42,6 +43,7 @@ async function relationalQuery(data: { numberValue: a.dataType === DATA_TYPE.number ? a.value : null, dateValue: a.dataType === DATA_TYPE.date ? new Date(a.value) : null, dataType: a.dataType, + createdAt, })); return prisma.client.eventData.createMany({ @@ -56,7 +58,7 @@ async function clickhouseQuery(data: { urlPath?: string; eventName?: string; eventData: DynamicData; - createdAt?: string; + createdAt?: Date; }) { const { websiteId, sessionId, eventId, urlPath, eventName, eventData, createdAt } = data; @@ -77,7 +79,7 @@ async function clickhouseQuery(data: { string_value: getStringValue(value, dataType), number_value: dataType === DATA_TYPE.number ? value : null, date_value: dataType === DATA_TYPE.date ? getUTCString(value) : null, - created_at: createdAt, + created_at: getUTCString(createdAt), }; }); diff --git a/src/queries/sql/sessions/saveSessionData.ts b/src/queries/sql/sessions/saveSessionData.ts index 35f0c712..a060e9a8 100644 --- a/src/queries/sql/sessions/saveSessionData.ts +++ b/src/queries/sql/sessions/saveSessionData.ts @@ -11,6 +11,7 @@ export async function saveSessionData(data: { websiteId: string; sessionId: string; sessionData: DynamicData; + createdAt?: Date; }) { return runQuery({ [PRISMA]: () => relationalQuery(data), @@ -22,9 +23,10 @@ export async function relationalQuery(data: { websiteId: string; sessionId: string; sessionData: DynamicData; + createdAt?: Date; }) { const { client } = prisma; - const { websiteId, sessionId, sessionData } = data; + const { websiteId, sessionId, sessionData, createdAt } = data; const jsonKeys = flattenJSON(sessionData); @@ -37,6 +39,7 @@ export async function relationalQuery(data: { numberValue: a.dataType === DATA_TYPE.number ? a.value : null, dateValue: a.dataType === DATA_TYPE.date ? new Date(a.value) : null, dataType: a.dataType, + createdAt, })); const existing = await client.sessionData.findMany({ @@ -77,12 +80,12 @@ async function clickhouseQuery(data: { websiteId: string; sessionId: string; sessionData: DynamicData; + createdAt?: Date; }) { - const { websiteId, sessionId, sessionData } = data; + const { websiteId, sessionId, sessionData, createdAt } = data; const { insert, getUTCString } = clickhouse; const { sendMessage } = kafka; - const createdAt = getUTCString(); const jsonKeys = flattenJSON(sessionData); @@ -95,7 +98,7 @@ async function clickhouseQuery(data: { string_value: getStringValue(value, dataType), number_value: dataType === DATA_TYPE.number ? value : null, date_value: dataType === DATA_TYPE.date ? getUTCString(value) : null, - created_at: createdAt, + created_at: getUTCString(createdAt), }; });