sdk improvement and get profile with events
This commit is contained in:
@@ -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),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user