Swetrix

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:

  1. The visitor's IP address
  2. The visitor's User‑Agent string (browser + OS)
  3. 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.

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 call reset() 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 localStorage value 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.

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 profileId stored in a cookie or localStorage — 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 deviceNoneWhatever you choose to persist
Identifier sourceHashed IP + User-Agent + rotating saltYour application's user ID
Accuracy on a typical consumer siteHigh — collisions are rarePer-user accurate (for signed-in users)
Accuracy behind a school / office NATLower — devices may collidePer-user accurate
Cross-device trackingNoYes (same profileId on each)
Pre-login activity linkedYes (via identify())
Works without JavaScriptYes (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.

On this page