This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# 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 tables** — `SiteConversion`, `SiteEvent`, `RealUserMetric`,
|
||||
`AiInteraction`, `AiUsageLog`, `PlatformEvent`, `MetricSnapshot`,
|
||||
`GscPage`, `TechnicalAudit`. For each: row count, min / max
|
||||
timestamp.
|
||||
- **TrackedSite-keyed tables** — `TrackedEvent`, `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 counts** — `SiteConversion`, `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
|
||||
|
||||
```text
|
||||
=== 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:203` — `prisma.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:208` — `prisma.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:389` — `prisma.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:628` — `prisma.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:148` — `prisma.trackedSession.count({ where: sessionWhere })` — **TrackedSession** — brand + optional date range — deduped — powers the admin Session Explorer list.
|
||||
- `src/app/api/admin/brands/[brandId]/route.ts:59` — `prisma.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:48` — `prisma.trackedSession.count()` — **TrackedSession** — no filter — all-time platform total.
|
||||
- `src/app/api/admin/analytics/data-moat/route.ts:62` — `prisma.trackedSession.count({ where: { startedAt: { gte: sevenDaysAgo } } })` — **TrackedSession** — 7d — for "growth per day" rate.
|
||||
- `src/app/api/admin/analytics/data-moat/route.ts:152` — `prisma.trackedSession.groupBy({ by: ["trackedSiteId"], _count: true })` — **TrackedSession** — all-time — powers the per-brand data-moat leaderboard.
|
||||
- `src/app/api/admin/analytics/funnel/route.ts:133` — `prisma.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:71` — `prisma.trackedSession.findMany({ where: { startedAt: { gte: start } }, select: { startedAt: true } })` — **TrackedSession** — monthly rollup — platform growth chart.
|
||||
- `src/app/api/admin/export/platform-metrics/route.ts:55` — `prisma.trackedSession.count()` — **TrackedSession** — all-time — export row "totals.sessions".
|
||||
- `src/app/api/admin/export/growth-data/route.ts:58` — `prisma.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:47` — `prisma.trackedSession.groupBy({ by: ["trackedSiteId"], where: { trackedSiteId: { in: trackedSiteIds } }, _count: true })` — **TrackedSession** — per-brand — brand export csv.
|
||||
- `src/app/api/admin/system-health/route.ts:45` — `prisma.trackedSession.count()` — **TrackedSession** — all-time — system-health volume row.
|
||||
- `src/app/api/content-audit/page-detail/route.ts:158` — `prisma.trackedSession.count({ where: { trackedSiteId, startedAt: { gte: since } } })` — **TrackedSession** — 30d — per-page conversion-rate denominator.
|
||||
- `src/app/api/content-audit/page-detail/route.ts:161` — `prisma.trackedSession.count({ where: { trackedSiteId, startedAt: { gte: prevSince, lt: since } } })` — **TrackedSession** — previous 30d — delta.
|
||||
- `src/app/api/content-briefs/insights/route.ts:267` — `prisma.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:174` — `prisma.trackedSession.count({ where: { trackedSiteId, startedAt: { gte: twentyFourHoursAgo } } })` — **TrackedSession** — 24h — signal-detection cron.
|
||||
- `src/app/api/site-tag/confidence/route.ts:220` — `prisma.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:94` — `prisma.trackedSession.count({ where: { trackedSiteId } })` — **TrackedSession** — all-time — snippet status card.
|
||||
- `src/lib/services/site-tag-analytics.ts:523` — `prisma.trackedSession.count({ where: { trackedSiteId, startedAt: { gte: from } } })` — **TrackedSession** — range — `getTrafficSummary()`, the Overview tab's Sessions KPI source.
|
||||
- `src/lib/services/site-tag-analytics.ts:661` — `prisma.trackedSession.count({ where: { trackedSiteId, startedAt: { gte: from } } })` — **TrackedSession** — range — `getConversionSummary()`, the Conversion Rate denominator.
|
||||
- `src/lib/services/brand-radar.ts:113` — `prisma.trackedSession.count({ where: { trackedSiteId, startedAt: { gte: d30 } } })` — **TrackedSession** — 30d — brand radar KPI.
|
||||
- `src/lib/services/brand-radar.ts:116` — `prisma.trackedSession.aggregate({ ... _avg: { pageCount } ... })` — **TrackedSession** — pages-per-session aggregate.
|
||||
- `src/lib/services/brand-radar.ts:396` — `prisma.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:490` — `const sessions = counts["session_start"] ?? 0` — derives the session count from the groupBy bucket — **TrackedEvent** — 28d — raw.
|
||||
- `src/app/api/ai-strategist/chat/route.ts:682` — `where: { 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:1032` — `prisma.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:145` — `prisma.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:380` — `prisma.trackedEvent.count({ where: { trackedSiteId, eventType: "session_start", timestamp: { gte: weekAgo } } })` — **TrackedEvent** — 7d — raw.
|
||||
- `src/lib/services/signal-detection.ts:383` — `prisma.trackedEvent.count({ where: { trackedSiteId, eventType: "session_start", timestamp: { gte: twoWeeksAgo, lt: weekAgo } } })` — **TrackedEvent** — previous 7d — raw.
|
||||
- `src/lib/services/signal-detection.ts:387` — `prisma.trackedEvent.count({ where: { trackedSiteId, eventType: "session_start", timestamp: { gte: twoDaysAgo } } })` — **TrackedEvent** — 48h — raw.
|
||||
- `src/lib/services/email-report-generator.ts:116` — `prisma.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, 386` — `fetchGa4` + `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:866` — `prisma.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:192` — `const 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:70` — `const 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.
|
||||
Reference in New Issue
Block a user