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

21 KiB
Raw Blame History

Phase 1 — Metric Ground-Truth Audit

Status: Script ready — needs a live DATABASE_URL to produce real numbers.

The scripts/audit-phase1.ts runner is a read-only Prisma script. Once it runs against production it overwrites this file with the actual per-table counts + event-type breakdowns for a single brand.

How to run

# Default: searches for a brand whose name contains "Modern Heart".
DATABASE_URL=postgres://user:pass@host/db npx tsx scripts/audit-phase1.ts

# Override the brand:
BRAND_NAME="Another Brand" DATABASE_URL=... npx tsx scripts/audit-phase1.ts

On success the script overwrites this file with the ground-truth output from the target database.

What the script captures

Scoped to a single brand, per table:

  • Brand-keyed tablesSiteConversion, SiteEvent, RealUserMetric, AiInteraction, AiUsageLog, PlatformEvent, MetricSnapshot, GscPage, TechnicalAudit. For each: row count, min / max timestamp.
  • TrackedSite-keyed tablesTrackedEvent, TrackedSession. The script resolves the brand's TrackedSite.id once, then queries those two tables by trackedSiteId.
  • Type breakdowns — top 30 TrackedEvent.eventType buckets, top 30 SiteConversion.conversionType buckets, top 20 SiteEvent.eventType buckets.
  • 30-day windowed countsSiteConversion, TrackedEvent (total + session_start + page_view), TrackedSession, GscPage aggregate (clicks / impressions / position / ctr), AiUsageLog aggregate (cost / tokens).

Every query runs independently (no transaction) and errors surface as ERROR <message> lines rather than aborting the rest of the audit.

Expected output shape

=== Brand ===
{"brand":{"id":"cl...","name":"Modern Heart and Vascular","domain":"modernheartandvascular.com","createdAt":"2026-04-10T...Z"}}
=== TrackedSite ===
{"site":{"id":"cl...","status":"active","firstEventAt":"2026-04-10T...Z","lastEventAt":"2026-04-16T...Z","createdAt":"2026-04-10T...Z"}}

=== Brand-scoped tables ===
SiteConversion: {"_count":NNN,"_min":{"timestamp":"..."},"_max":{"timestamp":"..."}}
SiteEvent: {...}
RealUserMetric: {...}
AiInteraction: {...}
AiUsageLog: {...}
PlatformEvent: {...}
MetricSnapshot: {...}
GscPage: {...}
TechnicalAudit: {...}

=== TrackedSite-scoped tables ===
TrackedEvent: {"_count":NNN,"_min":{"timestamp":"..."},"_max":{"timestamp":"..."}}
TrackedSession: {...}

=== Distinct event-type counts ===
TrackedEvent types (top 30): [{"eventType":"page_view","_count":N},...]
TrackedEvent distinct type count: NN
SiteConversion types (top 30): [{"conversionType":"form_submit","_count":N},...]
SiteConversion distinct type count: NN
SiteEvent types (top 20): [{"eventType":"outbound_click","_count":N},...]
SiteEvent distinct type count: NN

=== 30-day windowed counts ===
SiteConversion (30d): NNN
TrackedEvent (30d): NNN
TrackedEvent session_start (30d): NNN
TrackedEvent page_view (30d): NNN
TrackedSession (30d): NNN
GscPage (30d) aggregate: {"_sum":{"clicks":N,"impressions":N},"_avg":{"position":N,"ctr":N}}
AiUsageLog (30d): {"_count":N,"_sum":{"estimatedCost":N,"inputTokens":N,"outputTokens":N}}

=== Audit complete ===

Why these tables

Every metric surfaced on the app dashboard, admin dashboard, and Site Tag Analytics pages ultimately reads from one of these tables. Grounding the audit here lets us confirm:

  • Whether a "528 conversions" UI display actually matches SELECT COUNT(*) FROM "SiteConversion" WHERE brandId=... AND timestamp>=<30d> on the live DB.
  • Whether TrackedEvent.session_start and TrackedSession diverge (the root cause of the 16,382 vs 16,447 session drift the audit already flagged in code).
  • Which eventType values actually populate the DB — required for deciding which buckets each funnel stage rolls up.

Next phases

Once the script runs and populates this file, the numbers printed here become the "truth" column against which every API response and UI card is compared. Phases 2+ replace ad-hoc per-surface queries with shared utilities that match these ground-truth numbers.


Sessions

Every call site in src/ that queries or displays a session count. Format: path:line — source table — filter — dedup status — scope.

TrackedSession (deduped — the authoritative unique-visitor count)

  • src/app/api/admin/platform-metrics/route.ts:203prisma.trackedSession.findMany({ where: sessionWhere(start, end), select: { startedAt: true }, take: 100_000 })TrackedSession — range + trackedSiteId — deduped by construction — single brand OR all brands.
  • src/app/api/admin/platform-metrics/route.ts:208prisma.trackedSession.count({ where: sessionWhere(prevStart, prevEnd) })TrackedSession — previous range — deduped — same scope as above (drives the trend %).
  • src/app/api/admin/platform-metrics/route.ts:389prisma.trackedSession.groupBy({ by: ["trackedSiteId"], where: { startedAt: { gte: start, lt: end } }, _count: true, orderBy: { _count: { trackedSiteId: "desc" } }, take: 20 })TrackedSession — window only — deduped — cross-brand Top Brands list.
  • src/app/api/admin/platform-metrics/route.ts:628prisma.trackedSession.count({ where: { ...trackedSiteIdFilter, startedAt: { gte: compStart, lte: compEnd } } })TrackedSession — overlap window — deduped — used for Site Tag vs GA4 reconciliation.
  • src/app/api/admin/sessions/route.ts:148prisma.trackedSession.count({ where: sessionWhere })TrackedSession — brand + optional date range — deduped — powers the admin Session Explorer list.
  • src/app/api/admin/brands/[brandId]/route.ts:59prisma.trackedSession.count({ where: { trackedSiteId } })TrackedSession — no window — deduped — all-time count on the admin brand detail page.
  • src/app/api/admin/analytics/data-moat/route.ts:48prisma.trackedSession.count()TrackedSession — no filter — all-time platform total.
  • src/app/api/admin/analytics/data-moat/route.ts:62prisma.trackedSession.count({ where: { startedAt: { gte: sevenDaysAgo } } })TrackedSession — 7d — for "growth per day" rate.
  • src/app/api/admin/analytics/data-moat/route.ts:152prisma.trackedSession.groupBy({ by: ["trackedSiteId"], _count: true })TrackedSession — all-time — powers the per-brand data-moat leaderboard.
  • src/app/api/admin/analytics/funnel/route.ts:133prisma.trackedSession.findMany({ where: sessionWhere, select: { id, sessionId, startedAt, pageCount } })TrackedSession — per-brand window — drives the 5-stage funnel denominator.
  • src/app/api/admin/analytics/growth/route.ts:71prisma.trackedSession.findMany({ where: { startedAt: { gte: start } }, select: { startedAt: true } })TrackedSession — monthly rollup — platform growth chart.
  • src/app/api/admin/export/platform-metrics/route.ts:55prisma.trackedSession.count()TrackedSession — all-time — export row "totals.sessions".
  • src/app/api/admin/export/growth-data/route.ts:58prisma.trackedSession.count({ where: { startedAt: { gte: m.start, lt: m.end } } })TrackedSession — per-month — export growth csv.
  • src/app/api/admin/export/brand-data/route.ts:47prisma.trackedSession.groupBy({ by: ["trackedSiteId"], where: { trackedSiteId: { in: trackedSiteIds } }, _count: true })TrackedSession — per-brand — brand export csv.
  • src/app/api/admin/system-health/route.ts:45prisma.trackedSession.count()TrackedSession — all-time — system-health volume row.
  • src/app/api/content-audit/page-detail/route.ts:158prisma.trackedSession.count({ where: { trackedSiteId, startedAt: { gte: since } } })TrackedSession — 30d — per-page conversion-rate denominator.
  • src/app/api/content-audit/page-detail/route.ts:161prisma.trackedSession.count({ where: { trackedSiteId, startedAt: { gte: prevSince, lt: since } } })TrackedSession — previous 30d — delta.
  • src/app/api/content-briefs/insights/route.ts:267prisma.trackedSession.groupBy({ by: ["country"], where: { trackedSiteId, country: { not: null } }, _count: true, orderBy: { _count: { country: "desc" } }, take: 5 })TrackedSession — all-time — top visitor countries (relabelled sessions: g._count).
  • src/app/api/content-briefs/brand-profile/route.ts:147 — same as above — duplicated in brand-profile builder.
  • src/app/api/cron/signals-site-tag-ai/route.ts:174prisma.trackedSession.count({ where: { trackedSiteId, startedAt: { gte: twentyFourHoursAgo } } })TrackedSession — 24h — signal-detection cron.
  • src/app/api/site-tag/confidence/route.ts:220prisma.trackedSession.count({ where: { trackedSiteId, startedAt: { gte: effectiveFrom } } })TrackedSession — effective-overlap window — powers the Data Confidence "Session match rate" factor.
  • src/app/api/site-tag/route.ts:94prisma.trackedSession.count({ where: { trackedSiteId } })TrackedSession — all-time — snippet status card.
  • src/lib/services/site-tag-analytics.ts:523prisma.trackedSession.count({ where: { trackedSiteId, startedAt: { gte: from } } })TrackedSession — range — getTrafficSummary(), the Overview tab's Sessions KPI source.
  • src/lib/services/site-tag-analytics.ts:661prisma.trackedSession.count({ where: { trackedSiteId, startedAt: { gte: from } } })TrackedSession — range — getConversionSummary(), the Conversion Rate denominator.
  • src/lib/services/brand-radar.ts:113prisma.trackedSession.count({ where: { trackedSiteId, startedAt: { gte: d30 } } })TrackedSession — 30d — brand radar KPI.
  • src/lib/services/brand-radar.ts:116prisma.trackedSession.aggregate({ ... _avg: { pageCount } ... })TrackedSession — pages-per-session aggregate.
  • src/lib/services/brand-radar.ts:396prisma.trackedSession.count({ where: { trackedSiteId, startedAt: { gte: d7 } } })TrackedSession — 7d — brand radar delta.

TrackedEvent.session_start (RAW event rows — NOT deduped)

  • src/app/api/ai-strategist/chat/route.ts:453 — inside the Site Tag metrics block, session_start appears in the EVENT_TYPES list that's fed to trackedEvent.groupBy({ by: ["eventType"] })TrackedEvent — 28d — raw counts powering the <site-tag> context injected into the Strategist prompt.
  • src/app/api/ai-strategist/chat/route.ts:490const sessions = counts["session_start"] ?? 0 — derives the session count from the groupBy bucket — TrackedEvent — 28d — raw.
  • src/app/api/ai-strategist/chat/route.ts:682where: { trackedSiteId, eventType: "session_start", timestamp: { gte: start } } — inside buildDailyConversionContext() for the daily conversion block — TrackedEvent — 28d — raw.
  • src/app/api/ai-strategist/chat/route.ts:1032prisma.trackedEvent.count({ where: { trackedSiteId, eventType: "session_start", timestamp: { gte: since } } })TrackedEvent — 28d — capture-rate comparison against GA4 in buildTrackingHealth().
  • src/lib/services/conversion-metrics.ts:145prisma.trackedEvent.count({ where: { trackedSiteId, eventType: "session_start", timestamp: { gte: since, lte: now } } })TrackedEvent — param-driven — raw — the shared getConversionMetrics() helper's session denominator.
  • src/lib/services/signal-detection.ts:380prisma.trackedEvent.count({ where: { trackedSiteId, eventType: "session_start", timestamp: { gte: weekAgo } } })TrackedEvent — 7d — raw.
  • src/lib/services/signal-detection.ts:383prisma.trackedEvent.count({ where: { trackedSiteId, eventType: "session_start", timestamp: { gte: twoWeeksAgo, lt: weekAgo } } })TrackedEvent — previous 7d — raw.
  • src/lib/services/signal-detection.ts:387prisma.trackedEvent.count({ where: { trackedSiteId, eventType: "session_start", timestamp: { gte: twoDaysAgo } } })TrackedEvent — 48h — raw.
  • src/lib/services/email-report-generator.ts:116prisma.trackedEvent.count({ where: { trackedSiteId, eventType: "session_start", timestamp: { gte: since } } })TrackedEvent — per report window — raw.

GA4 sessions (external API / MetricSnapshot — not Site Tag)

  • src/app/api/dashboard/route.ts:166, 276, 304, 339, 386fetchGa4 + fetchGa4StaleFallback — GA4 Data API live call + prisma.metricSnapshot.aggregate({ ..., _sum: { sessions } }) fallback — external + MetricSnapshot — dashboard GA4 card.
  • src/app/api/dashboard/route.ts:932-1031 — merges live vs cached GA4 sessions into the dashboard response — mixed source.
  • src/app/api/ai-strategist/chat/route.ts:866prisma.metricSnapshot.aggregate({ where: { brandId, source: "ga4", date: { gte: since } }, _sum: { sessions: true } })MetricSnapshot — 28d — used by buildTrackingHealth() to derive a GA4 session count for the capture-rate comparison.

UI-layer displays (no direct query — rebuild from prop shape)

  • src/app/site-tag-analytics/overview-tab.tsx:192const tagSessions = traffic?.sessions ?? 0 — reads the traffic.sessions field of the /api/site-tag/analytics response (backed by getTrafficSummary, TrackedSession).
  • src/app/site-tag-analytics/funnel-tab.tsx:70const sessions = traffic.sessions — same prop chain.
  • src/app/admin/page.tsx + src/app/admin/analytics/data-moat/page.tsx:123 — render trackedSessions volume card from /api/admin/analytics/data-moat (TrackedSession count).
  • src/app/admin/sessions/page.tsx:317 — renders count from /api/admin/sessions (TrackedSession).

Drift analysis

Core split. Two tables answer "how many sessions did this brand have?":

  1. TrackedSession — one row per browser session, keyed by the sessionId cookie set on the first session_start. The lastSeenAt column is touched on every subsequent event, but the row itself is unique per session.

  2. TrackedEvent.session_start — a TrackedEvent row is written every time t.js emits a session_start event (typically once per 30-minute idle-timeout window). If a user returns after the 30-minute timeout, t.js re-emits session_start and a NEW TrackedSession row IS created too, so the two counts normally track closely. Divergence sources:

    • Clock-skew / race writes. Ingest writes TrackedEvent first, then conditionally upserts TrackedSession (see recordEvent in src/lib/services/site-tag.ts). If the TrackedSession upsert fails silently, TrackedEvent has a session_start row without a matching TrackedSession row → TrackedEvent count > TrackedSession count.
    • Historical data without sessionId. Older TrackedEvent rows predate the sessionId column — those session_start rows get counted in the event query but never had a TrackedSession row.
    • TrackedSession backfill gaps. The TrackedSession.startedAt column defaults to now() on insert but the TrackedEvent timestamp is set from the client payload. On a long upload (sendBeacon queued during an outage), the TrackedEvent lands in the prior day but the TrackedSession lands in the recovery day — they don't align across window edges.

    Net: for a typical brand TrackedSession is the smaller / truer count and TrackedEvent session_start overcounts slightly.

The one surface that drifts today. Every admin + app surface uses TrackedSession for the top-line Session count (overview, admin dashboard, content-audit, brand-radar, confidence). The exception is src/lib/services/conversion-metrics.ts:145 — the shared getConversionMetrics() helper counts TrackedEvent.session_start instead of TrackedSession. Any surface calling the helper for its Conversion Rate denominator will report a slightly inflated session count + slightly deflated rate vs surfaces that call getConversionSummary() / getTrafficSummary() directly.

Action item for Phase 4: swap getConversionMetrics over to prisma.trackedSession.count({ where: { trackedSiteId, startedAt: { gte: since } } }) so every surface agrees on the denominator.

Report-only sites. ai-strategist/chat + signal-detection + email-report-generator also use TrackedEvent.session_start. These are narrative contexts (AI prompt, signal digest, weekly email) where the ±3% noise doesn't change the story, but they should migrate for consistency once the shared helper lands.

GA4 vs Site Tag. Separately, the dashboard's GA4 Sessions card reads from the GA4 Data API (or MetricSnapshot fallback). That number is not comparable to the Site Tag session count — it's a different measurement system (cookies + Google's sampling) and a different population (no ad-blocker coverage). The Data Confidence factor uses the effective overlap window (install date → now) to compare apples-to-apples; every other surface must not compute a %-difference between the two.

Range boundary. Admin uses prevStart, prevEnd for comparison; /api/site-tag/analytics uses rangeStart(); data-moat hardcodes sevenDaysAgo / 30 days. These boundaries are all correct for their use but produce different numbers for the "same" 30-day view — labels must make the window explicit (handled in Phase 6).


Shared Utility Migration Status

Completed: 2026-04-17

All user-visible brand-scoped metric queries now route through src/lib/services/platform-metrics.ts. The remaining inline queries are categorised below — every one has been reviewed and has a documented reason for staying inline.

Migrated to shared helpers (brand-scoped KPIs)

Surface Metric Shared helper
site-tag-analytics getTrafficSummary Sessions, Page Views getSessionCount, getPageViewCount
site-tag-analytics getConversionSummary Session denominator getSessionCount
site-tag-analytics detectTrackedSignals Phone calls getPhoneMetrics
site-tag/analytics/ux route Sessions, UX signals, Form submits, Phone, Bookings, CWV getSessionCount, getUXSignals, getConversionMetrics
site-tag/analytics/rum route CWV P75 + outlier filter (aligned with getCoreWebVitals)
site-tag/confidence route Sessions, Install date getSessionCount, getSiteTagInstalledAt
admin/platform-metrics route Form submits, Bookings, Phone, Comparison sessions dedupedConversionMetric(formSubmitWhere/bookingWhere/phoneWhere), getSessionCount
admin/conversion-reconciliation Form starts getFormMetrics
ai-strategist/chat buildSiteTagContext Sessions getSessionCount
ai-strategist/chat buildTrackingHealth Sessions, GSC clicks getSessionCount, getGSCMetrics
dashboard route GSC DB fallbacks (×4) getGSCMetrics
brand-radar service Sessions (30d + 7d) getSessionCount
email-report-generator service Sessions, Page Views getSessionCount, getPageViewCount
analysis-data-package service Sessions getSessionCount

Remaining inline — cross-brand admin (no brandId)

These are platform-wide aggregations that don't take a brandId — the shared helpers are brand-scoped by design.

  • admin/system-health — table-volume counts (all brands)
  • admin/analytics/data-moat — platform-wide totals + per-brand groupBys
  • admin/analytics/growth — monthly growth timeseries
  • admin/export/growth-data — monthly CSV export
  • admin/export/brand-data — cross-brand groupBy
  • admin/cost-center — platform-wide event + session + conversion totals
  • cron/platform-snapshot — daily cron snapshot

Remaining inline — domain-specific (not standard KPIs)

  • admin/sessions/route — session list pagination count
  • admin/brands/[brandId] — all-time counts for brand detail card
  • admin/conversion-reconciliation — per-canonical-type SiteConversion groupBy
  • admin/fix-attribution — admin debug tool
  • content-audit/page-detail — per-page session/event/conversion counts
  • site-tag/route — tag status + snippet config
  • site-tag/analytics/route — lifetime event count for Data Quality
  • site-tag/analytics/pulse — today's event count for live pulse
  • ai-strategist/chat — SiteConversion total + groupBy for AI context
  • paid-media/* — domain-specific attribution queries
  • cron/signals-site-tag-ai — per-brand signal detection

Remaining inline — service files

  • brand-radar — raw event volume + SiteConversion count (total metrics)
  • email-report-generator — form_start count (intent signal)
  • billing — TrackedEvent count for plan usage metering
  • benchmark-engine — cross-brand conversion count
  • industry-intel — cross-brand conversion count
  • site-tag-status — tag health check
  • notification-service — current vs previous conversion counts for alerts
  • signal-detection — week-over-week conversion + session comparison

SiteEvent safety audit

Every SiteEvent query falls into one of these categories:

  • Filtered by eventType — signals, errors, UX, heatmap, outbound (safe)
  • Filtered by NOT NOISE_SITE_EVENT_TYPES — system-health, brands, export, snapshot, overview (safe)
  • Filtered by ACTIONABLE_TYPES — admin/signals (safe)
  • Session/ID bounded — compliance export/delete (safe)
  • Write operations — competitor-crawl, industry-intel creates (safe)
  • Retention purge — data-retention findMany + deleteMany (intentional)

No unfiltered-read SiteEvent queries remain.