Consent and privacy
What the SDK sends and stores before and after a consent choice, and which data never reaches the database.
Consent is checked in the browser, before an event leaves it, and personal data is cut down again on the server. This page explains what happens to events and stored ids in each consent state, and which data is never stored.
Where the check happens
Every event passes the same checks in the browser, in this order:
- If the browser has opted out, or has Do Not Track or Global Privacy Control on, the event is dropped.
- Plugins and
beforeSendmay change or drop it. - If the client has not started, or consent is required and no choice was made yet, the event is held in memory.
- Otherwise the consent state decides: the event is queued for sending, or dropped.
A dropped event never leaves the browser. analytics.on("drop", (event, reason) => ...) reports each one with the reason opt-out, dnt, consent or beforeSend.
Consent is not checked on the server. The API stores what it receives, so an event that should not be tracked has to be stopped in the browser.
Before a choice
With consent: "optional", the default, events are sent before any choice, and the visitor and session ids are stored as usual.
With consent: "required", events wait in memory and nothing is sent. The visitor and session ids exist in memory, but the SDK does not write them, identify data, registered props or failed batches to storage. The __ra key in localStorage is not written at all until sending is allowed, except for the visitor's own choices: consent, opt-out and the debug switch. 1.x keys are read at once so the visitor keeps its id, and moved into __ra and removed only once sending is allowed. The waiting list has no size limit and lives only as long as the page. A visitor who leaves or reloads before choosing takes those events with them, and in a site where each click loads a new document, events from pages before the choice are never sent.
After consent.grant()
The choice is saved in localStorage, the held events are sent with their original timestamps, and new events go out as usual. The ids that existed in memory are written to storage, so the events before and after the choice share one visitor and session.
After consent.revoke()
The SDK drops the held events and the queue, and forgets the visitor id, the session, the user id and traits from identify, the props from register, and any saved failed batches. It saves the choice, and from then on every event is dropped with the reason consent.
Events already sent stay stored. The browser has no way to delete them, since after revoking the SDK no longer knows the old visitor id either.
A later consent.grant() starts sending again with a new visitor id.
Opt-out
analytics.optOut() is separate from consent and wins over it: while it is set, nothing is sent in any consent state. It drops held and queued events and saves the choice until optIn(). The ignoreSelf plugin calls these two from ?ra=ignore and ?ra=track.
Choices made in one tab apply to other open tabs of the site at once, through the browser's storage event. A tab that revokes consent or opts out stops the others from sending, and they drop their queues. Every write re-reads __ra and changes only its own fields, so a tab opened before the change cannot overwrite it.
What never reaches the database
| Data | What happens to it |
|---|---|
| The IP address | Used in memory for the country, region, city and network lookups and for the daily hash, then dropped. Only the hash sha256(ip + sha256(secret + day)) is stored |
| Cookies | None are set for visitors. The only cookie is the dashboard's admin session |
| The page's query string | The path is sent without it. Only utm_source, utm_medium, utm_campaign, utm_term and utm_content are read from it |
| Query strings, emails, long tokens and long numbers in errors | Removed from error messages, stacks and breadcrumbs |
The /_ra proxy adds the visitor's IP to the request as X-Visitor-IP, because the API would otherwise see your server's address. The API treats it like any other visitor IP: it is looked up, hashed and dropped. Of the cookies the browser sends to your site, the proxy passes on only the admin session ra.session_token, so a signed-in admin's events are marked internal; visitors have no cookie to pass on.
The city lookup stores the city's approximate coordinates and postal code, not a street address.
How errors are scrubbed
The errors plugin replaces query strings, email addresses, runs of 20 or more letters, digits, underscores or dashes, and runs of 6 or more digits with [x] in each message, stack and breadcrumb before it is sent.
The API scrubs every error event again, whatever sent it, including captureError, captureMessage and ErrorBoundary, which the plugin does not touch. The server rules are the same, except that utm_ parameters in a query string are kept.
What the SDK does not decide for you
Props are stored as you send them. The SDK checks their size and shape, not their content, so an email address in a prop of a custom event is stored.
The page title and the referrer are sent as the browser reports them. If your titles contain a user's name, or your URLs carry personal data in the path, that data reaches the database. Use beforeSend to change or remove it:
import { createAnalytics } from "@spoar/sdk";
export const analytics = createAnalytics({
beforeSend: ({ page: { title, ...page }, ...event }) => ({ ...event, page }),
});Do Not Track and Global Privacy Control
The SDK reads both when the client is created. When navigator.doNotTrack is "1" or navigator.globalPrivacyControl is true, every event is dropped with the reason dnt, and nothing is written to storage except the visitor's own consent, opt-out and debug choices. consent.grant() does not override the signal. There is no option to turn this off.