mirror of
https://github.com/civitai/civitai.git
synced 2026-09-20 22:08:18 +08:00
7e15bca183
Upgrades @clickhouse/client from 0.2.10 to 1.23.1 in the root package.json and packages/civitai-clickhouse. apps/event-engine was already on 1.x, so the workspace now holds one version of the driver. A version upgrade, not a fix for the ClickHouse socket hang-ups. The pre-upgrade rate was recorded before this change so the post-deploy rate can be compared. - ResultSet.json<T>() returns T[] in 1.x: 13 call sites, 3 in src/ and 10 in apps/moderator, which the root typecheck does not cover. - host -> url; keep_alive.socket_ttl + retry_on_expired_socket -> idle_socket_ttl. - Three 1.x default changes held at their 0.2.x values: max_open_connections Infinity, request_timeout 300000, response compression on. - keep_alive.eagerly_destroy_stale_sockets: true is a deliberate non-default, more permissive than 0.2.x's retry, standing in for it. It confounds the before/after comparison. - 1.x adds ~1ms per request (await sleep(0)); not configurable. - apps/moderator ships on its own release. - New src/server/clickhouse/__tests__/client-config-pins.test.ts pins the values. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
81 lines
2.3 KiB
TypeScript
81 lines
2.3 KiB
TypeScript
import { getClickhouse } from './clickhouse';
|
|
import { usersByIds } from './users.service';
|
|
|
|
const TABLE = 'moderator_page_views';
|
|
|
|
export type PageVisit = {
|
|
userId: number;
|
|
/** Route id (the pattern, not the resolved pathname) so dynamic pages roll up to one row. */
|
|
location: string;
|
|
};
|
|
|
|
// Call ONLY after the auth guard has authorized the moderator, or unauthorized requests get recorded.
|
|
export async function recordPageVisit({ userId, location }: PageVisit): Promise<void> {
|
|
try {
|
|
await getClickhouse().insert({
|
|
table: TABLE,
|
|
values: [{ userId, location }],
|
|
format: 'JSONEachRow',
|
|
});
|
|
} catch (err) {
|
|
console.error('[page-visits] failed to record visit', err);
|
|
}
|
|
}
|
|
|
|
export type PageVisitSummaryRow = {
|
|
location: string;
|
|
visits: number;
|
|
distinctMods: number;
|
|
lastVisit: string;
|
|
};
|
|
|
|
export async function getPageVisitSummary(days = 30): Promise<PageVisitSummaryRow[]> {
|
|
return getClickhouse().$query<PageVisitSummaryRow>`
|
|
SELECT location,
|
|
count() AS visits,
|
|
uniqExact(userId) AS distinctMods,
|
|
max(visitedAt) AS lastVisit
|
|
FROM ${TABLE}
|
|
WHERE visitedAt >= now() - INTERVAL ${days} DAY
|
|
GROUP BY location
|
|
ORDER BY visits ASC
|
|
`;
|
|
}
|
|
|
|
export type RouteUserBreakdownRow = {
|
|
userId: number;
|
|
username: string | null;
|
|
visits: number;
|
|
lastVisit: string;
|
|
};
|
|
|
|
// `location` is user-supplied — pass it as a bound ClickHouse parameter, never interpolated.
|
|
export async function getRouteUserBreakdown(
|
|
location: string,
|
|
days = 30
|
|
): Promise<RouteUserBreakdownRow[]> {
|
|
const resultSet = await getClickhouse().query({
|
|
query: `
|
|
SELECT userId, count() AS visits, max(visitedAt) AS lastVisit
|
|
FROM ${TABLE}
|
|
WHERE location = {location:String}
|
|
AND visitedAt >= subtractDays(now(), {days:UInt32})
|
|
GROUP BY userId
|
|
ORDER BY visits DESC
|
|
`,
|
|
query_params: { location, days },
|
|
format: 'JSONEachRow',
|
|
});
|
|
const rows = await resultSet.json<{ userId: number; visits: number; lastVisit: string }>();
|
|
if (!rows.length) return [];
|
|
|
|
const nameById = await usersByIds(rows.map((r) => r.userId));
|
|
|
|
return rows.map((r) => ({
|
|
userId: r.userId,
|
|
username: nameById.get(r.userId)?.username ?? null,
|
|
visits: r.visits,
|
|
lastVisit: r.lastVisit,
|
|
}));
|
|
}
|