Behavior analysis needs a stable event identity and a declared user or session unit before any funnel is counted.
Product event contracts: identity, deduplication and eligibility
Specify one event precisely
A checkout-start event should have a stable event ID, actor ID, session ID, cart ID, occurrence time, recorded time, platform and schema version. A page view is not a checkout start merely because the checkout route loaded: retries, prefetches and refreshes can create extra views. Define the business event and its emission point. An event contract keeps production instrumentation and analysis aligned.
Choose the counting unit
A buyer can use two devices and three carts. Counting anonymous device IDs estimates device journeys; counting signed-in users estimates people only when identity is available and permitted. Session funnels answer a different question from user funnels. Document when anonymous and authenticated IDs are joined, and whether a late identity merge restates old reports. Dataset grain prevents an event-to-cart join from multiplying users.
Deduplicate before sequencing
At-least-once delivery can replay one event. Keep the first valid instance for each event ID under a declared revision rule, and record conflicting payloads rather than silently choosing one. If the same business action receives two distinct IDs, event-ID deduplication alone is insufficient; a cart transition key or source transaction ID may be needed. The analysis should publish raw, duplicate and accepted counts by day and platform.
Freeze eligibility
Decide whether employees, test accounts, bots and users without measurement consent are eligible before looking at conversion rates. Keep exclusion reasons and counts. A new consent banner can alter measured population composition even when customer behavior is unchanged. Coverage analysis asks who never entered the event frame.
Audit a concrete fixture
Create 53 raw checkout-start records with three replayed IDs, two conflicting payloads and one missing actor ID. The accepted event count depends on the collision policy, but every discarded record must be accounted for. Compare unique users and unique sessions separately. Never quote a conversion denominator from an event stream that has not passed this reconciliation.
Implementation
def dedupe_checkout_events(raw_events):
accepted = {}
conflicts = []
for event in raw_events:
event_id = event["event_id"]
prior = accepted.get(event_id)
if prior is None:
accepted[event_id] = event
elif prior != event:
conflicts.append(event_id)
return list(accepted.values()), conflicts
accepted_events, conflicting_ids = dedupe_checkout_events([
{"event_id": "evt-47", "actor_id": "buyer-19"},
{"event_id": "evt-47", "actor_id": "buyer-19"},
{"event_id": "evt-47", "actor_id": "buyer-24"},
])
assert len(accepted_events) == 1 and conflicting_ids == ["evt-47"]Performance and operating cost
Deduplication over N records takes O(N) expected time and O(U) memory for U unique event IDs. Identity resolution and conflict review have separate costs; a streaming implementation needs bounded state or a persistent event-key index.
Common Mistakes
- Do not equate a page view with a business event without an emission contract.
- Do not mix device, session and user denominators in one funnel.
- Do not drop conflicting event IDs without preserving an audit record.
Read next
- Ordered funnels: step order, entry rules and conversion windows
- Retention cohorts: return behavior only after a full observation window
- Sampling frames and coverage error: who could enter the analysis?
- Project: audit checkout conversion and repeat purchase
Continue the workflow: Identifier normalization and exact linkage without false merges.
