format:fix

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-03-13 08:55:24 +01:00
parent 558318c312
commit 48aecc4b37
117 changed files with 4455 additions and 667 deletions

View File

@@ -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 "@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";
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,48 +101,48 @@ 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`
)
);
const payload: Omit<IServiceCreateEventPayload, "id"> = {
const payload: Omit<IServiceCreateEventPayload, 'id'> = {
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,
}
};
contextLogger.send("server event is queued", {
contextLogger.send('server event is queued', {
ip,
origin,
ua,
@@ -156,27 +156,27 @@ export async function postEvent(
prevEvent: event,
});
eventsQueue.add("event", {
type: "createEvent",
eventsQueue.add('event', {
type: 'createEvent',
payload,
});
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 =
@@ -194,9 +194,9 @@ export async function postEvent(
deviceId = currentDeviceId;
// Queue session end
eventsQueue.add(
"event",
'event',
{
type: "createSessionEnd",
type: 'createSessionEnd',
payload: {
deviceId,
},
@@ -204,27 +204,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,
}),
@@ -243,27 +243,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,
});
@@ -271,21 +271,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,
});
@@ -294,23 +294,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,
@@ -328,14 +328,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);
}

View File

@@ -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 "@openpanel/common";
import type { IServiceCreateEventPayload } from "@openpanel/db";
import { getEvents, getLiveVisitors } from "@openpanel/db";
import { redis, redisPub, redisSub } from "@openpanel/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);
});
}

View File

@@ -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 "@openpanel/common";
import { redis } from "@openpanel/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');
}

View File

@@ -1,24 +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 "@openpanel/db";
import { getProfileById, upsertProfile } from '@openpanel/db';
import type {
IncrementProfilePayload,
UpdateProfilePayload,
} from "@openpanel/sdk";
} 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);
@@ -40,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({
@@ -78,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({

View File

@@ -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 "@openpanel/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);
}

View File

@@ -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 "@openpanel/db";
import type { PostEventPayload } from "@openpanel/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();

View File

@@ -1,41 +1,41 @@
import { stripTrailingSlash } from "@openpanel/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: '',
};
}