Tracking guide
Deduplication
How duplicate clicks, duplicate conversions and repeated postbacks are detected, and the settings that change the behaviour.
Dedupe is on by default and has three independent layers: clicks, conversions, and events on the wire.
Duplicate clicks
At click time the edge builds a session key and checks whether it has been seen inside the window. Per offer:
| Setting | Values | Meaning |
|---|---|---|
session_definition | cookie, ip, ip_user_agent, gaid, idfa, click_id_only | What identifies "the same visitor". The default is click_id_only, under which every click is unique; choose another definition to detect repeats. cookie is opt-in and sets a first-party cookie (dt_<offer>); ip_user_agent is the usual stateless choice; device ids suit app traffic. |
unique_click_session_hours | hours, default 24 | The window. |
duplicate_click_action | count, block, fail_traffic | count keeps the duplicate as a valid click, block and fail_traffic mark it invalid with reason duplicate. |
The first click per key is flagged is_unique; later ones is_duplicate. Reports expose clicks (valid, including duplicates unless blocked), unique_clicks and gross_clicks (all, including invalid).
Duplicate conversions
When a conversion arrives, ingest claims a de-duplication key atomically (Redis SETNX), so two concurrent requests cannot both succeed:
| Situation | Key |
|---|---|
txn_id present | offer + goal + txn_id |
no txn_id, click known | click + goal |
| neither | unique per request (no dedupe) |
| offer or goal allows duplicates | unique per request |
A second hit with the same key answers {"status":"duplicate"} and changes nothing. If the advertiser sends the same txn_id with a different status (and the advertiser uses a security mode other than none), the existing conversion is updated instead and the answer is updated; with security_token mode the token is re-checked.
| Setting | Level | Effect |
|---|---|---|
is_allow_duplicate_conversion | offer | Every hit creates a conversion. |
is_allow_duplicate | goal | Same for one goal. |
Several conversions from one click (for example recurring deposits) are fine as long as each has its own txn_id; without a txn_id a second conversion for the same click and goal is a duplicate. The offer fields track_multi_conversions and already_converted_action, and the goal field track_multi, are stored in the contract but are not consulted by the ingest pipeline yet.
Every new conversion's keys are also recorded in Postgres (conversion_keys) before the conversion is created, in one atomic statement, so exactly-once never depends on Redis keeping its data: if Redis is flushed, restarted without persistence or replaced by an empty replica, a resent postback finds its original there and is answered duplicate (or queued until the original is readable). If Redis or Postgres cannot answer, the conversion is queued ({"status":"queued"}) and processed once they can — never guessed as unique or as a duplicate. Redis keeps conversion state and keys for 45 days; older ones are read from ClickHouse and Postgres.
Retries after a crash
A claim remembers which ingest process holds it while the conversion is being created. If that process dies half-way (a crash, a deploy that kills it), the advertiser's request got no answer and is retried; the retry finds the claim of a process that is gone, takes it over and creates the conversion with the same conversion id — it is not answered duplicate. A retry that arrives while the first attempt is still running is answered queued and completes once that attempt has finished (or is known to be dead). A retry of a postback that was turned down because its click already converted gets the same answer again (duplicate, reason already_converted, with the id of the conversion that holds the click). Set DT_INGEST_CLAIM_OWNER=0 to switch this off (a claim is then taken over only after 30 seconds).
A queued answer is a promise: the conversion is created exactly once, under that id (or under the id of a copy of the same transaction that was queued too — find it by transaction id). Ingest never answers duplicate unless the conversion exists or the click's first conversion is known: if taking over a dead attempt's claim cannot be confirmed, the request is queued instead. How long a claim has been held is measured by Redis, so ingest servers whose clocks disagree still agree on it, and a restart lets the postbacks being processed finish before it stops.
Duplicate events on the wire
Internal events carry unique ids. NATS publishers set Nats-Msg-Id to the event id so JetStream drops redelivered duplicates, and the ClickHouse writers keep a dedupe token (tenant:id[:version]) in Redis. A status change is a new version of the same conversion; rows are append-only and the current view takes the latest version, so replays never double-count.
Outbound
Webhooks are at-least-once: de-duplicate on the envelope id. Ad-platform postbacks use the conversion id as the platform's event_id/orderId so the platform can de-duplicate against browser events. The web SDK also drops a repeated txn_id in the same browser.