Context
The engine that turns a ReportBlock.query config (#9) into numbers: time-bucketed aggregations over the events stream (#6), safely and fast. Built on bun-query-builder - no string-built SQL anywhere; when the builder cannot express something (time bucketing, filtered aggregates, percentiles), extend bun-query-builder upstream (~/Code/Libraries/bun-query-builder / ~/Code/Tools/bun-query-builder) and bun run release:patch.
Scope
app/Reports/engine.ts(or apackages/module): input = validated block query config + project + time range; output = a typed result set{ series: [{ key, points: [{ t, value }] }], total, comparison }.- Measures:
count,count_unique(user_key),sum(value),avg(value),min,max; funnel step counts for the funnel block. - Time grains: hour, day, week, month, with timezone-correct bucketing and gap-filling (empty buckets emit zero points; charts must not connect across gaps silently).
- Dimensions: group by a bounded-cardinality property with a top-N + "other" rollup.
- Filters: property equals/in/contains, event-name sets, user_key presence; all parameterized through the builder.
- Comparison ranges: same-length previous period, computed server-side.
- Rollups: pre-aggregated daily rollup table (from a model, per #5) maintained by a queued job (
app/Jobs/), used automatically for coarse grains over long ranges; raw events used for fine grains. Correctness test: rollup answer == raw answer on seeded data. - Caching:
@stacksjs/cachecache-aside on (project, config-hash, range) with short TTL; explicit bust on new-event thresholds is fine to defer. - HTTP surface:
POST /api/projects/{id}/query(bearer auth, validated config) used by viewer, builder preview, shares, and scheduled renders. - Bench note in the repo: seeded 1M-event project, p95 for a 30-day daily line query - with the number recorded, not hand-waved.
Acceptance
- Engine unit tests per measure/grain/filter; rollup-vs-raw equivalence test; the seeded demo reports (#9) return correct series verified against hand-computed fixtures.
Depends on #6, #9. Blocks #12, #13, #14, #15. Part of #1 (M3 Reporting core). Ground rules: see #1.