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 80db8f96..cc3b03cb 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,6 +26,7 @@ const schema = z.object({
tag: z.string().max(50).optional(),
ip: z.string().ip().optional(),
userAgent: z.string().optional(),
+ timestamp: z.coerce.number().int().optional(),
}),
});
@@ -55,6 +56,7 @@ export async function POST(request: Request) {
data,
title,
tag,
+ timestamp,
} = payload;
// Cache check
@@ -88,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) {
@@ -179,6 +182,7 @@ export async function POST(request: Request) {
subdivision2,
city,
tag,
+ createdAt,
});
}
@@ -191,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 65ee1175..148b03f3 100644
--- a/src/queries/sql/events/saveEvent.ts
+++ b/src/queries/sql/events/saveEvent.ts
@@ -29,6 +29,7 @@ export async function saveEvent(args: {
subdivision2?: string;
city?: string;
tag?: string;
+ createdAt?: Date;
}) {
return runQuery({
[PRISMA]: () => relationalQuery(args),
@@ -49,6 +50,7 @@ async function relationalQuery(data: {
eventName?: string;
eventData?: any;
tag?: string;
+ createdAt?: Date;
}) {
const {
websiteId,
@@ -63,6 +65,7 @@ async function relationalQuery(data: {
eventData,
pageTitle,
tag,
+ createdAt,
} = data;
const websiteEventId = uuid();
@@ -81,6 +84,7 @@ async function relationalQuery(data: {
eventType: eventName ? EVENT_TYPE.customEvent : EVENT_TYPE.pageView,
eventName: eventName ? eventName?.substring(0, EVENT_NAME_LENGTH) : null,
tag,
+ createdAt,
},
});
@@ -92,6 +96,7 @@ async function relationalQuery(data: {
urlPath: urlPath?.substring(0, URL_LENGTH),
eventName: eventName?.substring(0, EVENT_NAME_LENGTH),
eventData,
+ createdAt,
});
}
@@ -121,6 +126,7 @@ async function clickhouseQuery(data: {
subdivision2?: string;
city?: string;
tag?: string;
+ createdAt?: Date;
}) {
const {
websiteId,
@@ -139,12 +145,12 @@ async function clickhouseQuery(data: {
subdivision2,
city,
tag,
+ createdAt,
...args
} = data;
const { insert, getUTCString } = clickhouse;
const { sendMessage } = kafka;
const eventId = uuid();
- const createdAt = getUTCString();
const message = {
...args,
@@ -170,7 +176,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: createdAt,
+ 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),
};
});