rename mixan to OPENPANEL!
This commit is contained in:
@@ -1,19 +1,19 @@
|
||||
import { logger, logInfo, noop } from '@/utils/logger';
|
||||
import { getClientIp, parseIp } from '@/utils/parseIp';
|
||||
import { getReferrerWithQuery, parseReferrer } from '@/utils/parseReferrer';
|
||||
import { isUserAgentSet, parseUserAgent } from '@/utils/parseUserAgent';
|
||||
import { isSameDomain, parsePath } from '@/utils/url';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { omit } from 'ramda';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { logger, logInfo, noop } from "@/utils/logger";
|
||||
import { getClientIp, parseIp } from "@/utils/parseIp";
|
||||
import { getReferrerWithQuery, parseReferrer } from "@/utils/parseReferrer";
|
||||
import { isUserAgentSet, parseUserAgent } from "@/utils/parseUserAgent";
|
||||
import { isSameDomain, parsePath } from "@/utils/url";
|
||||
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||
import { omit } from "ramda";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import { generateDeviceId, getTime, toISOString } from '@mixan/common';
|
||||
import type { IServiceCreateEventPayload } from '@mixan/db';
|
||||
import { createEvent, getEvents, getSalts } from '@mixan/db';
|
||||
import type { JobsOptions } from '@mixan/queue';
|
||||
import { eventsQueue } from '@mixan/queue';
|
||||
import { findJobByPrefix } from '@mixan/queue/src/utils';
|
||||
import type { PostEventPayload } from '@mixan/sdk';
|
||||
import { generateDeviceId, getTime, toISOString } from "@openpanel/common";
|
||||
import type { IServiceCreateEventPayload } from "@openpanel/db";
|
||||
import { createEvent, getEvents, getSalts } from "@openpanel/db";
|
||||
import type { JobsOptions } from "@openpanel/queue";
|
||||
import { eventsQueue } from "@openpanel/queue";
|
||||
import { findJobByPrefix } from "@openpanel/queue/src/utils";
|
||||
import type { PostEventPayload } from "@openpanel/sdk";
|
||||
|
||||
const SESSION_TIMEOUT = 1000 * 60 * 30;
|
||||
const SESSION_END_TIMEOUT = SESSION_TIMEOUT + 1000;
|
||||
@@ -55,7 +55,7 @@ export async function postEvent(
|
||||
request: FastifyRequest<{
|
||||
Body: PostEventPayload;
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
const contextLogger = createContextLogger(request);
|
||||
let deviceId: string | null = null;
|
||||
@@ -65,23 +65,23 @@ export async function postEvent(
|
||||
// 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
|
||||
((properties[name] || properties[name.replace("__", "")]) as
|
||||
| string
|
||||
| null
|
||||
| undefined) ?? undefined
|
||||
);
|
||||
};
|
||||
const profileId = body.profileId ?? '';
|
||||
const profileId = body.profileId ?? "";
|
||||
const createdAt = new Date(body.timestamp);
|
||||
const url = getProperty('__path');
|
||||
const url = getProperty("__path");
|
||||
const { path, hash, query } = parsePath(url);
|
||||
const referrer = isSameDomain(getProperty('__referrer'), url)
|
||||
const referrer = isSameDomain(getProperty("__referrer"), url)
|
||||
? null
|
||||
: parseReferrer(getProperty('__referrer'));
|
||||
: parseReferrer(getProperty("__referrer"));
|
||||
const utmReferrer = getReferrerWithQuery(query);
|
||||
const ip = getClientIp(request)!;
|
||||
const origin = request.headers.origin!;
|
||||
const ua = request.headers['user-agent']!;
|
||||
const ua = request.headers["user-agent"]!;
|
||||
const uaInfo = parseUserAgent(ua);
|
||||
const salts = await getSalts();
|
||||
const currentDeviceId = generateDeviceId({
|
||||
@@ -101,66 +101,66 @@ export async function postEvent(
|
||||
|
||||
if (isServerEvent) {
|
||||
const [event] = await withTiming(
|
||||
'Get last event (server-event)',
|
||||
"Get last event (server-event)",
|
||||
getEvents(
|
||||
`SELECT * FROM events WHERE name = 'screen_view' AND profile_id = '${profileId}' AND project_id = '${projectId}' ORDER BY created_at DESC LIMIT 1`
|
||||
)
|
||||
`SELECT * FROM events WHERE name = 'screen_view' AND profile_id = '${profileId}' AND project_id = '${projectId}' ORDER BY created_at DESC LIMIT 1`,
|
||||
),
|
||||
);
|
||||
|
||||
eventsQueue.add('event', {
|
||||
type: 'createEvent',
|
||||
eventsQueue.add("event", {
|
||||
type: "createEvent",
|
||||
payload: {
|
||||
name: body.name,
|
||||
deviceId: event?.deviceId || '',
|
||||
sessionId: event?.sessionId || '',
|
||||
deviceId: event?.deviceId || "",
|
||||
sessionId: event?.sessionId || "",
|
||||
profileId,
|
||||
projectId,
|
||||
properties: Object.assign(
|
||||
{},
|
||||
omit(['__path', '__referrer'], properties),
|
||||
omit(["__path", "__referrer"], properties),
|
||||
{
|
||||
hash,
|
||||
query,
|
||||
}
|
||||
},
|
||||
),
|
||||
createdAt,
|
||||
country: event?.country ?? '',
|
||||
city: event?.city ?? '',
|
||||
region: event?.region ?? '',
|
||||
continent: event?.continent ?? '',
|
||||
os: event?.os ?? '',
|
||||
osVersion: event?.osVersion ?? '',
|
||||
browser: event?.browser ?? '',
|
||||
browserVersion: event?.browserVersion ?? '',
|
||||
device: event?.device ?? '',
|
||||
brand: event?.brand ?? '',
|
||||
model: event?.model ?? '',
|
||||
country: event?.country ?? "",
|
||||
city: event?.city ?? "",
|
||||
region: event?.region ?? "",
|
||||
continent: event?.continent ?? "",
|
||||
os: event?.os ?? "",
|
||||
osVersion: event?.osVersion ?? "",
|
||||
browser: event?.browser ?? "",
|
||||
browserVersion: event?.browserVersion ?? "",
|
||||
device: event?.device ?? "",
|
||||
brand: event?.brand ?? "",
|
||||
model: event?.model ?? "",
|
||||
duration: 0,
|
||||
path: event?.path ?? '',
|
||||
referrer: event?.referrer ?? '',
|
||||
referrerName: event?.referrerName ?? '',
|
||||
referrerType: event?.referrerType ?? '',
|
||||
path: event?.path ?? "",
|
||||
referrer: event?.referrer ?? "",
|
||||
referrerName: event?.referrerName ?? "",
|
||||
referrerType: event?.referrerType ?? "",
|
||||
profile: undefined,
|
||||
meta: undefined,
|
||||
},
|
||||
});
|
||||
return reply.status(200).send('');
|
||||
return reply.status(200).send("");
|
||||
}
|
||||
|
||||
const [geo, sessionEndJobCurrentDeviceId, sessionEndJobPreviousDeviceId] =
|
||||
await withTiming(
|
||||
'Get geo and jobs from queue',
|
||||
"Get geo and jobs from queue",
|
||||
Promise.all([
|
||||
parseIp(ip),
|
||||
findJobByPrefix(
|
||||
eventsQueue,
|
||||
`sessionEnd:${projectId}:${currentDeviceId}:`
|
||||
`sessionEnd:${projectId}:${currentDeviceId}:`,
|
||||
),
|
||||
findJobByPrefix(
|
||||
eventsQueue,
|
||||
`sessionEnd:${projectId}:${previousDeviceId}:`
|
||||
`sessionEnd:${projectId}:${previousDeviceId}:`,
|
||||
),
|
||||
])
|
||||
]),
|
||||
);
|
||||
|
||||
const createSessionStart =
|
||||
@@ -178,9 +178,9 @@ export async function postEvent(
|
||||
deviceId = currentDeviceId;
|
||||
// Queue session end
|
||||
eventsQueue.add(
|
||||
'event',
|
||||
"event",
|
||||
{
|
||||
type: 'createSessionEnd',
|
||||
type: "createSessionEnd",
|
||||
payload: {
|
||||
deviceId,
|
||||
},
|
||||
@@ -188,27 +188,27 @@ export async function postEvent(
|
||||
{
|
||||
delay: SESSION_END_TIMEOUT,
|
||||
jobId: `sessionEnd:${projectId}:${deviceId}:${Date.now()}`,
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const [[sessionStartEvent], prevEventJob] = await withTiming(
|
||||
'Get session start event',
|
||||
"Get session start event",
|
||||
Promise.all([
|
||||
getEvents(
|
||||
`SELECT * FROM events WHERE name = 'session_start' AND device_id = '${deviceId}' AND project_id = '${projectId}' ORDER BY created_at DESC LIMIT 1`
|
||||
`SELECT * FROM events WHERE name = 'session_start' AND device_id = '${deviceId}' AND project_id = '${projectId}' ORDER BY created_at DESC LIMIT 1`,
|
||||
),
|
||||
findJobByPrefix(eventsQueue, `event:${projectId}:${deviceId}:`),
|
||||
])
|
||||
]),
|
||||
);
|
||||
|
||||
const payload: Omit<IServiceCreateEventPayload, 'id'> = {
|
||||
const payload: Omit<IServiceCreateEventPayload, "id"> = {
|
||||
name: body.name,
|
||||
deviceId,
|
||||
profileId,
|
||||
projectId,
|
||||
sessionId: createSessionStart ? uuid() : sessionStartEvent?.sessionId ?? '',
|
||||
properties: Object.assign({}, omit(['__path', '__referrer'], properties), {
|
||||
sessionId: createSessionStart ? uuid() : sessionStartEvent?.sessionId ?? "",
|
||||
properties: Object.assign({}, omit(["__path", "__referrer"], properties), {
|
||||
hash,
|
||||
query,
|
||||
}),
|
||||
@@ -227,27 +227,27 @@ export async function postEvent(
|
||||
duration: 0,
|
||||
path: path,
|
||||
referrer: referrer?.url,
|
||||
referrerName: referrer?.name ?? utmReferrer?.name ?? '',
|
||||
referrerType: referrer?.type ?? utmReferrer?.type ?? '',
|
||||
referrerName: referrer?.name ?? utmReferrer?.name ?? "",
|
||||
referrerType: referrer?.type ?? utmReferrer?.type ?? "",
|
||||
profile: undefined,
|
||||
meta: undefined,
|
||||
};
|
||||
|
||||
const isDelayed = prevEventJob ? await prevEventJob?.isDelayed() : false;
|
||||
|
||||
if (isDelayed && prevEventJob && prevEventJob.data.type === 'createEvent') {
|
||||
if (isDelayed && prevEventJob && prevEventJob.data.type === "createEvent") {
|
||||
const prevEvent = prevEventJob.data.payload;
|
||||
const duration = getTime(payload.createdAt) - getTime(prevEvent.createdAt);
|
||||
contextLogger.add('prevEvent', prevEvent);
|
||||
contextLogger.add("prevEvent", prevEvent);
|
||||
|
||||
// Set path from prev screen_view event if current event is not a screen_view
|
||||
if (payload.name != 'screen_view') {
|
||||
if (payload.name != "screen_view") {
|
||||
payload.path = prevEvent.path;
|
||||
}
|
||||
|
||||
if (payload.name === 'screen_view') {
|
||||
if (payload.name === "screen_view") {
|
||||
if (duration < 0) {
|
||||
contextLogger.send('duration is wrong', {
|
||||
contextLogger.send("duration is wrong", {
|
||||
payload,
|
||||
duration,
|
||||
});
|
||||
@@ -255,21 +255,21 @@ export async function postEvent(
|
||||
// Skip update duration if it's wrong
|
||||
// Seems like request is not in right order
|
||||
await withTiming(
|
||||
'Update previous job with duration',
|
||||
"Update previous job with duration",
|
||||
prevEventJob.updateData({
|
||||
type: 'createEvent',
|
||||
type: "createEvent",
|
||||
payload: {
|
||||
...prevEvent,
|
||||
duration,
|
||||
},
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await withTiming('Promote previous job', prevEventJob.promote());
|
||||
await withTiming("Promote previous job", prevEventJob.promote());
|
||||
}
|
||||
} else if (payload.name !== 'screen_view') {
|
||||
contextLogger.send('no previous job', {
|
||||
} else if (payload.name !== "screen_view") {
|
||||
contextLogger.send("no previous job", {
|
||||
prevEventJob,
|
||||
payload,
|
||||
});
|
||||
@@ -278,23 +278,23 @@ export async function postEvent(
|
||||
if (createSessionStart) {
|
||||
// We do not need to queue session_start
|
||||
await withTiming(
|
||||
'Create session start event',
|
||||
"Create session start event",
|
||||
createEvent({
|
||||
...payload,
|
||||
name: 'session_start',
|
||||
name: "session_start",
|
||||
// @ts-expect-error
|
||||
createdAt: toISOString(getTime(payload.createdAt) - 100),
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const options: JobsOptions = {};
|
||||
if (payload.name === 'screen_view') {
|
||||
if (payload.name === "screen_view") {
|
||||
options.delay = SESSION_TIMEOUT;
|
||||
options.jobId = `event:${projectId}:${deviceId}:${Date.now()}`;
|
||||
}
|
||||
|
||||
contextLogger.send('event is queued', {
|
||||
contextLogger.send("event is queued", {
|
||||
ip,
|
||||
origin,
|
||||
ua,
|
||||
@@ -312,14 +312,14 @@ export async function postEvent(
|
||||
// Queue current event
|
||||
eventsQueue
|
||||
.add(
|
||||
'event',
|
||||
"event",
|
||||
{
|
||||
type: 'createEvent',
|
||||
type: "createEvent",
|
||||
payload,
|
||||
},
|
||||
options
|
||||
options,
|
||||
)
|
||||
.catch(noop('Failed to queue event'));
|
||||
.catch(noop("Failed to queue event"));
|
||||
|
||||
reply.status(202).send(deviceId);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import type * as WebSocket from 'ws';
|
||||
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||
import type * as WebSocket from "ws";
|
||||
|
||||
import { getSafeJson } from '@mixan/common';
|
||||
import type { IServiceCreateEventPayload } from '@mixan/db';
|
||||
import { getEvents, getLiveVisitors } from '@mixan/db';
|
||||
import { redis, redisPub, redisSub } from '@mixan/redis';
|
||||
import { getSafeJson } from "@openpanel/common";
|
||||
import type { IServiceCreateEventPayload } from "@openpanel/db";
|
||||
import { getEvents, getLiveVisitors } from "@openpanel/db";
|
||||
import { redis, redisPub, redisSub } from "@openpanel/redis";
|
||||
|
||||
export function getLiveEventInfo(key: string) {
|
||||
return key.split(':').slice(2) as [string, string];
|
||||
return key.split(":").slice(2) as [string, string];
|
||||
}
|
||||
|
||||
export async function test(
|
||||
@@ -16,20 +16,20 @@ export async function test(
|
||||
projectId: string;
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
const [event] = await getEvents(
|
||||
`SELECT * FROM events WHERE project_id = '${req.params.projectId}' AND name = 'screen_view' LIMIT 1`
|
||||
`SELECT * FROM events WHERE project_id = '${req.params.projectId}' AND name = 'screen_view' LIMIT 1`,
|
||||
);
|
||||
if (!event) {
|
||||
return reply.status(404).send('No event found');
|
||||
return reply.status(404).send("No event found");
|
||||
}
|
||||
redisPub.publish('event', JSON.stringify(event));
|
||||
redisPub.publish("event", JSON.stringify(event));
|
||||
redis.set(
|
||||
`live:event:${event.projectId}:${Math.random() * 1000}`,
|
||||
'',
|
||||
'EX',
|
||||
10
|
||||
"",
|
||||
"EX",
|
||||
10,
|
||||
);
|
||||
reply.status(202).send(event);
|
||||
}
|
||||
@@ -42,15 +42,15 @@ export function wsVisitors(
|
||||
Params: {
|
||||
projectId: string;
|
||||
};
|
||||
}>
|
||||
}>,
|
||||
) {
|
||||
const { params } = req;
|
||||
|
||||
redisSub.subscribe('event');
|
||||
redisSub.psubscribe('__key*:expired');
|
||||
redisSub.subscribe("event");
|
||||
redisSub.psubscribe("__key*:expired");
|
||||
|
||||
const message = (channel: string, message: string) => {
|
||||
if (channel === 'event') {
|
||||
if (channel === "event") {
|
||||
const event = getSafeJson<IServiceCreateEventPayload>(message);
|
||||
if (event?.projectId === params.projectId) {
|
||||
getLiveVisitors(params.projectId).then((count) => {
|
||||
@@ -68,14 +68,14 @@ export function wsVisitors(
|
||||
}
|
||||
};
|
||||
|
||||
redisSub.on('message', message);
|
||||
redisSub.on('pmessage', pmessage);
|
||||
redisSub.on("message", message);
|
||||
redisSub.on("pmessage", pmessage);
|
||||
|
||||
connection.socket.on('close', () => {
|
||||
redisSub.unsubscribe('event');
|
||||
redisSub.punsubscribe('__key*:expired');
|
||||
redisSub.off('message', message);
|
||||
redisSub.off('pmessage', pmessage);
|
||||
connection.socket.on("close", () => {
|
||||
redisSub.unsubscribe("event");
|
||||
redisSub.punsubscribe("__key*:expired");
|
||||
redisSub.off("message", message);
|
||||
redisSub.off("pmessage", pmessage);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -87,11 +87,11 @@ export function wsEvents(
|
||||
Params: {
|
||||
projectId: string;
|
||||
};
|
||||
}>
|
||||
}>,
|
||||
) {
|
||||
const { params } = req;
|
||||
|
||||
redisSub.subscribe('event');
|
||||
redisSub.subscribe("event");
|
||||
|
||||
const message = (channel: string, message: string) => {
|
||||
const event = getSafeJson<IServiceCreateEventPayload>(message);
|
||||
@@ -100,10 +100,10 @@ export function wsEvents(
|
||||
}
|
||||
};
|
||||
|
||||
redisSub.on('message', message);
|
||||
redisSub.on("message", message);
|
||||
|
||||
connection.socket.on('close', () => {
|
||||
redisSub.unsubscribe('event');
|
||||
redisSub.off('message', message);
|
||||
connection.socket.on("close", () => {
|
||||
redisSub.unsubscribe("event");
|
||||
redisSub.off("message", message);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import icoToPng from 'ico-to-png';
|
||||
import sharp from 'sharp';
|
||||
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||
import icoToPng from "ico-to-png";
|
||||
import sharp from "sharp";
|
||||
|
||||
import { createHash } from '@mixan/common';
|
||||
import { redis } from '@mixan/redis';
|
||||
import { createHash } from "@openpanel/common";
|
||||
import { redis } from "@openpanel/redis";
|
||||
|
||||
interface GetFaviconParams {
|
||||
url: string;
|
||||
@@ -12,9 +12,9 @@ interface GetFaviconParams {
|
||||
async function getImageBuffer(url: string) {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
const contentType = res.headers.get('content-type');
|
||||
const contentType = res.headers.get("content-type");
|
||||
|
||||
if (!contentType?.includes('image')) {
|
||||
if (!contentType?.includes("image")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ async function getImageBuffer(url: string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (contentType === 'image/x-icon' || url.endsWith('.ico')) {
|
||||
if (contentType === "image/x-icon" || url.endsWith(".ico")) {
|
||||
const arrayBuffer = await res.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
return await icoToPng(buffer, 30);
|
||||
@@ -30,36 +30,36 @@ async function getImageBuffer(url: string) {
|
||||
|
||||
return await sharp(await res.arrayBuffer())
|
||||
.resize(30, 30, {
|
||||
fit: 'cover',
|
||||
fit: "cover",
|
||||
})
|
||||
.png()
|
||||
.toBuffer();
|
||||
} catch (e) {
|
||||
console.log('Failed to get image from url', url);
|
||||
console.log("Failed to get image from url", url);
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
const imageExtensions = ['svg', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'ico'];
|
||||
const imageExtensions = ["svg", "png", "jpg", "jpeg", "gif", "webp", "ico"];
|
||||
|
||||
export async function getFavicon(
|
||||
request: FastifyRequest<{
|
||||
Querystring: GetFaviconParams;
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
function sendBuffer(buffer: Buffer, cacheKey?: string) {
|
||||
if (cacheKey) {
|
||||
redis.set(`favicon:${cacheKey}`, buffer.toString('base64'));
|
||||
redis.set(`favicon:${cacheKey}`, buffer.toString("base64"));
|
||||
}
|
||||
reply.type('image/png');
|
||||
console.log('buffer', buffer.byteLength);
|
||||
reply.type("image/png");
|
||||
console.log("buffer", buffer.byteLength);
|
||||
|
||||
return reply.send(buffer);
|
||||
}
|
||||
|
||||
if (!request.query.url) {
|
||||
return reply.status(404).send('Not found');
|
||||
return reply.status(404).send("Not found");
|
||||
}
|
||||
|
||||
const url = decodeURIComponent(request.query.url);
|
||||
@@ -69,7 +69,7 @@ export async function getFavicon(
|
||||
const cacheKey = createHash(url, 32);
|
||||
const cache = await redis.get(`favicon:${cacheKey}`);
|
||||
if (cache) {
|
||||
return sendBuffer(Buffer.from(cache, 'base64'));
|
||||
return sendBuffer(Buffer.from(cache, "base64"));
|
||||
}
|
||||
const buffer = await getImageBuffer(url);
|
||||
if (buffer && buffer.byteLength > 0) {
|
||||
@@ -80,7 +80,7 @@ export async function getFavicon(
|
||||
const { hostname, origin } = new URL(url);
|
||||
const cache = await redis.get(`favicon:${hostname}`);
|
||||
if (cache) {
|
||||
return sendBuffer(Buffer.from(cache, 'base64'));
|
||||
return sendBuffer(Buffer.from(cache, "base64"));
|
||||
}
|
||||
|
||||
// TRY FAVICON.ICO
|
||||
@@ -94,7 +94,7 @@ export async function getFavicon(
|
||||
|
||||
function findFavicon(res: string) {
|
||||
const match = res.match(
|
||||
/(\<link(.+?)image\/x-icon(.+?)\>|\<link(.+?)shortcut\sicon(.+?)\>)/
|
||||
/(\<link(.+?)image\/x-icon(.+?)\>|\<link(.+?)shortcut\sicon(.+?)\>)/,
|
||||
);
|
||||
if (!match) {
|
||||
return null;
|
||||
@@ -112,16 +112,16 @@ export async function getFavicon(
|
||||
}
|
||||
}
|
||||
|
||||
return reply.status(404).send('Not found');
|
||||
return reply.status(404).send("Not found");
|
||||
}
|
||||
|
||||
export async function clearFavicons(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
const keys = await redis.keys('favicon:*');
|
||||
const keys = await redis.keys("favicon:*");
|
||||
for (const key of keys) {
|
||||
await redis.del(key);
|
||||
}
|
||||
return reply.status(404).send('OK');
|
||||
return reply.status(404).send("OK");
|
||||
}
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import { getClientIp, parseIp } from '@/utils/parseIp';
|
||||
import { isUserAgentSet, parseUserAgent } from '@/utils/parseUserAgent';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { assocPath, pathOr } from 'ramda';
|
||||
import { getClientIp, parseIp } from "@/utils/parseIp";
|
||||
import { isUserAgentSet, parseUserAgent } from "@/utils/parseUserAgent";
|
||||
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||
import { assocPath, pathOr } from "ramda";
|
||||
|
||||
import { getProfileById, upsertProfile } from '@mixan/db';
|
||||
import type { IncrementProfilePayload, UpdateProfilePayload } from '@mixan/sdk';
|
||||
import { getProfileById, upsertProfile } from "@openpanel/db";
|
||||
import type {
|
||||
IncrementProfilePayload,
|
||||
UpdateProfilePayload,
|
||||
} from "@openpanel/sdk";
|
||||
|
||||
export async function updateProfile(
|
||||
request: FastifyRequest<{
|
||||
Body: UpdateProfilePayload;
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
const { profileId, properties, ...rest } = request.body;
|
||||
const projectId = request.projectId;
|
||||
const ip = getClientIp(request)!;
|
||||
const ua = request.headers['user-agent']!;
|
||||
const ua = request.headers["user-agent"]!;
|
||||
const uaInfo = parseUserAgent(ua);
|
||||
const geo = await parseIp(ip);
|
||||
|
||||
@@ -37,29 +40,29 @@ export async function incrementProfileProperty(
|
||||
request: FastifyRequest<{
|
||||
Body: IncrementProfilePayload;
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
const { profileId, property, value } = request.body;
|
||||
const projectId = request.projectId;
|
||||
|
||||
const profile = await getProfileById(profileId);
|
||||
if (!profile) {
|
||||
return reply.status(404).send('Not found');
|
||||
return reply.status(404).send("Not found");
|
||||
}
|
||||
|
||||
const parsed = parseInt(
|
||||
pathOr<string>('0', property.split('.'), profile.properties),
|
||||
10
|
||||
pathOr<string>("0", property.split("."), profile.properties),
|
||||
10,
|
||||
);
|
||||
|
||||
if (isNaN(parsed)) {
|
||||
return reply.status(400).send('Not number');
|
||||
return reply.status(400).send("Not number");
|
||||
}
|
||||
|
||||
profile.properties = assocPath(
|
||||
property.split('.'),
|
||||
property.split("."),
|
||||
parsed + value,
|
||||
profile.properties
|
||||
profile.properties,
|
||||
);
|
||||
|
||||
await upsertProfile({
|
||||
@@ -75,29 +78,29 @@ export async function decrementProfileProperty(
|
||||
request: FastifyRequest<{
|
||||
Body: IncrementProfilePayload;
|
||||
}>,
|
||||
reply: FastifyReply
|
||||
reply: FastifyReply,
|
||||
) {
|
||||
const { profileId, property, value } = request.body;
|
||||
const projectId = request.projectId;
|
||||
|
||||
const profile = await getProfileById(profileId);
|
||||
if (!profile) {
|
||||
return reply.status(404).send('Not found');
|
||||
return reply.status(404).send("Not found");
|
||||
}
|
||||
|
||||
const parsed = parseInt(
|
||||
pathOr<string>('0', property.split('.'), profile.properties),
|
||||
10
|
||||
pathOr<string>("0", property.split("."), profile.properties),
|
||||
10,
|
||||
);
|
||||
|
||||
if (isNaN(parsed)) {
|
||||
return reply.status(400).send('Not number');
|
||||
return reply.status(400).send("Not number");
|
||||
}
|
||||
|
||||
profile.properties = assocPath(
|
||||
property.split('.'),
|
||||
property.split("."),
|
||||
parsed - value,
|
||||
profile.properties
|
||||
profile.properties,
|
||||
);
|
||||
|
||||
await upsertProfile({
|
||||
|
||||
@@ -1,43 +1,43 @@
|
||||
import cors from '@fastify/cors';
|
||||
import Fastify from 'fastify';
|
||||
import cors from "@fastify/cors";
|
||||
import Fastify from "fastify";
|
||||
|
||||
import { redisPub } from '@mixan/redis';
|
||||
import { redisPub } from "@openpanel/redis";
|
||||
|
||||
import eventRouter from './routes/event.router';
|
||||
import liveRouter from './routes/live.router';
|
||||
import miscRouter from './routes/misc.router';
|
||||
import profileRouter from './routes/profile.router';
|
||||
import { logger, logInfo } from './utils/logger';
|
||||
import eventRouter from "./routes/event.router";
|
||||
import liveRouter from "./routes/live.router";
|
||||
import miscRouter from "./routes/misc.router";
|
||||
import profileRouter from "./routes/profile.router";
|
||||
import { logger, logInfo } from "./utils/logger";
|
||||
|
||||
declare module 'fastify' {
|
||||
declare module "fastify" {
|
||||
interface FastifyRequest {
|
||||
projectId: string;
|
||||
}
|
||||
}
|
||||
|
||||
const port = parseInt(process.env.API_PORT || '3000', 10);
|
||||
const port = parseInt(process.env.API_PORT || "3000", 10);
|
||||
|
||||
const startServer = async () => {
|
||||
logInfo('Starting server');
|
||||
logInfo("Starting server");
|
||||
try {
|
||||
const fastify = Fastify({
|
||||
logger: logger,
|
||||
});
|
||||
|
||||
fastify.register(cors, {
|
||||
origin: '*',
|
||||
origin: "*",
|
||||
});
|
||||
|
||||
fastify.decorateRequest('projectId', '');
|
||||
fastify.register(eventRouter, { prefix: '/event' });
|
||||
fastify.register(profileRouter, { prefix: '/profile' });
|
||||
fastify.register(liveRouter, { prefix: '/live' });
|
||||
fastify.register(miscRouter, { prefix: '/misc' });
|
||||
fastify.decorateRequest("projectId", "");
|
||||
fastify.register(eventRouter, { prefix: "/event" });
|
||||
fastify.register(profileRouter, { prefix: "/profile" });
|
||||
fastify.register(liveRouter, { prefix: "/live" });
|
||||
fastify.register(miscRouter, { prefix: "/misc" });
|
||||
fastify.setErrorHandler((error, request, reply) => {
|
||||
fastify.log.error(error);
|
||||
});
|
||||
fastify.get('/', (request, reply) => {
|
||||
reply.send({ name: 'openpanel sdk api' });
|
||||
fastify.get("/", (request, reply) => {
|
||||
reply.send({ name: "openpanel sdk api" });
|
||||
});
|
||||
// fastify.get('/health-check', async (request, reply) => {
|
||||
// try {
|
||||
@@ -47,8 +47,8 @@ const startServer = async () => {
|
||||
// reply.status(500).send()
|
||||
// }
|
||||
// })
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
for (const signal of ['SIGINT', 'SIGTERM']) {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
for (const signal of ["SIGINT", "SIGTERM"]) {
|
||||
process.on(signal, (err) => {
|
||||
logger.fatal(err, `uncaught exception detected ${signal}`);
|
||||
fastify.close().then((err) => {
|
||||
@@ -59,12 +59,12 @@ const startServer = async () => {
|
||||
}
|
||||
|
||||
await fastify.listen({
|
||||
host: process.env.NODE_ENV === 'production' ? '0.0.0.0' : 'localhost',
|
||||
host: process.env.NODE_ENV === "production" ? "0.0.0.0" : "localhost",
|
||||
port,
|
||||
});
|
||||
|
||||
// Notify when keys expires
|
||||
redisPub.config('SET', 'notify-keyspace-events', 'Ex');
|
||||
redisPub.config("SET", "notify-keyspace-events", "Ex");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
import { isBot } from '@/bots';
|
||||
import * as controller from '@/controllers/event.controller';
|
||||
import { validateSdkRequest } from '@/utils/auth';
|
||||
import type { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
||||
import { isBot } from "@/bots";
|
||||
import * as controller from "@/controllers/event.controller";
|
||||
import { validateSdkRequest } from "@/utils/auth";
|
||||
import type { FastifyPluginCallback, FastifyRequest } from "fastify";
|
||||
|
||||
import { createBotEvent } from '@mixan/db';
|
||||
import type { PostEventPayload } from '@mixan/sdk';
|
||||
import { createBotEvent } from "@openpanel/db";
|
||||
import type { PostEventPayload } from "@openpanel/sdk";
|
||||
|
||||
const eventRouter: FastifyPluginCallback = (fastify, opts, done) => {
|
||||
fastify.addHook(
|
||||
'preHandler',
|
||||
"preHandler",
|
||||
async (
|
||||
req: FastifyRequest<{
|
||||
Body: PostEventPayload;
|
||||
}>,
|
||||
reply
|
||||
reply,
|
||||
) => {
|
||||
try {
|
||||
const projectId = await validateSdkRequest(req.headers);
|
||||
req.projectId = projectId;
|
||||
|
||||
const bot = req.headers['user-agent']
|
||||
? isBot(req.headers['user-agent'])
|
||||
const bot = req.headers["user-agent"]
|
||||
? isBot(req.headers["user-agent"])
|
||||
: null;
|
||||
|
||||
if (bot) {
|
||||
const path = (req.body?.properties?.__path ||
|
||||
req.body?.properties?.path) as string | undefined;
|
||||
req.log.warn({ ...req.headers, bot }, 'Bot detected (event)');
|
||||
req.log.warn({ ...req.headers, bot }, "Bot detected (event)");
|
||||
await createBotEvent({
|
||||
...bot,
|
||||
projectId,
|
||||
path: path ?? '',
|
||||
path: path ?? "",
|
||||
createdAt: new Date(req.body?.timestamp),
|
||||
});
|
||||
reply.status(202).send('OK');
|
||||
reply.status(202).send("OK");
|
||||
}
|
||||
} catch (e) {
|
||||
req.log.error(e, 'Failed to create bot event');
|
||||
req.log.error(e, "Failed to create bot event");
|
||||
reply.status(401).send();
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
fastify.route({
|
||||
method: 'POST',
|
||||
url: '/',
|
||||
method: "POST",
|
||||
url: "/",
|
||||
handler: controller.postEvent,
|
||||
});
|
||||
fastify.route({
|
||||
method: 'GET',
|
||||
url: '/',
|
||||
method: "GET",
|
||||
url: "/",
|
||||
handler: controller.postEvent,
|
||||
});
|
||||
done();
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { RawRequestDefaultExpression } from 'fastify';
|
||||
|
||||
import { verifyPassword } from '@mixan/common';
|
||||
import { db } from '@mixan/db';
|
||||
import { verifyPassword } from '@openpanel/common';
|
||||
import { db } from '@openpanel/db';
|
||||
|
||||
export async function validateSdkRequest(
|
||||
headers: RawRequestDefaultExpression['headers']
|
||||
): Promise<string> {
|
||||
const clientId = headers['mixan-client-id'] as string;
|
||||
const clientSecret = headers['mixan-client-secret'] as string;
|
||||
const clientId = headers['openpanel-client-id'] as string;
|
||||
const clientSecret = headers['openpanel-client-secret'] as string;
|
||||
const origin = headers.origin;
|
||||
if (!clientId) {
|
||||
throw new Error('Misisng client id');
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
import { stripTrailingSlash } from '@mixan/common';
|
||||
import { stripTrailingSlash } from "@openpanel/common";
|
||||
|
||||
import referrers from '../referrers';
|
||||
import referrers from "../referrers";
|
||||
|
||||
function getHostname(url: string | undefined) {
|
||||
if (!url) {
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
return new URL(url).hostname;
|
||||
} catch (e) {
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function parseReferrer(url: string | undefined) {
|
||||
const hostname = getHostname(url);
|
||||
const match = referrers[hostname] ?? referrers[hostname.replace('www.', '')];
|
||||
const match = referrers[hostname] ?? referrers[hostname.replace("www.", "")];
|
||||
|
||||
return {
|
||||
name: match?.name ?? '',
|
||||
type: match?.type ?? 'unknown',
|
||||
url: stripTrailingSlash(url ?? ''),
|
||||
name: match?.name ?? "",
|
||||
type: match?.type ?? "unknown",
|
||||
url: stripTrailingSlash(url ?? ""),
|
||||
};
|
||||
}
|
||||
|
||||
export function getReferrerWithQuery(
|
||||
query: Record<string, string> | undefined
|
||||
query: Record<string, string> | undefined,
|
||||
) {
|
||||
if (!query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const source = query.utm_source ?? query.ref ?? query.utm_referrer ?? '';
|
||||
const source = query.utm_source ?? query.ref ?? query.utm_referrer ?? "";
|
||||
|
||||
const match = Object.values(referrers).find(
|
||||
(referrer) => referrer.name.toLowerCase() === source?.toLowerCase()
|
||||
(referrer) => referrer.name.toLowerCase() === source?.toLowerCase(),
|
||||
);
|
||||
|
||||
if (!match) {
|
||||
@@ -45,6 +45,6 @@ export function getReferrerWithQuery(
|
||||
return {
|
||||
name: match.name,
|
||||
type: match.type,
|
||||
url: '',
|
||||
url: "",
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user