Classic vs Split Mode Identity
An opt-in, per-project change to how Attribution stores visitor identities, and how it reshapes the visitors table in your data export.
If your project is switched to split identity mode, the rows in the visitors table of your data export are laid out differently from what the standard data schema describes. This page explains the difference and how to query the new layout.
No table structure changes. Every table keeps the same columns as in the standard schema. What changes is only how rows are written into the visitors table: the same data, laid out across rows differently. The events, users, browsers, visits, filters, params and properties tables are unaffected.
Classic mode (before)
In the classic mode a single visitor row can hold both identifiers at once. A row takes one of three shapes:
| Shape | user_id | browser_id |
|---|---|---|
| Anonymous visitor | NULL | set |
| Identified, no anonymous id (e.g. server-side) | set | NULL |
| Identified in a browser | set | set (combined row) |
Two behaviors of the classic mode worth knowing:
- The
browser_idon a combined row is frozen at the first browser that identified the user. It never changes afterwards, even when all of the user's later activity comes from other devices. migrated_tois set only when visitors are merged by analias()call. Most rows have it NULL.
Split mode (after)
In split identity mode every visitor row holds exactly one identifier, user_id or browser_id, never both. The combined row no longer exists. Instead, a person's identity is a small group of rows linked by migrated_to:
- Main visitor: the row with
migrated_to IS NULL. For an identified person it hasuser_idset andbrowser_idNULL. This row carries all events (events.visitor_idpoints here) and all traits. A visitor who never identified has their browser row as the main visitor. - Device rows: one row per browser or device the person used:
browser_idset,user_idNULL, andmigrated_topointing to the main visitor. Device rows carry no events.
So where the classic mode stored one combined row, the split mode stores two rows, or N+1 rows for a person seen on N devices. Every device is equally visible through migrated_to; there is no "first browser wins" freezing.
migrated_to therefore changes meaning: from an occasional alias-merge marker to the structural link of the identity. To resolve any visitor row to its identity, follow the migrated_to chain until you reach the row where it is NULL. That is the main visitor, and the events and traits live there.
Worked example
A person visits your site anonymously on a laptop (browser 301), then identifies as user 9001, then later opens the site on their phone (browser 302) and identifies again.
Classic mode, two rows:
id | user_id | browser_id | migrated_to | |
|---|---|---|---|---|
| 501 | 9001 | 301 | NULL | combined row, carries all events; keeps the laptop's browser_id forever, even though the phone was used last |
| 502 | NULL | 302 | 501 | anonymous phone visitor, merged into 501 |
Split mode, three rows:
id | user_id | browser_id | migrated_to | |
|---|---|---|---|---|
| 501 | 9001 | NULL | NULL | main visitor, carries all events and traits |
| 502 | NULL | 301 | 501 | laptop device row |
| 503 | NULL | 302 | 501 | phone device row |
How each tracking pattern is stored
The tables below map common tracking-call sequences to the resulting rows in both modes. A person is one user (user_id) on one or two browsers (cookies). In both modes user_id always sits on the main/combined visitor; the difference is in the visitor rows and where the browser ends up.
Classic mode
| Tracking calls | Visitor rows | Events | Browser | User |
|---|---|---|---|---|
identify (cookie + user) | 1 combined | 0 | on the combined row | on the combined row |
page (cookie) → identify (cookie + user) | 1 combined | 1, on the combined row | on the combined row | on the combined row |
server event (user) → identify (cookie + user) | 1 user-only | 1, on the user visitor | not linked to any visitor | on the user visitor |
identify (user) → identify (cookie + user) | 1 user-only | 0 | not linked to any visitor | on the user visitor |
server event (user) → page (user + cookie), no identify | 1 user-only | 2, on the user visitor | not linked (only recorded on the events) | on the user visitor |
two devices, each identifys as the same user | 2: combined + 2nd device (migrated_to combined) | 2, on the combined row | 1st on the combined row; 2nd on its migrated_to row | on the combined row |
Split mode
| Tracking calls | Visitor rows | Events | Browser | User |
|---|---|---|---|---|
identify (cookie + user) | 2: main + device (migrated_to main) | 0 | own device row, migrated_to main | main visitor (no browser) |
page (cookie) → identify (cookie + user) | 2 | 1, on the main visitor | device row, migrated_to main | main visitor |
server event (user) → identify (cookie + user) | 2 | 1, on the main visitor | device row, migrated_to main | main visitor |
identify (user) → identify (cookie + user) | 2 | 0 | device row, migrated_to main | main visitor |
server event (user) → page (user + cookie), no identify | 2 | 2, on the main visitor | device row, migrated_to main | main visitor |
two devices, each identifys as the same user | 3: main + 2 device rows | 2, on the main visitor | both device rows, migrated_to main | main visitor (no browser) |
The headline difference is in the rows where the classic mode says "not linked to any visitor": in the classic mode a browser that first appears after the user is identified, or that arrives without any identify call at all, never gets its own visitor row, so it is only discoverable on the events table. The split mode always gives that browser a device row linked to the main visitor via migrated_to. That, plus never combining a user and browser onto one row, is the gap the split mode closes.
Querying your export
Main visitors (one row per identity, where all events are attached):
SELECT * FROM visitors WHERE migrated_to IS NULL;Finding the devices of a person is the reverse lookup: query visitors where migrated_to equals the main visitor's id, then join browsers on their browser_id.
Resolving a visitor row to its identity means following migrated_to to the main visitor. Normally this is a single step, since device rows point directly at the main visitor's id, but chains can occasionally be longer (for example after alias merges), so resolve iteratively until you reach the row where migrated_to IS NULL.
Events belong to the identity and to the device at the same time: events.visitor_id and events.user_id point at the main visitor and its user, while events.browser_id keeps the original browser the event was captured on (when present). So event queries by visitor_id need no change, and the capturing device remains visible on every event.
Existing data is not migrated
Rows created before your project switched to split identity mode keep their classic shape, including combined rows with both user_id and browser_id. Only data written after the switch uses the split shape. Your queries should tolerate both shapes side by side. In practice that means checking both a visitor's own browser_id (classic) and its device rows linked via migrated_to (split) when looking up browsers.
If you have any questions, write to [email protected].
Updated 1 day ago
