feat(measurement): daily results rollup and an outcome importer SPI (#4, part 4) #20

Merged
KofTwentyTwo merged 5 commits from feature/measurement-rollup into develop 2026-10-14 17:02:52 +00:00
KofTwentyTwo commented 2026-10-14 16:51:23 +00:00 (Migrated from github.com)

Summary

Issue #4 item 4 (persisted measurement).

  • marketing_metric_daily (19th table): per campaign, UTC day (dayUtc; day is reserved in H2), stage, event type and test-data flag: raw count, unique actors, events without an actor. System-written and guarded like the other history tables; the "Daily Results" child list on the campaign; carries productId for the product lock. Hosts add the table (reference DDL regenerated).
  • Kept current by a post-insert/update/delete customizer on marketing_outcome_event, so every path (UI, API, CRM import, importers, worker) updates it; an update recomputes the old and new day. Rebuild Results campaign action (standard summary lines, prepare permission) rebuilds every day, streaming events by page.
  • MeasurementReport sums the rollup in the backend and counts campaign-wide unique actors with a backend COUNT_DISTINCT (daily uniques can't be summed). Output unchanged: all existing measurement tests pass (Not measured, lower bound, test-data exclusion).
  • Importer SPI: MarketingOutcomeImporter registered with MarketingQBitConfig.withOutcomeImporter(...), run by the Import Outcomes campaign action through the normal (source, external id) dedupe; a failing importer is reported without stopping others; listed in Configuration Status. No network importer ships.
  • New outcome events default occurredAt to now and isTestData to false; a blank actor key is stored as null.

Known limit (documented): each event write re-reads that day's events, so one very busy day makes writes slower.

Tests

MeasurementRollupTest (rollup vs raw across inserts/edits/deletes/test data, guard, Rebuild idempotence, importer dedupe) and a new H2 RDBMS case in PersistenceRestartTest (SQL SUM/COUNT_DISTINCT, restart, Rebuild). ./mvnw verify green locally after merging develop.

Implementation by a Sonnet subagent, reviewed by Claude Opus. #19's help gate will need entries for the new table once both merge (I'll add them).

🤖 Generated with Claude Code

## Summary Issue #4 item 4 (persisted measurement). - **`marketing_metric_daily`** (19th table): per campaign, UTC day (`dayUtc`; `day` is reserved in H2), stage, event type and test-data flag: raw count, unique actors, events without an actor. System-written and guarded like the other history tables; the "Daily Results" child list on the campaign; carries `productId` for the product lock. **Hosts add the table** (reference DDL regenerated). - **Kept current** by a post-insert/update/delete customizer on `marketing_outcome_event`, so every path (UI, API, CRM import, importers, worker) updates it; an update recomputes the old and new day. **Rebuild Results** campaign action (standard summary lines, prepare permission) rebuilds every day, streaming events by page. - **`MeasurementReport`** sums the rollup in the backend and counts campaign-wide unique actors with a backend `COUNT_DISTINCT` (daily uniques can't be summed). Output unchanged: all existing measurement tests pass (Not measured, lower bound, test-data exclusion). - **Importer SPI:** `MarketingOutcomeImporter` registered with `MarketingQBitConfig.withOutcomeImporter(...)`, run by the **Import Outcomes** campaign action through the normal (source, external id) dedupe; a failing importer is reported without stopping others; listed in Configuration Status. No network importer ships. - New outcome events default `occurredAt` to now and `isTestData` to false; a blank actor key is stored as null. Known limit (documented): each event write re-reads that day's events, so one very busy day makes writes slower. ## Tests `MeasurementRollupTest` (rollup vs raw across inserts/edits/deletes/test data, guard, Rebuild idempotence, importer dedupe) and a new H2 RDBMS case in `PersistenceRestartTest` (SQL SUM/COUNT_DISTINCT, restart, Rebuild). `./mvnw verify` green locally after merging develop. Implementation by a Sonnet subagent, reviewed by Claude Opus. #19's help gate will need entries for the new table once both merge (I'll add them). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.