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).
## 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)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Issue #4 item 4 (persisted measurement).
marketing_metric_daily(19th table): per campaign, UTC day (dayUtc;dayis 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; carriesproductIdfor the product lock. Hosts add the table (reference DDL regenerated).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.MeasurementReportsums the rollup in the backend and counts campaign-wide unique actors with a backendCOUNT_DISTINCT(daily uniques can't be summed). Output unchanged: all existing measurement tests pass (Not measured, lower bound, test-data exclusion).MarketingOutcomeImporterregistered withMarketingQBitConfig.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.occurredAtto now andisTestDatato 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 inPersistenceRestartTest(SQL SUM/COUNT_DISTINCT, restart, Rebuild)../mvnw verifygreen 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