Files
Andres Resiri 70daeb214a
CI / Quality gate (push) Has been cancelled
Initial project commit for Coolify staging
2026-08-03 10:45:41 -04:00

350 lines
12 KiB
TypeScript

/**
* Phase 1 — Metric Ground-Truth Audit
* ───────────────────────────────────────
*
* Run against a live DB to capture the *actual* counts per metric
* table for a single brand. Output is written to stdout AND to
* scripts/audit-results.md so the ground truth lives alongside the
* code-side audit.
*
* Usage:
* DATABASE_URL=postgres://... npx tsx scripts/audit-phase1.ts
* # or pick a different brand
* BRAND_NAME="Modern Heart" npx tsx scripts/audit-phase1.ts
*
* This script is READ-ONLY — it only runs aggregate() / groupBy()
* / count() queries. Safe to run in production.
*
* Scoping note: some metric tables are keyed by brandId directly
* (SiteConversion, SiteEvent, RealUserMetric, AiInteraction,
* AiUsageLog, PlatformEvent, MetricSnapshot, GscPage, TechnicalAudit)
* while TrackedEvent and TrackedSession are scoped via TrackedSite.
* The script resolves the brand's TrackedSite once and uses
* trackedSiteId for those two tables.
*/
import { PrismaClient } from "@prisma/client";
import { writeFileSync, mkdirSync } from "fs";
import { dirname } from "path";
const prisma = new PrismaClient();
// Accumulator for the markdown report. Every log() call appends to
// both stdout and this buffer; the buffer is flushed to
// scripts/audit-results.md at the end of the run.
const report: string[] = [];
function log(line: string) {
log(line);
report.push(line);
}
// JSON.stringify replacer that handles BigInt + Date cleanly.
function replacer(_key: string, value: unknown): unknown {
if (typeof value === "bigint") return value.toString();
if (value instanceof Date) return value.toISOString();
return value;
}
function j(v: unknown): string {
return JSON.stringify(v, replacer);
}
async function main() {
const brandNeedle = process.env.BRAND_NAME ?? "Modern Heart";
const brand = await prisma.brand.findFirst({
where: { name: { contains: brandNeedle, mode: "insensitive" } },
select: { id: true, name: true, domain: true, createdAt: true },
});
if (!brand) {
console.error(`No brand matching "${brandNeedle}" found.`);
process.exit(1);
}
const site = await prisma.trackedSite.findUnique({
where: { brandId: brand.id },
select: { id: true, status: true, firstEventAt: true, lastEventAt: true, createdAt: true },
});
log("=== Brand ===");
log(j({ brand }));
log("=== TrackedSite ===");
log(j({ site }));
// ─── Direct-brandId tables ──────────────────────────────────
//
// Each aggregate runs independently so a single failing query
// doesn't blackhole the rest of the audit. Empty / errored rows
// render as ERROR in the output.
const brandTables: Array<{ name: string; run: () => Promise<unknown> }> = [
{
name: "SiteConversion",
run: () => prisma.siteConversion.aggregate({
where: { brandId: brand.id },
_count: true,
_min: { timestamp: true },
_max: { timestamp: true },
}),
},
{
name: "SiteEvent",
run: () => prisma.siteEvent.aggregate({
where: { brandId: brand.id },
_count: true,
_min: { timestamp: true },
_max: { timestamp: true },
}),
},
{
name: "RealUserMetric",
run: () => prisma.realUserMetric.aggregate({
where: { brandId: brand.id },
_count: true,
_min: { timestamp: true },
_max: { timestamp: true },
}),
},
{
name: "AiInteraction",
run: () => prisma.aiInteraction.aggregate({
where: { brandId: brand.id },
_count: true,
_min: { timestamp: true },
_max: { timestamp: true },
}),
},
{
name: "AiUsageLog",
run: () => prisma.aiUsageLog.aggregate({
where: { brandId: brand.id },
_count: true,
_min: { timestamp: true },
_max: { timestamp: true },
}),
},
{
name: "PlatformEvent",
run: () => prisma.platformEvent.aggregate({
where: { brandId: brand.id },
_count: true,
_min: { timestamp: true },
_max: { timestamp: true },
}),
},
{
name: "MetricSnapshot",
run: () => prisma.metricSnapshot.aggregate({
where: { brandId: brand.id },
_count: true,
_min: { date: true },
_max: { date: true },
}),
},
{
name: "GscPage",
run: () => prisma.gscPage.aggregate({
where: { brandId: brand.id },
_count: true,
_min: { date: true },
_max: { date: true },
}),
},
{
name: "TechnicalAudit",
run: () => prisma.technicalAudit.aggregate({
where: { brandId: brand.id },
_count: true,
_min: { createdAt: true },
_max: { createdAt: true },
}),
},
];
log("\n=== Brand-scoped tables ===");
for (const t of brandTables) {
try {
const result = await t.run();
log(`${t.name}: ${j(result)}`);
} catch (err) {
log(`${t.name}: ERROR ${err instanceof Error ? err.message : String(err)}`);
}
}
// ─── TrackedSite-scoped tables ──────────────────────────────
log("\n=== TrackedSite-scoped tables ===");
if (!site) {
log("TrackedEvent / TrackedSession: SKIPPED (no TrackedSite for brand)");
} else {
try {
const te = await prisma.trackedEvent.aggregate({
where: { trackedSiteId: site.id },
_count: true,
_min: { timestamp: true },
_max: { timestamp: true },
});
log(`TrackedEvent: ${j(te)}`);
} catch (err) {
log(`TrackedEvent: ERROR ${err instanceof Error ? err.message : String(err)}`);
}
try {
const ts = await prisma.trackedSession.aggregate({
where: { trackedSiteId: site.id },
_count: true,
_min: { startedAt: true },
_max: { startedAt: true },
});
log(`TrackedSession: ${j(ts)}`);
} catch (err) {
log(`TrackedSession: ERROR ${err instanceof Error ? err.message : String(err)}`);
}
}
// ─── Type breakdowns (the numbers the UI actually renders) ──
log("\n=== Distinct event-type counts ===");
if (site) {
try {
const trackedTypes = await prisma.trackedEvent.groupBy({
by: ["eventType"],
where: { trackedSiteId: site.id },
_count: true,
});
trackedTypes.sort((a, b) => (b._count as unknown as number) - (a._count as unknown as number));
// Top 30 only — longer lists hide the signal.
log(`TrackedEvent types (top 30): ${j(trackedTypes.slice(0, 30))}`);
log(`TrackedEvent distinct type count: ${trackedTypes.length}`);
} catch (err) {
log(`TrackedEvent types: ERROR ${err instanceof Error ? err.message : String(err)}`);
}
}
try {
const siteConvTypes = await prisma.siteConversion.groupBy({
by: ["conversionType"],
where: { brandId: brand.id },
_count: true,
});
siteConvTypes.sort((a, b) => (b._count as unknown as number) - (a._count as unknown as number));
// All SiteConversion types — there are rarely more than ~20
// distinct values but capped at 30 for safety.
log(`SiteConversion types (top 30): ${j(siteConvTypes.slice(0, 30))}`);
log(`SiteConversion distinct type count: ${siteConvTypes.length}`);
} catch (err) {
log(`SiteConversion types: ERROR ${err instanceof Error ? err.message : String(err)}`);
}
try {
const siteEventTypes = await prisma.siteEvent.groupBy({
by: ["eventType"],
where: { brandId: brand.id },
_count: true,
});
siteEventTypes.sort((a, b) => (b._count as unknown as number) - (a._count as unknown as number));
log(`SiteEvent types (top 20): ${j(siteEventTypes.slice(0, 20))}`);
log(`SiteEvent distinct type count: ${siteEventTypes.length}`);
} catch (err) {
log(`SiteEvent types: ERROR ${err instanceof Error ? err.message : String(err)}`);
}
// ─── 30-day windowed ground truth ──────────────────────────
// The UI's default range is 30 days. Logging both all-time
// and 30-day counts side by side so "why does the dashboard
// show 528 conversions when the DB has 3000+?" is obvious.
log("\n=== 30-day windowed counts ===");
const since30d = new Date(Date.now() - 30 * 86_400_000);
try {
const conv30 = await prisma.siteConversion.count({
where: { brandId: brand.id, timestamp: { gte: since30d } },
});
log(`SiteConversion (30d): ${conv30}`);
} catch (err) {
log(`SiteConversion (30d): ERROR ${err instanceof Error ? err.message : String(err)}`);
}
if (site) {
try {
const te30 = await prisma.trackedEvent.count({
where: { trackedSiteId: site.id, timestamp: { gte: since30d } },
});
log(`TrackedEvent (30d): ${te30}`);
const sessStart30 = await prisma.trackedEvent.count({
where: { trackedSiteId: site.id, eventType: "session_start", timestamp: { gte: since30d } },
});
log(`TrackedEvent session_start (30d): ${sessStart30}`);
const pv30 = await prisma.trackedEvent.count({
where: { trackedSiteId: site.id, eventType: "page_view", timestamp: { gte: since30d } },
});
log(`TrackedEvent page_view (30d): ${pv30}`);
const ts30 = await prisma.trackedSession.count({
where: { trackedSiteId: site.id, startedAt: { gte: since30d } },
});
log(`TrackedSession (30d): ${ts30}`);
} catch (err) {
log(`TrackedEvent/TrackedSession (30d): ERROR ${err instanceof Error ? err.message : String(err)}`);
}
}
try {
const gsc30 = await prisma.gscPage.aggregate({
where: { brandId: brand.id, date: { gte: since30d } },
_sum: { clicks: true, impressions: true },
_avg: { position: true, ctr: true },
});
log(`GscPage (30d) aggregate: ${j(gsc30)}`);
} catch (err) {
log(`GscPage (30d): ERROR ${err instanceof Error ? err.message : String(err)}`);
}
try {
const ai30 = await prisma.aiUsageLog.aggregate({
where: { brandId: brand.id, timestamp: { gte: since30d } },
_count: true,
_sum: { estimatedCost: true, inputTokens: true, outputTokens: true },
});
log(`AiUsageLog (30d): ${j(ai30)}`);
} catch (err) {
log(`AiUsageLog (30d): ERROR ${err instanceof Error ? err.message : String(err)}`);
}
log("\n=== Audit complete ===");
// Flush the full run to scripts/audit-results.md so the result is
// checked in alongside the code-side audit. Wrapping in ``` keeps
// the markdown file renderable even for large JSON payloads.
const outPath = "scripts/audit-results.md";
const now = new Date().toISOString();
const body = [
"# Phase 1 — Metric Ground-Truth Audit",
"",
`**Generated:** ${now}`,
`**Brand:** ${brand.name} (${brand.id})`,
`**Domain:** ${brand.domain}`,
"",
"This file is produced by `scripts/audit-phase1.ts` and captures",
"the raw per-table counts + event-type breakdowns for a single",
"brand. These are the **ground-truth numbers** the UI surfaces",
"must agree with.",
"",
"Re-run with `npx tsx scripts/audit-phase1.ts` — the file is",
"overwritten in place.",
"",
"## Full output",
"",
"```text",
...report,
"```",
"",
].join("\n");
mkdirSync(dirname(outPath), { recursive: true });
writeFileSync(outPath, body, "utf8");
console.log(`\n[audit-phase1] wrote ${outPath} (${report.length} lines)`);
}
main()
.catch((err) => {
console.error("[audit-phase1] FATAL", err);
process.exit(1);
})
.finally(() => prisma.$disconnect());