diff --git a/apps/sdk-api/src/controllers/event.controller.ts b/apps/sdk-api/src/controllers/event.controller.ts index de2e5a46..3d20566c 100644 --- a/apps/sdk-api/src/controllers/event.controller.ts +++ b/apps/sdk-api/src/controllers/event.controller.ts @@ -1,5 +1,5 @@ import { parseIp } from '@/utils/parseIp'; -import { parseReferrer } from '@/utils/parseReferrer'; +import { getReferrerWithQuery, parseReferrer } from '@/utils/parseReferrer'; import { parseUserAgent } from '@/utils/parseUserAgent'; import type { FastifyReply, FastifyRequest } from 'fastify'; import { omit } from 'ramda'; @@ -15,16 +15,18 @@ import type { PostEventPayload } from '@mixan/types'; const SESSION_TIMEOUT = 1000 * 60 * 30; const SESSION_END_TIMEOUT = SESSION_TIMEOUT + 1000; -function parseSearchParams(params: URLSearchParams): Record { +function parseSearchParams( + params: URLSearchParams +): Record | undefined { const result: Record = {}; for (const [key, value] of params.entries()) { result[key] = value; } - return result; + return Object.keys(result).length ? result : undefined; } function parsePath(path?: string): { - query?: Record; + query?: Record; path: string; hash?: string; } { @@ -39,7 +41,7 @@ function parsePath(path?: string): { return { query: parseSearchParams(url.searchParams), path: url.pathname, - hash: url.hash, + hash: url.hash ?? undefined, }; } catch (error) { return { @@ -57,8 +59,10 @@ export async function postEvent( let profileId: string | null = null; const projectId = request.projectId; const body = request.body; + const createdAt = new Date(body.timestamp); const { path, hash, query } = parsePath(body.properties?.path); const referrer = parseReferrer(body.properties?.referrer); + const utmReferrer = getReferrerWithQuery(query); const ip = getClientIp(request)!; const origin = request.headers.origin!; const ua = request.headers['user-agent']!; @@ -132,7 +136,7 @@ export async function postEvent( hash, query, }), - createdAt: body.timestamp, + createdAt, country: geo.country, city: geo.city, region: geo.region, @@ -147,8 +151,8 @@ export async function postEvent( duration: 0, path: path, referrer: referrer.url, - referrerName: referrer.name, - referrerType: referrer.type, + referrerName: referrer.name ?? utmReferrer?.name ?? '', + referrerType: referrer.type ?? utmReferrer?.type ?? '', }; const job = findJobByPrefix(eventsJobs, `event:${projectId}:${profileId}:`); @@ -180,6 +184,7 @@ export async function postEvent( payload: { ...payload, name: 'session_start', + // @ts-expect-error createdAt: toISOString(getTime(payload.createdAt) - 10), }, }); diff --git a/apps/sdk-api/src/utils/parseReferrer.ts b/apps/sdk-api/src/utils/parseReferrer.ts index 2eea4045..ca9cf398 100644 --- a/apps/sdk-api/src/utils/parseReferrer.ts +++ b/apps/sdk-api/src/utils/parseReferrer.ts @@ -24,3 +24,27 @@ export function parseReferrer(url: string | undefined) { url: url ?? '', }; } + +export function getReferrerWithQuery( + query: Record | undefined +) { + if (!query) { + return null; + } + + const source = query.utm_source ?? query.ref ?? query.utm_referrer ?? ''; + + const match = Object.values(referrers).find( + (referrer) => referrer.name.toLowerCase() === source?.toLowerCase() + ); + + if (!match) { + return null; + } + + return { + name: match.name, + type: match.type, + url: '', + }; +} diff --git a/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list-item.tsx b/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list-item.tsx index 3fbfce4e..5d3f1038 100644 --- a/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list-item.tsx +++ b/apps/web/src/app/(app)/[organizationId]/[projectId]/events/event-list-item.tsx @@ -20,6 +20,7 @@ export function EventListItem({ createdAt, name, properties, + path, }: EventListItemProps) { const params = useAppParams(); @@ -46,16 +47,15 @@ export function EventListItem({ switch (name) { case 'screen_view': { - const route = (properties?.route || properties?.path)!; - if (route) { - bullets.push(route); + if (path) { + bullets.push(path); } break; } } return bullets; - }, [name, createdAt, profile, properties, params]); + }, [name, createdAt, profile, properties, params, path]); return ( session: auth(), }; }, + onError(opts) { + const { error, type, path, input, ctx, req } = opts; + console.error('---- TRPC ERROR'); + console.error('Error:', error); + console.error(); + }, }); export { handler as GET, handler as POST }; diff --git a/apps/web/src/components/general/ExpandableListItem.tsx b/apps/web/src/components/general/ExpandableListItem.tsx index 5b676540..0c40914e 100644 --- a/apps/web/src/components/general/ExpandableListItem.tsx +++ b/apps/web/src/components/general/ExpandableListItem.tsx @@ -27,8 +27,8 @@ export function ExpandableListItem({
{title}
- {bullets.map((bullet) => ( - {bullet} + {bullets.map((bullet, index) => ( + {bullet} ))}
diff --git a/apps/web/src/server/api/routers/event.ts b/apps/web/src/server/api/routers/event.ts index a7ca598b..4318bf70 100644 --- a/apps/web/src/server/api/routers/event.ts +++ b/apps/web/src/server/api/routers/event.ts @@ -3,7 +3,7 @@ import { transformEvent } from '@/server/services/event.service'; import { z } from 'zod'; import type { IDBEvent } from '@mixan/db'; -import { chQuery, createSqlBuilder } from '@mixan/db'; +import { chQuery, createSqlBuilder, getEvents } from '@mixan/db'; export const eventRouter = createTRPCRouter({ list: protectedProcedure @@ -31,6 +31,8 @@ export const eventRouter = createTRPCRouter({ sb.orderBy.created_at = 'created_at DESC'; - return (await chQuery(getSql())).map(transformEvent); + const res = await getEvents(getSql(), { profile: true }); + + return res; }), }); diff --git a/packages/common/src/date.ts b/packages/common/src/date.ts index 9cdf7e88..b2bbdf8d 100644 --- a/packages/common/src/date.ts +++ b/packages/common/src/date.ts @@ -1,7 +1,7 @@ -export function getTime(date: string | number) { +export function getTime(date: string | number | Date) { return new Date(date).getTime(); } -export function toISOString(date: string | number) { +export function toISOString(date: string | number | Date) { return new Date(date).toISOString(); } diff --git a/packages/db/src/services/event.service.ts b/packages/db/src/services/event.service.ts index 70d173b3..c960b966 100644 --- a/packages/db/src/services/event.service.ts +++ b/packages/db/src/services/event.service.ts @@ -1,9 +1,11 @@ +import type { IDBProfile } from '@/prisma-types'; import { omit } from 'ramda'; import { randomSplitName, toDots } from '@mixan/common'; import { redis, redisPub } from '@mixan/redis'; import { ch, chQuery, formatClickhouseDate } from '../clickhouse-client'; +import type { Prisma } from '../prisma-client'; import { db } from '../prisma-client'; export interface IClickhouseEvent { @@ -27,6 +29,7 @@ export interface IClickhouseEvent { device: string; brand: string; model: string; + profile?: IDBProfile; } export function transformEvent( @@ -37,7 +40,7 @@ export function transformEvent( profileId: event.profile_id, projectId: event.project_id, properties: event.properties, - createdAt: event.created_at, + createdAt: new Date(event.created_at), country: event.country, city: event.city, region: event.region, @@ -53,6 +56,7 @@ export function transformEvent( referrer: event.referrer, referrerName: event.referrer_name, referrerType: event.referrer_type, + profile: event.profile, }; } @@ -64,7 +68,7 @@ export interface IServiceCreateEventPayload { hash?: string; query?: Record; }; - createdAt: string; + createdAt: Date; country?: string | undefined; city?: string | undefined; region?: string | undefined; @@ -81,12 +85,33 @@ export interface IServiceCreateEventPayload { referrer: string | undefined; referrerName: string | undefined; referrerType: string | undefined; + profile?: IDBProfile; } -export function getEvents(sql: string) { - return chQuery(sql).then((events) => - events.map(transformEvent) - ); +interface GetEventsOptions { + profile?: boolean | Prisma.ProfileSelect; +} + +export async function getEvents(sql: string, options: GetEventsOptions = {}) { + const events = await chQuery(sql); + if (options.profile) { + const profileIds = events.map((e) => e.profile_id); + const profiles = await db.profile.findMany({ + where: { + id: { + in: profileIds, + }, + }, + select: options.profile === true ? undefined : options.profile, + }); + + for (const event of events) { + event.profile = profiles.find((p) => p.id === event.profile_id) as + | IDBProfile + | undefined; + } + } + return events.map(transformEvent); } export async function createEvent(payload: IServiceCreateEventPayload) {