How Swetrix identifies visitors (without cookies)
Swetrix is a cookieless analytics platform. We never set a cookie
on your visitors, never write to localStorage, and never read any
client-side identifier — yet we can still tell you how many unique
visitors you had today, how many sessions started on /pricing, and
which of your campaigns are bringing back returning users.
This page explains, in plain English, how that's possible, what the trade‑offs are (especially for schools and large companies behind a NAT), and how you can opt into more accurate identification when you control your users' identity.
How "no cookies" actually works
Most analytics tools stamp every visitor with a cookie like
_ga=GA1.2.123456789 so they can recognise the same person on their
next pageview. Swetrix doesn't do that. Instead, every time a pageview
arrives at our servers we compute a short‑lived anonymous fingerprint
from three pieces of information that are already part of the request:
- The visitor's IP address
- The visitor's User‑Agent string (browser + OS)
- Your project ID
Those three values are combined with a secret rotating salt (a
random string only our backend knows) and run through a one‑way hash
function. The result is an opaque ID like anon_8214637194021987452.
That ID is what we use to deduplicate pageviews — same ID = same visitor — but because the salt is rotated and the original IP / UA are never stored, there's no way to go from the ID back to a real person.
We use two salts that rotate on different schedules:
- Session salt — rotates every 24 hours at UTC midnight. Used to recognise the same visitor across the pageviews they make during a day.
- Profile salt — rotates every month. Used to detect returning visitors over a longer period (DAU / WAU / MAU charts).
When a salt rotates, the previous fingerprints become permanently unlinkable to the new ones. This design is what allows Swetrix to operate without persistent identifiers on the visitor's device.
A session is a separate thing from either salt: it starts on a visitor's first tracked event and ends after 30 minutes of inactivity. Someone who reads a page in the morning and comes back in the evening counts as one visitor and two sessions. Because a session is bounded by inactivity rather than by the salt, a session that is still running at UTC midnight is split when the session salt rotates.
What we don't store
We never persist your visitors' IP addresses or User‑Agent strings in their original form. The salted hash is computed at the edge, written to ClickHouse, and the raw values are discarded. After 30 minutes (or at the next UTC midnight, whichever comes first) even the temporary in‑memory mapping is gone.
Why this is good enough for ~95% of sites
For a typical content site, SaaS landing page, or e‑commerce store the IP+UA fingerprint is unique per visitor in well over 95% of cases. Two completely unrelated people would have to be on the same network, using the same browser version, on the same OS version, at the same time to collide.
For the remaining 5% — and the edge cases where this assumption breaks down more often — read on.
When the cookieless model is less accurate
There are a few situations where many real people share the same IP + User‑Agent fingerprint, which means Swetrix may undercount unique visitors (multiple real users → one fingerprint).
For consumer‑facing sites with broad audiences these edge cases tend to wash out in the aggregate. For internal dashboards, B2B SaaS, education products, or enterprise tools where the audience really is sitting behind a small number of NATs, the next section is for you.
Optional: accurate tracking with profileId
If you already know who your visitors are — they sign in to your app, have an account, or even just have a stable client-side identifier you generate yourself — you can tell Swetrix about it and get per‑user accuracy that doesn't depend on IP or User‑Agent at all.
The recommended way is identify():
swetrix.init("YOUR_PROJECT_ID");
swetrix.trackViews();
// After the user logs in (or on page load if they're already logged in)
swetrix.identify("user-12345");
// On logout
swetrix.reset();Your user ID is stored as you send it — bar surrounding whitespace,
which is trimmed — behind a usr_ prefix, so the profile that shows up
in your dashboard is usr_user-12345, and you can look a user up by the
same ID you use in your own database. Only send identifiers you're
comfortable seeing in the dashboard.
Alternatively, you can pass profileId to individual tracking methods —
it's supported on init(), track(), pageview(),
getFeatureFlags() / getFeatureFlag(), and getExperiments() /
getExperiment(). See the script reference
for the full list. The difference: a plain profileId only stamps the
events it's passed with, while identify() additionally links the
visitor's anonymous history, as described below.
How identify() links anonymous history
A visitor usually browses your site anonymously before they sign in —
so by the time you know who they are, Swetrix has already recorded
their pageviews under an anonymous anon_… fingerprint. Without extra
care, identifying them would simply start a second, separate profile
and the pre-login activity would stay orphaned.
identify() solves this: when called, the server links the visitor's
current anonymous profile to the identified one, and the dashboard
attributes the anonymous activity — sessions, pageviews, events,
errors — to the identified profile. Even the session during which the
user signed in stays intact: the pageviews before and after login show
up as one session under the identified profile.
The rules, in short:
- First identification wins. An anonymous profile can only ever be linked to one identified profile. If a second person logs into a different account on a shared device, their events are tracked under their own identity going forward, but the shared anonymous history stays with the first account. This mirrors how PostHog and other analytics tools guard against runaway profile merging.
- About a month of history. Anonymous fingerprints rotate monthly (see above), so the linking covers the visitor's recent anonymous activity on that device and network. Each month they return logged-in, the new anonymous fingerprint is linked too.
- Cross-device works. Call
identify()with the same ID on any device or browser, and all of that activity is unified under one profile in the dashboard. - Nothing is stored client-side. Swetrix remains cookieless — call
identify()on every page load while the user is logged in (repeated calls are deduplicated), and callreset()on logout so the next person on a shared device isn't tracked under the previous user's identity.
What you can use as a profile ID
Anything stable, opaque to outsiders, and ideally per‑user. Some common choices:
- Your application's user ID from your own database (e.g.
user-12345). The most accurate option for signed‑in users. - A first‑party cookie or
localStoragevalue that you generate yourself (e.g.crypto.randomUUID()stored on the client). This works for anonymous users too, but because you (not Swetrix) are now setting a persistent identifier on the device, you should review whether your local privacy regulations require user consent for it. - A pseudonymous account identifier if you already use one for other systems. Just make sure it's stable for the same user.
- A device or installation ID in mobile or desktop apps embedding a webview.
The profileId is stored as provided, apart from surrounding whitespace, which is trimmed —
Swetrix doesn't hash or otherwise transform it. Prefer an opaque internal ID over email addresses,
full names or phone numbers, and pseudonymise on your side if your privacy policy requires it. If
you do want a user's email on their profile, attach it as a trait instead of using
it as the ID.
User traits
An ID on its own tells you that two sessions belong to the same person, not who they are. Pass traits alongside it and Swetrix shows them in a "User traits" section on the profile page:
swetrix.identify("user-12345", {
email: "john@example.com",
name: "John Doe",
plan: "premium",
signupDate: "2026-01-14",
});Traits are free-form — any key / value pair you find useful. They're
merged per key across calls, so a later identify() (or
setTraits()) only overwrites the
keys it carries, and passing null removes one:
// Later, without repeating the user ID
swetrix.setTraits({ plan: "enterprise", trialEndsAt: null });The limits per call: up to 50 keys, keys of up to 128 characters, and
2000 characters for all keys and values combined. Values must be
strings, numbers, booleans or null, and are stored as strings.
Traits are the one place where personal data legitimately ends up in Swetrix — an email address on a profile is exactly what the feature is for. Send only what you actually need, and make sure your privacy policy covers it.
Notes on consent
In its default cookieless mode, Swetrix does not set any identifier on the visitor's device — the IP+UA fingerprint is computed on our servers, salted with a rotating secret, and the raw inputs are discarded. Many teams operating in jurisdictions like the EU, UK, and Brazil have concluded that this design lets them run Swetrix without showing a cookie / consent banner, but the final call always depends on your specific use case and your legal advice.
Once you opt into profileId and persist that ID on the client (a
cookie, localStorage, etc.), you've added a persistent identifier
that local privacy regulations may treat differently. Two patterns we
see most often:
- Signed‑in users — many teams cover this under the same lawful basis they already use for the user account itself, and disclose analytics processing in their privacy policy.
- Anonymous users with a client-generated
profileIdstored in a cookie orlocalStorage— this is closer to traditional cookie-based tracking, and most teams gate it behind their existing consent flow.
A common hybrid setup:
// Cookieless analytics from the first pageview.
swetrix.init("YOUR_PROJECT_ID");
swetrix.trackViews();
// Upgrade to accurate per-user tracking once the user signs in
// (or once the user grants consent in your banner, if applicable).
// This also links their pre-login anonymous activity to the profile.
afterLogin((user) => {
swetrix.identify(user.id);
});This pattern gives you analytics on the public site immediately and accurate identified-user tracking inside the logged-in product, while keeping each surface aligned with whatever consent posture you've chosen.
This page is not legal advice. Privacy regulations vary by jurisdiction and by use case — review your specific setup with a qualified advisor before deciding whether you need a consent banner.
Summary
| Cookieless (default) | With profileId | |
|---|---|---|
| Persistent identifier on the device | None | Whatever you choose to persist |
| Identifier source | Hashed IP + User-Agent + rotating salt | Your application's user ID |
| Accuracy on a typical consumer site | High — collisions are rare | Per-user accurate (for signed-in users) |
| Accuracy behind a school / office NAT | Lower — devices may collide | Per-user accurate |
| Cross-device tracking | No | Yes (same profileId on each) |
| Pre-login activity linked | — | Yes (via identify()) |
| Works without JavaScript | Yes (via the noscript pixel) | No (needs the JS tracker) |
Pick the default for marketing sites, blogs, docs, and anywhere you
want a low‑friction, no‑identifier setup. Layer in profileId
whenever you need exact per‑user numbers — typically inside the
authenticated parts of a SaaS product, in B2B / enterprise tools, or
in any context where shared NAT is going to dominate the IP+UA signal.
Help us improve Swetrix
Was this page helpful to you?
