chore:little fixes and formating and linting and patches

This commit is contained in:
2026-03-31 15:50:54 +02:00
parent a1ce71ffb6
commit 9b197abcfa
815 changed files with 22960 additions and 8982 deletions

View File

@@ -65,4 +65,4 @@
"tsdown": "0.14.2",
"typescript": "catalog:"
}
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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]}`
);
}
}

View File

@@ -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 });
},
});

View File

@@ -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>

View File

@@ -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(),
});

View File

@@ -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) {

View File

@@ -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,
}),
})
);
};
}

View File

@@ -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

View File

@@ -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({

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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']) {

View File

@@ -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<

View File

@@ -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';

View File

@@ -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({

View File

@@ -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({

View File

@@ -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) => {

View File

@@ -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({

View File

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

View File

@@ -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({

View File

@@ -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({

View File

@@ -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({

View File

@@ -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);

View File

@@ -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({

View File

@@ -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({}),

View File

@@ -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}\`

View File

@@ -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'
);
}

View File

@@ -20,10 +20,10 @@ export async function isDuplicatedEvent({
origin,
projectId,
},
'md5',
'md5'
)}`,
'1',
100,
100
);
if (locked) {

View File

@@ -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';

View File

@@ -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) {

View File

@@ -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];
}),
})
);
};

View File

@@ -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) {

View File

@@ -1,5 +1,5 @@
import { defineConfig } from 'tsdown';
import type { Options } from 'tsdown';
import { defineConfig } from 'tsdown';
const options: Options = {
clean: true,