Skip to content

feat(domain): hourly per-service AI vendor discovery rollup - #453

Closed
JeremyFunk wants to merge 9 commits into
ai2/02-ingest-write-pathfrom
ai2/03-vendors-rollup
Closed

feat(domain): hourly per-service AI vendor discovery rollup#453
JeremyFunk wants to merge 9 commits into
ai2/02-ingest-write-pathfrom
ai2/03-vendors-rollup

Conversation

@JeremyFunk

@JeremyFunk JeremyFunk commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Stack 3 of 3. Base: ai2/02-ingest-write-path (writes the AI columns). This PR adds the aggregate that makes them queryable at dashboard speed.

What this does

Adds service_ai_vendors_hourly: an hourly rollup of AI activity per (org, service, vendor), fed by a materialized view over traces. It answers "which AI frameworks does each service run, how many spans/traces, and how good is session-key coverage" without scanning raw traces — and it's retained 400 days, far past the 30-day raw-trace TTL, so vendor history survives the raw data.

Per row (one per org × service × vendor × hour):

  • Span counts by session-key state — total, weighted by sample rate, and one counter per state (key absent / invalid / sub-session / session) so coverage ratios are computable.
  • Approximate distinct counts as uniqCombined(12) sketches: traces total, traces with a session key, and distinct sessions (via AiSessionKeyHash). Sketches merge across hours/services at query time; 12 gives ~1.6% error.
  • Rules-version min/max, so a reader can tell when two time ranges were classified by different rulesets.

The same definition lands in all three backends — ClickHouse migration 0017, the Tinybird datasource + materialization, and the CLI's local store (schema v7, generated snapshot). The MV SELECT is one shared constant (ai-vendors-rollup-sql.ts); a test asserts the migration and the Tinybird materialization stay byte-identical, so the two deployment paths can't drift.

Rollout

  • 0017 is not ingest-gating — it touches nothing the gateway inserts, so BYO clusters aren't un-readied over a read-path table.
  • No POPULATE, no backfill. The MV only sees spans written after it exists, and hours during the classification-flag ramp are partially classified in a way rows can't express. The cutover runbook (migration header) has the operator record the first fully-classified hour in AI_VENDORS_ROLLUP_ENABLEMENT_HOUR; readers must treat earlier hours as nonexistent, not zero — "no AI spans" and "wasn't recording" are different claims. Nothing consumes it yet; the read path is a later stack.
  • Counters are at-least-once (same as every existing Maple rollup); the one sanctioned mutation is org deletion, which the ORDER BY (OrgId, …) prefix keeps prunable.

Testing

  • E2e against a real ClickHouse (new CI step): inserts spans with known-by-construction answers, then asserts the counter identities, HLL sketch merging at the right grouping, and sane behavior across merged parts — all failure modes that render as plausible numbers if broken.
  • Migration index tests: ordering, ingest-gate stays 16, shared-SELECT byte-identity, no-POPULATE.
  • Local store: v6→v7 migration replays the exported DDL (single source), append-only schema gate green.
  • @maple/domain 490, @maple/cli 435, cargo test 136+70, repo typecheck 40/40 green.

🤖 Generated with Claude Code


Update 2026-08-18 (5678f325b): the classification flag no longer exists (see the base PR), so the rollout runbook is restated flag-free — AI_VENDORS_ROLLUP_ENABLEMENT_HOUR is the first full hour after the classifying gateway binary reaches the whole fleet. The reader rule is unchanged: hours before the boundary are nonexistent, not zero.

JeremyFunk and others added 3 commits August 17, 2026 12:54
Migration 0016 adds `service_ai_vendors_hourly` and the materialized view that
writes it: one row per (org, service, vendor, hour), carrying span and
session-key-state counters plus `uniqCombined(12)` states for traces and
sessions. It backs service pruning, per-service vendor lookup, session-key health
and sampling-exemption suggestions.

The view filters `AiVendor != ''`, which is both the cost model and the
semantics: the platform's ordinary HTTP and DB spans never enter MV processing,
and after enablement "no rows" means "no AI spans". It groups on the stored,
clamped `AiRollupHour`, never `toStartOfHour(Timestamp)`, so a skewed client
cannot open a partition in 2038. No POPULATE and no backfill: correctness depends
on the source rows having been classified, not on the view having existed.

Because the view runs synchronously inside the INSERT pipeline, this target's
part-count and merge-lag are trace-ingestion health — a `too many parts` here
fails the INSERT into `traces`. `requiredForIngest` stays false all the same: the
gateway never writes this table, and bumping the ingest gate would un-ready every
BYO-ClickHouse org over a read-path change.

Reader contract, pinned in the datasource doc and the tests: plain aggregates
plus `uniqCombinedMerge(12)`, never `FINAL`, and never an assumption of one row
per key. Headline coverage merges across vendor rows — a per-vendor ratio
systematically understates co-occurring passthrough vendors. Hours before the
recorded enablement hour do not exist for readers, and that boundary is an
operator step, not something the migration can enforce; a partly-classified hour
is internally consistent and looks healthy.

400-day retention over a 30-day source is deliberate and now the matrix's only
400-day tier: past the raw horizon these rows cannot be rebuilt.

The migrated view body and the bootstrapped one come from one exported constant,
and a test asserts neither copy drifted: a cluster migrated to 16 and a freshly
bootstrapped one must compute the same coverage ratio.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Local schema v6: the rollup table and its view, and nothing else — asserted as a
name delta against the frozen v5 manifest so a stray object cannot ride along.
The v5 -> v6 module and a frozen v6 DDL snapshot keep an existing local store
readable, and the manifest gate checks that snapshot's identity like the ones
before it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The rollup's output is a coverage percentage shown to a customer, and every way
it can be wrong renders fine: a broken counter identity, an HLL state merged at
the wrong grouping, a reader assuming merged parts. Only a real server over spans
with known-by-construction answers catches those, so this suite runs against one
— asserting the counter identities, the trace-level coverage ratio merged across
vendors, the hour grouping, and that reading without `FINAL` over deliberately
unmerged parts still gives the right answer.

Wired into `ch:test` and into the ClickHouse CI job alongside the existing
warehouse suites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JeremyFunk
JeremyFunk force-pushed the ai2/03-vendors-rollup branch from 3584d63 to 68db7e7 Compare August 17, 2026 10:55
JeremyFunk and others added 6 commits August 17, 2026 13:05
Same pass as the earlier stack branches. Also drops migration 0017's
stale claim that the ingest gate is "15" — index.test.ts pins 16.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… flag

The flag is gone (ingest classifies unconditionally), so the boundary is the
hour the classifying gateway binary reaches the whole fleet.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
02 dropped the AiRollupHour column, so the rollup now buckets on
toStartOfHour(toDateTime(Timestamp)) — the same span-time hour every
other hourly MV uses. The clock-skew clamp test went with it: a skewed
client now lands in the rollup exactly where it lands in raw traces.

Local-store v7 snapshot and pinned identity hashes regenerated on top
of the new v6. Also corrects the two e2e migration-version assertions
that still pointed one version back after the earlier renumbering
(15→16 classification, 16→17 rollup); both passed vacuously before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🍁 Maple PR preview

Note

Preview resources were removed when this pull request closed.

Final commit 6a3b4cd · View workflow run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant