sdk improvement and get profile with events

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-02-11 23:26:30 +01:00
parent 85e4922dc3
commit 6f2aeffdff
8 changed files with 86 additions and 24 deletions

View File

@@ -1,5 +1,5 @@
import { parseIp } from '@/utils/parseIp'; import { parseIp } from '@/utils/parseIp';
import { parseReferrer } from '@/utils/parseReferrer'; import { getReferrerWithQuery, parseReferrer } from '@/utils/parseReferrer';
import { parseUserAgent } from '@/utils/parseUserAgent'; import { parseUserAgent } from '@/utils/parseUserAgent';
import type { FastifyReply, FastifyRequest } from 'fastify'; import type { FastifyReply, FastifyRequest } from 'fastify';
import { omit } from 'ramda'; import { omit } from 'ramda';
@@ -15,16 +15,18 @@ import type { PostEventPayload } from '@mixan/types';
const SESSION_TIMEOUT = 1000 * 60 * 30; const SESSION_TIMEOUT = 1000 * 60 * 30;
const SESSION_END_TIMEOUT = SESSION_TIMEOUT + 1000; const SESSION_END_TIMEOUT = SESSION_TIMEOUT + 1000;
function parseSearchParams(params: URLSearchParams): Record<string, string> { function parseSearchParams(
params: URLSearchParams
): Record<string, string> | undefined {
const result: Record<string, string> = {}; const result: Record<string, string> = {};
for (const [key, value] of params.entries()) { for (const [key, value] of params.entries()) {
result[key] = value; result[key] = value;
} }
return result; return Object.keys(result).length ? result : undefined;
} }
function parsePath(path?: string): { function parsePath(path?: string): {
query?: Record<string, unknown>; query?: Record<string, string>;
path: string; path: string;
hash?: string; hash?: string;
} { } {
@@ -39,7 +41,7 @@ function parsePath(path?: string): {
return { return {
query: parseSearchParams(url.searchParams), query: parseSearchParams(url.searchParams),
path: url.pathname, path: url.pathname,
hash: url.hash, hash: url.hash ?? undefined,
}; };
} catch (error) { } catch (error) {
return { return {
@@ -57,8 +59,10 @@ export async function postEvent(
let profileId: string | null = null; let profileId: string | null = null;
const projectId = request.projectId; const projectId = request.projectId;
const body = request.body; const body = request.body;
const createdAt = new Date(body.timestamp);
const { path, hash, query } = parsePath(body.properties?.path); const { path, hash, query } = parsePath(body.properties?.path);
const referrer = parseReferrer(body.properties?.referrer); const referrer = parseReferrer(body.properties?.referrer);
const utmReferrer = getReferrerWithQuery(query);
const ip = getClientIp(request)!; const ip = getClientIp(request)!;
const origin = request.headers.origin!; const origin = request.headers.origin!;
const ua = request.headers['user-agent']!; const ua = request.headers['user-agent']!;
@@ -132,7 +136,7 @@ export async function postEvent(
hash, hash,
query, query,
}), }),
createdAt: body.timestamp, createdAt,
country: geo.country, country: geo.country,
city: geo.city, city: geo.city,
region: geo.region, region: geo.region,
@@ -147,8 +151,8 @@ export async function postEvent(
duration: 0, duration: 0,
path: path, path: path,
referrer: referrer.url, referrer: referrer.url,
referrerName: referrer.name, referrerName: referrer.name ?? utmReferrer?.name ?? '',
referrerType: referrer.type, referrerType: referrer.type ?? utmReferrer?.type ?? '',
}; };
const job = findJobByPrefix(eventsJobs, `event:${projectId}:${profileId}:`); const job = findJobByPrefix(eventsJobs, `event:${projectId}:${profileId}:`);
@@ -180,6 +184,7 @@ export async function postEvent(
payload: { payload: {
...payload, ...payload,
name: 'session_start', name: 'session_start',
// @ts-expect-error
createdAt: toISOString(getTime(payload.createdAt) - 10), createdAt: toISOString(getTime(payload.createdAt) - 10),
}, },
}); });

View File

@@ -24,3 +24,27 @@ export function parseReferrer(url: string | undefined) {
url: url ?? '', url: url ?? '',
}; };
} }
export function getReferrerWithQuery(
query: Record<string, string> | 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: '',
};
}

View File

@@ -20,6 +20,7 @@ export function EventListItem({
createdAt, createdAt,
name, name,
properties, properties,
path,
}: EventListItemProps) { }: EventListItemProps) {
const params = useAppParams(); const params = useAppParams();
@@ -46,16 +47,15 @@ export function EventListItem({
switch (name) { switch (name) {
case 'screen_view': { case 'screen_view': {
const route = (properties?.route || properties?.path)!; if (path) {
if (route) { bullets.push(path);
bullets.push(route);
} }
break; break;
} }
} }
return bullets; return bullets;
}, [name, createdAt, profile, properties, params]); }, [name, createdAt, profile, properties, params, path]);
return ( return (
<ExpandableListItem <ExpandableListItem

View File

@@ -12,6 +12,12 @@ const handler = (req: Request) =>
session: auth(), 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 }; export { handler as GET, handler as POST };

View File

@@ -27,8 +27,8 @@ export function ExpandableListItem({
<div className="flex flex-col flex-1 gap-1 min-w-0"> <div className="flex flex-col flex-1 gap-1 min-w-0">
<span className="text-lg font-medium leading-none mb-1">{title}</span> <span className="text-lg font-medium leading-none mb-1">{title}</span>
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-4 text-sm text-muted-foreground"> <div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-4 text-sm text-muted-foreground">
{bullets.map((bullet) => ( {bullets.map((bullet, index) => (
<span key={bullet}>{bullet}</span> <span key={index}>{bullet}</span>
))} ))}
</div> </div>
</div> </div>

View File

@@ -3,7 +3,7 @@ import { transformEvent } from '@/server/services/event.service';
import { z } from 'zod'; import { z } from 'zod';
import type { IDBEvent } from '@mixan/db'; import type { IDBEvent } from '@mixan/db';
import { chQuery, createSqlBuilder } from '@mixan/db'; import { chQuery, createSqlBuilder, getEvents } from '@mixan/db';
export const eventRouter = createTRPCRouter({ export const eventRouter = createTRPCRouter({
list: protectedProcedure list: protectedProcedure
@@ -31,6 +31,8 @@ export const eventRouter = createTRPCRouter({
sb.orderBy.created_at = 'created_at DESC'; sb.orderBy.created_at = 'created_at DESC';
return (await chQuery<IDBEvent>(getSql())).map(transformEvent); const res = await getEvents(getSql(), { profile: true });
return res;
}), }),
}); });

View File

@@ -1,7 +1,7 @@
export function getTime(date: string | number) { export function getTime(date: string | number | Date) {
return new Date(date).getTime(); return new Date(date).getTime();
} }
export function toISOString(date: string | number) { export function toISOString(date: string | number | Date) {
return new Date(date).toISOString(); return new Date(date).toISOString();
} }

View File

@@ -1,9 +1,11 @@
import type { IDBProfile } from '@/prisma-types';
import { omit } from 'ramda'; import { omit } from 'ramda';
import { randomSplitName, toDots } from '@mixan/common'; import { randomSplitName, toDots } from '@mixan/common';
import { redis, redisPub } from '@mixan/redis'; import { redis, redisPub } from '@mixan/redis';
import { ch, chQuery, formatClickhouseDate } from '../clickhouse-client'; import { ch, chQuery, formatClickhouseDate } from '../clickhouse-client';
import type { Prisma } from '../prisma-client';
import { db } from '../prisma-client'; import { db } from '../prisma-client';
export interface IClickhouseEvent { export interface IClickhouseEvent {
@@ -27,6 +29,7 @@ export interface IClickhouseEvent {
device: string; device: string;
brand: string; brand: string;
model: string; model: string;
profile?: IDBProfile;
} }
export function transformEvent( export function transformEvent(
@@ -37,7 +40,7 @@ export function transformEvent(
profileId: event.profile_id, profileId: event.profile_id,
projectId: event.project_id, projectId: event.project_id,
properties: event.properties, properties: event.properties,
createdAt: event.created_at, createdAt: new Date(event.created_at),
country: event.country, country: event.country,
city: event.city, city: event.city,
region: event.region, region: event.region,
@@ -53,6 +56,7 @@ export function transformEvent(
referrer: event.referrer, referrer: event.referrer,
referrerName: event.referrer_name, referrerName: event.referrer_name,
referrerType: event.referrer_type, referrerType: event.referrer_type,
profile: event.profile,
}; };
} }
@@ -64,7 +68,7 @@ export interface IServiceCreateEventPayload {
hash?: string; hash?: string;
query?: Record<string, unknown>; query?: Record<string, unknown>;
}; };
createdAt: string; createdAt: Date;
country?: string | undefined; country?: string | undefined;
city?: string | undefined; city?: string | undefined;
region?: string | undefined; region?: string | undefined;
@@ -81,12 +85,33 @@ export interface IServiceCreateEventPayload {
referrer: string | undefined; referrer: string | undefined;
referrerName: string | undefined; referrerName: string | undefined;
referrerType: string | undefined; referrerType: string | undefined;
profile?: IDBProfile;
} }
export function getEvents(sql: string) { interface GetEventsOptions {
return chQuery<IClickhouseEvent>(sql).then((events) => profile?: boolean | Prisma.ProfileSelect;
events.map(transformEvent) }
);
export async function getEvents(sql: string, options: GetEventsOptions = {}) {
const events = await chQuery<IClickhouseEvent>(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) { export async function createEvent(payload: IServiceCreateEventPayload) {