session activity identify stitching

This commit is contained in:
Francis Cao committed 2026-07-15 11:20:27 -07:00
1 parent 8a75208d5b
commit 7afc4ccf6b
15 files changed
+374 -23

No files matched your search

+4
View File
@@ -43,7 +43,10 @@ export * from './reports/getJourney';
export * from './reports/getRetention';
export * from './reports/getUTM';
export * from './sessions/createSession';
export * from './sessions/getLinkedDistinctIds';
export * from './sessions/getLinkedSessionIds';
export * from './sessions/getSessionActivity';
export * from './sessions/getSessionActivityBounds';
export * from './sessions/getSessionData';
export * from './sessions/getSessionDataActivityStats';
export * from './sessions/getSessionDataArraySeries';
@@ -61,3 +64,4 @@ export * from './sessions/getWebsiteSession';
export * from './sessions/getWebsiteSessionStats';
export * from './sessions/getWebsiteSessions';
export * from './sessions/saveSessionData';
export * from './sessions/saveSessionLink';
@@ -0,0 +1,44 @@
import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
const FUNCTION_NAME = 'getLinkedDistinctIds';
export async function getLinkedDistinctIds(
...args: [websiteId: string, sessionId: string]
): Promise<string[]> {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(websiteId: string, sessionId: string): Promise<string[]> {
const { rawQuery } = prisma;
return rawQuery(
`
select distinct distinct_id as "distinctId"
from session_link
where website_id = {{websiteId::uuid}}
and session_id = {{sessionId::uuid}}
`,
{ websiteId, sessionId },
FUNCTION_NAME,
).then(result => (result as { distinctId: string }[]).map(({ distinctId }) => distinctId));
}
async function clickhouseQuery(websiteId: string, sessionId: string): Promise<string[]> {
const { rawQuery } = clickhouse;
return rawQuery(
`
select distinct distinct_id as distinctId
from session_link
where website_id = {websiteId:UUID}
and session_id = {sessionId:UUID}
`,
{ websiteId, sessionId },
FUNCTION_NAME,
).then(result => (result as { distinctId: string }[]).map(({ distinctId }) => distinctId));
}
@@ -0,0 +1,50 @@
import clickhouse from '@/lib/clickhouse';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
const FUNCTION_NAME = 'getLinkedSessionIds';
export async function getLinkedSessionIds(
...args: [websiteId: string, distinctId: string]
): Promise<{ sessionId: string; createdAt: string }[]> {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(websiteId: string, distinctId: string) {
const { rawQuery } = prisma;
return rawQuery(
`
select
session_id as "sessionId",
min(created_at) as "createdAt"
from session_link
where website_id = {{websiteId::uuid}}
and distinct_id = {{distinctId}}
group by session_id
`,
{ websiteId, distinctId },
FUNCTION_NAME,
);
}
async function clickhouseQuery(websiteId: string, distinctId: string) {
const { rawQuery } = clickhouse;
return rawQuery(
`
select
session_id as sessionId,
min(created_at) as createdAt
from session_link
where website_id = {websiteId:UUID}
and distinct_id = {distinctId:String}
group by session_id
`,
{ websiteId, distinctId },
FUNCTION_NAME,
);
}
+11 -11
View File
@@ -7,7 +7,7 @@ import type { QueryFilters } from '@/lib/types';
const FUNCTION_NAME = 'getSessionActivity';
export async function getSessionActivity(
...args: [websiteId: string, sessionId: string, filters: QueryFilters]
...args: [websiteId: string, sessionIds: string[], filters: QueryFilters]
) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
@@ -15,7 +15,7 @@ export async function getSessionActivity(
});
}
async function relationalQuery(websiteId: string, sessionId: string, filters: QueryFilters) {
async function relationalQuery(websiteId: string, sessionIds: string[], filters: QueryFilters) {
const { rawQuery } = prisma;
const { startDate, endDate } = filters;
@@ -37,18 +37,18 @@ async function relationalQuery(websiteId: string, sessionId: string, filters: Qu
and created_at between {{startDate}} and {{endDate}}) AS "hasData"
from website_event
where website_id = {{websiteId::uuid}}
and session_id = {{sessionId::uuid}}
and session_id = any({{sessionIds}}::uuid[])
and event_type != ${EVENT_TYPE.performance}
and created_at between {{startDate}} and {{endDate}}
order by created_at desc
limit 500
`,
{ websiteId, sessionId, startDate, endDate },
{ websiteId, sessionIds, startDate, endDate },
FUNCTION_NAME,
);
}
async function clickhouseQuery(websiteId: string, sessionId: string, filters: QueryFilters) {
async function clickhouseQuery(websiteId: string, sessionIds: string[], filters: QueryFilters) {
const { rawQuery } = clickhouse;
const { startDate, endDate } = filters;
@@ -64,20 +64,20 @@ async function clickhouseQuery(websiteId: string, sessionId: string, filters: Qu
event_name as eventName,
visit_id as visitId,
hostname,
event_id IN (select event_id
from event_data
where website_id = {websiteId:UUID}
and session_id = {sessionId:UUID}
event_id IN (select event_id
from event_data
where website_id = {websiteId:UUID}
and session_id in {sessionIds:Array(UUID)}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}) AS hasData
from website_event
where website_id = {websiteId:UUID}
and session_id = {sessionId:UUID}
and session_id in {sessionIds:Array(UUID)}
and event_type != ${EVENT_TYPE.performance}
and created_at between {startDate:DateTime64} and {endDate:DateTime64}
order by created_at desc
limit 500
`,
{ websiteId, sessionId, startDate, endDate },
{ websiteId, sessionIds, startDate, endDate },
FUNCTION_NAME,
);
}
@@ -0,0 +1,62 @@
import clickhouse from '@/lib/clickhouse';
import { EVENT_TYPE } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import prisma from '@/lib/prisma';
const FUNCTION_NAME = 'getSessionActivityBounds';
interface SessionActivityBounds {
firstAt: string | Date;
lastAt: string | Date;
}
export async function getSessionActivityBounds(
...args: [websiteId: string, sessionIds: string[]]
): Promise<SessionActivityBounds | null> {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(
websiteId: string,
sessionIds: string[],
): Promise<SessionActivityBounds | null> {
const { rawQuery } = prisma;
return rawQuery(
`
select
min(created_at) as "firstAt",
max(created_at) as "lastAt"
from website_event
where website_id = {{websiteId::uuid}}
and session_id = any({{sessionIds}}::uuid[])
and event_type != ${EVENT_TYPE.performance}
`,
{ websiteId, sessionIds },
FUNCTION_NAME,
).then(result => (result?.[0]?.firstAt && result?.[0]?.lastAt ? result[0] : null));
}
async function clickhouseQuery(
websiteId: string,
sessionIds: string[],
): Promise<SessionActivityBounds | null> {
const { rawQuery, getDateStringSQL } = clickhouse;
return rawQuery(
`
select
${getDateStringSQL('min(created_at)')} as firstAt,
${getDateStringSQL('max(created_at)')} as lastAt
from website_event
where website_id = {websiteId:UUID}
and session_id in {sessionIds:Array(UUID)}
and event_type != ${EVENT_TYPE.performance}
`,
{ websiteId, sessionIds },
FUNCTION_NAME,
).then(result => (result?.[0]?.firstAt && result?.[0]?.lastAt ? result[0] : null));
}
@@ -0,0 +1,79 @@
import clickhouse from '@/lib/clickhouse';
import { FIELD_LENGTH } from '@/lib/constants';
import { CLICKHOUSE, PRISMA, runQuery } from '@/lib/db';
import { truncateString } from '@/lib/format';
import kafka from '@/lib/kafka';
import prisma from '@/lib/prisma';
const FUNCTION_NAME = 'saveSessionLink';
export interface SaveSessionLinkArgs {
websiteId: string;
sessionId: string;
distinctId: string;
createdAt?: Date;
}
export async function saveSessionLink(data: SaveSessionLinkArgs) {
return runQuery({
[PRISMA]: () => relationalQuery(data),
[CLICKHOUSE]: () => clickhouseQuery(data),
});
}
async function relationalQuery({
websiteId,
sessionId,
distinctId,
createdAt,
}: SaveSessionLinkArgs) {
const { writeRawQuery } = prisma;
await writeRawQuery(
`
insert into session_link (
website_id,
session_id,
distinct_id,
created_at
)
values (
{{websiteId}},
{{sessionId}},
{{distinctId}},
{{createdAt}}
)
on conflict (website_id, distinct_id, session_id) do nothing
`,
{
websiteId,
sessionId,
distinctId: truncateString(distinctId, FIELD_LENGTH.distinctId),
createdAt,
},
FUNCTION_NAME,
);
}
async function clickhouseQuery({
websiteId,
sessionId,
distinctId,
createdAt,
}: SaveSessionLinkArgs) {
const { insert, getUTCString } = clickhouse;
const { sendMessage } = kafka;
const message = {
website_id: websiteId,
session_id: sessionId,
distinct_id: truncateString(distinctId, FIELD_LENGTH.distinctId),
created_at: getUTCString(createdAt),
};
if (kafka.enabled) {
await sendMessage('session_link', [message]);
} else {
await insert('session_link', [message]);
}
}