Privacy and data

Betterloop is built to answer "what should we fix?", which needs behaviour, not identity. This page is the factual description of what the SDK sends and what the server keeps, so you can check it against your own policy. It is a technical document, not legal advice.

What is stored, per event

FieldContentsNote
pathlocation.pathname plus location.search, truncated to 500 characters.The query string is included. See the warning below.
typeOne of the nine event types.
sidA random 16-character session id generated in the browser.Per tab, kept in sessionStorage under _fp_sid. Not a cookie, not stable across tabs or visits.
tsEvent timestamp, clamped to a window around server time.
originlocation.origin of the page that produced the event.Origin only, never the full URL.
referrerdocument.referrer, truncated to 500 characters.This is a full URL and can carry a query string from the referring site.
vw, vhViewport width and height in pixels.Used for layout and responsiveness signals.
uaThe User-Agent request header, truncated to 300 characters.Read server-side from the request, not from JavaScript.
attrsPer-event details: page title, element descriptor and label, error message and stack, vital name and value, your custom attributes.Capped at 8192 bytes of JSON per event.

What is never collected

  • No cookies. The SDK sets none and reads none. The only storage it touches is one sessionStorage key.
  • No IP addresses in the database. The client IP is used in memory as a rate-limiting key and is never written to an event row.
  • No form or input values. Clicks record the shape of the element - tag names, id, up to two class names, four levels of ancestors - and a short label, never what a user typed.
  • No cross-site or cross-visit identity. There is no device fingerprinting, no user id, no persistent identifier. Close the tab and the session id is gone.
  • No screen recording, no session replay, no keystrokes, no mouse paths. Betterloop records that a click happened on a described element, not how the pointer got there.
  • No third-party trackers loaded by the SDK. It has zero dependencies and talks to exactly one endpoint - yours.

Do Not Track and Global Privacy Control

If navigator.doNotTrack is "1" or navigator.globalPrivacyControl is set, init() returns before anything happens: no listeners, no observers, no session id, no requests. Those visitors are invisible to Betterloop and are not counted anywhere.

This is the default. The script-tag build has no way to turn it off at all. The bundled build exposes respectDNT: false, which we would rather you did not use.

Three places you can leak data into Betterloop

The SDK cannot tell whether a string on your page is personal data. These are the paths worth auditing on your side.

  1. Query strings. The recorded path is pathname + search, so /invite?email=someone@example.com is stored exactly like that, and it becomes the grouping key for pages and insights. If your app puts emails, tokens, or ids in the query string, strip them before they reach the URL, or expect to see them here.
  2. Interactive element text. A click label is the aria-label or the text of the nearest link, button, or label element, capped at 60 characters. A button reading "Delete account for jane@example.com" will be captured as written.
  3. Error messages and custom events. Error messages (300 chars) and stacks (1000 chars) are stored verbatim, and thrown errors sometimes embed request payloads. Whatever you pass to track() is stored as-is too. Both are entirely under your control.

Where data goes when the AI layer runs

If the instance is configured with an Anthropic API key, each insight run sends an aggregate snapshot to the Anthropic API: weekly counts, up to 15 top pages with view and session counts, web vitals p75 values, up to 10 error messages truncated to 150 characters with their counts, and the rule findings with their evidence. No raw events, no session ids, no user agents, and no referrers are included. If no key is configured the layer is skipped and no data leaves the instance at all. See How insights work.

Controls you have

  • Origin allowlist. Project Settings takes a comma-separated list of origins. With it set, ingest rejects any request whose Origin header is missing or not on the list, so a leaked write key cannot be used to write junk into your project from elsewhere.
  • Do not install it everywhere. The SDK only sees the pages you put it on. Leaving it off authenticated admin screens is a legitimate way to keep sensitive paths out entirely.
  • Delete a project. Project Settings has a delete button. It removes the project and cascades to every event, insight, and usage counter belonging to it.

There is no automatic retention cutoff today: the detectors only look at the last 7 days, but raw events are kept until you delete the project. If you need a shorter retention window than that, deleting and recreating the project is the blunt tool that exists right now.

Keys and access

  • fp_ write keys are public by design, embedded in your page like any analytics key. They can only append events to one project. They cannot read anything.
  • blk_ API keys are secret, are shown once at creation, and are stored only as a SHA-256 hash - a database dump does not yield usable keys. They are read-only across your account and can be revoked at any time.
  • Every read endpoint checks project ownership on every request. A key can never see another account's data, and a project it does not own returns 404 rather than confirming the project exists.

Want the field-level detail for a specific event type? The install page lists what each collector captures, and the API reference documents the exact wire format.