Attribution event-grain storage realignment
marketing_sales is transaction/line-grain. Its normalized lifecycle classes
(initial_sale, initial_component, upsell, rebill, refund) may produce
several attribution events for one order. Before migration 070,
marketing_attribution_events replaced rows by (client, date, sale, ad, model), so sibling transactions resolving to the same ad collapsed under
FINAL. Parent/child report totals remained additive, but revenue and purchase
credit were incomplete before entering the hierarchy.
Migration 070 rebuilds the physical replacement identity as (client_id, date, event_id). ClickHouse Cloud rejected an in-place extension because event_id
was already an existing column. The migration therefore accepts and re-verifies
the retry-safe marketing_attribution_events__pre070_final_snapshot against
canonical FINAL truth. The polluted __pre070_sale_grain retry table remains
untouched as forensic evidence. Only after the clean snapshot gate passes does
the migration discard the stale non-FINAL candidate, rebuild it solely
from canonical FINAL, verify event-identity coverage and five event-latest
totals, and perform two
adjacent single-table renames supported by Shared databases.
The original runtime table remains available separately. Ordinary views remain
name-bound. Migration 070 initially recreates and seeds mv_attribution_daily
because its insert trigger was attached to the original table UUID. Migration
071 immediately replaces that insert-trigger rollup with a consumer-compatible
ordinary view over v_attribution_latest. This is required because an additive
SummingMergeTree trigger cannot retract an older event version when a
re-resolution or tombstone appends its replacement; the latest-state view can.
Account-association evidence policy
A configured client-to-ad-account relationship is an allow-list, not proof that
an order belongs to that account. The UTM resolver may use configured account
IDs to constrain candidate paid rows, but it resolves account_id only when
the order independently and uniquely matches a campaign or a deeper entity
(ad set, ad, or creative). A known traffic platform plus one configured account
is insufficient on its own.
Orders that establish a paid platform but do not match campaign-or-deeper paid identity remain in the canonical client CRM ledger with blank resolved hierarchy IDs. Reporting and Sync Health must show them as unattributed CRM / source-known, account-unresolved. They must not be included in an account, campaign, ad-set, ad, or creative numerator merely to force parity with a legacy report.
The post-resolution hierarchy normalizer follows the same rule. It may recover
an account from a unique (client, platform, campaign) warehouse identity, but
it must not fall back to the client's sole platform account when the event has
no resolved campaign. This prevents a later normalization stage from restoring
the account-only association that the UTM resolver correctly refused to make.
Production runbook
-
Pause marketing attribution writers (scheduled marketing sync and manual sync triggers). Record the latest successful sync time.
-
Record
SHOW CREATE TABLE marketing_attribution_events, canonicalFINALrow count, and canonicalFINALrevenue, conversion value, weight, leads, and add-to-cart totals. Non-FINAL distinct event inventory is not a valid baseline because old-key background merges have already collapsed siblings. -
Leave the polluted
marketing_attribution_events__pre070_sale_grainuntouched. The migration creates or reuses the known-clean final snapshot, verifies its expected old sorting key, refreshes it from canonicalFINAL, and requires exact raw canonical-to-snapshotFINALrow/totals parity. Confirm the stalemarketing_attribution_events__event_grain_candidatemay be dropped only after those gates pass. Confirmmarketing_attribution_events__pre070_runtime_sale_graindoes not exist. -
Apply migration 070 only through the ledgered migration runner. Never paste individual statements into production. The two single-table renames are adjacent but create a minimal canonical-name gap. If the first succeeds and the second fails, immediately run:
RENAME TABLE marketing_attribution_events__pre070_runtime_sale_grain TO marketing_attribution_events;Then diagnose the candidate; do not resume writers or mark 070 applied.
-
Apply migration 071 before re-resolution or resuming writers. Verify
mv_attribution_dailyhas engineView, reads fromv_attribution_latest, and preserves the legacy column contract:client_id,date,source_platform,attribution_model,ad_platform,ad_id,campaign_id,attributed_revenue,attributed_conversion_value,attribution_weight_sum,attributed_sales_state, andlast_synced_at. Do not certify the temporary SummingMergeTree created by 070: it adds replacement history and will drift as soon as an event is re-resolved. -
Verify the canonical sorting key is
client_id, date, event_id; the clean snapshot, forensic backup, and runtime rollback tables exist; ordinary serving views query successfully;mv_attribution_dailymatches a direct aggregation ofv_attribution_latest; and pre-backfill serving totals equal the recorded latest-event totals. The candidate gate intentionally does not require raw row parity: old-key canonical truth can retain the sameevent_idunder changed ad or tombstone keys. It groups canonicalFINALby(client_id, date, event_id)and compares the fiveargMax(..., synced_at)totals and event count with candidateFINAL, which matchesv_attribution_latestsemantics. -
While writers remain paused, re-resolve each active UTM-owned connector, newest completed windows first:
node --env-file=.env.production --import tsx scripts/repair-utm-attribution-event-grain.ts --client=top-speed-ts --since=2026-07-20 --until=2026-08-18 --applynode --env-file=.env.production --import tsx scripts/repair-utm-attribution-event-grain.ts --client=vertical-line-swing-vls --since=2026-07-20 --until=2026-08-18 --applynode --env-file=.env.production --import tsx scripts/repair-utm-attribution-event-grain.ts --client=pup-labs-pl --since=2026-07-20 --until=2026-08-18 --applyDo not run HYROS or Converge through this resolver; they retain direct-source ownership. Resume writers only after the bounded repairs succeed.
-
Run attribution-ledger, commerce/CRM, report-surface, and per-order reconciliation gates. Require exact policy revenue and purchase weight for every eligible Shopify, Checkout Champ, and CAKE order before certification.
-
Retain the backup through at least one full sync/lookback cycle. Drop it only through a later reviewed migration after certification.
Rollback
Pause writers and export rows written after migration 070. Drop and recreate
mv_attribution_daily only after atomically renaming the event-grain table
aside and restoring marketing_attribution_events__pre070_runtime_sale_grain
to the canonical name. If that runtime table is unavailable, the clean
marketing_attribution_events__pre070_final_snapshot is the rollback source;
the polluted forensic backup is never promoted. Ordinary views remain untouched because they resolve the
canonical table name. Recreate the latest-state ordinary rollup from the
restored canonical view, verify the recorded pre-migration totals, and replay
exported rows deliberately before resuming writers.