This runbook covers release QA, on-call checks and maintenance for multi-touch attribution in Prosper202 1.9.76 and later. Each check comes with the p202 command that runs it. The few jobs only SQL or a PHP script can do are marked as such. Commands that read need an API key with the attribution:read scope; model changes also need attribution:write.
How the engine works
- Every conversion write, from a pixel, postback, upload or the API, records a ledger row and queues the conversion in the outbox,
202_attribution_pending. - The attribution worker drains the outbox. The minutely cron (
202-cronjobs/index.php) runs it for 20 seconds each minute.202-cronjobs/attribution-worker.phpruns the same worker on its own, 50 seconds by default. A MySQL named lock keeps two workers from overlapping. - For each conversion, the worker builds one journey: up to 25 clicks by the same visitor, inside the widest lookback of your active models (never less than 30 days). Clicks are linked through visitor keys, never by IP address or user agent. The journey is written to
202_attribution_journeys, and how it was built goes to202_attribution_journey_meta. - The worker then computes credits for every active model into
202_attribution_credits. Under each model, a conversion's credits add up to exactly 1, and its credited revenue adds up to exactly its counted amount. - Each write marks the report hours it touched. The worker re-sums those hours into
202_attribution_rollup, which reports and exports read.
Changing a model's type, weighting or lookback re-queues every conversion in the account. When two visitors are found to be one person, that person's conversions are re-queued too.
Upgrading from 1.9.55 or earlier: the hourly engine is gone. The removed pieces are:
- The tables
202_conversion_touchpoints,202_attribution_settings,202_attribution_snapshotsand202_attribution_touchpoints.- The
/api/v2/attributionendpoints.- The scripts
attribution-rebuild.phpandbackfill-conversion-journeys.php. Delete any crontab lines that call them.- The Dashboard System Checks page.
The worker brings existing conversions into multi-touch attribution on its own, at roughly 20 minutes of worker time per million conversions.
backfillinp202 attribution queueshows its progress. The upgrade is one-way, so back up the database first; see Upgrading.
1. Health check
Run this after every deploy or upgrade, and at the start of an on-call investigation.
- Cron is ticking.
p202 system cronshows the last run for each job type. The every-minute row should be under two minutes old.202-cronjobs/health.phptreats over 2 minutes as a warning and over 10 as critical. - The backlog is draining. Run
p202 attribution queue. A healthy install looks like this:backfill: failing: 0 merges_awaiting_requeue: 0 models_awaiting_recompute: 0 oldest_enqueued_at: pending: 0 rows: []pendingrises with traffic and should fall back within a minute or two. If it keeps growing, the worker isn't running.models_awaiting_recomputeis above 0 only right after a model change.backfillstays empty once the post-upgrade backfill has finished. - Nothing is failing. When
failingis above 0,rowslists each conversion with itsattemptsandlast_error. Failed rows retry on their own, waiting longer each time, from one minute up to a day. To retry them now, fix whatlast_errornames and runphp 202-cronjobs/attribution-worker.php --retry-now. A database error stops the run without spending any row's retries, so nothing is lost. - Every model is usable. In
p202 attribution model list, each model should beactiveorinactive.invalidmeans its stored definition failed validation.p202 attribution model get <id>shows why instatus_reason. The other models keep computing in the meantime.
2. Models and configuration
- One default per account, always active. A unique key in the database enforces this (
one_defaulton202_attribution_models). The default can't be unset, deactivated or deleted. To replace it, make another model the default first. - Types and weighting. The weighting config is a JSON object:
Lookback isn't part of the weighting config. It's a model setting,Type Weighting config last_touch,first_touch,linearnone time_decay{"half_life_hours":48}, up to 8760position_based{"first_weight":0.4,"last_weight":0.4}, each 0–1, together at most 1--lookback-days, from 1 to 365 days, default 30. Changing a model's type resets its weighting to that type's defaults. - Create a QA model:
p202 attribution model create --model-name "QA linear" --model-type linear. The next worker run computes its credits for existing conversions. Watchmodels_awaiting_recomputego to 1, then back to 0. - Change a model:
p202 attribution model update <id> --weighting-config '{"half_life_hours":24}'. The response setsrecompute_pending, and the queue shows the recompute until it finishes. - Per-campaign model:
p202 campaign update <id> --attribution-model-id <model_id>credits that campaign's conversions with a model other than the default.0returns the campaign to the default. - Audit trail: every model create, update and delete, and every export, writes a row to
202_attribution_audit. The actions aremodel_created,model_updated(with the fields changed and whether it recomputed),model_deletedandexport_created. Neither the CLI nor the API reads these rows; this is SQL only:SELECT FROM_UNIXTIME(created_at) AS at, action, model_id, metadata FROM 202_attribution_audit WHERE user_id = 1 ORDER BY audit_id DESC LIMIT 20;
3. Journey validation
- Check one conversion end to end.
p202 attribution journey <conv_id>shows three things.toucheslists each click in order, with the signals that linked it.journeyrecords how the journey was built.creditsgives every model's split. From a two-touch conversion worth 208.00:
Expect positions to start at 0 and run without gaps, with the converting click last and present exactly once. Under each model, credits add up to 1 and revenue to"journey": {"built_lookback_days": 30, "identified": true, "touches": 2, "truncated": false}, "credits": [ {"model_name": "Last touch", "touches": [{"position": 1, "credit": "1.00000000", "revenue": "208.00000"}]}, {"model_name": "First touch", "touches": [{"position": 0, "credit": "1.00000000", "revenue": "208.00000"}]}, {"model_name": "Position based (40/20/40)", "touches": [ {"position": 0, "credit": "0.50000000", "revenue": "104.00000"}, {"position": 1, "credit": "0.50000000", "revenue": "104.00000"}]} ]amount. A conversion withcounted: falsehas no credits; it was superseded, deleted or fully reversed.p202 click conversions <click-id>explains why a conversion counts or doesn't. - Check the population.
p202 attribution journeys --period last7gives the journey-length distribution, time to convert, and the one-touch share by browser. If nearly every journey is one touch, clicks aren't being joined. Check:- The campaign's
--identity-signalssetting (1 links its clicks into journeys). - That landing pages load the tracking script.
- That clicks go through your tracking domain.
- The campaign's
- Long journeys. A journey over 25 touches keeps the newest 25, including the converting click, and records
truncated: 1in its journey meta. - Database invariants (read-only SQL). Both queries should return no rows:
-- every stored journey has positions 0..touches-1 SELECT m.conv_id FROM 202_attribution_journey_meta m JOIN 202_attribution_journeys j ON j.conv_id = m.conv_id GROUP BY m.conv_id, m.touches HAVING COUNT(*) <> m.touches OR MAX(j.position) <> m.touches - 1; -- under every model, a conversion's credits add up to exactly 1 SELECT conv_id, model_id, SUM(credit) AS total FROM 202_attribution_credits GROUP BY conv_id, model_id HAVING total <> 1;
4. Reports and exports
- Totals reconcile.
p202 attribution breakdown --group-by campaign --period last30credits each campaign with its own model, or the default if it has none.--model <id>picks one model, and--compare-model <id>adds a second model's columns side by side. Every model credits the same total revenue; they differ only in which clicks receive it.--cohort clickcounts what the range's clicks earned, as the classic reports do. - 409 from a report means the model it asked for is inactive or invalid. Report on an active model, or fix that one.
- Exports.
p202 attribution export createqueues a breakdown to CSV, optionally sent to a signed https webhook. The minutely cron runs due exports. For details and recovery:p202 attribution export get <id>shows an export's status, row count, webhook answer andlast_error.p202 attribution export retry <id>queues a failed export again.p202 attribution export download <id>fetches the file, whatever the webhook did.- To watch an export run, run
php 202-cronjobs/attribution-exports.phpby hand.
P202_WEBHOOK_ALLOW_NETWORKSin202-config.php, written in CIDR form.
5. Recompute and repair
- Don't delete journey or credit rows by hand. Nothing re-queues a conversion whose rows are gone. A raw
DELETEalso skips the report-hour marks every engine write leaves, so reports for those hours go stale. - Recompute a model by changing it, with
p202 attribution model update. Any change to its type, weighting or lookback re-queues every conversion in the account. - Watch the worker by running it in a terminal:
php 202-cronjobs/attribution-worker.php --budget=300. Its only options are--budget=N(1–3600 seconds) and--retry-now. It prints one line per run, which also lands in the cron log:attribution-worker: processed 42 (cleared=2, credited=40); merges re-queued 1; model changes fanned out 0; still due 0
6. Incident response and rollback
- A model is producing bad credit.
- If it's the default, move the default to a safe model first:
p202 attribution model update <safe_id> --default. - Then switch the bad model off:
p202 attribution model update <bad_id> --status inactive. The other models keep computing. - To remove it entirely, run
p202 attribution model delete <id> --dry-runfirst. Delete removes the model's credits and exports, and is refused while one of its exports is running.
PUT /api/v3/attribution/models/{id}with{"status": "inactive"}. The API refuses fields it doesn't know with a 422, including the oldis_active. - If it's the default, move the default to a safe model first:
- Reports have stopped updating. Work through the health check. A growing
pendingwith a stale every-minute cron row means the cron isn't running. Run the worker by hand to see its error. - An upgrade went wrong. The upgrade changes the database in place, so restoring the backup taken before it is the only way back. Putting the old files back doesn't roll it back.
- Escalate with these attached:
- The output of
p202 attribution queue --jsonandp202 attribution model list --json. - The worker's cron log lines.
- The latest
202_attribution_auditrows. p202 system info.
- The output of
Written for Prosper202 1.9.77. Check this runbook against the schema, cron and commands whenever the attribution engine changes.