chore:little fixes and formating and linting and patches
This commit is contained in:
@@ -65,4 +65,4 @@
|
||||
"tsdown": "0.14.2",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { dirname } from 'node:path';
|
||||
import path, { dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
// Regex special characters that indicate we need actual regex
|
||||
const regexSpecialChars = /[|^$.*+?(){}\[\]\\]/;
|
||||
const regexSpecialChars = /[|^$.*+?(){}[\]\\]/;
|
||||
|
||||
function transformBots(bots: any[]): any[] {
|
||||
return bots.map((bot) => {
|
||||
@@ -28,7 +28,7 @@ async function main() {
|
||||
// Get document, or throw exception on error
|
||||
try {
|
||||
const data = await fetch(
|
||||
'https://raw.githubusercontent.com/matomo-org/device-detector/master/regexes/bots.yml',
|
||||
'https://raw.githubusercontent.com/matomo-org/device-detector/master/regexes/bots.yml'
|
||||
).then((res) => res.text());
|
||||
|
||||
const parsedData = yaml.load(data) as any[];
|
||||
@@ -45,11 +45,11 @@ async function main() {
|
||||
'export default bots;',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
console.log(
|
||||
`✅ Generated bots.ts with ${transformedBots.length} bot entries`,
|
||||
`✅ Generated bots.ts with ${transformedBots.length} bot entries`
|
||||
);
|
||||
const regexCount = transformedBots.filter((b) => 'regex' in b).length;
|
||||
const includesCount = transformedBots.filter((b) => 'includes' in b).length;
|
||||
|
||||
@@ -133,7 +133,7 @@ function generateEvents(): Event[] {
|
||||
clientId,
|
||||
profile: profiles[i % PROFILE_COUNT]!,
|
||||
eventsCount: Math.floor(Math.random() * 10),
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -150,7 +150,7 @@ let lastTriggeredIndex = 0;
|
||||
async function triggerEvents(generatedEvents: any[]) {
|
||||
const EVENTS_PER_SECOND = Number.parseInt(
|
||||
process.env.EVENTS_PER_SECOND || '100',
|
||||
10,
|
||||
10
|
||||
);
|
||||
const INTERVAL_MS = 1000 / EVENTS_PER_SECOND;
|
||||
|
||||
@@ -164,7 +164,7 @@ async function triggerEvents(generatedEvents: any[]) {
|
||||
await trackit(event);
|
||||
console.log(`Event ${lastTriggeredIndex + 1} sent successfully`);
|
||||
console.log(
|
||||
`sending ${event.track.payload?.properties?.__path} from user ${event.headers['user-agent']}`,
|
||||
`sending ${event.track.payload?.properties?.__path} from user ${event.headers['user-agent']}`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`Failed to send event ${lastTriggeredIndex + 1}:`, error);
|
||||
@@ -174,7 +174,7 @@ async function triggerEvents(generatedEvents: any[]) {
|
||||
const remainingEvents = generatedEvents.length - lastTriggeredIndex;
|
||||
|
||||
console.log(
|
||||
`Triggered ${lastTriggeredIndex} events. Remaining: ${remainingEvents}`,
|
||||
`Triggered ${lastTriggeredIndex} events. Remaining: ${remainingEvents}`
|
||||
);
|
||||
|
||||
if (remainingEvents > 0) {
|
||||
@@ -215,7 +215,7 @@ async function createMock(file: string) {
|
||||
fs.writeFileSync(
|
||||
file,
|
||||
JSON.stringify(insertFakeEvents(scrambleEvents(generateEvents())), null, 2),
|
||||
'utf-8',
|
||||
'utf-8'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -438,7 +438,7 @@ async function simultaneousRequests() {
|
||||
if (group.parallel && group.tracks.length > 1) {
|
||||
// Parallel execution for same-flagged tracks
|
||||
console.log(
|
||||
`Firing ${group.tracks.length} parallel requests with flag '${group.parallel}'`,
|
||||
`Firing ${group.tracks.length} parallel requests with flag '${group.parallel}'`
|
||||
);
|
||||
const promises = group.tracks.map(async (track) => {
|
||||
const { name, parallel, ...properties } = track;
|
||||
|
||||
@@ -14,7 +14,7 @@ const CLIENT_ID = process.env.CLIENT_ID!;
|
||||
const CLIENT_SECRET = process.env.CLIENT_SECRET!;
|
||||
const API_BASE_URL = process.env.API_URL || 'http://localhost:3333';
|
||||
|
||||
if (!CLIENT_ID || !CLIENT_SECRET) {
|
||||
if (!(CLIENT_ID && CLIENT_SECRET)) {
|
||||
console.error('CLIENT_ID and CLIENT_SECRET must be set');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -34,7 +34,7 @@ const results: TestResult[] = [];
|
||||
async function makeRequest(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: any,
|
||||
body?: any
|
||||
): Promise<TestResult> {
|
||||
const url = `${API_BASE_URL}${path}`;
|
||||
const headers: Record<string, string> = {
|
||||
@@ -90,9 +90,11 @@ async function testProjects() {
|
||||
});
|
||||
results.push(createResult);
|
||||
console.log(
|
||||
`✓ POST /manage/projects: ${createResult.success ? '✅' : '❌'} ${createResult.status}`,
|
||||
`✓ POST /manage/projects: ${createResult.success ? '✅' : '❌'} ${createResult.status}`
|
||||
);
|
||||
if (createResult.error) console.log(` Error: ${createResult.error}`);
|
||||
if (createResult.error) {
|
||||
console.log(` Error: ${createResult.error}`);
|
||||
}
|
||||
|
||||
const projectId = createResult.data?.data?.id;
|
||||
const clientId = createResult.data?.data?.client?.id;
|
||||
@@ -100,15 +102,19 @@ async function testProjects() {
|
||||
|
||||
if (projectId) {
|
||||
console.log(` Created project: ${projectId}`);
|
||||
if (clientId) console.log(` Created client: ${clientId}`);
|
||||
if (clientSecret) console.log(` Client secret: ${clientSecret}`);
|
||||
if (clientId) {
|
||||
console.log(` Created client: ${clientId}`);
|
||||
}
|
||||
if (clientSecret) {
|
||||
console.log(` Client secret: ${clientSecret}`);
|
||||
}
|
||||
}
|
||||
|
||||
// List projects
|
||||
const listResult = await makeRequest('GET', '/manage/projects');
|
||||
results.push(listResult);
|
||||
console.log(
|
||||
`✓ GET /manage/projects: ${listResult.success ? '✅' : '❌'} ${listResult.status}`,
|
||||
`✓ GET /manage/projects: ${listResult.success ? '✅' : '❌'} ${listResult.status}`
|
||||
);
|
||||
if (listResult.data?.data?.length) {
|
||||
console.log(` Found ${listResult.data.data.length} projects`);
|
||||
@@ -119,7 +125,7 @@ async function testProjects() {
|
||||
const getResult = await makeRequest('GET', `/manage/projects/${projectId}`);
|
||||
results.push(getResult);
|
||||
console.log(
|
||||
`✓ GET /manage/projects/:id: ${getResult.success ? '✅' : '❌'} ${getResult.status}`,
|
||||
`✓ GET /manage/projects/:id: ${getResult.success ? '✅' : '❌'} ${getResult.status}`
|
||||
);
|
||||
|
||||
// Update project
|
||||
@@ -129,21 +135,21 @@ async function testProjects() {
|
||||
{
|
||||
name: 'Updated Test Project',
|
||||
crossDomain: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
results.push(updateResult);
|
||||
console.log(
|
||||
`✓ PATCH /manage/projects/:id: ${updateResult.success ? '✅' : '❌'} ${updateResult.status}`,
|
||||
`✓ PATCH /manage/projects/:id: ${updateResult.success ? '✅' : '❌'} ${updateResult.status}`
|
||||
);
|
||||
|
||||
// Delete project (soft delete)
|
||||
const deleteResult = await makeRequest(
|
||||
'DELETE',
|
||||
`/manage/projects/${projectId}`,
|
||||
`/manage/projects/${projectId}`
|
||||
);
|
||||
results.push(deleteResult);
|
||||
console.log(
|
||||
`✓ DELETE /manage/projects/:id: ${deleteResult.success ? '✅' : '❌'} ${deleteResult.status}`,
|
||||
`✓ DELETE /manage/projects/:id: ${deleteResult.success ? '✅' : '❌'} ${deleteResult.status}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,26 +167,30 @@ async function testClients(projectId?: string) {
|
||||
});
|
||||
results.push(createResult);
|
||||
console.log(
|
||||
`✓ POST /manage/clients: ${createResult.success ? '✅' : '❌'} ${createResult.status}`,
|
||||
`✓ POST /manage/clients: ${createResult.success ? '✅' : '❌'} ${createResult.status}`
|
||||
);
|
||||
if (createResult.error) console.log(` Error: ${createResult.error}`);
|
||||
if (createResult.error) {
|
||||
console.log(` Error: ${createResult.error}`);
|
||||
}
|
||||
|
||||
const clientId = createResult.data?.data?.id;
|
||||
const clientSecret = createResult.data?.data?.secret;
|
||||
|
||||
if (clientId) {
|
||||
console.log(` Created client: ${clientId}`);
|
||||
if (clientSecret) console.log(` Client secret: ${clientSecret}`);
|
||||
if (clientSecret) {
|
||||
console.log(` Client secret: ${clientSecret}`);
|
||||
}
|
||||
}
|
||||
|
||||
// List clients
|
||||
const listResult = await makeRequest(
|
||||
'GET',
|
||||
projectId ? `/manage/clients?projectId=${projectId}` : '/manage/clients',
|
||||
projectId ? `/manage/clients?projectId=${projectId}` : '/manage/clients'
|
||||
);
|
||||
results.push(listResult);
|
||||
console.log(
|
||||
`✓ GET /manage/clients: ${listResult.success ? '✅' : '❌'} ${listResult.status}`,
|
||||
`✓ GET /manage/clients: ${listResult.success ? '✅' : '❌'} ${listResult.status}`
|
||||
);
|
||||
if (listResult.data?.data?.length) {
|
||||
console.log(` Found ${listResult.data.data.length} clients`);
|
||||
@@ -191,7 +201,7 @@ async function testClients(projectId?: string) {
|
||||
const getResult = await makeRequest('GET', `/manage/clients/${clientId}`);
|
||||
results.push(getResult);
|
||||
console.log(
|
||||
`✓ GET /manage/clients/:id: ${getResult.success ? '✅' : '❌'} ${getResult.status}`,
|
||||
`✓ GET /manage/clients/:id: ${getResult.success ? '✅' : '❌'} ${getResult.status}`
|
||||
);
|
||||
|
||||
// Update client
|
||||
@@ -200,21 +210,21 @@ async function testClients(projectId?: string) {
|
||||
`/manage/clients/${clientId}`,
|
||||
{
|
||||
name: 'Updated Test Client',
|
||||
},
|
||||
}
|
||||
);
|
||||
results.push(updateResult);
|
||||
console.log(
|
||||
`✓ PATCH /manage/clients/:id: ${updateResult.success ? '✅' : '❌'} ${updateResult.status}`,
|
||||
`✓ PATCH /manage/clients/:id: ${updateResult.success ? '✅' : '❌'} ${updateResult.status}`
|
||||
);
|
||||
|
||||
// Delete client
|
||||
const deleteResult = await makeRequest(
|
||||
'DELETE',
|
||||
`/manage/clients/${clientId}`,
|
||||
`/manage/clients/${clientId}`
|
||||
);
|
||||
results.push(deleteResult);
|
||||
console.log(
|
||||
`✓ DELETE /manage/clients/:id: ${deleteResult.success ? '✅' : '❌'} ${deleteResult.status}`,
|
||||
`✓ DELETE /manage/clients/:id: ${deleteResult.success ? '✅' : '❌'} ${deleteResult.status}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -236,9 +246,11 @@ async function testReferences(projectId?: string) {
|
||||
});
|
||||
results.push(createResult);
|
||||
console.log(
|
||||
`✓ POST /manage/references: ${createResult.success ? '✅' : '❌'} ${createResult.status}`,
|
||||
`✓ POST /manage/references: ${createResult.success ? '✅' : '❌'} ${createResult.status}`
|
||||
);
|
||||
if (createResult.error) console.log(` Error: ${createResult.error}`);
|
||||
if (createResult.error) {
|
||||
console.log(` Error: ${createResult.error}`);
|
||||
}
|
||||
|
||||
const referenceId = createResult.data?.data?.id;
|
||||
|
||||
@@ -249,11 +261,11 @@ async function testReferences(projectId?: string) {
|
||||
// List references
|
||||
const listResult = await makeRequest(
|
||||
'GET',
|
||||
`/manage/references?projectId=${projectId}`,
|
||||
`/manage/references?projectId=${projectId}`
|
||||
);
|
||||
results.push(listResult);
|
||||
console.log(
|
||||
`✓ GET /manage/references: ${listResult.success ? '✅' : '❌'} ${listResult.status}`,
|
||||
`✓ GET /manage/references: ${listResult.success ? '✅' : '❌'} ${listResult.status}`
|
||||
);
|
||||
if (listResult.data?.data?.length) {
|
||||
console.log(` Found ${listResult.data.data.length} references`);
|
||||
@@ -263,11 +275,11 @@ async function testReferences(projectId?: string) {
|
||||
// Get reference
|
||||
const getResult = await makeRequest(
|
||||
'GET',
|
||||
`/manage/references/${referenceId}`,
|
||||
`/manage/references/${referenceId}`
|
||||
);
|
||||
results.push(getResult);
|
||||
console.log(
|
||||
`✓ GET /manage/references/:id: ${getResult.success ? '✅' : '❌'} ${getResult.status}`,
|
||||
`✓ GET /manage/references/:id: ${getResult.success ? '✅' : '❌'} ${getResult.status}`
|
||||
);
|
||||
|
||||
// Update reference
|
||||
@@ -278,21 +290,21 @@ async function testReferences(projectId?: string) {
|
||||
title: 'Updated Test Reference',
|
||||
description: 'Updated description',
|
||||
datetime: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
);
|
||||
results.push(updateResult);
|
||||
console.log(
|
||||
`✓ PATCH /manage/references/:id: ${updateResult.success ? '✅' : '❌'} ${updateResult.status}`,
|
||||
`✓ PATCH /manage/references/:id: ${updateResult.success ? '✅' : '❌'} ${updateResult.status}`
|
||||
);
|
||||
|
||||
// Delete reference
|
||||
const deleteResult = await makeRequest(
|
||||
'DELETE',
|
||||
`/manage/references/${referenceId}`,
|
||||
`/manage/references/${referenceId}`
|
||||
);
|
||||
results.push(deleteResult);
|
||||
console.log(
|
||||
`✓ DELETE /manage/references/:id: ${deleteResult.success ? '✅' : '❌'} ${deleteResult.status}`,
|
||||
`✓ DELETE /manage/references/:id: ${deleteResult.success ? '✅' : '❌'} ${deleteResult.status}`
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -328,7 +340,9 @@ async function main() {
|
||||
.filter((r) => !r.success)
|
||||
.forEach((r) => {
|
||||
console.log(` ❌ ${r.name} (${r.status})`);
|
||||
if (r.error) console.log(` Error: ${r.error}`);
|
||||
if (r.error) {
|
||||
console.log(` Error: ${r.error}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { type IClickhouseEvent, ch, createEvent } from '@openpanel/db';
|
||||
import { formatClickhouseDate } from '@openpanel/db';
|
||||
import { ch, formatClickhouseDate, type IClickhouseEvent } from '@openpanel/db';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
async function main() {
|
||||
const startDate = new Date('2025-01-01T00:00:00Z');
|
||||
const endDate = new Date();
|
||||
const eventsPerDay = 25000;
|
||||
const eventsPerDay = 25_000;
|
||||
const variance = 3000;
|
||||
|
||||
// Event names to randomly choose from
|
||||
@@ -36,7 +35,7 @@ async function main() {
|
||||
device_id: `device_${Math.floor(Math.random() * 1000)}`,
|
||||
profile_id: `profile_${Math.floor(Math.random() * 1000)}`,
|
||||
project_id: 'testing',
|
||||
session_id: `session_${Math.floor(Math.random() * 10000)}`,
|
||||
session_id: `session_${Math.floor(Math.random() * 10_000)}`,
|
||||
properties: {
|
||||
hash: 'test-hash',
|
||||
'query.utm_source': 'test',
|
||||
@@ -75,7 +74,7 @@ async function main() {
|
||||
|
||||
// Log progress
|
||||
console.log(
|
||||
`Created ${dailyEvents} events for ${currentDate.toISOString().split('T')[0]}`,
|
||||
`Created ${dailyEvents} events for ${currentDate.toISOString().split('T')[0]}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { db, getOrganizationByProjectIdCached } from '@openpanel/db';
|
||||
import { getProjectAccess } from '@openpanel/trpc/src/access';
|
||||
import { appendResponseMessages, type Message, streamText } from 'ai';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { getChatModel, getChatSystemPrompt } from '@/utils/ai';
|
||||
import {
|
||||
getAllEventNames,
|
||||
@@ -8,10 +12,6 @@ import {
|
||||
getReport,
|
||||
} from '@/utils/ai-tools';
|
||||
import { HttpError } from '@/utils/errors';
|
||||
import { db, getOrganizationByProjectIdCached } from '@openpanel/db';
|
||||
import { getProjectAccess } from '@openpanel/trpc/src/access';
|
||||
import { type Message, appendResponseMessages, streamText } from 'ai';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
|
||||
export async function chat(
|
||||
request: FastifyRequest<{
|
||||
@@ -22,7 +22,7 @@ export async function chat(
|
||||
messages: Message[];
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
const { session } = request.session;
|
||||
const { messages } = request.body;
|
||||
@@ -117,7 +117,7 @@ export async function chat(
|
||||
},
|
||||
});
|
||||
},
|
||||
onError: async (error) => {
|
||||
onError: (error) => {
|
||||
request.log.error('chat error', { error });
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Error - OpenPanel</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Error - OpenPanel</title>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap"
|
||||
rel="stylesheet"
|
||||
>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-family: "Inter", sans-serif;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
@@ -47,16 +49,21 @@
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<img src="https://openpanel.dev/logo.svg" alt="OpenPanel Logo" class="logo">
|
||||
<h1>Oops! Something went wrong</h1>
|
||||
<p>We encountered an error while processing your request. Please try again later or contact support if the problem
|
||||
persists.</p>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<img
|
||||
src="https://openpanel.dev/logo.svg"
|
||||
alt="OpenPanel Logo"
|
||||
class="logo"
|
||||
>
|
||||
<h1>Oops! Something went wrong</h1>
|
||||
<p>
|
||||
We encountered an error while processing your request. Please try again
|
||||
later or contact support if the problem persists.
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -71,22 +71,22 @@ const eventsScheme = z.object({
|
||||
page: z.coerce.number().optional().default(1),
|
||||
limit: z.coerce.number().optional().default(50),
|
||||
includes: z
|
||||
.preprocess(
|
||||
(arg) => {
|
||||
if (arg == null) {
|
||||
return undefined;
|
||||
}
|
||||
if (Array.isArray(arg)) {
|
||||
return arg;
|
||||
}
|
||||
if (typeof arg === 'string') {
|
||||
const parts = arg.split(',').map((s) => s.trim()).filter(Boolean);
|
||||
return parts;
|
||||
}
|
||||
.preprocess((arg) => {
|
||||
if (arg == null) {
|
||||
return undefined;
|
||||
}
|
||||
if (Array.isArray(arg)) {
|
||||
return arg;
|
||||
},
|
||||
z.array(z.string())
|
||||
)
|
||||
}
|
||||
if (typeof arg === 'string') {
|
||||
const parts = arg
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
return parts;
|
||||
}
|
||||
return arg;
|
||||
}, z.array(z.string()))
|
||||
.optional(),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
|
||||
import { toDots } from '@openpanel/common';
|
||||
import type { IClickhouseEvent } from '@openpanel/db';
|
||||
import { TABLE_NAMES, ch, formatClickhouseDate } from '@openpanel/db';
|
||||
import { ch, formatClickhouseDate, TABLE_NAMES } from '@openpanel/db';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
|
||||
export async function importEvents(
|
||||
request: FastifyRequest<{
|
||||
Body: IClickhouseEvent[];
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
const projectId = request.client?.projectId;
|
||||
if (!projectId) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { parseQueryString } from '@/utils/parse-zod-query-string';
|
||||
import { getDefaultIntervalByDates } from '@openpanel/constants';
|
||||
import {
|
||||
eventBuffer,
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
import { zChartEventFilter, zRange } from '@openpanel/validation';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { z } from 'zod';
|
||||
import { parseQueryString } from '@/utils/parse-zod-query-string';
|
||||
|
||||
const zGetMetricsQuery = z.object({
|
||||
startDate: z.string().nullish(),
|
||||
@@ -22,7 +22,7 @@ export async function getMetrics(
|
||||
Params: { projectId: string };
|
||||
Querystring: z.infer<typeof zGetMetricsQuery>;
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
const { timezone } = await getSettingsForProject(request.params.projectId);
|
||||
const parsed = zGetMetricsQuery.safeParse(parseQueryString(request.query));
|
||||
@@ -41,11 +41,11 @@ export async function getMetrics(
|
||||
await overviewService.getMetrics({
|
||||
projectId: request.params.projectId,
|
||||
filters: parsed.data.filters,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
startDate,
|
||||
endDate,
|
||||
interval: getDefaultIntervalByDates(startDate, endDate) ?? 'day',
|
||||
timezone,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export async function getLiveVisitors(
|
||||
request: FastifyRequest<{
|
||||
Params: { projectId: string };
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
reply.send({
|
||||
visitors: await eventBuffer.getActiveVisitorCount(request.params.projectId),
|
||||
@@ -76,7 +76,7 @@ export async function getPages(
|
||||
Params: { projectId: string };
|
||||
Querystring: z.infer<typeof zGetTopPagesQuery>;
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
const { timezone } = await getSettingsForProject(request.params.projectId);
|
||||
const { startDate, endDate } = getChartStartEndDate(request.query, timezone);
|
||||
@@ -93,8 +93,8 @@ export async function getPages(
|
||||
return overviewService.getTopPages({
|
||||
projectId: request.params.projectId,
|
||||
filters: parsed.data.filters,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
startDate,
|
||||
endDate,
|
||||
timezone,
|
||||
});
|
||||
}
|
||||
@@ -132,19 +132,19 @@ const zGetOverviewGenericQuery = z.object({
|
||||
});
|
||||
|
||||
export function getOverviewGeneric(
|
||||
column: z.infer<typeof zGetOverviewGenericQuery>['column'],
|
||||
column: z.infer<typeof zGetOverviewGenericQuery>['column']
|
||||
) {
|
||||
return async (
|
||||
request: FastifyRequest<{
|
||||
Params: { projectId: string; key: string };
|
||||
Querystring: z.infer<typeof zGetOverviewGenericQuery>;
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
reply: FastifyReply
|
||||
) => {
|
||||
const { timezone } = await getSettingsForProject(request.params.projectId);
|
||||
const { startDate, endDate } = getChartStartEndDate(
|
||||
request.query,
|
||||
timezone,
|
||||
timezone
|
||||
);
|
||||
const parsed = zGetOverviewGenericQuery.safeParse({
|
||||
...parseQueryString(request.query),
|
||||
@@ -165,10 +165,10 @@ export function getOverviewGeneric(
|
||||
column,
|
||||
projectId: request.params.projectId,
|
||||
filters: parsed.data.filters,
|
||||
startDate: startDate,
|
||||
endDate: endDate,
|
||||
startDate,
|
||||
endDate,
|
||||
timezone,
|
||||
}),
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { logger } from '@/utils/logger';
|
||||
import { parseUrlMeta } from '@/utils/parseUrlMeta';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import sharp from 'sharp';
|
||||
|
||||
import {
|
||||
DEFAULT_IP_HEADER_ORDER,
|
||||
getClientIpFromHeaders,
|
||||
} from '@openpanel/common/server/get-client-ip';
|
||||
import { TABLE_NAMES, ch, chQuery, formatClickhouseDate } from '@openpanel/db';
|
||||
import { ch, chQuery, formatClickhouseDate, TABLE_NAMES } from '@openpanel/db';
|
||||
import { type GeoLocation, getGeoLocation } from '@openpanel/geo';
|
||||
import { getCache, getRedisCache } from '@openpanel/redis';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import sharp from 'sharp';
|
||||
import { logger } from '@/utils/logger';
|
||||
import { parseUrlMeta } from '@/utils/parseUrlMeta';
|
||||
|
||||
interface GetFaviconParams {
|
||||
url: string;
|
||||
@@ -29,7 +28,9 @@ function createCacheKey(url: string, prefix = 'favicon'): string {
|
||||
|
||||
function validateUrl(raw?: string): URL | null {
|
||||
try {
|
||||
if (!raw) throw new Error('Missing ?url');
|
||||
if (!raw) {
|
||||
throw new Error('Missing ?url');
|
||||
}
|
||||
const url = new URL(raw);
|
||||
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
|
||||
throw new Error('Only http/https URLs are allowed');
|
||||
@@ -42,7 +43,7 @@ function validateUrl(raw?: string): URL | null {
|
||||
|
||||
// Binary cache functions (more efficient than base64)
|
||||
async function getFromCacheBinary(
|
||||
key: string,
|
||||
key: string
|
||||
): Promise<{ buffer: Buffer; contentType: string } | null> {
|
||||
const redis = getRedisCache();
|
||||
const [bufferBase64, contentType] = await Promise.all([
|
||||
@@ -50,14 +51,16 @@ async function getFromCacheBinary(
|
||||
redis.get(`${key}:ctype`),
|
||||
]);
|
||||
|
||||
if (!bufferBase64 || !contentType) return null;
|
||||
if (!(bufferBase64 && contentType)) {
|
||||
return null;
|
||||
}
|
||||
return { buffer: Buffer.from(bufferBase64, 'base64'), contentType };
|
||||
}
|
||||
|
||||
async function setToCacheBinary(
|
||||
key: string,
|
||||
buffer: Buffer,
|
||||
contentType: string,
|
||||
contentType: string
|
||||
): Promise<void> {
|
||||
const redis = getRedisCache();
|
||||
await Promise.all([
|
||||
@@ -68,7 +71,7 @@ async function setToCacheBinary(
|
||||
|
||||
// Fetch image with timeout and size limits
|
||||
async function fetchImage(
|
||||
url: URL,
|
||||
url: URL
|
||||
): Promise<{ buffer: Buffer; contentType: string; status: number }> {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 1000); // 10s timeout
|
||||
@@ -132,7 +135,7 @@ function isSvgFile(url: string, contentType?: string): boolean {
|
||||
async function processImage(
|
||||
buffer: Buffer,
|
||||
originalUrl?: string,
|
||||
contentType?: string,
|
||||
contentType?: string
|
||||
): Promise<Buffer> {
|
||||
// If it's an ICO file, just return it as-is (no conversion needed)
|
||||
if (originalUrl && isIcoFile(originalUrl, contentType)) {
|
||||
@@ -183,10 +186,10 @@ async function processImage(
|
||||
async function processOgImage(
|
||||
buffer: Buffer,
|
||||
originalUrl?: string,
|
||||
contentType?: string,
|
||||
contentType?: string
|
||||
): Promise<Buffer> {
|
||||
// If buffer is small enough, return it as-is
|
||||
if (buffer.length < 10000) {
|
||||
if (buffer.length < 10_000) {
|
||||
logger.debug('Serving OG image directly without processing', {
|
||||
originalUrl,
|
||||
bufferSize: buffer.length,
|
||||
@@ -227,7 +230,7 @@ export async function getFavicon(
|
||||
request: FastifyRequest<{
|
||||
Querystring: GetFaviconParams;
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
logger.info('getFavicon', {
|
||||
@@ -295,7 +298,7 @@ export async function getFavicon(
|
||||
if (buffer.length === 0 && !imageUrl.hostname.includes('duckduckgo.com')) {
|
||||
const { hostname } = url;
|
||||
const duckduckgoUrl = new URL(
|
||||
`https://icons.duckduckgo.com/ip3/${hostname}.ico`,
|
||||
`https://icons.duckduckgo.com/ip3/${hostname}.ico`
|
||||
);
|
||||
|
||||
logger.info('Trying DuckDuckGo favicon service', {
|
||||
@@ -328,7 +331,7 @@ export async function getFavicon(
|
||||
const processedBuffer = await processImage(
|
||||
buffer,
|
||||
imageUrl.toString(),
|
||||
contentType,
|
||||
contentType
|
||||
);
|
||||
|
||||
logger.info('Favicon processing result', {
|
||||
@@ -380,7 +383,7 @@ export async function getFavicon(
|
||||
|
||||
export async function clearFavicons(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
const redis = getRedisCache();
|
||||
const keys = await redis.keys('favicon:*');
|
||||
@@ -396,7 +399,7 @@ export async function clearFavicons(
|
||||
|
||||
export async function clearOgImages(
|
||||
request: FastifyRequest,
|
||||
reply: FastifyReply,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
const redis = getRedisCache();
|
||||
const keys = await redis.keys('og:*');
|
||||
@@ -417,7 +420,7 @@ export async function ping(
|
||||
count: number;
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
await ch.insert({
|
||||
@@ -449,10 +452,10 @@ export async function ping(
|
||||
export async function stats(request: FastifyRequest, reply: FastifyReply) {
|
||||
const res = await getCache('api:stats', 60 * 60, async () => {
|
||||
const projects = await chQuery<{ project_id: string; count: number }>(
|
||||
`SELECT project_id, count(*) as count from ${TABLE_NAMES.events} GROUP by project_id order by count()`,
|
||||
`SELECT project_id, count(*) as count from ${TABLE_NAMES.events} GROUP by project_id order by count()`
|
||||
);
|
||||
const last24h = await chQuery<{ count: number }>(
|
||||
`SELECT count(*) as count from ${TABLE_NAMES.events} WHERE created_at > now() - interval '24 hours'`,
|
||||
`SELECT count(*) as count from ${TABLE_NAMES.events} WHERE created_at > now() - interval '24 hours'`
|
||||
);
|
||||
return { projects, last24hCount: last24h[0]?.count || 0 };
|
||||
});
|
||||
@@ -474,7 +477,7 @@ export async function getGeo(request: FastifyRequest, reply: FastifyReply) {
|
||||
ip,
|
||||
geo: await getGeoLocation(ip),
|
||||
};
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
if (!ip) {
|
||||
@@ -492,7 +495,7 @@ export async function getGeo(request: FastifyRequest, reply: FastifyReply) {
|
||||
acc[other.header] = other;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, { ip: string; header: string; geo: GeoLocation }>,
|
||||
{} as Record<string, { ip: string; header: string; geo: GeoLocation }>
|
||||
),
|
||||
});
|
||||
}
|
||||
@@ -503,7 +506,7 @@ export async function getOgImage(
|
||||
url: string;
|
||||
};
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const url = validateUrl(request.query.url);
|
||||
@@ -547,7 +550,7 @@ export async function getOgImage(
|
||||
const processedBuffer = await processOgImage(
|
||||
buffer,
|
||||
imageUrl.toString(),
|
||||
contentType,
|
||||
contentType
|
||||
);
|
||||
|
||||
// Cache the result
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { assocPath, pathOr } from 'ramda';
|
||||
|
||||
import { parseUserAgent } from '@openpanel/common/server';
|
||||
import { getProfileById, upsertProfile } from '@openpanel/db';
|
||||
import { getGeoLocation } from '@openpanel/geo';
|
||||
@@ -8,12 +5,14 @@ import type {
|
||||
DeprecatedIncrementProfilePayload,
|
||||
DeprecatedUpdateProfilePayload,
|
||||
} from '@openpanel/validation';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { assocPath, pathOr } from 'ramda';
|
||||
|
||||
export async function updateProfile(
|
||||
request: FastifyRequest<{
|
||||
Body: DeprecatedUpdateProfilePayload;
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
const payload = request.body;
|
||||
const projectId = request.client!.projectId;
|
||||
@@ -54,7 +53,7 @@ export async function incrementProfileProperty(
|
||||
request: FastifyRequest<{
|
||||
Body: DeprecatedIncrementProfilePayload;
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
const { profileId, property, value } = request.body;
|
||||
const projectId = request.client!.projectId;
|
||||
@@ -69,7 +68,7 @@ export async function incrementProfileProperty(
|
||||
|
||||
const parsed = Number.parseInt(
|
||||
pathOr<string>('0', property.split('.'), profile.properties),
|
||||
10,
|
||||
10
|
||||
);
|
||||
|
||||
if (Number.isNaN(parsed)) {
|
||||
@@ -79,7 +78,7 @@ export async function incrementProfileProperty(
|
||||
profile.properties = assocPath(
|
||||
property.split('.'),
|
||||
parsed + value,
|
||||
profile.properties,
|
||||
profile.properties
|
||||
);
|
||||
|
||||
await upsertProfile({
|
||||
@@ -96,7 +95,7 @@ export async function decrementProfileProperty(
|
||||
request: FastifyRequest<{
|
||||
Body: DeprecatedIncrementProfilePayload;
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
const { profileId, property, value } = request.body;
|
||||
const projectId = request.client?.projectId;
|
||||
@@ -111,7 +110,7 @@ export async function decrementProfileProperty(
|
||||
|
||||
const parsed = Number.parseInt(
|
||||
pathOr<string>('0', property.split('.'), profile.properties),
|
||||
10,
|
||||
10
|
||||
);
|
||||
|
||||
if (Number.isNaN(parsed)) {
|
||||
@@ -121,7 +120,7 @@ export async function decrementProfileProperty(
|
||||
profile.properties = assocPath(
|
||||
property.split('.'),
|
||||
parsed - value,
|
||||
profile.properties,
|
||||
profile.properties
|
||||
);
|
||||
|
||||
await upsertProfile({
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { dirname } from 'node:path';
|
||||
import path, { dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
import { db, getOrganizationByProjectIdCached } from '@openpanel/db';
|
||||
import {
|
||||
sendSlackNotification,
|
||||
slackInstaller,
|
||||
} from '@openpanel/integrations/src/slack';
|
||||
import {
|
||||
PolarWebhookVerificationError,
|
||||
getProduct,
|
||||
PolarWebhookVerificationError,
|
||||
validatePolarEvent,
|
||||
} from '@openpanel/payments';
|
||||
import { publishEvent } from '@openpanel/redis';
|
||||
@@ -34,7 +34,7 @@ export async function slackWebhook(
|
||||
request: FastifyRequest<{
|
||||
Querystring: unknown;
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
const parsedParams = paramsSchema.safeParse(request.query);
|
||||
|
||||
@@ -45,10 +45,10 @@ export async function slackWebhook(
|
||||
|
||||
const veryfiedState = await slackInstaller.stateStore?.verifyStateParam(
|
||||
new Date(),
|
||||
parsedParams.data.state,
|
||||
parsedParams.data.state
|
||||
);
|
||||
const parsedMetadata = metadataSchema.safeParse(
|
||||
JSON.parse(veryfiedState?.metadata ?? '{}'),
|
||||
JSON.parse(veryfiedState?.metadata ?? '{}')
|
||||
);
|
||||
|
||||
if (!parsedMetadata.success) {
|
||||
@@ -75,7 +75,7 @@ export async function slackWebhook(
|
||||
zod: parsedJson,
|
||||
json,
|
||||
},
|
||||
'Failed to parse slack auth response',
|
||||
'Failed to parse slack auth response'
|
||||
);
|
||||
const html = fs.readFileSync(path.join(__dirname, 'error.html'), 'utf8');
|
||||
return reply.status(500).header('Content-Type', 'text/html').send(html);
|
||||
@@ -104,7 +104,7 @@ export async function slackWebhook(
|
||||
});
|
||||
|
||||
return reply.redirect(
|
||||
`${process.env.DASHBOARD_URL || process.env.NEXT_PUBLIC_DASHBOARD_URL}/${organizationId}/integrations/installed`,
|
||||
`${process.env.DASHBOARD_URL || process.env.NEXT_PUBLIC_DASHBOARD_URL}/${organizationId}/integrations/installed`
|
||||
);
|
||||
} catch (err) {
|
||||
request.log.error(err);
|
||||
@@ -128,13 +128,13 @@ export async function polarWebhook(
|
||||
request: FastifyRequest<{
|
||||
Querystring: unknown;
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const event = validatePolarEvent(
|
||||
request.rawBody!,
|
||||
request.headers as Record<string, string>,
|
||||
process.env.POLAR_WEBHOOK_SECRET ?? '',
|
||||
process.env.POLAR_WEBHOOK_SECRET ?? ''
|
||||
);
|
||||
|
||||
switch (event.type) {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { SdkAuthError, validateSdkRequest } from '@/utils/auth';
|
||||
import type {
|
||||
DeprecatedPostEventPayload,
|
||||
ITrackHandlerPayload,
|
||||
} from '@openpanel/validation';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
||||
import { SdkAuthError, validateSdkRequest } from '@/utils/auth';
|
||||
|
||||
export async function clientHook(
|
||||
req: FastifyRequest<{
|
||||
Body: ITrackHandlerPayload | DeprecatedPostEventPayload;
|
||||
}>,
|
||||
reply: FastifyReply,
|
||||
reply: FastifyReply
|
||||
) {
|
||||
try {
|
||||
const client = await validateSdkRequest(req);
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import type {
|
||||
FastifyReply,
|
||||
FastifyRequest,
|
||||
HookHandlerDoneFunction,
|
||||
} from 'fastify';
|
||||
import type { FastifyRequest } from 'fastify';
|
||||
|
||||
export async function requestIdHook(request: FastifyRequest) {
|
||||
if (!request.headers['request-id']) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
||||
import * as controller from '@/controllers/ai.controller';
|
||||
import { activateRateLimiter } from '@/utils/rate-limiter';
|
||||
import type { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
||||
|
||||
const aiRouter: FastifyPluginCallback = async (fastify) => {
|
||||
await activateRateLimiter<
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as controller from '@/controllers/event.controller';
|
||||
import type { FastifyPluginCallback } from 'fastify';
|
||||
|
||||
import * as controller from '@/controllers/event.controller';
|
||||
import { clientHook } from '@/hooks/client.hook';
|
||||
import { duplicateHook } from '@/hooks/duplicate.hook';
|
||||
import { isBotHook } from '@/hooks/is-bot.hook';
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Prisma } from '@openpanel/db';
|
||||
import type { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
||||
import * as controller from '@/controllers/export.controller';
|
||||
import { validateExportRequest } from '@/utils/auth';
|
||||
import { activateRateLimiter } from '@/utils/rate-limiter';
|
||||
import { Prisma } from '@openpanel/db';
|
||||
import type { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
||||
|
||||
const exportRouter: FastifyPluginCallback = async (fastify) => {
|
||||
await activateRateLimiter({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { gscGoogleCallback } from '@/controllers/gsc-oauth-callback.controller';
|
||||
import type { FastifyPluginCallback } from 'fastify';
|
||||
import { gscGoogleCallback } from '@/controllers/gsc-oauth-callback.controller';
|
||||
|
||||
const router: FastifyPluginCallback = async (fastify) => {
|
||||
fastify.route({
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { Prisma } from '@openpanel/db';
|
||||
import type { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
||||
import * as controller from '@/controllers/import.controller';
|
||||
import { validateImportRequest } from '@/utils/auth';
|
||||
import type { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
||||
|
||||
import { Prisma } from '@openpanel/db';
|
||||
|
||||
const importRouter: FastifyPluginCallback = async (fastify) => {
|
||||
fastify.addHook('preHandler', async (req: FastifyRequest, reply) => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Prisma } from '@openpanel/db';
|
||||
import type { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
||||
import * as controller from '@/controllers/insights.controller';
|
||||
import { validateExportRequest } from '@/utils/auth';
|
||||
import { activateRateLimiter } from '@/utils/rate-limiter';
|
||||
import { Prisma } from '@openpanel/db';
|
||||
import type { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
||||
|
||||
const insightsRouter: FastifyPluginCallback = async (fastify) => {
|
||||
await activateRateLimiter({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as controller from '@/controllers/live.controller';
|
||||
import fastifyWS from '@fastify/websocket';
|
||||
import type { FastifyPluginCallback } from 'fastify';
|
||||
import * as controller from '@/controllers/live.controller';
|
||||
|
||||
const liveRouter: FastifyPluginCallback = async (fastify) => {
|
||||
fastify.register(fastifyWS);
|
||||
@@ -9,22 +9,22 @@ const liveRouter: FastifyPluginCallback = async (fastify) => {
|
||||
fastify.get(
|
||||
'/organization/:organizationId',
|
||||
{ websocket: true },
|
||||
controller.wsOrganizationEvents,
|
||||
controller.wsOrganizationEvents
|
||||
);
|
||||
fastify.get(
|
||||
'/visitors/:projectId',
|
||||
{ websocket: true },
|
||||
controller.wsVisitors,
|
||||
controller.wsVisitors
|
||||
);
|
||||
fastify.get(
|
||||
'/events/:projectId',
|
||||
{ websocket: true },
|
||||
controller.wsProjectEvents,
|
||||
controller.wsProjectEvents
|
||||
);
|
||||
fastify.get(
|
||||
'/notifications/:projectId',
|
||||
{ websocket: true },
|
||||
controller.wsProjectNotifications,
|
||||
controller.wsProjectNotifications
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Prisma } from '@openpanel/db';
|
||||
import type { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
||||
import * as controller from '@/controllers/manage.controller';
|
||||
import { validateManageRequest } from '@/utils/auth';
|
||||
import { activateRateLimiter } from '@/utils/rate-limiter';
|
||||
import { Prisma } from '@openpanel/db';
|
||||
import type { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
||||
|
||||
const manageRouter: FastifyPluginCallback = async (fastify) => {
|
||||
await activateRateLimiter({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as controller from '@/controllers/misc.controller';
|
||||
import type { FastifyPluginCallback } from 'fastify';
|
||||
import * as controller from '@/controllers/misc.controller';
|
||||
|
||||
const miscRouter: FastifyPluginCallback = async (fastify) => {
|
||||
fastify.route({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as controller from '@/controllers/oauth-callback.controller';
|
||||
import type { FastifyPluginCallback } from 'fastify';
|
||||
import * as controller from '@/controllers/oauth-callback.controller';
|
||||
|
||||
const router: FastifyPluginCallback = async (fastify) => {
|
||||
fastify.route({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { FastifyPluginCallback } from 'fastify';
|
||||
import * as controller from '@/controllers/profile.controller';
|
||||
import { clientHook } from '@/hooks/client.hook';
|
||||
import { isBotHook } from '@/hooks/is-bot.hook';
|
||||
import type { FastifyPluginCallback } from 'fastify';
|
||||
|
||||
const eventRouter: FastifyPluginCallback = async (fastify) => {
|
||||
fastify.addHook('preHandler', clientHook);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as controller from '@/controllers/webhook.controller';
|
||||
import type { FastifyPluginCallback } from 'fastify';
|
||||
import * as controller from '@/controllers/webhook.controller';
|
||||
|
||||
const webhookRouter: FastifyPluginCallback = async (fastify) => {
|
||||
fastify.route({
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import { chartTypes } from '@openpanel/constants';
|
||||
import type { IClickhouseSession } from '@openpanel/db';
|
||||
import {
|
||||
ch,
|
||||
clix,
|
||||
type IClickhouseEvent,
|
||||
type IClickhouseProfile,
|
||||
TABLE_NAMES,
|
||||
ch,
|
||||
clix,
|
||||
} from '@openpanel/db';
|
||||
import { ChartEngine } from '@openpanel/db';
|
||||
import { getCache } from '@openpanel/redis';
|
||||
import { zReportInput } from '@openpanel/validation';
|
||||
import { tool } from 'ai';
|
||||
import { z } from 'zod';
|
||||
|
||||
export function getReport({
|
||||
projectId,
|
||||
}: {
|
||||
projectId: string;
|
||||
}) {
|
||||
export function getReport({ projectId }: { projectId: string }) {
|
||||
return tool({
|
||||
description: `Generate a report (a chart) for
|
||||
- ${chartTypes.area}
|
||||
@@ -67,11 +62,7 @@ export function getReport({
|
||||
},
|
||||
});
|
||||
}
|
||||
export function getConversionReport({
|
||||
projectId,
|
||||
}: {
|
||||
projectId: string;
|
||||
}) {
|
||||
export function getConversionReport({ projectId }: { projectId: string }) {
|
||||
return tool({
|
||||
description:
|
||||
'Generate a report (a chart) for conversions between two actions a unique user took.',
|
||||
@@ -92,11 +83,7 @@ export function getConversionReport({
|
||||
},
|
||||
});
|
||||
}
|
||||
export function getFunnelReport({
|
||||
projectId,
|
||||
}: {
|
||||
projectId: string;
|
||||
}) {
|
||||
export function getFunnelReport({ projectId }: { projectId: string }) {
|
||||
return tool({
|
||||
description:
|
||||
'Generate a report (a chart) for funnel between two or more actions a unique user (session_id or profile_id) took.',
|
||||
@@ -118,11 +105,7 @@ export function getFunnelReport({
|
||||
});
|
||||
}
|
||||
|
||||
export function getProfiles({
|
||||
projectId,
|
||||
}: {
|
||||
projectId: string;
|
||||
}) {
|
||||
export function getProfiles({ projectId }: { projectId: string }) {
|
||||
return tool({
|
||||
description: 'Get profiles',
|
||||
parameters: z.object({
|
||||
@@ -188,11 +171,7 @@ export function getProfiles({
|
||||
});
|
||||
}
|
||||
|
||||
export function getProfile({
|
||||
projectId,
|
||||
}: {
|
||||
projectId: string;
|
||||
}) {
|
||||
export function getProfile({ projectId }: { projectId: string }) {
|
||||
return tool({
|
||||
description: 'Get a specific profile',
|
||||
parameters: z.object({
|
||||
@@ -276,11 +255,7 @@ export function getProfile({
|
||||
});
|
||||
}
|
||||
|
||||
export function getEvents({
|
||||
projectId,
|
||||
}: {
|
||||
projectId: string;
|
||||
}) {
|
||||
export function getEvents({ projectId }: { projectId: string }) {
|
||||
return tool({
|
||||
description: 'Get events for a project or specific profile',
|
||||
parameters: z.object({
|
||||
@@ -369,11 +344,7 @@ export function getEvents({
|
||||
});
|
||||
}
|
||||
|
||||
export function getSessions({
|
||||
projectId,
|
||||
}: {
|
||||
projectId: string;
|
||||
}) {
|
||||
export function getSessions({ projectId }: { projectId: string }) {
|
||||
return tool({
|
||||
description: 'Get sessions for a project or specific profile',
|
||||
parameters: z.object({
|
||||
@@ -458,11 +429,7 @@ export function getSessions({
|
||||
});
|
||||
}
|
||||
|
||||
export function getAllEventNames({
|
||||
projectId,
|
||||
}: {
|
||||
projectId: string;
|
||||
}) {
|
||||
export function getAllEventNames({ projectId }: { projectId: string }) {
|
||||
return tool({
|
||||
description: 'Get the top 50 event names in a comma separated list',
|
||||
parameters: z.object({}),
|
||||
|
||||
@@ -14,11 +14,7 @@ export const getChatModel = () => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getChatSystemPrompt = ({
|
||||
projectId,
|
||||
}: {
|
||||
projectId: string;
|
||||
}) => {
|
||||
export const getChatSystemPrompt = ({ projectId }: { projectId: string }) => {
|
||||
return `You're an product and web analytics expert. Don't generate more than the user asks for. Follow all rules listed below!
|
||||
## General:
|
||||
- projectId: \`${projectId}\`
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import type { FastifyRequest, RawRequestDefaultExpression } from 'fastify';
|
||||
|
||||
import { verifyPassword } from '@openpanel/common/server';
|
||||
import type { IServiceClientWithProject } from '@openpanel/db';
|
||||
import { ClientType, getClientByIdCached } from '@openpanel/db';
|
||||
@@ -10,6 +8,7 @@ import type {
|
||||
IProjectFilterProfileId,
|
||||
ITrackHandlerPayload,
|
||||
} from '@openpanel/validation';
|
||||
import type { FastifyRequest, RawRequestDefaultExpression } from 'fastify';
|
||||
import { path } from 'ramda';
|
||||
|
||||
const cleanDomain = (domain: string) =>
|
||||
@@ -31,7 +30,7 @@ export class SdkAuthError extends Error {
|
||||
clientId?: string;
|
||||
clientSecret?: string;
|
||||
origin?: string;
|
||||
},
|
||||
}
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'SdkAuthError';
|
||||
@@ -43,7 +42,7 @@ export class SdkAuthError extends Error {
|
||||
export async function validateSdkRequest(
|
||||
req: FastifyRequest<{
|
||||
Body: ITrackHandlerPayload | DeprecatedPostEventPayload;
|
||||
}>,
|
||||
}>
|
||||
): Promise<IServiceClientWithProject> {
|
||||
const { headers, clientIp } = req;
|
||||
const clientIdNew = headers['openpanel-client-id'] as string;
|
||||
@@ -70,7 +69,7 @@ export async function validateSdkRequest(
|
||||
|
||||
if (
|
||||
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(
|
||||
clientId,
|
||||
clientId
|
||||
)
|
||||
) {
|
||||
throw createError('Ingestion: Client ID must be a valid UUIDv4');
|
||||
@@ -88,7 +87,7 @@ export async function validateSdkRequest(
|
||||
|
||||
// Filter out blocked IPs
|
||||
const ipFilter = client.project.filters.filter(
|
||||
(filter): filter is IProjectFilterIp => filter.type === 'ip',
|
||||
(filter): filter is IProjectFilterIp => filter.type === 'ip'
|
||||
);
|
||||
if (ipFilter.some((filter) => filter.ip === clientIp)) {
|
||||
throw createError('Ingestion: IP address is blocked by project filter');
|
||||
@@ -96,7 +95,7 @@ export async function validateSdkRequest(
|
||||
|
||||
// Filter out blocked profile ids
|
||||
const profileFilter = client.project.filters.filter(
|
||||
(filter): filter is IProjectFilterProfileId => filter.type === 'profile_id',
|
||||
(filter): filter is IProjectFilterProfileId => filter.type === 'profile_id'
|
||||
);
|
||||
const profileId =
|
||||
path<string | undefined>(['payload', 'profileId'], req.body) || // Track handler
|
||||
@@ -113,12 +112,11 @@ export async function validateSdkRequest(
|
||||
// Only allow revenue tracking if it was sent with a client secret
|
||||
// or if the project has allowUnsafeRevenueTracking enabled
|
||||
if (
|
||||
!client.project.allowUnsafeRevenueTracking &&
|
||||
!clientSecret &&
|
||||
!(client.project.allowUnsafeRevenueTracking || clientSecret) &&
|
||||
typeof revenue !== 'undefined'
|
||||
) {
|
||||
throw createError(
|
||||
'Ingestion: Revenue tracking is not allowed without a client secret',
|
||||
'Ingestion: Revenue tracking is not allowed without a client secret'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -132,7 +130,7 @@ export async function validateSdkRequest(
|
||||
// support wildcard domains `*.foo.com`
|
||||
if (cleanedDomain.includes('*')) {
|
||||
const regex = new RegExp(
|
||||
`${cleanedDomain.replaceAll('.', '\\.').replaceAll('*', '.+?')}`,
|
||||
`${cleanedDomain.replaceAll('.', '\\.').replaceAll('*', '.+?')}`
|
||||
);
|
||||
|
||||
return regex.test(origin || '');
|
||||
@@ -157,7 +155,7 @@ export async function validateSdkRequest(
|
||||
`client:auth:${clientId}:${Buffer.from(clientSecret).toString('base64')}`,
|
||||
60 * 5,
|
||||
async () => await verifyPassword(clientSecret, client.secret!),
|
||||
true,
|
||||
true
|
||||
);
|
||||
if (isVerified) {
|
||||
return client;
|
||||
@@ -168,14 +166,14 @@ export async function validateSdkRequest(
|
||||
}
|
||||
|
||||
export async function validateExportRequest(
|
||||
headers: RawRequestDefaultExpression['headers'],
|
||||
headers: RawRequestDefaultExpression['headers']
|
||||
): Promise<IServiceClientWithProject> {
|
||||
const clientId = headers['openpanel-client-id'] as string;
|
||||
const clientSecret = (headers['openpanel-client-secret'] as string) || '';
|
||||
|
||||
if (
|
||||
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(
|
||||
clientId,
|
||||
clientId
|
||||
)
|
||||
) {
|
||||
throw new Error('Export: Client ID must be a valid UUIDv4');
|
||||
@@ -203,14 +201,14 @@ export async function validateExportRequest(
|
||||
}
|
||||
|
||||
export async function validateImportRequest(
|
||||
headers: RawRequestDefaultExpression['headers'],
|
||||
headers: RawRequestDefaultExpression['headers']
|
||||
): Promise<IServiceClientWithProject> {
|
||||
const clientId = headers['openpanel-client-id'] as string;
|
||||
const clientSecret = (headers['openpanel-client-secret'] as string) || '';
|
||||
|
||||
if (
|
||||
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(
|
||||
clientId,
|
||||
clientId
|
||||
)
|
||||
) {
|
||||
throw new Error('Import: Client ID must be a valid UUIDv4');
|
||||
@@ -238,14 +236,14 @@ export async function validateImportRequest(
|
||||
}
|
||||
|
||||
export async function validateManageRequest(
|
||||
headers: RawRequestDefaultExpression['headers'],
|
||||
headers: RawRequestDefaultExpression['headers']
|
||||
): Promise<IServiceClientWithProject> {
|
||||
const clientId = headers['openpanel-client-id'] as string;
|
||||
const clientSecret = (headers['openpanel-client-secret'] as string) || '';
|
||||
|
||||
if (
|
||||
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(
|
||||
clientId,
|
||||
clientId
|
||||
)
|
||||
) {
|
||||
throw new Error('Manage: Client ID must be a valid UUIDv4');
|
||||
@@ -263,7 +261,7 @@ export async function validateManageRequest(
|
||||
|
||||
if (client.type !== ClientType.root) {
|
||||
throw new Error(
|
||||
'Manage: Only root clients are allowed to manage resources',
|
||||
'Manage: Only root clients are allowed to manage resources'
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@ export async function isDuplicatedEvent({
|
||||
origin,
|
||||
projectId,
|
||||
},
|
||||
'md5',
|
||||
'md5'
|
||||
)}`,
|
||||
'1',
|
||||
100,
|
||||
100
|
||||
);
|
||||
|
||||
if (locked) {
|
||||
|
||||
@@ -4,7 +4,7 @@ export class LogError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
payload?: Record<string, unknown>,
|
||||
options?: ErrorOptions,
|
||||
options?: ErrorOptions
|
||||
) {
|
||||
super(message, options);
|
||||
this.name = 'LogError';
|
||||
@@ -26,7 +26,7 @@ export class HttpError extends Error {
|
||||
fingerprint?: string;
|
||||
extra?: Record<string, unknown>;
|
||||
error?: Error | unknown;
|
||||
},
|
||||
}
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'HttpError';
|
||||
|
||||
@@ -29,7 +29,7 @@ export function isShuttingDown() {
|
||||
export async function shutdown(
|
||||
fastify: FastifyInstance,
|
||||
signal: string,
|
||||
exitCode = 0,
|
||||
exitCode = 0
|
||||
) {
|
||||
if (isShuttingDown()) {
|
||||
logger.warn('Shutdown already in progress, ignoring signal', { signal });
|
||||
@@ -96,7 +96,7 @@ export async function shutdown(
|
||||
if (redis.status === 'ready') {
|
||||
await redis.quit();
|
||||
}
|
||||
}),
|
||||
})
|
||||
);
|
||||
logger.info('Redis connections closed');
|
||||
} catch (error) {
|
||||
|
||||
@@ -3,14 +3,21 @@ import { getSafeJson } from '@openpanel/json';
|
||||
export const parseQueryString = (obj: Record<string, any>): any => {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj).map(([k, v]) => {
|
||||
if (typeof v === 'object') return [k, parseQueryString(v)];
|
||||
if (typeof v === 'object') {
|
||||
return [k, parseQueryString(v)];
|
||||
}
|
||||
if (
|
||||
/^-?[0-9]+(\.[0-9]+)?$/i.test(v) &&
|
||||
!Number.isNaN(Number.parseFloat(v))
|
||||
)
|
||||
) {
|
||||
return [k, Number.parseFloat(v)];
|
||||
if (v === 'true') return [k, true];
|
||||
if (v === 'false') return [k, false];
|
||||
}
|
||||
if (v === 'true') {
|
||||
return [k, true];
|
||||
}
|
||||
if (v === 'false') {
|
||||
return [k, false];
|
||||
}
|
||||
if (typeof v === 'string') {
|
||||
if (getSafeJson(v) !== null) {
|
||||
return [k, getSafeJson(v)];
|
||||
@@ -18,6 +25,6 @@ export const parseQueryString = (obj: Record<string, any>): any => {
|
||||
return [k, v];
|
||||
}
|
||||
return [k, null];
|
||||
}),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ function findBestFavicon(favicons: UrlMetaData['favicons']) {
|
||||
(favicon) =>
|
||||
favicon.rel === 'shortcut icon' ||
|
||||
favicon.rel === 'icon' ||
|
||||
favicon.rel === 'apple-touch-icon',
|
||||
favicon.rel === 'apple-touch-icon'
|
||||
);
|
||||
|
||||
if (match) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { defineConfig } from 'tsdown';
|
||||
import type { Options } from 'tsdown';
|
||||
import { defineConfig } from 'tsdown';
|
||||
|
||||
const options: Options = {
|
||||
clean: true,
|
||||
|
||||
Reference in New Issue
Block a user