getParams (or decide), the SDK produces a decision: which parameters were resolved, which allocations the user fell into, and a decisionId that links downstream events back to this point in time.
Decisions are the backbone of attribution — they’re how a purchase track event ends up tied to the treatment variant of the pricing_test policy that the user saw fifteen minutes earlier.
The decision record
Conceptually:decide(). getParams() returns just the resolved assignments map, but the decision is still produced and tracked internally.
What gets emitted
A single resolution can emit three things:| Event | Emitted when | Purpose |
|---|---|---|
| Decision event | Always (unless trackDecisions: false) | Records that the SDK decided. Used for intent-to-treat. |
| Exposure event | When parameter values are read | Records the user saw the variant. Used for treatment-on-treated metrics. |
| Track events | When you call track() | Records outcomes. Causally linked via decisionId. |
Decision vs exposure: when does it matter?
Decision vs exposure: when does it matter?
Suppose a user lands on a page where the SDK resolves a layout parameter — the SDK records a decision. But the user closes the tab before the layout renders.
- A decision-event-based analysis counts this user in the variant they were assigned to. This is intent-to-treat: the experiment offered them the variant, regardless of whether they saw it.
- An exposure-event-based analysis only counts users whose code paths actually read the variant. This is treatment-on-treated: only people who really saw the change.
The decisionId
The decisionId is the link between a parameter resolution and the events that follow it.
decisionId automatically — track() calls inherit the most recent decision for that unit key. You don’t usually need to pass it explicitly.
The places where you do need to pass decisionId explicitly are:
- Cross-process — backend resolves, frontend tracks. Send
decisionIdin your API response and pass it totrack()on the client. - Batch / offline — your batch job decides, the outcome event comes from an email provider or webhook days later. Thread
decisionIdthrough the external system.
Attribution modes
The SDK supports two attribution modes:| Mode | What it does | Use when |
|---|---|---|
cumulative (default) | Attributes a track event to all the layers the user was exposed to during the session. | The outcome plausibly depends on more than one experiment — e.g. catalog → PDP → checkout funnels. |
decision | Attributes only to the specific decision named in the track event. | You need clean single-decision attribution, often for warehouse-native analyses. |
How attribution actually runs
There are two layers of attribution worth understanding: 1. In the SDK (per track event). When you calltrack(), the SDK builds an attribution array and embeds it on the event payload. In cumulative mode (default), that array contains every layer/policy/allocation the user has been exposed to in this session — deduplicated, last-write-wins per layer. In decision mode, it contains only the layers from the decision named by decisionId. This array is the SDK’s contribution to attribution.
2. In the pipeline (per metric). When the warehouse-native pipeline computes a metric, it joins track events to assignments on unit_key with a temporal constraint: a track event counts toward an allocation if it happened after the user’s first exposure to that allocation. The join key is unit_key, not decisionId. The temporal ordering replaces what a “time window” would do — but it’s open-ended, not capped.
The practical implications:
- For events sent through the SDK (the normal case), the SDK’s
attributionarray drives dashboard breakdowns, and the pipeline’s temporal join drives metric numerators. Both are populated automatically — you don’t have to think about it. - For events sent from outside the SDK (a webhook hitting
/v1/eventsdirectly), the pipeline’s temporal join still works as long as the event carries theunit_keyof an already-exposed user. NodecisionIdrequired. - Pass
decisionIdexplicitly when you need it: indecisionmode (where attribution is restricted to a single named decision), or across processes where the cumulative session cache lives on a different machine. The SDK’s per-eventattributionarray picks it up.
External assignments
Decisions don’t have to come from a Traffical SDK at all. If your assignments live in your data warehouse — say, aexperiment_assignments table maintained by another tool — you can use those as the source of truth and have Traffical compute metrics on top. This is the warehouse-native mode. The assignment table replaces the decision event stream; everything else (track events, metrics, significance) works the same way.
See warehouse-native for the setup.
Next steps
Events
Exposure, decision, and track events in detail.
Warehouse-native
Use your warehouse as the source of truth for assignments and metrics.
Canonical experiments
See the patterns where
decisionId matters.