Privacy & PII
Onelo Monitor scrubs sensitive data at three layers — SDK preflight (every SDK), the backend ingest endpoint, and a Postgres trigger that runs on every insert. This page documents what gets stripped, what stays, and what you control.
Three-layer scrubbing
Defense in depth: even if the SDK preflight is bypassed (curl, a malicious client, an old SDK version), the backend ingest endpoint re-runs the full key + value scrub on every request, and the database trigger redacts the key denylist again before the row is committed.
| Layer | Where | Coverage |
|---|---|---|
| SDK preflight | Every SDK — Swift, JS, Electron, React Native, Android, Flutter, Python, Node.js, PHP (each ships its own scrubber module) | Key denylist + value-regex on meta and error, before anything leaves the device/process. |
| Backend ingest | backend/app/routes/sdk_monitor.py — on every events/batch request | Key denylist + value-regex on meta and error. Applies to ALL platforms and raw HTTP clients. |
| Postgres trigger | monitor_events_pii_scrub — BEFORE INSERT/UPDATE on monitor_events | Key denylist on meta. Last line of defense. |
What gets scrubbed (key denylist)
When any key inside meta matches the regex below (substring, case-insensitive) — or contains a standalone key segment like x-anthropic-key — the value is replaced with [REDACTED]. Backend scrubbing recurses through nested objects up to 10 levels deep.
password|passwd|secret|token|api_key|apikey|authorization|cookie|credential|private_key|client_secret|cvv|ssn
Whenever a key is redacted, the scrubber (SDK preflight and backend alike) adds an audit list at meta._onelo_redacted so you can see what was stripped without exposing the raw values.
// before
{ "user": { "id": "u_42", "api_key": "onelo_sk_live_…" } }
// after
{
"user": { "id": "u_42", "api_key": "[REDACTED]" },
"_onelo_redacted": ["api_key"]
}What gets scrubbed (value regex)
Even if the surrounding key is innocuous (notes, message, error), values matching these patterns are still redacted in-place. Boundary anchors are ASCII-only so non-ASCII padding cannot escape.
| Pattern | Example |
|---|---|
| Bearer tokens | Bearer eyJ0eXAi… |
| JWTs (3-part dotted base64url) | eyJhbGciOi… . eyJzdWIi… . sig |
| Stripe-style keys | sk_live_…, sk_test_…, pk_live_…, pk_test_…, rk_live_…, rk_test_… |
| Onelo keys | onelo_pk_test_…, onelo_pk_live_…, onelo_sk_… |
| Credit cards (separated) | 4242 4242 4242 4242 |
| Credit cards (unseparated) | 4242424242424242 |
| Amex format | 3782 822463 10005 |
Source: backend/app/routes/sdk_monitor.py (PII_VALUE_PATTERNS).
What does NOT get scrubbed by default
A few categories are intentionally left alone — useful for debugging, low PII risk, or opaque enough that they can't be reversed.
| Field | Why | Opt out |
|---|---|---|
| Email addresses | Often the only useful debugging signal. | Python SDK: pass strict_email_scrub=True. Other SDKs: filter manually before passing to meta. |
| userId | Opaque string ≤ 256 chars. Not hashed by Onelo. | Pass an opaque internal id instead of a real identifier (e.g. SHA-256 of email). |
| Free-form error string (≤ 8 KB) | Stack traces are essential for triage. | Value-regex still runs inside it — Bearer tokens, JWTs, etc. are stripped. |
Retention
Retention is plan-gated. The dashboard query window equals the storage retention on every plan.
| Plan | Storage retention | Dashboard window |
|---|---|---|
| Free | 7 days | 7 days |
| Pro | 30 days | 30 days |
| Business | 90 days | 90 days |
Cleanup is a nightly pg_cron job at 03:00 UTC that purges monitor_events, monitor_runs (the full per-run history), monitor_aggregates, and monitor_checks per plan. Resolved incidents are purged at 2× retention so you keep historical context for postmortems.
Data location
Monitor events are stored in Onelo's Supabase deployment, hosted in the EU. Specific region details are available on request — email [email protected].
GDPR compliance
| Topic | Status |
|---|---|
| Role | Onelo is a data processor for monitor events. You (the developer) are the controller. |
| DSR — export | Export runs via the API (GET /api/monitor/features/{name}/runs/export?format=csv|json) and filter the file by userId offline — a userId filter parameter does not exist yet. |
| DSR — delete | No per-user delete endpoint yet. Workarounds: delete the app (CASCADE), or contact support for manual deletion. On the roadmap. |
| DPA | Available on request — mailto:[email protected]. |
| Sub-processors | Supabase (Postgres, hosted on AWS) and a transactional email provider (SMTP). Full current list available on request. |
DELETE /api/monitor/users/{userId}) does not exist yet. If you need to honor a delete request before that ships, email support with the app id and the userId — we'll execute it manually.Anonymous mode
Onelo Monitor does not auto-collect user identifiers. To track anonymously, simply don't call setUserId — events land with userId = null. To track with identifiers, call setUserId(opaque_id); the server stores it as an opaque string with no further processing.
Consent integration
If you collect runs after a user has revoked tracking consent, that is your responsibility. The Onelo SDK does not gate track() / event() on consent. Wrap calls in your own check, or call destroy() to stop the SDK entirely.
/docs/consent page is on the roadmap. Until then, see docs/consent-system.md in the repo for the waitlist-flavoured implementation — the same patterns apply.