feat: add batch data for tracking payload

This commit is contained in:
harry 2024-11-20 09:32:33 +07:00
parent 7ec87553cc
commit e30315ba53
6 changed files with 68 additions and 14 deletions

View file

@ -52,6 +52,12 @@ export interface DynamicData {
[key: string]: number | string | number[] | string[]; [key: string]: number | string | number[] | string[];
} }
export interface JsonKeyDynamicData {
key: string;
value: any;
dataType: DynamicDataType;
}
export interface Auth { export interface Auth {
user?: { user?: {
id: string; id: string;

View file

@ -21,6 +21,7 @@ export interface CollectRequestBody {
payload: { payload: {
website: string; website: string;
data?: { [key: string]: any }; data?: { [key: string]: any };
batchData?: Array<{ [key: string]: any }>;
hostname?: string; hostname?: string;
ip?: string; ip?: string;
language?: string; language?: string;
@ -61,7 +62,8 @@ const schema = {
payload: yup payload: yup
.object() .object()
.shape({ .shape({
data: yup.object(), data: yup.object().optional(),
batchData: yup.array().of(yup.object()).optional(),
hostname: yup.string().matches(HOSTNAME_REGEX).max(100), hostname: yup.string().matches(HOSTNAME_REGEX).max(100),
ip: yup.string().matches(IP_REGEX), ip: yup.string().matches(IP_REGEX),
language: yup.string().max(35), language: yup.string().max(35),
@ -90,13 +92,16 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => {
} }
await useValidate(schema, req, res); await useValidate(schema, req, res);
if (req.body.payload.batchData && req.body.payload.data) {
return badRequest(res, 'cannot send both data and batchData.');
}
if (hasBlockedIp(req)) { if (hasBlockedIp(req)) {
return forbidden(res); return forbidden(res);
} }
const { type, payload } = req.body; const { type, payload } = req.body;
const { url, referrer, name: eventName, data, title } = payload; const { url, referrer, name: eventName, data, title, batchData } = payload;
const pageTitle = safeDecodeURI(title); const pageTitle = safeDecodeURI(title);
await useSession(req, res); await useSession(req, res);
@ -141,6 +146,7 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => {
pageTitle, pageTitle,
eventName, eventName,
eventData: data, eventData: data,
eventBatchData: batchData,
...session, ...session,
sessionId: session.id, sessionId: session.id,
}); });

View file

@ -18,6 +18,7 @@ export async function saveEvent(args: {
pageTitle?: string; pageTitle?: string;
eventName?: string; eventName?: string;
eventData?: any; eventData?: any;
eventBatchData?: any[];
hostname?: string; hostname?: string;
browser?: string; browser?: string;
os?: string; os?: string;
@ -47,6 +48,7 @@ async function relationalQuery(data: {
pageTitle?: string; pageTitle?: string;
eventName?: string; eventName?: string;
eventData?: any; eventData?: any;
eventBatchData?: Array<any>;
}) { }) {
const { const {
websiteId, websiteId,
@ -60,6 +62,7 @@ async function relationalQuery(data: {
eventName, eventName,
eventData, eventData,
pageTitle, pageTitle,
eventBatchData,
} = data; } = data;
const websiteEventId = uuid(); const websiteEventId = uuid();
@ -80,7 +83,7 @@ async function relationalQuery(data: {
}, },
}); });
if (eventData) { if (eventData || eventBatchData) {
await saveEventData({ await saveEventData({
websiteId, websiteId,
sessionId, sessionId,
@ -89,6 +92,7 @@ async function relationalQuery(data: {
urlPath: urlPath?.substring(0, URL_LENGTH), urlPath: urlPath?.substring(0, URL_LENGTH),
eventName: eventName?.substring(0, EVENT_NAME_LENGTH), eventName: eventName?.substring(0, EVENT_NAME_LENGTH),
eventData, eventData,
eventBatchData,
}); });
} }
@ -107,6 +111,7 @@ async function clickhouseQuery(data: {
pageTitle?: string; pageTitle?: string;
eventName?: string; eventName?: string;
eventData?: any; eventData?: any;
eventBatchData?: any[];
hostname?: string; hostname?: string;
browser?: string; browser?: string;
os?: string; os?: string;
@ -130,6 +135,7 @@ async function clickhouseQuery(data: {
pageTitle, pageTitle,
eventName, eventName,
eventData, eventData,
eventBatchData,
country, country,
subdivision1, subdivision1,
subdivision2, subdivision2,
@ -173,7 +179,7 @@ async function clickhouseQuery(data: {
await insert('website_event', [message]); await insert('website_event', [message]);
} }
if (eventData) { if (eventData || eventBatchData) {
await saveEventData({ await saveEventData({
websiteId, websiteId,
sessionId, sessionId,
@ -182,6 +188,7 @@ async function clickhouseQuery(data: {
urlPath: urlPath?.substring(0, URL_LENGTH), urlPath: urlPath?.substring(0, URL_LENGTH),
eventName: eventName?.substring(0, EVENT_NAME_LENGTH), eventName: eventName?.substring(0, EVENT_NAME_LENGTH),
eventData, eventData,
eventBatchData,
createdAt, createdAt,
}); });
} }

View file

@ -6,7 +6,7 @@ import { flattenDynamicData, flattenJSON, getStringValue } from 'lib/data';
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import kafka from 'lib/kafka'; import kafka from 'lib/kafka';
import prisma from 'lib/prisma'; import prisma from 'lib/prisma';
import { DynamicData } from 'lib/types'; import { DynamicData, JsonKeyDynamicData } from 'lib/types';
export async function saveEventData(data: { export async function saveEventData(data: {
websiteId: string; websiteId: string;
@ -15,7 +15,8 @@ export async function saveEventData(data: {
visitId?: string; visitId?: string;
urlPath?: string; urlPath?: string;
eventName?: string; eventName?: string;
eventData: DynamicData; eventData?: DynamicData;
eventBatchData?: Array<DynamicData>;
createdAt?: string; createdAt?: string;
}) { }) {
return runQuery({ return runQuery({
@ -27,11 +28,17 @@ export async function saveEventData(data: {
async function relationalQuery(data: { async function relationalQuery(data: {
websiteId: string; websiteId: string;
eventId: string; eventId: string;
eventData: DynamicData; eventData?: DynamicData;
eventBatchData?: Array<DynamicData>;
}): Promise<Prisma.BatchPayload> { }): Promise<Prisma.BatchPayload> {
const { websiteId, eventId, eventData } = data; const { websiteId, eventId, eventData, eventBatchData } = data;
const jsonKeys = flattenJSON(eventData); let jsonKeys: Array<JsonKeyDynamicData> = [];
if (eventData) {
jsonKeys = flattenJSON(eventData);
} else if (eventBatchData) {
jsonKeys = eventBatchData.flatMap(d => flattenJSON(d));
}
// id, websiteEventId, eventStringValue // id, websiteEventId, eventStringValue
const flattenedData = jsonKeys.map(a => ({ const flattenedData = jsonKeys.map(a => ({
@ -57,15 +64,31 @@ async function clickhouseQuery(data: {
visitId?: string; visitId?: string;
urlPath?: string; urlPath?: string;
eventName?: string; eventName?: string;
eventData: DynamicData; eventData?: DynamicData;
eventBatchData?: Array<DynamicData>;
createdAt?: string; createdAt?: string;
}) { }) {
const { websiteId, sessionId, visitId, eventId, urlPath, eventName, eventData, createdAt } = data; const {
websiteId,
sessionId,
visitId,
eventId,
urlPath,
eventName,
eventData,
eventBatchData,
createdAt,
} = data;
const { sendMessages, sendMessage } = kafka; const { sendMessages, sendMessage } = kafka;
const { insert, getUTCString } = clickhouse; const { insert, getUTCString } = clickhouse;
const jsonKeys = flattenJSON(eventData); let jsonKeys: Array<JsonKeyDynamicData> = [];
if (eventData) {
jsonKeys = flattenJSON(eventData);
} else if (eventBatchData) {
jsonKeys = eventBatchData.flatMap(d => flattenJSON(d));
}
const messages = jsonKeys.map(({ key, value, dataType }) => { const messages = jsonKeys.map(({ key, value, dataType }) => {
return { return {

View file

@ -75,6 +75,7 @@ export type EventProperties = {
*/ */
name: string; name: string;
data?: EventData; data?: EventData;
batchData?: EventData[];
} & WithRequired<TrackedProperties, 'website'>; } & WithRequired<TrackedProperties, 'website'>;
export type PageViewProperties = WithRequired<TrackedProperties, 'website'>; export type PageViewProperties = WithRequired<TrackedProperties, 'website'>;
export type CustomEventFunction = ( export type CustomEventFunction = (
@ -125,7 +126,7 @@ export type UmamiTracker = {
* umami.track('signup-button', { name: 'newsletter', id: 123 }); * umami.track('signup-button', { name: 'newsletter', id: 123 });
* ``` * ```
*/ */
(eventName: string, obj: EventData): Promise<string>; (eventName: string, obj: EventData | Array<EventData>): Promise<string>;
/** /**
* Tracks a page view with custom properties * Tracks a page view with custom properties

View file

@ -234,10 +234,21 @@
const track = (obj, data) => { const track = (obj, data) => {
if (typeof obj === 'string') { if (typeof obj === 'string') {
let singleData = undefined;
let batchData = undefined;
if (typeof data === 'object') {
if (Array.isArray(data)) {
batchData = data;
} else {
singleData = data;
}
}
return send({ return send({
...getPayload(), ...getPayload(),
name: obj, name: obj,
data: typeof data === 'object' ? data : undefined, data: singleData,
batchData,
}); });
} else if (typeof obj === 'object') { } else if (typeof obj === 'object') {
return send(obj); return send(obj);