Resolves incoming tracking signals to a single visitor record.
Signal priority (highest to lowest):
- person_external_id - post-signup, definitive identity
- ga_id cookie - our own attribution cookie, high confidence
- person_email - email address, high confidence
- fingerprint - ThumbmarkJS browser fingerprint, ~90% unique
- anonymous_id - random cookie, session-level identity
- click_id - from a specific link click
When multiple signals match different visitor records, we MERGE them into a single record (keeping the oldest as primary), subject to merge confidence rules (fingerprint alone never triggers a merge).
Summary
Functions
Creates a new visitor from signals.
Finds visitor candidates matching the given signals.
Identifies a visitor with the host app's known person attributes.
Determines if the matching signals are strong enough to merge candidates.
Merges duplicate visitors into the primary (oldest) visitor.
Resolves signals to a visitor. Creates, updates, or merges as needed.
Updates an existing visitor with new signals.
Functions
Creates a new visitor from signals.
Finds visitor candidates matching the given signals.
Builds a dynamic OR query across all signal types, scoped to workspace and excluding merged visitors. Results are ordered by first_seen_at (oldest first, so primary is deterministic).
Identifies a visitor with the host app's known person attributes.
If person_external_id or person_email already exists on another
non-merged visitor in the same workspace, this triggers a merge into the
older visitor (by first_seen_at) and then applies the remaining
person_attrs (name, metadata, the un-collided identifier, etc.) to
the survivor. The two collision keys are checked in order — person_external_id
first, then person_email — so a single call carrying both can never
produce two merges.
Determines if the matching signals are strong enough to merge candidates.
Merge is allowed when at least one strong signal matches, or when two or more weak signals match. Fingerprint alone never triggers a merge.
Strong signals: person_external_id, ga_id, person_email Weak signals: fingerprint, anonymous_id
Merges duplicate visitors into the primary (oldest) visitor.
Uses Ecto.Multi to atomically:
- Consolidate identity signals into the primary
- Apply new incoming signals
- Reassign all events from duplicates to primary
- Soft-delete duplicates (status = "merged")
- Fire :visitor_merged hook after commit
Resolves signals to a visitor. Creates, updates, or merges as needed.
Options
:workspace_id- required, scopes the lookup
Returns {:ok, visitor} or {:error, reason}.
Updates an existing visitor with new signals.