mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
tests/integration/query-counter.ts counts the statements the app sends while a function runs, by wrapping Client.prototype.query on the copy of pg the Prisma adapter itself resolves (so a second pg in node_modules cannot leave it reading zero; a window with no statement throws). Background tasks are settled before the window closes, so fire-and-forget writes count with the request that started them. query-budget-hot-routes.test.ts seeds a small account (four months of the main types with overlapping sources, stats rows, sleep, glucose, a medication, workouts and mood), warms each route once, evicts the user's caches and counts the next call. Budgets, measured on this fixture: /api/auth/me 18, dashboard summary 86, dashboard snapshot 103, pulse series 30/365/3650 days 6 each, day view 48, insights health status 60, correlations 44, a 500-row batch ingest 31. A failure prints every statement grouped by text. Proof: one extra `SELECT 1` before every handler in apiHandler failed all ten budgets by exactly one (18 → 19, 86 → 87, … 31 → 32), which also shows the counts are deterministic; with the probe removed all ten pass.
89 lines
3.0 KiB
TypeScript
89 lines
3.0 KiB
TypeScript
/**
|
||
* Counts the SQL statements the application sends to Postgres while a
|
||
* function runs.
|
||
*
|
||
* Every statement the Prisma client sends goes through `pg`'s
|
||
* `Client.prototype.query` (the driver adapter's pool hands out `pg`
|
||
* clients, raw queries included, transactions' BEGIN/COMMIT included). The
|
||
* prototype is patched once, on the `pg` instance the adapter itself
|
||
* resolves, so a second copy of `pg` elsewhere in node_modules cannot leave
|
||
* the counter reading zero; `countStatements` refuses a window that saw no
|
||
* statement at all for the same reason.
|
||
*
|
||
* Background work a request starts and does not await (the fire-and-forget
|
||
* tasks) is settled before the window closes, so it is counted with the
|
||
* request that caused it.
|
||
*/
|
||
import { createRequire } from "node:module";
|
||
|
||
interface PgClientPrototype {
|
||
query: (...args: unknown[]) => unknown;
|
||
}
|
||
|
||
const requireHere = createRequire(import.meta.url);
|
||
const adapterEntry = requireHere.resolve("@prisma/adapter-pg");
|
||
const pg = createRequire(adapterEntry)("pg") as {
|
||
Client: { prototype: PgClientPrototype };
|
||
};
|
||
|
||
const state = { active: false, statements: [] as string[] };
|
||
const PATCHED = Symbol.for("healthlog.queryCounter");
|
||
|
||
const proto = pg.Client.prototype as PgClientPrototype & {
|
||
[PATCHED]?: boolean;
|
||
};
|
||
if (!proto[PATCHED]) {
|
||
const original = proto.query;
|
||
proto.query = function patchedQuery(this: unknown, ...args: unknown[]) {
|
||
if (state.active) {
|
||
const first = args[0] as string | { text?: string } | undefined;
|
||
const text = typeof first === "string" ? first : (first?.text ?? "?");
|
||
state.statements.push(text.replace(/\s+/g, " ").trim().slice(0, 160));
|
||
}
|
||
return original.apply(this, args);
|
||
};
|
||
proto[PATCHED] = true;
|
||
}
|
||
|
||
export interface StatementCount<T> {
|
||
result: T;
|
||
count: number;
|
||
statements: string[];
|
||
}
|
||
|
||
export async function countStatements<T>(
|
||
fn: () => Promise<T>,
|
||
): Promise<StatementCount<T>> {
|
||
const { settleBackgroundTasks } =
|
||
await import("@/lib/logging/background-tasks");
|
||
await settleBackgroundTasks();
|
||
state.statements = [];
|
||
state.active = true;
|
||
let result: T;
|
||
try {
|
||
result = await fn();
|
||
await settleBackgroundTasks();
|
||
} finally {
|
||
state.active = false;
|
||
}
|
||
const statements = state.statements;
|
||
if (statements.length === 0) {
|
||
throw new Error(
|
||
"countStatements saw no statement at all. Either the call reads " +
|
||
"nothing from Postgres, or the counter is patched on a different " +
|
||
"copy of pg than the one the Prisma adapter uses.",
|
||
);
|
||
}
|
||
return { result, count: statements.length, statements };
|
||
}
|
||
|
||
/** The statements grouped by text, most frequent first, for a failure message. */
|
||
export function summariseStatements(statements: readonly string[]): string {
|
||
const counts = new Map<string, number>();
|
||
for (const s of statements) counts.set(s, (counts.get(s) ?? 0) + 1);
|
||
return [...counts.entries()]
|
||
.sort((a, b) => b[1] - a[1])
|
||
.map(([s, n]) => `${String(n).padStart(4)} × ${s}`)
|
||
.join("\n");
|
||
}
|