Attribution (OrderEvent -> Attribution)¶
Enum: AttributionMethod in app/core/models/attributions.py
Methods¶
| Method | Meaning |
|---|---|
| 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_atnotsent_at) - Guards
if order.emailbefore email-based queries (orders without email skip email matching) - Sets
holdout=True, marksprocessed=Trueimmediately - Uses
list_holdout_attributions_for_campaign()withexcluded_recipient_idsandexcluded_methodsparams 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, marksprocessed=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:
list_all_attributions_for_recipient_and_campaign_id()fetches all attributions (regardless ofpasses_rules) for a recipient-campaign pair- Recipients already attributed as sent are collected into
excluded_recipient_ids list_holdout_attributions_for_campaign()acceptsexcluded_recipient_idsandexcluded_methodsto skip those recipients during holdout attribution- 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_dateAND recipientcreated_at/sent_at-- the later of the two is the effective window start - Holdout uses
created_at; experimental usessent_atorcreated_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