247 lines
7.1 KiB
TypeScript
247 lines
7.1 KiB
TypeScript
import { getReferrerWithQuery, parseReferrer } from '@/utils/parse-referrer';
|
|
import { parseUserAgent } from '@/utils/parse-user-agent';
|
|
import type { Job } from 'bullmq';
|
|
import { omit } from 'ramda';
|
|
import { v4 as uuid } from 'uuid';
|
|
|
|
import { getTime, isSameDomain, parsePath } from '@openpanel/common';
|
|
import type { IServiceCreateEventPayload } from '@openpanel/db';
|
|
import { createEvent } from '@openpanel/db';
|
|
import { getLastScreenViewFromProfileId } from '@openpanel/db/src/services/event.service';
|
|
import { findJobByPrefix, sessionsQueue } from '@openpanel/queue';
|
|
import type {
|
|
EventsQueuePayloadCreateSessionEnd,
|
|
EventsQueuePayloadIncomingEvent,
|
|
} from '@openpanel/queue';
|
|
import { getRedisQueue } from '@openpanel/redis';
|
|
|
|
const GLOBAL_PROPERTIES = ['__path', '__referrer'];
|
|
export const SESSION_TIMEOUT = 1000 * 60 * 30;
|
|
|
|
export async function incomingEvent(job: Job<EventsQueuePayloadIncomingEvent>) {
|
|
const {
|
|
geo,
|
|
event: body,
|
|
headers,
|
|
projectId,
|
|
currentDeviceId,
|
|
previousDeviceId,
|
|
priority,
|
|
} = job.data.payload;
|
|
const properties = body.properties ?? {};
|
|
const getProperty = (name: string): string | undefined => {
|
|
// replace thing is just for older sdks when we didn't have `__`
|
|
// remove when kiddokitchen app (24.09.02) is not used anymore
|
|
return (
|
|
((properties[name] || properties[name.replace('__', '')]) as
|
|
| string
|
|
| null
|
|
| undefined) ?? undefined
|
|
);
|
|
};
|
|
|
|
// this will get the profileId from the alias table if it exists
|
|
const profileId = body.profileId ? String(body.profileId) : '';
|
|
const createdAt = new Date(body.timestamp);
|
|
const url = getProperty('__path');
|
|
const { path, hash, query, origin } = parsePath(url);
|
|
const referrer = isSameDomain(getProperty('__referrer'), url)
|
|
? null
|
|
: parseReferrer(getProperty('__referrer'));
|
|
const utmReferrer = getReferrerWithQuery(query);
|
|
const userAgent = headers['user-agent'];
|
|
const sdkName = headers['openpanel-sdk-name'];
|
|
const sdkVersion = headers['openpanel-sdk-version'];
|
|
const uaInfo = parseUserAgent(userAgent);
|
|
|
|
if (uaInfo.isServer) {
|
|
const event = await getLastScreenViewFromProfileId({
|
|
profileId,
|
|
projectId,
|
|
});
|
|
|
|
const payload: IServiceCreateEventPayload = {
|
|
name: body.name,
|
|
deviceId: event?.deviceId || '',
|
|
sessionId: event?.sessionId || '',
|
|
profileId,
|
|
projectId,
|
|
properties: {
|
|
...omit(GLOBAL_PROPERTIES, properties),
|
|
user_agent: userAgent,
|
|
},
|
|
createdAt,
|
|
country: event?.country || geo.country || '',
|
|
city: event?.city || geo.city || '',
|
|
region: event?.region || geo.region || '',
|
|
longitude: geo.longitude,
|
|
latitude: geo.latitude,
|
|
os: event?.os ?? '',
|
|
osVersion: event?.osVersion ?? '',
|
|
browser: event?.browser ?? '',
|
|
browserVersion: event?.browserVersion ?? '',
|
|
device: event?.device ?? uaInfo.device ?? '',
|
|
brand: event?.brand ?? '',
|
|
model: event?.model ?? '',
|
|
duration: 0,
|
|
path: event?.path ?? '',
|
|
origin: event?.origin ?? '',
|
|
referrer: event?.referrer ?? '',
|
|
referrerName: event?.referrerName ?? '',
|
|
referrerType: event?.referrerType ?? '',
|
|
sdkName,
|
|
sdkVersion,
|
|
};
|
|
|
|
return createEvent(payload);
|
|
}
|
|
|
|
const sessionEnd = await getSessionEndWithPriority(priority)({
|
|
projectId,
|
|
currentDeviceId,
|
|
previousDeviceId,
|
|
});
|
|
|
|
const sessionEndPayload =
|
|
sessionEnd?.job.data.payload ||
|
|
({
|
|
sessionId: uuid(),
|
|
deviceId: currentDeviceId,
|
|
profileId,
|
|
projectId,
|
|
} satisfies EventsQueuePayloadCreateSessionEnd['payload']);
|
|
|
|
const payload: IServiceCreateEventPayload = {
|
|
name: body.name,
|
|
deviceId: sessionEndPayload.deviceId,
|
|
sessionId: sessionEndPayload.sessionId,
|
|
profileId,
|
|
projectId,
|
|
properties: Object.assign({}, omit(GLOBAL_PROPERTIES, properties), {
|
|
__hash: hash,
|
|
__query: query,
|
|
}),
|
|
createdAt,
|
|
country: geo.country,
|
|
city: geo.city,
|
|
region: geo.region,
|
|
longitude: geo.longitude,
|
|
latitude: geo.latitude,
|
|
os: uaInfo?.os ?? '',
|
|
osVersion: uaInfo?.osVersion ?? '',
|
|
browser: uaInfo?.browser ?? '',
|
|
browserVersion: uaInfo?.browserVersion ?? '',
|
|
device: uaInfo?.device ?? '',
|
|
brand: uaInfo?.brand ?? '',
|
|
model: uaInfo?.model ?? '',
|
|
duration: 0,
|
|
path: path,
|
|
origin: origin,
|
|
referrer: sessionEnd ? sessionEndPayload.referrer : referrer?.url || '',
|
|
referrerName: sessionEnd
|
|
? sessionEndPayload.referrerName
|
|
: referrer?.name || utmReferrer?.name || '',
|
|
referrerType: sessionEnd
|
|
? sessionEndPayload.referrerType
|
|
: referrer?.type || utmReferrer?.type || '',
|
|
sdkName,
|
|
sdkVersion,
|
|
};
|
|
|
|
const sessionEndJobId =
|
|
sessionEnd?.job.id ??
|
|
`sessionEnd:${projectId}:${sessionEndPayload.deviceId}:${getTime(createdAt)}`;
|
|
|
|
if (sessionEnd) {
|
|
// If for some reason we have a session end job that is not a createSessionEnd job
|
|
if (sessionEnd.job.data.type !== 'createSessionEnd') {
|
|
throw new Error('Invalid session end job');
|
|
}
|
|
|
|
await sessionEnd.job.changeDelay(SESSION_TIMEOUT);
|
|
} else {
|
|
await sessionsQueue.add(
|
|
'session',
|
|
{
|
|
type: 'createSessionEnd',
|
|
payload,
|
|
},
|
|
{
|
|
delay: SESSION_TIMEOUT,
|
|
jobId: sessionEndJobId,
|
|
},
|
|
);
|
|
}
|
|
|
|
if (!sessionEnd) {
|
|
await createEvent({
|
|
...payload,
|
|
name: 'session_start',
|
|
createdAt: new Date(getTime(payload.createdAt) - 100),
|
|
});
|
|
}
|
|
|
|
return createEvent(payload);
|
|
}
|
|
|
|
function getSessionEndWithPriority(
|
|
priority: boolean,
|
|
count = 0,
|
|
): typeof getSessionEnd {
|
|
return async (args) => {
|
|
const res = await getSessionEnd(args);
|
|
|
|
if (count > 10) {
|
|
throw new Error('Failed to get session end');
|
|
}
|
|
|
|
// if we get simultaneous requests we want to avoid race conditions with getting the session end
|
|
// one of the events will get priority and the other will wait for the first to finish
|
|
if (res === null && priority === false) {
|
|
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
return getSessionEndWithPriority(priority, count + 1)(args);
|
|
}
|
|
|
|
return res;
|
|
};
|
|
}
|
|
|
|
async function getSessionEnd({
|
|
projectId,
|
|
currentDeviceId,
|
|
previousDeviceId,
|
|
}: {
|
|
projectId: string;
|
|
currentDeviceId: string;
|
|
previousDeviceId: string;
|
|
}) {
|
|
const currentSessionEndKeys = await getRedisQueue().keys(
|
|
`*:sessionEnd:${projectId}:${currentDeviceId}:*`,
|
|
);
|
|
|
|
const sessionEndJobCurrentDeviceId = await findJobByPrefix(
|
|
sessionsQueue,
|
|
currentSessionEndKeys,
|
|
`sessionEnd:${projectId}:${currentDeviceId}:`,
|
|
);
|
|
if (sessionEndJobCurrentDeviceId) {
|
|
return { deviceId: currentDeviceId, job: sessionEndJobCurrentDeviceId };
|
|
}
|
|
|
|
const previousSessionEndKeys = await getRedisQueue().keys(
|
|
`*:sessionEnd:${projectId}:${previousDeviceId}:*`,
|
|
);
|
|
|
|
const sessionEndJobPreviousDeviceId = await findJobByPrefix(
|
|
sessionsQueue,
|
|
previousSessionEndKeys,
|
|
`sessionEnd:${projectId}:${previousDeviceId}:`,
|
|
);
|
|
if (sessionEndJobPreviousDeviceId) {
|
|
return { deviceId: previousDeviceId, job: sessionEndJobPreviousDeviceId };
|
|
}
|
|
|
|
// Create session
|
|
return null;
|
|
}
|