perf(worker): try to improve perf of worker
This commit is contained in:
@@ -3,8 +3,6 @@ import { last } from 'ramda';
|
|||||||
|
|
||||||
import { getTime } from '@openpanel/common';
|
import { getTime } from '@openpanel/common';
|
||||||
import {
|
import {
|
||||||
IClickhouseEvent,
|
|
||||||
type IServiceEvent,
|
|
||||||
TABLE_NAMES,
|
TABLE_NAMES,
|
||||||
createEvent,
|
createEvent,
|
||||||
eventBuffer,
|
eventBuffer,
|
||||||
@@ -45,10 +43,24 @@ export async function createSessionEnd(
|
|||||||
});
|
});
|
||||||
|
|
||||||
const payload = job.data.payload;
|
const payload = job.data.payload;
|
||||||
const eventsInBuffer: IServiceEvent[] = [];
|
const lastScreenView = await eventBuffer.getLastScreenView({
|
||||||
// const eventsInBuffer = await eventBuffer.findMany(
|
projectId: payload.projectId,
|
||||||
// (item) => item.session_id === payload.sessionId,
|
profileId: payload.profileId || payload.deviceId,
|
||||||
// );
|
});
|
||||||
|
|
||||||
|
const eventsInBuffer = lastScreenView
|
||||||
|
? [lastScreenView]
|
||||||
|
: await eventBuffer.findMany(
|
||||||
|
(item) => item.session_id === payload.sessionId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (lastScreenView) {
|
||||||
|
logger.info('found last screen view in buffer');
|
||||||
|
} else if (eventsInBuffer.length > 0) {
|
||||||
|
logger.info('found events in buffer');
|
||||||
|
} else {
|
||||||
|
logger.info('no events in buffer');
|
||||||
|
}
|
||||||
|
|
||||||
let eventsInDb = await getCompleteSession({
|
let eventsInDb = await getCompleteSession({
|
||||||
projectId: payload.projectId,
|
projectId: payload.projectId,
|
||||||
@@ -66,6 +78,16 @@ export async function createSessionEnd(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If session_start does not exist, try to find it the last 72 hours
|
||||||
|
if (!eventsInDb.find((event) => event.name === 'session_start')) {
|
||||||
|
logger.warn('Checking last 72 hours for session_start');
|
||||||
|
eventsInDb = await getCompleteSession({
|
||||||
|
projectId: payload.projectId,
|
||||||
|
sessionId: payload.sessionId,
|
||||||
|
hoursInterval: 72,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// sort last inserted first
|
// sort last inserted first
|
||||||
const events = [...eventsInBuffer, ...eventsInDb].sort(
|
const events = [...eventsInBuffer, ...eventsInDb].sort(
|
||||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { getTime, isSameDomain, parsePath } from '@openpanel/common';
|
|||||||
import type { IServiceCreateEventPayload } from '@openpanel/db';
|
import type { IServiceCreateEventPayload } from '@openpanel/db';
|
||||||
import { createEvent } from '@openpanel/db';
|
import { createEvent } from '@openpanel/db';
|
||||||
import { getLastScreenViewFromProfileId } from '@openpanel/db/src/services/event.service';
|
import { getLastScreenViewFromProfileId } from '@openpanel/db/src/services/event.service';
|
||||||
|
import { createLogger } from '@openpanel/logger';
|
||||||
import { findJobByPrefix, sessionsQueue } from '@openpanel/queue';
|
import { findJobByPrefix, sessionsQueue } from '@openpanel/queue';
|
||||||
import type {
|
import type {
|
||||||
EventsQueuePayloadCreateSessionEnd,
|
EventsQueuePayloadCreateSessionEnd,
|
||||||
@@ -18,6 +19,13 @@ import { getRedisQueue } from '@openpanel/redis';
|
|||||||
const GLOBAL_PROPERTIES = ['__path', '__referrer'];
|
const GLOBAL_PROPERTIES = ['__path', '__referrer'];
|
||||||
export const SESSION_TIMEOUT = 1000 * 60 * 30;
|
export const SESSION_TIMEOUT = 1000 * 60 * 30;
|
||||||
|
|
||||||
|
const logger = createLogger({
|
||||||
|
name: 'job:incoming-event',
|
||||||
|
});
|
||||||
|
|
||||||
|
const getSessionEndJobId = (projectId: string, deviceId: string) =>
|
||||||
|
`sessionEnd:${projectId}:${deviceId}`;
|
||||||
|
|
||||||
export async function incomingEvent(job: Job<EventsQueuePayloadIncomingEvent>) {
|
export async function incomingEvent(job: Job<EventsQueuePayloadIncomingEvent>) {
|
||||||
const {
|
const {
|
||||||
geo,
|
geo,
|
||||||
@@ -148,10 +156,6 @@ export async function incomingEvent(job: Job<EventsQueuePayloadIncomingEvent>) {
|
|||||||
sdkVersion,
|
sdkVersion,
|
||||||
};
|
};
|
||||||
|
|
||||||
const sessionEndJobId =
|
|
||||||
sessionEnd?.job.id ??
|
|
||||||
`sessionEnd:${projectId}:${sessionEndPayload.deviceId}:${getTime(createdAt)}`;
|
|
||||||
|
|
||||||
if (sessionEnd) {
|
if (sessionEnd) {
|
||||||
// If for some reason we have a session end job that is not a createSessionEnd job
|
// If for some reason we have a session end job that is not a createSessionEnd job
|
||||||
if (sessionEnd.job.data.type !== 'createSessionEnd') {
|
if (sessionEnd.job.data.type !== 'createSessionEnd') {
|
||||||
@@ -168,7 +172,7 @@ export async function incomingEvent(job: Job<EventsQueuePayloadIncomingEvent>) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
delay: SESSION_TIMEOUT,
|
delay: SESSION_TIMEOUT,
|
||||||
jobId: sessionEndJobId,
|
jobId: getSessionEndJobId(projectId, sessionEndPayload.deviceId),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -215,6 +219,21 @@ async function getSessionEnd({
|
|||||||
currentDeviceId: string;
|
currentDeviceId: string;
|
||||||
previousDeviceId: string;
|
previousDeviceId: string;
|
||||||
}) {
|
}) {
|
||||||
|
const job = await sessionsQueue.getJob(
|
||||||
|
getSessionEndJobId(projectId, currentDeviceId),
|
||||||
|
);
|
||||||
|
if (job && (await job.isDelayed())) {
|
||||||
|
return { deviceId: currentDeviceId, job };
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousJob = await sessionsQueue.getJob(
|
||||||
|
getSessionEndJobId(projectId, previousDeviceId),
|
||||||
|
);
|
||||||
|
if (previousJob && (await previousJob.isDelayed())) {
|
||||||
|
return { deviceId: previousDeviceId, job: previousJob };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback during migration period
|
||||||
const currentSessionEndKeys = await getRedisQueue().keys(
|
const currentSessionEndKeys = await getRedisQueue().keys(
|
||||||
`bull:sessions:sessionEnd:${projectId}:${currentDeviceId}:*`,
|
`bull:sessions:sessionEnd:${projectId}:${currentDeviceId}:*`,
|
||||||
);
|
);
|
||||||
@@ -225,6 +244,7 @@ async function getSessionEnd({
|
|||||||
`sessionEnd:${projectId}:${currentDeviceId}:`,
|
`sessionEnd:${projectId}:${currentDeviceId}:`,
|
||||||
);
|
);
|
||||||
if (sessionEndJobCurrentDeviceId) {
|
if (sessionEndJobCurrentDeviceId) {
|
||||||
|
logger.info('found session end job for current device (old)');
|
||||||
return { deviceId: currentDeviceId, job: sessionEndJobCurrentDeviceId };
|
return { deviceId: currentDeviceId, job: sessionEndJobCurrentDeviceId };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,6 +258,7 @@ async function getSessionEnd({
|
|||||||
`sessionEnd:${projectId}:${previousDeviceId}:`,
|
`sessionEnd:${projectId}:${previousDeviceId}:`,
|
||||||
);
|
);
|
||||||
if (sessionEndJobPreviousDeviceId) {
|
if (sessionEndJobPreviousDeviceId) {
|
||||||
|
logger.info('found session end job for previous device (old)');
|
||||||
return { deviceId: previousDeviceId, job: sessionEndJobPreviousDeviceId };
|
return { deviceId: previousDeviceId, job: sessionEndJobPreviousDeviceId };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,351 +1,351 @@
|
|||||||
import { type Mock, beforeEach, describe, expect, it, mock } from 'bun:test';
|
// import { type Mock, beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||||
import { getTime, toISOString } from '@openpanel/common';
|
// import { getTime, toISOString } from '@openpanel/common';
|
||||||
import type { Job } from 'bullmq';
|
// import type { Job } from 'bullmq';
|
||||||
import { SESSION_TIMEOUT, incomingEvent } from './events.incoming-event';
|
// import { SESSION_TIMEOUT, incomingEvent } from './events.incoming-event';
|
||||||
|
|
||||||
const projectId = 'test-project';
|
// const projectId = 'test-project';
|
||||||
const currentDeviceId = 'device-123';
|
// const currentDeviceId = 'device-123';
|
||||||
const previousDeviceId = 'device-456';
|
// const previousDeviceId = 'device-456';
|
||||||
const geo = {
|
// const geo = {
|
||||||
country: 'US',
|
// country: 'US',
|
||||||
city: 'New York',
|
// city: 'New York',
|
||||||
region: 'NY',
|
// region: 'NY',
|
||||||
longitude: 0,
|
// longitude: 0,
|
||||||
latitude: 0,
|
// latitude: 0,
|
||||||
};
|
// };
|
||||||
|
|
||||||
const createEvent = mock(() => {});
|
// const createEvent = mock(() => {});
|
||||||
const getLastScreenViewFromProfileId = mock();
|
// const getLastScreenViewFromProfileId = mock();
|
||||||
// // Mock dependencies
|
// // // Mock dependencies
|
||||||
mock.module('@openpanel/db', () => ({
|
// mock.module('@openpanel/db', () => ({
|
||||||
createEvent,
|
// createEvent,
|
||||||
getLastScreenViewFromProfileId,
|
// getLastScreenViewFromProfileId,
|
||||||
}));
|
// }));
|
||||||
|
|
||||||
const sessionsQueue = { add: mock(() => Promise.resolve({})) };
|
// const sessionsQueue = { add: mock(() => Promise.resolve({})) };
|
||||||
|
|
||||||
const findJobByPrefix = mock();
|
// const findJobByPrefix = mock();
|
||||||
|
|
||||||
mock.module('@openpanel/queue', () => ({
|
// mock.module('@openpanel/queue', () => ({
|
||||||
sessionsQueue,
|
// sessionsQueue,
|
||||||
findJobByPrefix,
|
// findJobByPrefix,
|
||||||
}));
|
// }));
|
||||||
|
|
||||||
const getRedisQueue = mock(() => ({
|
// const getRedisQueue = mock(() => ({
|
||||||
keys: mock(() => Promise.resolve([])),
|
// keys: mock(() => Promise.resolve([])),
|
||||||
}));
|
// }));
|
||||||
|
|
||||||
mock.module('@openpanel/redis', () => ({
|
// mock.module('@openpanel/redis', () => ({
|
||||||
getRedisQueue,
|
// getRedisQueue,
|
||||||
}));
|
// }));
|
||||||
|
|
||||||
describe('incomingEvent', () => {
|
// describe('incomingEvent', () => {
|
||||||
beforeEach(() => {
|
// beforeEach(() => {
|
||||||
createEvent.mockClear();
|
// createEvent.mockClear();
|
||||||
findJobByPrefix.mockClear();
|
// findJobByPrefix.mockClear();
|
||||||
sessionsQueue.add.mockClear();
|
// sessionsQueue.add.mockClear();
|
||||||
getLastScreenViewFromProfileId.mockClear();
|
// getLastScreenViewFromProfileId.mockClear();
|
||||||
});
|
|
||||||
|
|
||||||
it('should create a session start and an event', async () => {
|
|
||||||
const timestamp = new Date();
|
|
||||||
// Mock job data
|
|
||||||
const jobData = {
|
|
||||||
payload: {
|
|
||||||
geo,
|
|
||||||
event: {
|
|
||||||
name: 'test_event',
|
|
||||||
timestamp: timestamp.toISOString(),
|
|
||||||
properties: { __path: 'https://example.com/test' },
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
'user-agent':
|
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
|
||||||
'openpanel-sdk-name': 'web',
|
|
||||||
'openpanel-sdk-version': '1.0.0',
|
|
||||||
},
|
|
||||||
projectId,
|
|
||||||
currentDeviceId,
|
|
||||||
previousDeviceId,
|
|
||||||
priority: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const job = { data: jobData } as Job;
|
|
||||||
|
|
||||||
// Execute the job
|
|
||||||
await incomingEvent(job);
|
|
||||||
|
|
||||||
const event = {
|
|
||||||
name: 'test_event',
|
|
||||||
deviceId: currentDeviceId,
|
|
||||||
// @ts-expect-error
|
|
||||||
sessionId: createEvent.mock.calls[1][0].sessionId,
|
|
||||||
profileId: '',
|
|
||||||
projectId,
|
|
||||||
properties: {
|
|
||||||
__hash: undefined,
|
|
||||||
__query: undefined,
|
|
||||||
},
|
|
||||||
createdAt: timestamp,
|
|
||||||
country: 'US',
|
|
||||||
city: 'New York',
|
|
||||||
region: 'NY',
|
|
||||||
longitude: 0,
|
|
||||||
latitude: 0,
|
|
||||||
os: 'Windows',
|
|
||||||
osVersion: '10',
|
|
||||||
browser: 'Chrome',
|
|
||||||
browserVersion: '91.0.4472.124',
|
|
||||||
device: 'desktop',
|
|
||||||
brand: '',
|
|
||||||
model: '',
|
|
||||||
duration: 0,
|
|
||||||
path: '/test',
|
|
||||||
origin: 'https://example.com',
|
|
||||||
referrer: '',
|
|
||||||
referrerName: '',
|
|
||||||
referrerType: 'unknown',
|
|
||||||
sdkName: 'web',
|
|
||||||
sdkVersion: '1.0.0',
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(sessionsQueue.add.mock.calls[0]).toMatchObject([
|
|
||||||
'session',
|
|
||||||
{
|
|
||||||
type: 'createSessionEnd',
|
|
||||||
payload: event,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
delay: SESSION_TIMEOUT,
|
|
||||||
jobId: `sessionEnd:${projectId}:${event.deviceId}:${timestamp.getTime()}`,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Assertions
|
|
||||||
// Issue: https://github.com/oven-sh/bun/issues/10380
|
|
||||||
// expect(createEvent).toHaveBeenCalledWith(...)
|
|
||||||
expect(createEvent.mock.calls[0]).toMatchObject([
|
|
||||||
{
|
|
||||||
name: 'session_start',
|
|
||||||
deviceId: currentDeviceId,
|
|
||||||
sessionId: expect.stringMatching(
|
|
||||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
|
||||||
),
|
|
||||||
profileId: '',
|
|
||||||
projectId,
|
|
||||||
properties: {
|
|
||||||
__hash: undefined,
|
|
||||||
__query: undefined,
|
|
||||||
},
|
|
||||||
createdAt: new Date(timestamp.getTime() - 100),
|
|
||||||
country: 'US',
|
|
||||||
city: 'New York',
|
|
||||||
region: 'NY',
|
|
||||||
longitude: 0,
|
|
||||||
latitude: 0,
|
|
||||||
os: 'Windows',
|
|
||||||
osVersion: '10',
|
|
||||||
browser: 'Chrome',
|
|
||||||
browserVersion: '91.0.4472.124',
|
|
||||||
device: 'desktop',
|
|
||||||
brand: '',
|
|
||||||
model: '',
|
|
||||||
duration: 0,
|
|
||||||
path: '/test',
|
|
||||||
origin: 'https://example.com',
|
|
||||||
referrer: '',
|
|
||||||
referrerName: '',
|
|
||||||
referrerType: 'unknown',
|
|
||||||
sdkName: 'web',
|
|
||||||
sdkVersion: '1.0.0',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
expect(createEvent.mock.calls[1]).toMatchObject([event]);
|
|
||||||
|
|
||||||
// Add more specific assertions based on the expected behavior
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reuse existing session', async () => {
|
|
||||||
// Mock job data
|
|
||||||
const jobData = {
|
|
||||||
payload: {
|
|
||||||
geo,
|
|
||||||
event: {
|
|
||||||
name: 'test_event',
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
properties: { __path: 'https://example.com/test' },
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
'user-agent':
|
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
|
||||||
'openpanel-sdk-name': 'web',
|
|
||||||
'openpanel-sdk-version': '1.0.0',
|
|
||||||
},
|
|
||||||
projectId,
|
|
||||||
currentDeviceId,
|
|
||||||
previousDeviceId,
|
|
||||||
priority: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const changeDelay = mock();
|
|
||||||
findJobByPrefix.mockReturnValueOnce({
|
|
||||||
changeDelay,
|
|
||||||
data: {
|
|
||||||
type: 'createSessionEnd',
|
|
||||||
payload: {
|
|
||||||
sessionId: 'session-123',
|
|
||||||
deviceId: currentDeviceId,
|
|
||||||
profileId: currentDeviceId,
|
|
||||||
projectId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const job = { data: jobData } as Job;
|
|
||||||
|
|
||||||
// Execute the job
|
|
||||||
await incomingEvent(job);
|
|
||||||
|
|
||||||
expect(changeDelay.mock.calls[0]).toMatchObject([SESSION_TIMEOUT]);
|
|
||||||
|
|
||||||
// Assertions
|
|
||||||
// Issue: https://github.com/oven-sh/bun/issues/10380
|
|
||||||
// expect(createEvent).toHaveBeenCalledWith(...)
|
|
||||||
expect(createEvent.mock.calls[0]).toMatchObject([
|
|
||||||
{
|
|
||||||
name: 'test_event',
|
|
||||||
deviceId: currentDeviceId,
|
|
||||||
profileId: '',
|
|
||||||
sessionId: 'session-123',
|
|
||||||
projectId,
|
|
||||||
properties: {
|
|
||||||
__hash: undefined,
|
|
||||||
__query: undefined,
|
|
||||||
},
|
|
||||||
createdAt: expect.any(Date),
|
|
||||||
country: 'US',
|
|
||||||
city: 'New York',
|
|
||||||
region: 'NY',
|
|
||||||
longitude: 0,
|
|
||||||
latitude: 0,
|
|
||||||
os: 'Windows',
|
|
||||||
osVersion: '10',
|
|
||||||
browser: 'Chrome',
|
|
||||||
browserVersion: '91.0.4472.124',
|
|
||||||
device: 'desktop',
|
|
||||||
brand: '',
|
|
||||||
model: '',
|
|
||||||
duration: 0,
|
|
||||||
path: '/test',
|
|
||||||
origin: 'https://example.com',
|
|
||||||
referrer: '',
|
|
||||||
referrerName: '',
|
|
||||||
referrerType: 'unknown',
|
|
||||||
sdkName: 'web',
|
|
||||||
sdkVersion: '1.0.0',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Add more specific assertions based on the expected behavior
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle server events', async () => {
|
|
||||||
const timestamp = new Date();
|
|
||||||
const jobData = {
|
|
||||||
payload: {
|
|
||||||
geo,
|
|
||||||
event: {
|
|
||||||
name: 'server_event',
|
|
||||||
timestamp: timestamp.toISOString(),
|
|
||||||
properties: { custom_property: 'test_value' },
|
|
||||||
profileId: 'profile-123',
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
'user-agent': 'OpenPanel Server/1.0',
|
|
||||||
'openpanel-sdk-name': 'server',
|
|
||||||
'openpanel-sdk-version': '1.0.0',
|
|
||||||
},
|
|
||||||
projectId,
|
|
||||||
currentDeviceId: '',
|
|
||||||
previousDeviceId: '',
|
|
||||||
priority: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const job = { data: jobData } as Job;
|
|
||||||
|
|
||||||
const mockLastScreenView = {
|
|
||||||
deviceId: 'last-device-123',
|
|
||||||
sessionId: 'last-session-456',
|
|
||||||
country: 'CA',
|
|
||||||
city: 'Toronto',
|
|
||||||
region: 'ON',
|
|
||||||
os: 'iOS',
|
|
||||||
osVersion: '15.0',
|
|
||||||
browser: 'Safari',
|
|
||||||
browserVersion: '15.0',
|
|
||||||
device: 'mobile',
|
|
||||||
brand: 'Apple',
|
|
||||||
model: 'iPhone',
|
|
||||||
path: '/last-path',
|
|
||||||
origin: 'https://example.com',
|
|
||||||
referrer: 'https://google.com',
|
|
||||||
referrerName: 'Google',
|
|
||||||
referrerType: 'search',
|
|
||||||
};
|
|
||||||
|
|
||||||
getLastScreenViewFromProfileId.mockReturnValueOnce(mockLastScreenView);
|
|
||||||
|
|
||||||
await incomingEvent(job);
|
|
||||||
|
|
||||||
// expect(getLastScreenViewFromProfileId).toHaveBeenCalledWith({
|
|
||||||
// profileId: 'profile-123',
|
|
||||||
// projectId,
|
|
||||||
// });
|
// });
|
||||||
|
|
||||||
expect(createEvent.mock.calls[0]).toMatchObject([
|
// it('should create a session start and an event', async () => {
|
||||||
{
|
// const timestamp = new Date();
|
||||||
name: 'server_event',
|
// // Mock job data
|
||||||
deviceId: 'last-device-123',
|
// const jobData = {
|
||||||
sessionId: 'last-session-456',
|
// payload: {
|
||||||
profileId: 'profile-123',
|
// geo,
|
||||||
projectId,
|
// event: {
|
||||||
properties: {
|
// name: 'test_event',
|
||||||
custom_property: 'test_value',
|
// timestamp: timestamp.toISOString(),
|
||||||
user_agent: 'OpenPanel Server/1.0',
|
// properties: { __path: 'https://example.com/test' },
|
||||||
},
|
// },
|
||||||
createdAt: timestamp,
|
// headers: {
|
||||||
country: 'CA',
|
// 'user-agent':
|
||||||
city: 'Toronto',
|
// 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||||
region: 'ON',
|
// 'openpanel-sdk-name': 'web',
|
||||||
longitude: 0,
|
// 'openpanel-sdk-version': '1.0.0',
|
||||||
latitude: 0,
|
// },
|
||||||
os: 'iOS',
|
// projectId,
|
||||||
osVersion: '15.0',
|
// currentDeviceId,
|
||||||
browser: 'Safari',
|
// previousDeviceId,
|
||||||
browserVersion: '15.0',
|
// priority: true,
|
||||||
device: 'mobile',
|
// },
|
||||||
brand: 'Apple',
|
// };
|
||||||
model: 'iPhone',
|
|
||||||
duration: 0,
|
|
||||||
path: '/last-path',
|
|
||||||
origin: 'https://example.com',
|
|
||||||
referrer: 'https://google.com',
|
|
||||||
referrerName: 'Google',
|
|
||||||
referrerType: 'search',
|
|
||||||
sdkName: 'server',
|
|
||||||
sdkVersion: '1.0.0',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
expect(sessionsQueue.add).not.toHaveBeenCalled();
|
// const job = { data: jobData } as Job;
|
||||||
expect(findJobByPrefix).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add more test cases for different scenarios:
|
// // Execute the job
|
||||||
// - Server events
|
// await incomingEvent(job);
|
||||||
// - Existing sessions
|
|
||||||
// - Different priorities
|
// const event = {
|
||||||
// - Error cases
|
// name: 'test_event',
|
||||||
});
|
// deviceId: currentDeviceId,
|
||||||
|
// // @ts-expect-error
|
||||||
|
// sessionId: createEvent.mock.calls[1][0].sessionId,
|
||||||
|
// profileId: '',
|
||||||
|
// projectId,
|
||||||
|
// properties: {
|
||||||
|
// __hash: undefined,
|
||||||
|
// __query: undefined,
|
||||||
|
// },
|
||||||
|
// createdAt: timestamp,
|
||||||
|
// country: 'US',
|
||||||
|
// city: 'New York',
|
||||||
|
// region: 'NY',
|
||||||
|
// longitude: 0,
|
||||||
|
// latitude: 0,
|
||||||
|
// os: 'Windows',
|
||||||
|
// osVersion: '10',
|
||||||
|
// browser: 'Chrome',
|
||||||
|
// browserVersion: '91.0.4472.124',
|
||||||
|
// device: 'desktop',
|
||||||
|
// brand: '',
|
||||||
|
// model: '',
|
||||||
|
// duration: 0,
|
||||||
|
// path: '/test',
|
||||||
|
// origin: 'https://example.com',
|
||||||
|
// referrer: '',
|
||||||
|
// referrerName: '',
|
||||||
|
// referrerType: 'unknown',
|
||||||
|
// sdkName: 'web',
|
||||||
|
// sdkVersion: '1.0.0',
|
||||||
|
// };
|
||||||
|
|
||||||
|
// expect(sessionsQueue.add.mock.calls[0]).toMatchObject([
|
||||||
|
// 'session',
|
||||||
|
// {
|
||||||
|
// type: 'createSessionEnd',
|
||||||
|
// payload: event,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// delay: SESSION_TIMEOUT,
|
||||||
|
// jobId: `sessionEnd:${projectId}:${event.deviceId}:${timestamp.getTime()}`,
|
||||||
|
// },
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// // Assertions
|
||||||
|
// // Issue: https://github.com/oven-sh/bun/issues/10380
|
||||||
|
// // expect(createEvent).toHaveBeenCalledWith(...)
|
||||||
|
// expect(createEvent.mock.calls[0]).toMatchObject([
|
||||||
|
// {
|
||||||
|
// name: 'session_start',
|
||||||
|
// deviceId: currentDeviceId,
|
||||||
|
// sessionId: expect.stringMatching(
|
||||||
|
// /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
||||||
|
// ),
|
||||||
|
// profileId: '',
|
||||||
|
// projectId,
|
||||||
|
// properties: {
|
||||||
|
// __hash: undefined,
|
||||||
|
// __query: undefined,
|
||||||
|
// },
|
||||||
|
// createdAt: new Date(timestamp.getTime() - 100),
|
||||||
|
// country: 'US',
|
||||||
|
// city: 'New York',
|
||||||
|
// region: 'NY',
|
||||||
|
// longitude: 0,
|
||||||
|
// latitude: 0,
|
||||||
|
// os: 'Windows',
|
||||||
|
// osVersion: '10',
|
||||||
|
// browser: 'Chrome',
|
||||||
|
// browserVersion: '91.0.4472.124',
|
||||||
|
// device: 'desktop',
|
||||||
|
// brand: '',
|
||||||
|
// model: '',
|
||||||
|
// duration: 0,
|
||||||
|
// path: '/test',
|
||||||
|
// origin: 'https://example.com',
|
||||||
|
// referrer: '',
|
||||||
|
// referrerName: '',
|
||||||
|
// referrerType: 'unknown',
|
||||||
|
// sdkName: 'web',
|
||||||
|
// sdkVersion: '1.0.0',
|
||||||
|
// },
|
||||||
|
// ]);
|
||||||
|
// expect(createEvent.mock.calls[1]).toMatchObject([event]);
|
||||||
|
|
||||||
|
// // Add more specific assertions based on the expected behavior
|
||||||
|
// });
|
||||||
|
|
||||||
|
// it('should reuse existing session', async () => {
|
||||||
|
// // Mock job data
|
||||||
|
// const jobData = {
|
||||||
|
// payload: {
|
||||||
|
// geo,
|
||||||
|
// event: {
|
||||||
|
// name: 'test_event',
|
||||||
|
// timestamp: new Date().toISOString(),
|
||||||
|
// properties: { __path: 'https://example.com/test' },
|
||||||
|
// },
|
||||||
|
// headers: {
|
||||||
|
// 'user-agent':
|
||||||
|
// 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
||||||
|
// 'openpanel-sdk-name': 'web',
|
||||||
|
// 'openpanel-sdk-version': '1.0.0',
|
||||||
|
// },
|
||||||
|
// projectId,
|
||||||
|
// currentDeviceId,
|
||||||
|
// previousDeviceId,
|
||||||
|
// priority: false,
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
// const changeDelay = mock();
|
||||||
|
// findJobByPrefix.mockReturnValueOnce({
|
||||||
|
// changeDelay,
|
||||||
|
// data: {
|
||||||
|
// type: 'createSessionEnd',
|
||||||
|
// payload: {
|
||||||
|
// sessionId: 'session-123',
|
||||||
|
// deviceId: currentDeviceId,
|
||||||
|
// profileId: currentDeviceId,
|
||||||
|
// projectId,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
// const job = { data: jobData } as Job;
|
||||||
|
|
||||||
|
// // Execute the job
|
||||||
|
// await incomingEvent(job);
|
||||||
|
|
||||||
|
// expect(changeDelay.mock.calls[0]).toMatchObject([SESSION_TIMEOUT]);
|
||||||
|
|
||||||
|
// // Assertions
|
||||||
|
// // Issue: https://github.com/oven-sh/bun/issues/10380
|
||||||
|
// // expect(createEvent).toHaveBeenCalledWith(...)
|
||||||
|
// expect(createEvent.mock.calls[0]).toMatchObject([
|
||||||
|
// {
|
||||||
|
// name: 'test_event',
|
||||||
|
// deviceId: currentDeviceId,
|
||||||
|
// profileId: '',
|
||||||
|
// sessionId: 'session-123',
|
||||||
|
// projectId,
|
||||||
|
// properties: {
|
||||||
|
// __hash: undefined,
|
||||||
|
// __query: undefined,
|
||||||
|
// },
|
||||||
|
// createdAt: expect.any(Date),
|
||||||
|
// country: 'US',
|
||||||
|
// city: 'New York',
|
||||||
|
// region: 'NY',
|
||||||
|
// longitude: 0,
|
||||||
|
// latitude: 0,
|
||||||
|
// os: 'Windows',
|
||||||
|
// osVersion: '10',
|
||||||
|
// browser: 'Chrome',
|
||||||
|
// browserVersion: '91.0.4472.124',
|
||||||
|
// device: 'desktop',
|
||||||
|
// brand: '',
|
||||||
|
// model: '',
|
||||||
|
// duration: 0,
|
||||||
|
// path: '/test',
|
||||||
|
// origin: 'https://example.com',
|
||||||
|
// referrer: '',
|
||||||
|
// referrerName: '',
|
||||||
|
// referrerType: 'unknown',
|
||||||
|
// sdkName: 'web',
|
||||||
|
// sdkVersion: '1.0.0',
|
||||||
|
// },
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// // Add more specific assertions based on the expected behavior
|
||||||
|
// });
|
||||||
|
|
||||||
|
// it('should handle server events', async () => {
|
||||||
|
// const timestamp = new Date();
|
||||||
|
// const jobData = {
|
||||||
|
// payload: {
|
||||||
|
// geo,
|
||||||
|
// event: {
|
||||||
|
// name: 'server_event',
|
||||||
|
// timestamp: timestamp.toISOString(),
|
||||||
|
// properties: { custom_property: 'test_value' },
|
||||||
|
// profileId: 'profile-123',
|
||||||
|
// },
|
||||||
|
// headers: {
|
||||||
|
// 'user-agent': 'OpenPanel Server/1.0',
|
||||||
|
// 'openpanel-sdk-name': 'server',
|
||||||
|
// 'openpanel-sdk-version': '1.0.0',
|
||||||
|
// },
|
||||||
|
// projectId,
|
||||||
|
// currentDeviceId: '',
|
||||||
|
// previousDeviceId: '',
|
||||||
|
// priority: true,
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const job = { data: jobData } as Job;
|
||||||
|
|
||||||
|
// const mockLastScreenView = {
|
||||||
|
// deviceId: 'last-device-123',
|
||||||
|
// sessionId: 'last-session-456',
|
||||||
|
// country: 'CA',
|
||||||
|
// city: 'Toronto',
|
||||||
|
// region: 'ON',
|
||||||
|
// os: 'iOS',
|
||||||
|
// osVersion: '15.0',
|
||||||
|
// browser: 'Safari',
|
||||||
|
// browserVersion: '15.0',
|
||||||
|
// device: 'mobile',
|
||||||
|
// brand: 'Apple',
|
||||||
|
// model: 'iPhone',
|
||||||
|
// path: '/last-path',
|
||||||
|
// origin: 'https://example.com',
|
||||||
|
// referrer: 'https://google.com',
|
||||||
|
// referrerName: 'Google',
|
||||||
|
// referrerType: 'search',
|
||||||
|
// };
|
||||||
|
|
||||||
|
// getLastScreenViewFromProfileId.mockReturnValueOnce(mockLastScreenView);
|
||||||
|
|
||||||
|
// await incomingEvent(job);
|
||||||
|
|
||||||
|
// // expect(getLastScreenViewFromProfileId).toHaveBeenCalledWith({
|
||||||
|
// // profileId: 'profile-123',
|
||||||
|
// // projectId,
|
||||||
|
// // });
|
||||||
|
|
||||||
|
// expect(createEvent.mock.calls[0]).toMatchObject([
|
||||||
|
// {
|
||||||
|
// name: 'server_event',
|
||||||
|
// deviceId: 'last-device-123',
|
||||||
|
// sessionId: 'last-session-456',
|
||||||
|
// profileId: 'profile-123',
|
||||||
|
// projectId,
|
||||||
|
// properties: {
|
||||||
|
// custom_property: 'test_value',
|
||||||
|
// user_agent: 'OpenPanel Server/1.0',
|
||||||
|
// },
|
||||||
|
// createdAt: timestamp,
|
||||||
|
// country: 'CA',
|
||||||
|
// city: 'Toronto',
|
||||||
|
// region: 'ON',
|
||||||
|
// longitude: 0,
|
||||||
|
// latitude: 0,
|
||||||
|
// os: 'iOS',
|
||||||
|
// osVersion: '15.0',
|
||||||
|
// browser: 'Safari',
|
||||||
|
// browserVersion: '15.0',
|
||||||
|
// device: 'mobile',
|
||||||
|
// brand: 'Apple',
|
||||||
|
// model: 'iPhone',
|
||||||
|
// duration: 0,
|
||||||
|
// path: '/last-path',
|
||||||
|
// origin: 'https://example.com',
|
||||||
|
// referrer: 'https://google.com',
|
||||||
|
// referrerName: 'Google',
|
||||||
|
// referrerType: 'search',
|
||||||
|
// sdkName: 'server',
|
||||||
|
// sdkVersion: '1.0.0',
|
||||||
|
// },
|
||||||
|
// ]);
|
||||||
|
|
||||||
|
// expect(sessionsQueue.add).not.toHaveBeenCalled();
|
||||||
|
// expect(findJobByPrefix).not.toHaveBeenCalled();
|
||||||
|
// });
|
||||||
|
|
||||||
|
// // Add more test cases for different scenarios:
|
||||||
|
// // - Server events
|
||||||
|
// // - Existing sessions
|
||||||
|
// // - Different priorities
|
||||||
|
// // - Error cases
|
||||||
|
// });
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { groupBy, omit } from 'ramda';
|
import { groupBy, omit } from 'ramda';
|
||||||
import SuperJSON from 'superjson';
|
import SuperJSON from 'superjson';
|
||||||
|
|
||||||
import { deepMergeObjects } from '@openpanel/common';
|
import { deepMergeObjects, getSafeJson } from '@openpanel/common';
|
||||||
import { getRedisCache, getRedisPub } from '@openpanel/redis';
|
import { getRedisCache, getRedisPub } from '@openpanel/redis';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -25,6 +25,51 @@ export class EventBuffer extends RedisBuffer<BufferType> {
|
|||||||
super(TABLE_NAMES.events, null);
|
super(TABLE_NAMES.events, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLastEventKey({
|
||||||
|
projectId,
|
||||||
|
profileId,
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
profileId: string;
|
||||||
|
}) {
|
||||||
|
return `session:last_screen_view:${projectId}:${profileId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getLastScreenView({
|
||||||
|
projectId,
|
||||||
|
profileId,
|
||||||
|
}: {
|
||||||
|
projectId: string;
|
||||||
|
profileId: string;
|
||||||
|
}): Promise<IServiceEvent | null> {
|
||||||
|
const event = await getRedisCache().get(
|
||||||
|
this.getLastEventKey({ projectId, profileId }),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (event) {
|
||||||
|
const parsed = getSafeJson<BufferType>(event);
|
||||||
|
if (parsed) {
|
||||||
|
return transformEvent(parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async add(event: BufferType) {
|
||||||
|
await super.add(event);
|
||||||
|
if (event.name === 'screen_view') {
|
||||||
|
await getRedisCache().set(
|
||||||
|
this.getLastEventKey({
|
||||||
|
projectId: event.project_id,
|
||||||
|
profileId: event.profile_id,
|
||||||
|
}),
|
||||||
|
JSON.stringify(event),
|
||||||
|
'EX',
|
||||||
|
60 * 31,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public onAdd(event: BufferType) {
|
public onAdd(event: BufferType) {
|
||||||
getRedisPub().publish(
|
getRedisPub().publish(
|
||||||
'event:received',
|
'event:received',
|
||||||
|
|||||||
@@ -219,27 +219,4 @@ export class ProfileBuffer extends RedisBuffer<BufferType> {
|
|||||||
});
|
});
|
||||||
await multi.exec();
|
await multi.exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
public findMany: FindMany<IClickhouseProfile, IServiceProfile> = async (
|
|
||||||
callback,
|
|
||||||
) => {
|
|
||||||
return this.getQueue(-1)
|
|
||||||
.then((queue) => {
|
|
||||||
return queue.filter(callback).map(transformProfile);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
public find: Find<IClickhouseProfile, IServiceProfile> = async (callback) => {
|
|
||||||
return this.getQueue(-1)
|
|
||||||
.then((queue) => {
|
|
||||||
const match = queue.find(callback);
|
|
||||||
return match ? transformProfile(match) : null;
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -588,9 +588,10 @@ export async function getLastScreenViewFromProfileId({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventInBuffer = await eventBuffer.find(
|
const eventInBuffer = await eventBuffer.getLastScreenView({
|
||||||
(item) => item.profile_id === profileId,
|
projectId,
|
||||||
);
|
profileId,
|
||||||
|
});
|
||||||
|
|
||||||
if (eventInBuffer) {
|
if (eventInBuffer) {
|
||||||
return eventInBuffer;
|
return eventInBuffer;
|
||||||
|
|||||||
Reference in New Issue
Block a user