Skip to content

Attribution (OrderEvent -> Attribution)

Enum: AttributionMethod in app/core/models/attributions.py

Methods

Method Meaning
Email Order email matches recipient email (case-insensitive). Highest priority.
PhysicalAddress Shipping address matches recipient address (normalized). Disqualified if active subscription pre-send.
DiscountCode Discount code matches campaign/recipient code. Only method that works for unsent recipients.
Unknown Fallback when method cannot be determined.

Status fields

Field Meaning
processed Has been validated through process_attribution(). Unprocessed = created during order sync.
passes_rules Passed timing windows and business rules. Used for stats inclusion.
holdout Control group attribution (not in experimental group).
archived Attributed campaign is archived. Excluded from reporting.

Pipeline (3 stages)

Stage 1 -- Order Sync

celery/sync_orders.py, every 5min

  • Fetches orders from Klaviyo/Shopify/Ometria
  • find_best_recipient() -> tries email match, then physical address
  • Creates unprocessed Attribution record, caches in Redis

Stage 2 -- Holdout Check

celery/sync_orders.py:check_for_holdout_order()

  • Checks control group recipients (uses created_at not sent_at)
  • Guards if order.email before email-based queries (orders without email skip email matching)
  • Sets holdout=True, marks processed=True immediately
  • Uses list_holdout_attributions_for_campaign() with excluded_recipient_ids and excluded_methods params to avoid double-counting (see Holdout Deduplication below)

Stage 3 -- Attribution Processing

methods/attributions.py:process_attribution()

  • Validates timing windows (min days: 1-3, max days: 60-180 depending on campaign)
  • Checks discount codes, subscription status, BFCM cutoff
  • Sets passes_rules=True/False, marks processed=True

Holdout Deduplication

A recipient can appear as both sent (in the experimental group) and holdout (in the control group) for the same campaign — for example, if they were re-added to a segment after being held out in an earlier cohort. Without deduplication, the same order could be attributed twice: once as a sent attribution and once as a holdout attribution.

The fix uses exclusion lists during holdout order attribution:

  1. list_all_attributions_for_recipient_and_campaign_id() fetches all attributions (regardless of passes_rules) for a recipient-campaign pair
  2. Recipients already attributed as sent are collected into excluded_recipient_ids
  3. list_holdout_attributions_for_campaign() accepts excluded_recipient_ids and excluded_methods to skip those recipients during holdout attribution
  4. This ensures each order is counted exactly once — either as sent or holdout, not both

Key design notes

  • Discount codes override timing windows
  • Two timestamps matter: campaign first_send_date AND recipient created_at/sent_at -- the later of the two is the effective window start
  • Holdout uses created_at; experimental uses sent_at or created_at

Key files

  • Model: core/models/attributions.py, core/models/orders.py
  • Sync: celery/sync_orders.py
  • Processing: methods/attributions.py
  • Docs: docs/attribution_methodology.md