Every SDK, one page
The same content as the four platform pages, in one document. Useful when you want to search across all of them at once, print it, or hand the whole thing to someone else.
Prefer one platform at a time? Start from the overview.
Flutter
Published on pub.dev · drengr_flutter_sdk.
Before you start
| Runtime | Flutter 3.16.0 or newer, Dart SDK 3.0.0 up to but not including 4.0.0. |
|---|---|
| Permissions | Nothing beyond what your app already needs to make network calls. |
| Publishable key | Console, under Settings. It is a publishable key, safe to ship in client code: it can only append events. |
Install
flutter pub add drengr_flutter_sdkStart it
In lib/main.dart. Your publishable key is safe to ship in client code: it can only write.
import 'package:drengr_flutter_sdk/drengr_flutter_sdk.dart';void main() { final sink = IngestSink( url: 'https://ziryfxrwrvnunwjupgfg.supabase.co/functions/v1/ingest', publishableKey: 'drengr_pk_YOUR_KEY', context: {'app_package': 'com.example.myapp'}, ); Drengr.start(onEvent: sink.addNetwork); runApp(const MyApp());}// screen_view needs this observer — without it you get taps and network only.// MaterialApp(navigatorObservers: [Drengr.navigatorObserver])Add the navigator observer, or you get no screens
MaterialApp(navigatorObservers: [Drengr.navigatorObserver])What arrives on its own
You do not instrument these. They are captured from the moment start() runs. The third column names the Flutter API behind each one, so you can check the claim against your own app.
| Signal | What it records | Where it comes from |
|---|---|---|
| screen_view | Every screen or route the user lands on | DrengrNavigatorObserver, a RouteObserver on didPush and didPop |
| tap | Taps, with the element's own label when it has one | GestureBinding.instance.pointerRouter.addGlobalRoute |
| net / net_fail | Outgoing requests: host, path, status, duration, size | DrengrHttpOverrides, an HttpOverrides subclass that chains your existing one |
| crash | Uncaught exceptions with their stack | FlutterError.onError plus PlatformDispatcher.instance.onError |
| rage_tap / dead_tap | Repeated taps that did nothing — the friction signal | The same global pointer route as tap |
| rage_scroll / dead_scroll | Scroll thrash and scrolls that moved nothing | The same global pointer route as tap |
| app_foreground / app_background | Lifecycle, which is what sessions are built from | WidgetsBindingObserver.didChangeAppLifecycleState |
| identify | Your own user id, when you choose to send one | Your own call to Drengr.identify() |
IngestSink batches in memory and flushes on background, so a dropped connection costs nothing.
app_package is the identity the console groups by. Use your real bundle id, not a display name.
Labels are redacted and capped on device before they leave the app.
What never leaves the device
Emails, phone numbers, card numbers and government ids are detected and sealed on the device before the first byte is sent. Our servers hold ciphertext they cannot open. Labels and URLs are redacted and length-capped on the device too.
And what is never collected in the first place:
| Not collected | How you can tell |
|---|---|
| Screen recordings, screenshots or DOM snapshots | There is no column for pixels or markup on the events table, and no SDK reads the framebuffer. |
| Keystrokes | Tap capture records that a tap happened and the element's own accessibility label. No SDK attaches a key or text listener. |
| Raw request and response bodies | What is stored is a safe dotted-path projection, redacted and capped at 64 KiB on the device before anything is sent. |
| Authorization headers, cookies and API keys | Masked by name before capture, along with password, pin, cvv, ssn, otp and iban field names. |
| An IP address on the event | The events table has no ip or geo column, so no stored event carries one. |
| Advertising or cross-app identifiers | Identity is a random install_id the SDK generates. No IDFA, no GAID, and no device fingerprint is used for identity. |
Most autocapture tools promise not to look. This is built so we cannot. Full design in Security and Privacy.
Consent and opt-out
Start paused behind a consent gate with Drengr.start(enabled: false), then resume once the user agrees. A persisted opt-out always wins over that argument, so an opted-out install stays paused across restarts.
| Start paused | Drengr.start(enabled: false) |
|---|---|
| Opt out | Drengr.optOut() |
| Opt back in | Drengr.optIn() |
| Where the choice is stored | SharedPreferences (drengr.opt_out), mirrored to a temp marker for the synchronous check at start |
Retention, deletion and what a GDPR or CCPA request maps to are on Privacy controls.
Check it worked
Run the app and use it for a few seconds, then open the console. Overview checks for your first event every 15 seconds on its own, so you do not need to keep reloading.
Nothing showing up
| What you see | Most likely cause |
|---|---|
| Console stays on “Get your first events in” | The key belongs to a different organisation, so the events landed in someone else’s tenant. |
| Events arrive, but the app you picked looks empty | The app_package you passed to start() is not the one you are scoped to in the console. Switch the scope, or match the value. |
| Nothing until you close the app | The first batch flushes on background. Send the app to the background once, or wait for the flush interval. |
| Taps and network arrive, screens do not | The navigator observer is not attached — see the step above. |
| Nothing at all, ever | start() is not reached, or it runs after the events you expected. Put it as early as you can and check for a thrown error before it. |
Where to go next
Once events are arriving, these are the things worth doing in order.
- Set up consent and opt-out before you ship to real users.
- Name your events. Drengr proposes names from the traffic itself; you accept or reject them in the console's review queue.
- Bring your history across from whatever you use today, so the numbers do not start at zero.
iOS
Published on Swift Package Manager · CocoaPods.
Before you start
| Runtime | iOS 13 or newer. The package also builds for macOS 11 and tvOS 13. Swift tools 5.7. |
|---|---|
| Permissions | None, and no App Tracking Transparency prompt, because no advertising identifier is read. |
| Publishable key | Console, under Settings. It is a publishable key, safe to ship in client code: it can only append events. |
Install
.package(url: "https://github.com/SharminSirajudeen/drengr-sdk.git", from: "0.3.0")Start it
In AppDelegate.swift / your App struct. Your publishable key is safe to ship in client code: it can only write.
// Xcode → Add Package: https://github.com/SharminSirajudeen/drengr-sdk.git (from 0.3.0)
import Drengr
Drengr.start(
publishableKey: "drengr_pk_YOUR_KEY",
ingestURL: "https://ziryfxrwrvnunwjupgfg.supabase.co/functions/v1/ingest",
appPackage: "com.example.app"
)What arrives on its own
You do not instrument these. They are captured from the moment start() runs. The third column names the iOS API behind each one, so you can check the claim against your own app.
| Signal | What it records | Where it comes from |
|---|---|---|
| screen_view | Every screen or route the user lands on | A swizzle of UIViewController.viewDidAppear(_:) |
| tap | Taps, with the element's own label when it has one | A swizzle of UIWindow.sendEvent(_:) |
| net / net_fail | Outgoing requests: host, path, status, duration, size | A URLSession swizzle, so requests are captured without you changing call sites |
| crash | Uncaught exceptions with their stack | NSSetUncaughtExceptionHandler, chaining any handler already installed |
| rage_tap / dead_tap | Repeated taps that did nothing — the friction signal | The same UIWindow.sendEvent swizzle as tap |
| rage_scroll / dead_scroll | Scroll thrash and scrolls that moved nothing | The same UIWindow.sendEvent swizzle as tap |
| app_foreground / app_background | Lifecycle, which is what sessions are built from | UIApplication lifecycle notifications |
| identify | Your own user id, when you choose to send one | Your own call to Drengr.identify() |
Call start() once, as early as you can — anything before it is not captured.
CocoaPods works too: add pod 'Drengr' and run pod install.
URLSession traffic is captured through a protocol hook; no swizzling of your own code.
What never leaves the device
Emails, phone numbers, card numbers and government ids are detected and sealed on the device before the first byte is sent. Our servers hold ciphertext they cannot open. Labels and URLs are redacted and length-capped on the device too.
And what is never collected in the first place:
| Not collected | How you can tell |
|---|---|
| Screen recordings, screenshots or DOM snapshots | There is no column for pixels or markup on the events table, and no SDK reads the framebuffer. |
| Keystrokes | Tap capture records that a tap happened and the element's own accessibility label. No SDK attaches a key or text listener. |
| Raw request and response bodies | What is stored is a safe dotted-path projection, redacted and capped at 64 KiB on the device before anything is sent. |
| Authorization headers, cookies and API keys | Masked by name before capture, along with password, pin, cvv, ssn, otp and iban field names. |
| An IP address on the event | The events table has no ip or geo column, so no stored event carries one. |
| Advertising or cross-app identifiers | Identity is a random install_id the SDK generates. No IDFA, no GAID, and no device fingerprint is used for identity. |
Most autocapture tools promise not to look. This is built so we cannot. Full design in Security and Privacy.
Consent and opt-out
Start paused behind a consent gate with Drengr.start(startEnabled: false), then resume once the user agrees. A persisted opt-out always wins over that argument, so an opted-out install stays paused across restarts.
| Start paused | Drengr.start(startEnabled: false) |
|---|---|
| Opt out | Drengr.optOut() |
| Opt back in | Drengr.optIn() |
| Where the choice is stored | UserDefaults, key dev.drengr.opt_out |
Retention, deletion and what a GDPR or CCPA request maps to are on Privacy controls.
Check it worked
Run the app and use it for a few seconds, then open the console. Overview checks for your first event every 15 seconds on its own, so you do not need to keep reloading.
Nothing showing up
| What you see | Most likely cause |
|---|---|
| Console stays on “Get your first events in” | The key belongs to a different organisation, so the events landed in someone else’s tenant. |
| Events arrive, but the app you picked looks empty | The app_package you passed to start() is not the one you are scoped to in the console. Switch the scope, or match the value. |
| Nothing until you close the app | The first batch flushes on background. Send the app to the background once, or wait for the flush interval. |
| Taps and network arrive, screens do not | Screen capture attaches at start(); check that start() runs before your first screen. |
| Nothing at all, ever | start() is not reached, or it runs after the events you expected. Put it as early as you can and check for a thrown error before it. |
Where to go next
Once events are arriving, these are the things worth doing in order.
- Set up consent and opt-out before you ship to real users.
- Name your events. Drengr proposes names from the traffic itself; you accept or reject them in the console's review queue.
- Bring your history across from whatever you use today, so the numbers do not start at zero.
Android
Published on Maven Central · dev.drengr:analytics-android.
Before you start
| Runtime | minSdk 21 (Android 5.0) or newer, compiled against SDK 34. |
|---|---|
| Permissions | android.permission.INTERNET, declared by the SDK's own manifest and merged into yours. You do not need to add it. |
| Publishable key | Console, under Settings. It is a publishable key, safe to ship in client code: it can only append events. |
Install
implementation("dev.drengr:analytics-android:0.2.0")Start it
In Application.onCreate(). Your publishable key is safe to ship in client code: it can only write.
// build.gradle.kts: implementation("dev.drengr:analytics-android:0.2.0")val client = OkHttpClient.Builder() .addInterceptor( Drengr.start( context = applicationContext, publishableKey = "drengr_pk_YOUR_KEY", ingestUrl = "https://ziryfxrwrvnunwjupgfg.supabase.co/functions/v1/ingest", appPackage = "com.example.app", ) ) .build()What arrives on its own
You do not instrument these. They are captured from the moment start() runs. The third column names the Android API behind each one, so you can check the claim against your own app.
| Signal | What it records | Where it comes from |
|---|---|---|
| screen_view | Every screen or route the user lands on | Application.registerActivityLifecycleCallbacks, on onActivityResumed. Jetpack Navigation and Navigation-Compose can be added with Drengr.trackNavigation() |
| tap | Taps, with the element's own label when it has one | A wrapper around the activity's Window.Callback |
| net / net_fail | Outgoing requests: host, path, status, duration, size | The OkHttp Interceptor that start() returns, plus an HttpsURLConnection hook |
| crash | Uncaught exceptions with their stack | Thread.setDefaultUncaughtExceptionHandler, chaining the previous handler |
| rage_tap / dead_tap | Repeated taps that did nothing — the friction signal | The same Window.Callback wrapper as tap |
| rage_scroll / dead_scroll | Scroll thrash and scrolls that moved nothing | The same Window.Callback wrapper as tap |
| app_foreground / app_background | Lifecycle, which is what sessions are built from | Application.registerActivityLifecycleCallbacks |
| identify | Your own user id, when you choose to send one | Your own call to Drengr.identify() |
Drengr.start() returns an OkHttp Interceptor. Add it to the client your app already uses — requests made through other clients are not captured.
Screen and tap capture attach through the Application lifecycle, so no per-Activity wiring.
What never leaves the device
Emails, phone numbers, card numbers and government ids are detected and sealed on the device before the first byte is sent. Our servers hold ciphertext they cannot open. Labels and URLs are redacted and length-capped on the device too.
And what is never collected in the first place:
| Not collected | How you can tell |
|---|---|
| Screen recordings, screenshots or DOM snapshots | There is no column for pixels or markup on the events table, and no SDK reads the framebuffer. |
| Keystrokes | Tap capture records that a tap happened and the element's own accessibility label. No SDK attaches a key or text listener. |
| Raw request and response bodies | What is stored is a safe dotted-path projection, redacted and capped at 64 KiB on the device before anything is sent. |
| Authorization headers, cookies and API keys | Masked by name before capture, along with password, pin, cvv, ssn, otp and iban field names. |
| An IP address on the event | The events table has no ip or geo column, so no stored event carries one. |
| Advertising or cross-app identifiers | Identity is a random install_id the SDK generates. No IDFA, no GAID, and no device fingerprint is used for identity. |
Most autocapture tools promise not to look. This is built so we cannot. Full design in Security and Privacy.
Consent and opt-out
Start paused behind a consent gate with Drengr.start(startEnabled = false), then resume once the user agrees. A persisted opt-out always wins over that argument, so an opted-out install stays paused across restarts.
| Start paused | Drengr.start(startEnabled = false) |
|---|---|
| Opt out | Drengr.optOut() |
| Opt back in | Drengr.optIn() |
| Where the choice is stored | SharedPreferences file drengr_sdk, key opt_out |
Retention, deletion and what a GDPR or CCPA request maps to are on Privacy controls.
Check it worked
Run the app and use it for a few seconds, then open the console. Overview checks for your first event every 15 seconds on its own, so you do not need to keep reloading.
Nothing showing up
| What you see | Most likely cause |
|---|---|
| Console stays on “Get your first events in” | The key belongs to a different organisation, so the events landed in someone else’s tenant. |
| Events arrive, but the app you picked looks empty | The app_package you passed to start() is not the one you are scoped to in the console. Switch the scope, or match the value. |
| Nothing until you close the app | The first batch flushes on background. Send the app to the background once, or wait for the flush interval. |
| Taps and network arrive, screens do not | Screen capture attaches at start(); check that start() runs before your first screen. |
| Nothing at all, ever | start() is not reached, or it runs after the events you expected. Put it as early as you can and check for a thrown error before it. |
Where to go next
Once events are arriving, these are the things worth doing in order.
- Set up consent and opt-out before you ship to real users.
- Name your events. Drengr proposes names from the traffic itself; you accept or reject them in the console's review queue.
- Bring your history across from whatever you use today, so the numbers do not start at zero.
Web
Published on npm · drengr-js.
Before you start
| Runtime | Any browser with fetch and localStorage. Ships ESM and CommonJS with TypeScript types. |
|---|---|
| Permissions | None. Runs client-side only. |
| Publishable key | Console, under Settings. It is a publishable key, safe to ship in client code: it can only append events. |
Install
npm install drengr-jsStart it
In your app entry (or a root layout). Your publishable key is safe to ship in client code: it can only write.
// npm install drengr-js
import { Drengr } from 'drengr-js';
Drengr.start({
ingestUrl: 'https://ziryfxrwrvnunwjupgfg.supabase.co/functions/v1/ingest',
publishableKey: 'drengr_pk_YOUR_KEY',
appPackage: 'com.example.myapp',
});What arrives on its own
You do not instrument these. They are captured from the moment start() runs. The third column names the Web API behind each one, so you can check the claim against your own app.
| Signal | What it records | Where it comes from |
|---|---|---|
| screen_view | Every screen or route the user lands on | A History API patch on pushState and replaceState, plus popstate, hashchange and the initial load |
| tap | Taps, with the element's own label when it has one | A capture-phase click listener on document |
| net / net_fail | Outgoing requests: host, path, status, duration, size | A fetch wrapper plus XMLHttpRequest.prototype.open and send |
| crash | Uncaught exceptions with their stack | window error and unhandledrejection listeners |
| rage_tap / dead_tap | Repeated taps that did nothing — the friction signal | The same capture-phase click listener as tap |
| rage_scroll / dead_scroll | Scroll thrash and scrolls that moved nothing | A passive capture-phase wheel listener on document |
| app_foreground / app_background | Lifecycle, which is what sessions are built from | visibilitychange and pagehide |
| identify | Your own user id, when you choose to send one | Your own call to identify() |
fetch and XMLHttpRequest are both wrapped, so requests are captured regardless of which your app uses.
History routing is followed automatically — single-page navigations register as screen views.
Runs client-side only. In Next.js, call start() from a client component.
What never leaves the device
Emails, phone numbers, card numbers and government ids are detected and sealed on the device before the first byte is sent. Our servers hold ciphertext they cannot open. Labels and URLs are redacted and length-capped on the device too.
And what is never collected in the first place:
| Not collected | How you can tell |
|---|---|
| Screen recordings, screenshots or DOM snapshots | There is no column for pixels or markup on the events table, and no SDK reads the framebuffer. |
| Keystrokes | Tap capture records that a tap happened and the element's own accessibility label. No SDK attaches a key or text listener. |
| Raw request and response bodies | What is stored is a safe dotted-path projection, redacted and capped at 64 KiB on the device before anything is sent. |
| Authorization headers, cookies and API keys | Masked by name before capture, along with password, pin, cvv, ssn, otp and iban field names. |
| An IP address on the event | The events table has no ip or geo column, so no stored event carries one. |
| Advertising or cross-app identifiers | Identity is a random install_id the SDK generates. No IDFA, no GAID, and no device fingerprint is used for identity. |
Most autocapture tools promise not to look. This is built so we cannot. Full design in Security and Privacy.
Consent and opt-out
Start paused behind a consent gate with start({ enabled: false }), then resume once the user agrees. A persisted opt-out always wins over that argument, so an opted-out install stays paused across restarts.
| Start paused | start({ enabled: false }) |
|---|---|
| Opt out | optOut() |
| Opt back in | optIn() |
| Where the choice is stored | localStorage, key drengr_opt_out |
Retention, deletion and what a GDPR or CCPA request maps to are on Privacy controls.
Check it worked
Run the app and use it for a few seconds, then open the console. Overview checks for your first event every 15 seconds on its own, so you do not need to keep reloading.
Nothing showing up
| What you see | Most likely cause |
|---|---|
| Console stays on “Get your first events in” | The key belongs to a different organisation, so the events landed in someone else’s tenant. |
| Events arrive, but the app you picked looks empty | The app_package you passed to start() is not the one you are scoped to in the console. Switch the scope, or match the value. |
| Nothing until you close the app | The first batch flushes on background. Send the app to the background once, or wait for the flush interval. |
| Taps and network arrive, screens do not | Screen capture attaches at start(); check that start() runs before your first screen. |
| Nothing at all, ever | start() is not reached, or it runs after the events you expected. Put it as early as you can and check for a thrown error before it. |
Where to go next
Once events are arriving, these are the things worth doing in order.
- Set up consent and opt-out before you ship to real users.
- Name your events. Drengr proposes names from the traffic itself; you accept or reject them in the console's review queue.
- Bring your history across from whatever you use today, so the numbers do not start at zero.