fix: remove old event queue, cleaned up session handling, remove hacks
This commit is contained in:
@@ -23,7 +23,7 @@
|
||||
"@openpanel/redis": "workspace:*",
|
||||
"bullmq": "^5.8.7",
|
||||
"express": "^4.18.2",
|
||||
"groupmq": "1.0.0-next.17",
|
||||
"groupmq": "1.0.0-next.18",
|
||||
"prom-client": "^15.1.3",
|
||||
"ramda": "^0.29.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
type EventsQueuePayloadIncomingEvent,
|
||||
cronQueue,
|
||||
eventsGroupQueue,
|
||||
eventsQueue,
|
||||
miscQueue,
|
||||
notificationQueue,
|
||||
queueLogger,
|
||||
@@ -45,7 +44,6 @@ export async function bootWorkers() {
|
||||
},
|
||||
});
|
||||
eventsGroupWorker.run();
|
||||
const eventsWorker = new Worker(eventsQueue.name, eventsJob, workerOptions);
|
||||
const sessionsWorker = new Worker(
|
||||
sessionsQueue.name,
|
||||
sessionsJob,
|
||||
@@ -61,7 +59,6 @@ export async function bootWorkers() {
|
||||
|
||||
const workers = [
|
||||
sessionsWorker,
|
||||
eventsWorker,
|
||||
cronWorker,
|
||||
notificationWorker,
|
||||
miscWorker,
|
||||
|
||||
@@ -7,7 +7,6 @@ import { createInitialSalts } from '@openpanel/db';
|
||||
import {
|
||||
cronQueue,
|
||||
eventsGroupQueue,
|
||||
eventsQueue,
|
||||
miscQueue,
|
||||
notificationQueue,
|
||||
sessionsQueue,
|
||||
@@ -36,7 +35,6 @@ async function start() {
|
||||
createBullBoard({
|
||||
queues: [
|
||||
new BullBoardGroupMQAdapter(eventsGroupQueue) as any,
|
||||
new BullMQAdapter(eventsQueue),
|
||||
new BullMQAdapter(sessionsQueue),
|
||||
new BullMQAdapter(cronQueue),
|
||||
new BullMQAdapter(notificationQueue),
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { Job } from 'bullmq';
|
||||
import { logger as baseLogger } from '@/utils/logger';
|
||||
import { getTime } from '@openpanel/common';
|
||||
import {
|
||||
type IClickhouseSession,
|
||||
type IServiceCreateEventPayload,
|
||||
type IServiceEvent,
|
||||
TABLE_NAMES,
|
||||
checkNotificationRulesForSessionEnd,
|
||||
@@ -10,37 +12,33 @@ import {
|
||||
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 getNecessarySessionEvents({
|
||||
async function getSessionEvents({
|
||||
projectId,
|
||||
sessionId,
|
||||
createdAt,
|
||||
startAt,
|
||||
endAt,
|
||||
}: {
|
||||
projectId: string;
|
||||
sessionId: string;
|
||||
createdAt: Date;
|
||||
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 >= '${formatClickhouseDate(new Date(new Date(createdAt).getTime() - 1000 * 60 * 5))}'
|
||||
AND (
|
||||
name IN ('screen_view', 'session_start')
|
||||
OR created_at = (
|
||||
SELECT MAX(created_at)
|
||||
FROM ${TABLE_NAMES.events}
|
||||
WHERE session_id = '${sessionId}'
|
||||
AND project_id = '${projectId}'
|
||||
AND created_at >= '${formatClickhouseDate(new Date(new Date(createdAt).getTime() - 1000 * 60 * 5))}'
|
||||
AND name NOT IN ('screen_view', 'session_start')
|
||||
)
|
||||
)
|
||||
ORDER BY created_at DESC;
|
||||
AND created_at BETWEEN '${formatClickhouseDate(startAt)}' AND '${formatClickhouseDate(endAt)}'
|
||||
ORDER BY created_at DESC LIMIT ${MAX_SESSION_EVENTS};
|
||||
`;
|
||||
|
||||
const [lastScreenView, eventsInDb] = await Promise.all([
|
||||
@@ -63,62 +61,77 @@ async function getNecessarySessionEvents({
|
||||
export async function createSessionEnd(
|
||||
job: Job<EventsQueuePayloadCreateSessionEnd>,
|
||||
) {
|
||||
const { payload } = job.data;
|
||||
const logger = baseLogger.child({
|
||||
payload: job.data.payload,
|
||||
payload,
|
||||
jobId: job.id,
|
||||
reqId: job.data.payload.properties?.__reqId ?? 'unknown',
|
||||
reqId: payload.properties?.__reqId ?? 'unknown',
|
||||
});
|
||||
|
||||
logger.info('Processing session end job');
|
||||
|
||||
const payload = job.data.payload;
|
||||
const session = await sessionBuffer.getExistingSession(payload.sessionId);
|
||||
|
||||
const events = await getNecessarySessionEvents({
|
||||
if (!session) {
|
||||
throw new Error('Session not found');
|
||||
}
|
||||
|
||||
try {
|
||||
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,
|
||||
createdAt: payload.createdAt,
|
||||
});
|
||||
|
||||
const sessionStart = events.find((event) => event.name === 'session_start');
|
||||
const screenViews = events.filter((event) => event.name === 'screen_view');
|
||||
const lastEvent = events[0];
|
||||
|
||||
if (!sessionStart) {
|
||||
throw new Error('No session_start found');
|
||||
}
|
||||
|
||||
if (!lastEvent) {
|
||||
throw new Error('No last event found');
|
||||
}
|
||||
|
||||
const sessionDuration =
|
||||
lastEvent.createdAt.getTime() - sessionStart.createdAt.getTime();
|
||||
|
||||
await checkNotificationRulesForSessionEnd(events).catch(() => {
|
||||
logger.error('Error checking notification rules for session end', {
|
||||
data: job.data,
|
||||
});
|
||||
});
|
||||
|
||||
logger.info('Creating session_end event', {
|
||||
sessionStart,
|
||||
lastEvent,
|
||||
screenViews,
|
||||
sessionDuration,
|
||||
events,
|
||||
});
|
||||
|
||||
// Create session end event
|
||||
return createEvent({
|
||||
...sessionStart,
|
||||
...payload,
|
||||
properties: {
|
||||
...sessionStart.properties,
|
||||
...(screenViews[0]?.properties ?? {}),
|
||||
__bounce: screenViews.length <= 1,
|
||||
...payload.properties,
|
||||
...(lastScreenView?.properties ?? {}),
|
||||
__bounce: session.is_bounce,
|
||||
},
|
||||
name: 'session_end',
|
||||
duration: sessionDuration,
|
||||
path: screenViews[0]?.path ?? '',
|
||||
createdAt: new Date(getTime(lastEvent.createdAt) + 1000),
|
||||
profileId: lastEvent.profileId || sessionStart.profileId,
|
||||
duration: session.duration ?? 0,
|
||||
path: lastScreenView?.path ?? '',
|
||||
createdAt: new Date(getTime(session.ended_at) + 1000),
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,18 +7,13 @@ import {
|
||||
profileBuffer,
|
||||
sessionBuffer,
|
||||
} from '@openpanel/db';
|
||||
import {
|
||||
cronQueue,
|
||||
eventsGroupQueue,
|
||||
eventsQueue,
|
||||
sessionsQueue,
|
||||
} from '@openpanel/queue';
|
||||
import { cronQueue, eventsGroupQueue, sessionsQueue } from '@openpanel/queue';
|
||||
|
||||
const Registry = client.Registry;
|
||||
|
||||
export const register = new Registry();
|
||||
|
||||
const queues = [eventsQueue, sessionsQueue, cronQueue, eventsGroupQueue];
|
||||
const queues = [sessionsQueue, cronQueue, eventsGroupQueue];
|
||||
|
||||
queues.forEach((queue) => {
|
||||
register.registerMetric(
|
||||
|
||||
@@ -65,12 +65,6 @@ export async function getSessionEnd({
|
||||
});
|
||||
|
||||
if (sessionEnd) {
|
||||
// Hack: if session end job just got created, we want to give it a chance to complete
|
||||
// So the order is correct
|
||||
if (sessionEnd.job.timestamp > Date.now() - 50) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const existingSessionIsAnonymous =
|
||||
sessionEnd.job.data.payload.profileId ===
|
||||
sessionEnd.job.data.payload.deviceId;
|
||||
|
||||
Reference in New Issue
Block a user