One person across browser and backend
A visitor reads Acme’s pricing page, clicks into the app, signs up, and later pays from a webhook. That is four places and at least two domains, and it should be one person in every report. This walkthrough wires it up end to end and shows you how to check it.
Updated
The story
- Maya lands on
acme.com/pricingfrom an ad. She’s anonymous. - She clicks Start free trial and arrives on
app.acme.com. - She signs up. Acme’s ID for her is
user_4821. - The API records
Workspace created; a week later billing recordsInvoice paid. - She signs in on her phone, and signs out of a shared laptop.
ClickClacks never guesses who someone is from an IP address or device. It joins these moments only through the IDs you give it, so each step below is about passing the right ID at the right time. This walkthrough assumes the tracker is installed on both domains and at least one server source exists (see the first events and Node walkthroughs).
1. Choose the ID
Pick one ID per user and use it everywhere: the browser’s identify, every server distinctId, every distinct_id over HTTP.
- Stable and internal, such as your database key. Not an email address, which can change.
- The exact same string on every side.
user_4821,4821andUSER_4821are three different people. - 1 to 128 characters. If your IDs are numbers, turn them into strings the same way everywhere.
2. List and carry your domains
Browsers keep storage separate for every hostname, so acme.com can’t read the ID set on app.acme.com. ClickClacks carries the anonymous ID across on link clicks between listed domains.
- Open Sources › Identity.
- Check every hostname visitors see is listed:
acme.com,www.acme.comandapp.acme.com. Click Add a domain for any that are missing. - Turn on Carry identity between these domains.
- If you added a domain, copy the updated snippet from the source (or add the hostname to
data-domainsyourself). The browser checks the attribute; the server checks the saved list.
3. Follow the anonymous visit
On her first page load the tracker gives Maya’s browser a random anonymous ID (per_…) in local storage. When she clicks a link to app.acme.com, the tracker adds a short _ccid parameter carrying that ID and her session; the app adopts it and removes the parameter from the address bar. Try it yourself:
// On acme.com, then again on app.acme.com after clicking through
localStorage.getItem('ccp') // 'per_…' the anonymous ID: the same on both after the hop
localStorage.getItem('cci') // null until identify; then 'user_4821'Only real link clicks carry identity. A typed address, a bookmark, a link in an email, a form submission or navigation done in script starts a fresh anonymous visitor on the other domain. Where carry-over stops has the full list.
4. Identify at sign-up and sign-in
When Maya signs up, call identify with her ID. Everything the browser did while anonymous, including the carried-over pricing page visit, now belongs to user_4821. Call it again at every sign-in and on page loads while she’s signed in: repeating it is harmless, and $identify events are free.
// app.acme.com: after sign-up, after sign-in, and on each page load while signed in
if (currentUser) {
window.clickclacks?.('identify', currentUser.id, { plan: currentUser.plan })
} The identified ID is stored per hostname and doesn’t travel with the link. If people are also signed in on acme.com (say, a “Go to app” button), call identify there too.
5. Send server events with the same ID
Server events join the person who owns the distinctId. The order doesn’t matter: if the server sends an ID no browser has identified with yet, ClickClacks creates the person, and the browser joins them at its first identify.
clickclacks.track({
event: 'Workspace created',
distinctId: String(req.user.id), // 'user_4821': the same string the browser identified with
properties: { template: 'blank' },
}) A server event without a session ID isn’t part of any browser visit, so it doesn’t add to session counts. To put a server event in the visit’s path, pass the browser’s ses_… session ID as sessionId; the next step shows how to get it to your server, and Sessions has the rules.
6. Link the browser from your backend
The browser identify can be missed: an ad blocker, a tab closed during a redirect. Your backend is more reliable, and it can link the browser too. Send the tracker’s IDs with the sign-up request:
// app.acme.com: send the tracker's IDs with the sign-up request
await fetch('/api/signup', {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-clickclacks-anonymous-id': localStorage.getItem('ccp') ?? '',
'x-clickclacks-session-id': sessionStorage.getItem('ccs') ?? '',
},
body: JSON.stringify(form),
})Then identify with both IDs on the server:
app.post('/api/signup', async (req, res) => {
const user = await createUser(req.body)
const anonymousId = req.get('x-clickclacks-anonymous-id') ?? ''
// Links that browser to the new user, exactly as identify in the browser would.
clickclacks.identify({
distinctId: String(user.id),
anonymousId: anonymousId.startsWith('per_') ? anonymousId : undefined,
properties: { plan: 'free' },
})
res.status(201).json(user)
})- An identify with both
distinctIdandanonymousIdlinks that browser to the ID, exactly as the browser’s ownidentifywould. - An anonymous ID that doesn’t start with
per_is refused (invalid_anonymous_id). Before the tracker has run, or after the visitor opted out, there isn’t one, so leave the field out, as above. - Keep the browser’s
identifyas well. It sets the ID in the browser, so later events carry it.
7. Reset on sign-out
async function signOut() {
await fetch('/api/sign-out', { method: 'POST' })
window.clickclacks?.('reset')
}reset forgets the identified ID and starts a new anonymous ID and session. Without it, the next person on a shared laptop is recorded as Maya until they sign in, and a browser that already belongs to one ID isn’t moved to another.
8. Check the person
- Open People and search for
user_4821. - Open the profile. Read Activities, then the Identity panel in the right-hand rail.
Links between IDs are recorded just after the request is accepted, so a brand-new link can take a moment to show. Reports read identity when they run, so a later identify also joins events sent before it.
9. A second device
There’s no separate linking call. When Maya signs in on her phone and your app calls identify('user_4821'), that browser is linked to her, with its earlier anonymous history. The first browser to identify with an ID owns it; later ones join it.
10. Fix a split person
If one human still shows up as two records (say, two sign-ups with different emails before you fixed the ID), merge them by hand:
- Open one of them in People and choose ⋯ › Merge with….
- Search for the other, and under Keep ID and traits from choose which record wins.
- Review the totals and click Merge records.
Events are never rewritten, and Unmerge splits them again. Merging needs the Manage Organization Settings permission and is part of the Pro plan. See Merging two records.
Pitfalls
- Email as the ID. Someone changes their email and becomes a new person. Put email in traits if your team needs it, never in the ID.
- A different format on each side. The browser sends
user_4821, the server sends4821. Build the ID in one shared function. - Identify only on the sign-up page. People already signed in before you added ClickClacks, or signed in on another domain, never get identified. Call it on every page load while signed in.
- Calling
resetanywhere but sign-out. On every page load it throws the visitor’s history away each time. - A redirect that drops the query string between your domains removes
_ccid. So doesrel="noreferrer"or ano-referrerpolicy, because the destination only accepts the ID when the referrer is a listed domain. A hop below 80% usually means one of these. - Sending a guessed anonymous ID. Only send a
per_…value you read from the tracker; anything else is refused. - Separate projects for site and app. People are joined within one project. Put every surface of the product in one project, as separate sources.
Next
Not on Node or PHP? The last developer walkthrough sends the same events from any language: Any language over HTTP. Or jump to the analyst track and find where signups drop off, which depends on everything above.