Files
stats/apps/worker/src/jobs/events.create-session-end.ts
Carl-Gerhard Lindesvärd da59622dce fix: overall perf improvements
* fix: ignore private ips

* fix: performance related fixes

* fix: simply event buffer

* fix: default to 1 events queue shard

* add: cleanup scripts

* fix: comments

* fix comments

* fix

* fix: groupmq

* wip

* fix: sync cachable

* remove cluster names and add it behind env flag (if someone want to scale)

* fix

* wip

* better logger

* remove reqid and user agent

* fix lock

* remove wait_for_async_insert
2025-11-15 22:13:59 +01:00

139 lines
3.4 KiB
TypeScript

import type { Job } from 'bullmq';
import { logger as baseLogger } from '@/utils/logger';
import {
type IClickhouseSession,
type IServiceCreateEventPayload,
type IServiceEvent,
TABLE_NAMES,
checkNotificationRulesForSessionEnd,
convertClickhouseDateToJs,
createEvent,
eventBuffer,
formatClickhouseDate,
getEvents,
getHasFunnelRules,
getNotificationRulesByProjectId,
sessionBuffer,
} from '@openpanel/db';
import type { EventsQueuePayloadCreateSessionEnd } from '@openpanel/queue';
const MAX_SESSION_EVENTS = 500;
// Grabs session_start and screen_views + the last occured event
async function getSessionEvents({
projectId,
sessionId,
startAt,
endAt,
}: {
projectId: string;
sessionId: string;
startAt: Date;
endAt: Date;
}): Promise<ReturnType<typeof getEvents>> {
const sql = `
SELECT * FROM ${TABLE_NAMES.events}
WHERE
session_id = '${sessionId}'
AND project_id = '${projectId}'
AND created_at BETWEEN '${formatClickhouseDate(startAt)}' AND '${formatClickhouseDate(endAt)}'
ORDER BY created_at DESC LIMIT ${MAX_SESSION_EVENTS};
`;
const [lastScreenView, eventsInDb] = await Promise.all([
eventBuffer.getLastScreenView({
projectId,
sessionId,
}),
getEvents(sql),
]);
// sort last inserted first
return [lastScreenView, ...eventsInDb]
.filter((event): event is IServiceEvent => !!event)
.sort(
(a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
);
}
export async function createSessionEnd(
job: Job<EventsQueuePayloadCreateSessionEnd>,
) {
const { payload } = job.data;
const logger = baseLogger.child({
payload,
jobId: job.id,
});
logger.debug('Processing session end job');
const session = await sessionBuffer.getExistingSession(payload.sessionId);
if (!session) {
throw new Error('Session not found');
}
try {
await handleSessionEndNotifications({
session,
payload,
});
} catch (error) {
logger.error('Creating notificatios for session end failed', {
error,
});
}
const lastScreenView = await eventBuffer.getLastScreenView({
projectId: payload.projectId,
sessionId: payload.sessionId,
});
// Create session end event
return createEvent({
...payload,
properties: {
...payload.properties,
...(lastScreenView?.properties ?? {}),
__bounce: session.is_bounce,
},
name: 'session_end',
duration: session.duration ?? 0,
path: lastScreenView?.path ?? '',
createdAt: new Date(
convertClickhouseDateToJs(session.ended_at).getTime() + 100,
),
profileId: lastScreenView?.profileId || payload.profileId,
});
}
async function handleSessionEndNotifications({
session,
payload,
}: {
session: IClickhouseSession;
payload: IServiceCreateEventPayload;
}) {
const notificationRules = await getNotificationRulesByProjectId(
payload.projectId,
);
const hasFunnelRules = getHasFunnelRules(notificationRules);
const isEventCountReasonable =
session.event_count + session.screen_view_count < MAX_SESSION_EVENTS;
if (hasFunnelRules && isEventCountReasonable) {
const events = await getSessionEvents({
projectId: payload.projectId,
sessionId: payload.sessionId,
startAt: new Date(session.created_at),
endAt: new Date(session.ended_at),
});
if (events.length > 0) {
await checkNotificationRulesForSessionEnd(events);
}
}
}