Identify and traits
Attach a person to a visitor, and keep your onboarding answers on them.
When someone logs in or gives you their details, say so. Everything that visitor did before is attached to them, and everything after.
identify alone does not send a network request. It stores the user id and
contact details on the tag; the next track (usually sign_up or login)
carries them. Call identify first, then the event. Without
advertising consent, contact details are held back
even when you pass them; traits are sent either way.
vibiz.setConsent({ advertising: true }); // or from your CMP
vibiz.identify('cust_8842', {
email: 'mario.rossi@example.com',
phone: '+39 333 123 4567',
});
vibiz.track('sign_up', { method: 'email' });This closes the gap between browsing and buying. Someone browses anonymously for a week, logs in once, and buys a month later. The login is the only moment they say who they are, and it is rarely the moment that matters commercially.
Where to call it
identify lives in memory for the page it runs on. Nothing is remembered for
the next page load, so calling it only in the login handler names a person once
and never again: a returning user with a live session never passes that screen.
Call it wherever your app READS the session — the layout, the auth provider, the effect that restores a session — and again, in the same handler, right before the events that become records:
| Moment | Why |
|---|---|
| Every load where a session exists | Returning visitors never log in again |
| Signup, login | The person tells you who they are |
begin_checkout, purchase | This is the row you will want a name on |
schedule, contact, a lead form | Same: they become people in your CRM |
subscribe, start_trial, refund | Money events belong to a person |
// wherever the session is read, not only at login
useEffect(() => {
if (!user) return;
vibiz.identify(user.id, { email: user.email });
}, [user]);
// and again where it counts, from the order rather than the session:
// a guest checkout has no session, but the form has the details
vibiz.identify(order.userId ?? order.id, {
email: order.email,
phone: order.phone,
});
vibiz.track('purchase', { value: order.total, currency: 'EUR' });Two details that decide whether this works:
- The user id travels without advertising consent. It is not a contact detail. Passing it always gives the record a handle instead of an anonymous row, and merges that person's visits.
- Identify does not survive a redirect. A call on a page that navigates away immediately sends nothing, because nothing was tracked from it. Call it on the page that lands.
If the purchase is confirmed by your server (a payment webhook), that report carries the same identifiers. A browser that identified and a server that reported an anonymous sale still leave a record with no name.
Contact details accumulate rather than replace. A login gives an email, a checkout later adds a phone and an address, and both are kept.
Each contact detail is stored twice: hashed, which is the form Meta and Google match on, and readable, which is what lets you answer who signed up rather than only how many. Both come from the same normalised value, so the two can never disagree. From the browser, no contact detail is stored without advertising consent; a form reported from your server keeps the readable ones for your CRM (see consent).
Traits
Pass anything else you know about someone in the same call. Onboarding answers are the usual case, and the most useful: they are first party, structured, and given deliberately by the person answering.
vibiz.identify('cust_8842', {
email: 'mario@agency.com', // recognised: hashed for matching, kept readable
business_type: 'agency', // anything else becomes a trait
team_size: 12,
goals: ['leads', 'sales'],
plan: 'free',
});One bag on the wire, split for you. Trait names are yours: there is no list to
pick from. Keys are levelled first, so businessType, business_type and
Business Type are one trait rather than three rows that each hold part of the
answer.
A contact detail is recognised by its key: email, phone, first_name,
last_name, city, region, postal_code, country, gender,
date_of_birth and external_id. Every other key is a trait, and traits do not
wait for advertising consent: they stay in Vibiz and never reach an ad platform.
That makes the key matter. A contact detail under a name of your own, such as
work_email, is sent and kept as a trait whether or not the visitor consented,
so put contact details under their own keys.
With the npm package, traits type check from @vibiz/analytics 0.5.0.
Traits live on the person, so an answer given at signup is still there on a purchase a month later. They accumulate, and a newer value replaces an older one:
// at signup
vibiz.identify('cust_8842', { plan: 'free', business_type: 'agency' });
// a month later, on upgrade
vibiz.identify('cust_8842', { plan: 'pro' });
// stored: { plan: 'pro', business_type: 'agency' }A multi step onboarding
Send each step as it is answered rather than collecting everything and sending it at the end. People abandon onboarding halfway, and the answers they did give are the ones that explain why.
// step 1, who they are
vibiz.identify(user.id, { email, business_type: businessType });
// step 2, what they want
vibiz.identify(user.id, { goals, team_size: teamSize });
// step 3, budget
vibiz.identify(user.id, { monthly_budget: monthlyBudget });
vibiz.track('sign_up', { method: 'email' });Each call adds to the profile. Nothing needs resending, and a visitor who stops at step two still carries the two answers they gave.
This is what lets a question be answered that neither analytics nor an ad platform can answer alone: which creative brings the customers who say they are an agency, and are they the ones who upgrade.
Traits are attributes, not notes
Strings, numbers, booleans and lists of strings, up to 50 of them. Nested objects are dropped, because an answer that cannot be grouped by cannot segment anything. Traits are never forwarded to an ad platform, so do not use them as a place to keep free text about a person.