fix: add filters for sessions table
This commit is contained in:
@@ -12,7 +12,6 @@ import {
|
||||
import { clix } from '../clickhouse/query-builder';
|
||||
import { createSqlBuilder } from '../sql-builder';
|
||||
import { getEventFiltersWhereClause } from './chart.service';
|
||||
import { getOrganizationByProjectIdCached } from './organization.service';
|
||||
import { getProfilesCached, type IServiceProfile } from './profile.service';
|
||||
|
||||
export interface IClickhouseSession {
|
||||
@@ -106,7 +105,12 @@ export interface GetSessionListOptions {
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
search?: string;
|
||||
cursor?: Cursor | null;
|
||||
cursor?: Date;
|
||||
minPageViews?: number | null;
|
||||
maxPageViews?: number | null;
|
||||
minEvents?: number | null;
|
||||
maxEvents?: number | null;
|
||||
dateIntervalInDays?: number;
|
||||
}
|
||||
|
||||
export function transformSession(session: IClickhouseSession): IServiceSession {
|
||||
@@ -151,35 +155,51 @@ export function transformSession(session: IClickhouseSession): IServiceSession {
|
||||
};
|
||||
}
|
||||
|
||||
interface PageInfo {
|
||||
next?: Cursor; // use last row
|
||||
}
|
||||
export async function getSessionList(options: GetSessionListOptions) {
|
||||
const {
|
||||
cursor,
|
||||
take,
|
||||
projectId,
|
||||
profileId,
|
||||
filters,
|
||||
startDate,
|
||||
endDate,
|
||||
search,
|
||||
minPageViews,
|
||||
maxPageViews,
|
||||
minEvents,
|
||||
maxEvents,
|
||||
dateIntervalInDays = 0.5,
|
||||
} = options;
|
||||
|
||||
interface Cursor {
|
||||
createdAt: string; // ISO 8601 with ms
|
||||
id: string;
|
||||
}
|
||||
|
||||
export async function getSessionList({
|
||||
cursor,
|
||||
take,
|
||||
projectId,
|
||||
profileId,
|
||||
filters,
|
||||
startDate,
|
||||
endDate,
|
||||
search,
|
||||
}: GetSessionListOptions) {
|
||||
const { sb, getSql } = createSqlBuilder();
|
||||
|
||||
sb.from = `${TABLE_NAMES.sessions} FINAL`;
|
||||
sb.limit = take;
|
||||
sb.where.projectId = `project_id = ${sqlstring.escape(projectId)}`;
|
||||
|
||||
if (startDate && endDate) {
|
||||
sb.where.range = `created_at BETWEEN toDateTime('${formatClickhouseDate(startDate)}') AND toDateTime('${formatClickhouseDate(endDate)}')`;
|
||||
const MAX_DATE_INTERVAL_IN_DAYS = 365;
|
||||
// Cap the date interval to prevent infinity
|
||||
const safeDateIntervalInDays = Math.min(
|
||||
dateIntervalInDays,
|
||||
MAX_DATE_INTERVAL_IN_DAYS
|
||||
);
|
||||
|
||||
if (cursor instanceof Date) {
|
||||
sb.where.cursorWindow = `created_at >= toDateTime64(${sqlstring.escape(formatClickhouseDate(cursor))}, 3) - INTERVAL ${safeDateIntervalInDays} DAY`;
|
||||
sb.where.cursor = `created_at < ${sqlstring.escape(formatClickhouseDate(cursor))}`;
|
||||
}
|
||||
|
||||
if (!(cursor || (startDate && endDate))) {
|
||||
sb.where.cursorWindow = `created_at >= toDateTime64(${sqlstring.escape(formatClickhouseDate(new Date()))}, 3) - INTERVAL ${safeDateIntervalInDays} DAY`;
|
||||
}
|
||||
|
||||
if (startDate && endDate) {
|
||||
sb.where.created_at = `toDate(created_at) BETWEEN toDate('${formatClickhouseDate(startDate)}') AND toDate('${formatClickhouseDate(endDate)}')`;
|
||||
}
|
||||
|
||||
sb.orderBy.created_at = 'created_at DESC';
|
||||
|
||||
if (profileId) {
|
||||
sb.where.profileId = `profile_id = ${sqlstring.escape(profileId)}`;
|
||||
}
|
||||
@@ -190,27 +210,19 @@ export async function getSessionList({
|
||||
if (filters?.length) {
|
||||
Object.assign(sb.where, getEventFiltersWhereClause(filters));
|
||||
}
|
||||
|
||||
const organization = await getOrganizationByProjectIdCached(projectId);
|
||||
// This will speed up the query quite a lot for big organizations
|
||||
const dateIntervalInDays =
|
||||
organization?.subscriptionPeriodEventsLimit &&
|
||||
organization?.subscriptionPeriodEventsLimit > 1_000_000
|
||||
? 2
|
||||
: 360;
|
||||
|
||||
if (cursor) {
|
||||
const cAt = sqlstring.escape(cursor.createdAt);
|
||||
sb.where.cursor = `created_at < toDateTime64(${cAt}, 3)`;
|
||||
sb.where.cursorWindow = `created_at >= toDateTime64(${cAt}, 3) - INTERVAL ${dateIntervalInDays} DAY`;
|
||||
sb.orderBy.created_at = 'created_at DESC';
|
||||
} else {
|
||||
sb.orderBy.created_at = 'created_at DESC';
|
||||
sb.where.created_at = `created_at > now() - INTERVAL ${dateIntervalInDays} DAY`;
|
||||
if (minPageViews != null) {
|
||||
sb.where.minPageViews = `screen_view_count >= ${minPageViews}`;
|
||||
}
|
||||
if (maxPageViews != null) {
|
||||
sb.where.maxPageViews = `screen_view_count <= ${maxPageViews}`;
|
||||
}
|
||||
if (minEvents != null) {
|
||||
sb.where.minEvents = `event_count >= ${minEvents}`;
|
||||
}
|
||||
if (maxEvents != null) {
|
||||
sb.where.maxEvents = `event_count <= ${maxEvents}`;
|
||||
}
|
||||
|
||||
// ==== Select columns (as you had) ====
|
||||
// sb.select.id = 'id'; sb.select.project_id = 'project_id'; ... etc.
|
||||
const columns = [
|
||||
'created_at',
|
||||
'ended_at',
|
||||
@@ -249,17 +261,17 @@ export async function getSessionList({
|
||||
}
|
||||
>(sql);
|
||||
|
||||
// Compute cursors from page edges
|
||||
const last = data[take - 1];
|
||||
|
||||
const meta: PageInfo = {
|
||||
next: last
|
||||
? {
|
||||
createdAt: last.created_at,
|
||||
id: last.id,
|
||||
}
|
||||
: undefined,
|
||||
};
|
||||
// If no results and we haven't reached the max window, retry with a larger interval
|
||||
if (
|
||||
data.length === 0 &&
|
||||
sb.where.cursorWindow &&
|
||||
safeDateIntervalInDays < MAX_DATE_INTERVAL_IN_DAYS
|
||||
) {
|
||||
return getSessionList({
|
||||
...options,
|
||||
dateIntervalInDays: dateIntervalInDays * 2,
|
||||
});
|
||||
}
|
||||
|
||||
// Profile hydration (unchanged)
|
||||
const profileIds = data
|
||||
@@ -283,6 +295,13 @@ export async function getSessionList({
|
||||
},
|
||||
}));
|
||||
|
||||
// Compute cursors from page edges
|
||||
const last = items.at(-1);
|
||||
|
||||
const meta = {
|
||||
next: last ? last.createdAt.toISOString() : undefined,
|
||||
};
|
||||
|
||||
return { items, meta };
|
||||
}
|
||||
|
||||
@@ -370,8 +389,41 @@ export async function getSessionReplayChunksFrom(
|
||||
};
|
||||
}
|
||||
|
||||
export const SESSION_DISTINCT_FIELDS = [
|
||||
'referrer_name',
|
||||
'country',
|
||||
'os',
|
||||
'browser',
|
||||
'device',
|
||||
] as const;
|
||||
|
||||
export type SessionDistinctField = (typeof SESSION_DISTINCT_FIELDS)[number];
|
||||
|
||||
export async function getSessionDistinctValues(
|
||||
projectId: string,
|
||||
field: SessionDistinctField,
|
||||
limit = 200
|
||||
): Promise<string[]> {
|
||||
const sql = `
|
||||
SELECT ${field} AS value, count() AS cnt
|
||||
FROM ${TABLE_NAMES.sessions}
|
||||
WHERE project_id = ${sqlstring.escape(projectId)}
|
||||
AND ${field} != ''
|
||||
AND sign = 1
|
||||
AND created_at > now() - INTERVAL 90 DAY
|
||||
GROUP BY value
|
||||
ORDER BY cnt DESC
|
||||
LIMIT ${limit}
|
||||
`;
|
||||
const results = await chQuery<{ value: string }>(sql);
|
||||
return results.map((r) => r.value).filter(Boolean);
|
||||
}
|
||||
|
||||
class SessionService {
|
||||
constructor(private client: typeof ch) {}
|
||||
private readonly client: typeof ch;
|
||||
constructor(client: typeof ch) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async byId(sessionId: string, projectId: string) {
|
||||
const [sessionRows, hasReplayRows] = await Promise.all([
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
getSessionDistinctValues,
|
||||
getSessionList,
|
||||
getSessionReplayChunksFrom,
|
||||
SESSION_DISTINCT_FIELDS,
|
||||
sessionService,
|
||||
} from '@openpanel/db';
|
||||
import { zChartEventFilter } from '@openpanel/validation';
|
||||
@@ -42,20 +44,28 @@ export const sessionRouter = createTRPCRouter({
|
||||
endDate: z.date().optional(),
|
||||
search: z.string().optional(),
|
||||
take: z.number().default(50),
|
||||
minPageViews: z.number().nullish(),
|
||||
maxPageViews: z.number().nullish(),
|
||||
minEvents: z.number().nullish(),
|
||||
maxEvents: z.number().nullish(),
|
||||
})
|
||||
)
|
||||
.query(async ({ input }) => {
|
||||
const cursor = input.cursor ? decodeCursor(input.cursor) : null;
|
||||
const data = await getSessionList({
|
||||
.query(({ input }) => {
|
||||
return getSessionList({
|
||||
...input,
|
||||
cursor,
|
||||
cursor: input.cursor ? new Date(input.cursor) : undefined,
|
||||
});
|
||||
return {
|
||||
data: data.items,
|
||||
meta: {
|
||||
next: data.meta.next ? encodeCursor(data.meta.next) : undefined,
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
distinctValues: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
field: z.enum(SESSION_DISTINCT_FIELDS),
|
||||
})
|
||||
)
|
||||
.query(({ input }) => {
|
||||
return getSessionDistinctValues(input.projectId, input.field);
|
||||
}),
|
||||
|
||||
byId: protectedProcedure
|
||||
|
||||
Reference in New Issue
Block a user