On this page
Privacy controls
Every control on this page is one that exists in the SDK or the backend today. Where a platform is weaker than the others, it says so on that row rather than in a footnote.
Start behind a consent gate
Pass the argument below and the SDK installs paused: hooks are attached, nothing is captured and nothing is queued. Call the opt-in method once the user agrees.
| Platform | Start paused | Resume |
|---|---|---|
| Flutter | Drengr.start(enabled: false) | Drengr.optIn() |
| iOS | Drengr.start(startEnabled: false) | Drengr.optIn() |
| Android | Drengr.start(startEnabled = false) | Drengr.optIn() |
| Web | start({ enabled: false }) | optIn() |
Opt-out and opt-in
Opt-out is persistent and outranks the start argument: an opted-out install stays paused on every later launch until you opt back in. Where that persistence is written differs by platform, and one of them is weaker than the rest.
| Platform | Opt out | Where the choice is stored |
|---|---|---|
| Flutter | Drengr.optOut() | SharedPreferences (drengr.opt_out), mirrored to a temp marker for the synchronous check at start |
| iOS | Drengr.optOut() | UserDefaults, key dev.drengr.opt_out |
| Android | Drengr.optOut() | SharedPreferences file drengr_sdk, key opt_out |
| Web | optOut() | localStorage, key drengr_opt_out |
Every SDK records the decision in the platform's durable store: localStorage on web, SharedPreferences on Android and Flutter, UserDefaults on iOS. An opted-out install stays opted out across restarts and app updates.
You can still hold your own consent record and pass it in, which is worth doing if consent is something you must be able to produce on request:
// Your own consent record is the source of truth. Read it first,// then let the SDK's own opt-out act as a second gate.final consented = await myConsentStore.hasConsented();Drengr.start( publishableKey: 'drengr_pk_YOUR_KEY', appPackage: 'com.example.myapp', enabled: consented,);Do Not Track and GPC
We would rather say this plainly than let an unchecked box imply a control we have not built.
// Web: honour GPC / DNT yourself until the SDK does it natively.
const nav = navigator as Navigator & { globalPrivacyControl?: boolean };
const refused = nav.globalPrivacyControl === true || nav.doNotTrack === '1';
start({ publishableKey: 'drengr_pk_YOUR_KEY', enabled: !refused });How long data is kept
Events are kept for 180 days and then dropped by the storage engine itself. There is no job to fail and no switch to forget to flip.
| What | Window | Counted from |
|---|---|---|
| Raw events | 180 days | ingested_at |
| Derived semantic events | 180 days | ingested_at |
| Request-shape inventory | 180 days | last_seen |
Why the clock starts at receipt, not at the event
The window keys off ingested_at, the server clock when we received the row, rather than occurred_at, the client clock when it happened. Two reasons, and the second is the one that bites.
A device that has been offline for a week, or one whose clock is simply wrong, would otherwise deliver rows that are already older than the window. Keying off receipt means a backfilled row is never born expired: it gets the full 180 days from the moment it reaches us. It also lets whole partitions drop cleanly instead of deleting scattered rows.
Deleting a person's data
One authenticated call erases a subject from both the raw and derived stores. The tenant is taken from your credentials, never from the request body, so you cannot reach another organisation's rows.
curl -X POST https://analytics.drengr.dev/functions/v1/forget-events \
-H "Authorization: Bearer $DRENGR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"install_id": "6f1c…"}'The body takes exactly one selector. Sending both, or neither, is rejected.
| Selector | Use it when |
|---|---|
| install_id | You are erasing one app install. This is the identifier the SDK generates and the only one that exists for a user who never signed in. |
| subject_hash | You are erasing a signed-in person across every device they used. This is the pseudonymous join key derived from the id you passed to identify(). |
Deriving subject_hash from your own user id
You never send us a raw email or user id to delete. subject_hash is sh_followed by base64url of HMAC-SHA256 over your organisation's pepper and the normalised id. The SDK computes it on the device, and the same input always produces the same key, so you can recompute it server-side to build a deletion request.
What a GDPR or CCPA request maps to
| Request | What you do |
|---|---|
| Erasure, or “delete my data” | POST to forget-eventswith the person's subject_hash, or their install_id if they never signed in. |
| Objection, or “stop collecting” | Call the platform's opt-out. On Flutter, also record the choice in your own storage, per the caution above. |
| Withdrawing consent | Opt out to stop future capture, then call the deletion endpoint to remove what was already collected. Opt-out on its own is not retroactive. |
| Access, or “what do you have on me” | Query the console scoped to that subject_hash. The columns we store are listed on every platform page under “What arrives on its own”. |
| Sealed personal data | Detected values are encrypted on the device to your organisation's key. We hold ciphertext we cannot open, so erasure removes the envelope without us ever having read it. |
Drengr Actuator, the device-driving MCP server, is a separate product with its own erasure path. See drengr uninstall.
This page describes mechanisms, not legal advice. Our own commitments are in Privacy and Security.