Identity & cross‑device¶
nilalytics tracks users across devices without knowing who they are, using a deterministic, pseudonymous model. There is no fingerprinting.
Three IDs¶
| Attribute | Meaning | Set when |
|---|---|---|
anonymous.id |
a device / browser (random UUID, cookieless) | always |
session.id |
one visit / app session | always |
user.id |
a person, as a salted hash | after the user connects a shared key (login) |
Before login, two devices are genuinely unlinkable — the privacy‑correct default. They merge only once the user connects them.
Pseudonymous by design¶
user.id is a salted hash of a shared key (email, account id), computed
client‑side. The lake stores a stable person‑key it cannot reverse into a
real identity.
# how nilalytics hashes (client-side)
import hashlib
def hash_key(raw, salt):
return hashlib.sha256((salt + raw).encode()).hexdigest()[:32]
The salt is NILA_ID_SALT (auto‑generated and stored in the secrets file).
How you define a user (any key)¶
nilalytics is key‑agnostic — the key can be anything stable and unique:
| Key | Works? | Note |
|---|---|---|
| internal account / user id | ✅ best | stable, unique, you control the format |
| ✅ | can change; normalize first | |
| phone | ✅ | normalize to one format (E.164) |
Two rules:
- Normalize before hashing —
A@x.comanda@x.comhash to different people. Lowercase emails, use E.164 for phones. - Use the same key at login and at lookup.
identify("alireza@example.com") -> person_id 3f9a…
lookup "alireza@example.com" -> person_id 3f9a… ✅ matches
Look someone up by their key¶
You don't need to hash by hand. identify prints the person id for a raw key, and
query user --key does the hashing for you:
nilalytics identify alireza@example.com # -> 3f9a1c...
nilalytics query user --key alireza@example.com 3 # that person's activity, last 3 days
The identify event¶
When a device learns the user, it emits an identify event linking that
device's anonymous.id to the hashed user.id. That single event is what makes
cross‑device stitching possible.
flowchart LR
P["phone<br/>anonymous.id = A1"] -->|identify| U(("user.id<br/>= hash(email)"))
L["laptop<br/>anonymous.id = B7"] -->|identify| U
U --> S["same hash → same person"]
Stitching¶
The stitch query builds the identity graph and unifies a person's activity
across all their devices:
person -> devices:
88a52ac6... 2 device(s) <-- multi-device
...
persons seen on >1 device: 5
unified events per person (across their devices):
88a52ac6... 16 events across devices
In your apps¶
- Web (Faro):
faro.api.setSession({id}),faro.api.setUser({id: hash}), and a persistedanonymous.id. See Web. - Mobile (OTel): the same three attributes, with
anonymous.idin Keychain / SharedPreferences. See Mobile.
Privacy notes¶
- Deterministic + hashed + consented is the defensible model; fingerprinting is not.
- Add a consent flag to events so you can honor opt‑outs.
- This is guidance, not legal advice.