On this page

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

Requirements for the Flutter SDK
RuntimeFlutter 3.16.0 or newer, Dart SDK 3.0.0 up to but not including 4.0.0.
PermissionsNothing beyond what your app already needs to make network calls.
Publishable keyConsole, under Settings. It is a publishable key, safe to ship in client code: it can only append events.

Install

bash
flutter pub add drengr_flutter_sdk

Start it

In lib/main.dart. Your publishable key is safe to ship in client code: it can only write.

lib/main.dartdart
import 'package:drengr_flutter_sdk/drengr_flutter_sdk.dart';void main() {  Drengr.start(    publishableKey: 'drengr_pk_YOUR_KEY',    ingestUrl: 'https://ziryfxrwrvnunwjupgfg.supabase.co/functions/v1/ingest',    appPackage: 'com.example.myapp',  );  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

dart
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.

Signals the Flutter SDK captures automatically, and the platform hook behind each
SignalWhat it recordsWhere it comes from
screen_viewEvery screen or route the user lands onDrengrNavigatorObserver, a RouteObserver on didPush and didPop
tapTaps, with the element's own label when it has oneGestureBinding.instance.pointerRouter.addGlobalRoute
net / net_failOutgoing requests: host, path, status, duration, sizeDrengrHttpOverrides, an HttpOverrides subclass that chains your existing one
crashUncaught exceptions with their stackFlutterError.onError plus PlatformDispatcher.instance.onError
rage_tap / dead_tapRepeated taps that did nothing — the friction signalThe same global pointer route as tap
rage_scroll / dead_scrollScroll thrash and scrolls that moved nothingThe same global pointer route as tap
app_foreground / app_backgroundLifecycle, which is what sessions are built fromWidgetsBindingObserver.didChangeAppLifecycleState
identifyYour own user id, when you choose to send oneYour 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:

What the Drengr SDKs do not collect, and why that is structural
Not collectedHow you can tell
Screen recordings, screenshots or DOM snapshotsThere is no column for pixels or markup on the events table, and no SDK reads the framebuffer.
KeystrokesTap 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 bodiesWhat 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 keysMasked by name before capture, along with password, pin, cvv, ssn, otp and iban field names.
An IP address on the eventThe events table has no ip or geo column, so no stored event carries one.
Advertising or cross-app identifiersIdentity 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.

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.

Consent controls on the Flutter SDK
Start pausedDrengr.start(enabled: false)
Opt outDrengr.optOut()
Opt back inDrengr.optIn()
Where the choice is storedSharedPreferences (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

The first five happen on every platform. The rest are specific to Flutter.

Failure modes on the Flutter SDK, with the cause and the fix
What you seeWhyFix
No events at all, and Overview never leaves its empty stateThe key belongs to a different organisation, so the writes are accepted against a tenant you are not looking at.Copy the key again from Settings in the organisation you have open in the console.
Events exist but your app is missing from the pickerapp_package does not match the scope you are viewing.Use your real bundle or package id, the same string on every launch. A display name will create a second app.
Events appear only after you close the appNot a bug. The queue batches in memory and flushes on background, which is what makes a dropped connection free.Background the app once, or wait for the next flush. Nothing is lost in the meantime.
Screens are empty but taps and network arriveScreen capture is the one signal that can be wired separately from start().Check the screen hook for your platform in the table above.
Nothing arrives after a user opted outWorking as intended. A persisted opt-out outranks the start argument and survives restarts.Call the opt-in method for your platform. See Privacy controls.
Taps and network arrive, screen_view never doesnavigatorObservers was not wired, so no route change is observed.Add Drengr.navigatorObserver to MaterialApp.navigatorObservers, as shown above.
A handful of events arrived from someone who had opted outThe durable check is async and start() is not. Opt-out is stored in SharedPreferences and mirrored to a temp marker for the synchronous read at start; if the OS purged that marker, capture runs until the reconcile lands.Nothing to configure, and the window is short. If you need a guarantee of zero events, gate with start(enabled: false) from your own consent record and opt in after.
Requests through package:http are missingCapture is installed through HttpOverrides, which covers dart:io HttpClient.Use a client that goes through dart:io, or file an issue with the client you use.

Where to go next

Once events are arriving, these are the things worth doing, in order.

iOS

Published on Swift Package Manager · CocoaPods.

Before you start

Requirements for the iOS SDK
RuntimeiOS 13 or newer. The package also builds for macOS 11 and tvOS 13. Swift tools 5.7.
PermissionsNone, and no App Tracking Transparency prompt, because no advertising identifier is read.
Publishable keyConsole, under Settings. It is a publishable key, safe to ship in client code: it can only append events.

Install

swift
.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.

AppDelegate.swift / your App structswift
// 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.

Signals the iOS SDK captures automatically, and the platform hook behind each
SignalWhat it recordsWhere it comes from
screen_viewEvery screen or route the user lands onA swizzle of UIViewController.viewDidAppear(_:)
tapTaps, with the element's own label when it has oneA swizzle of UIWindow.sendEvent(_:)
net / net_failOutgoing requests: host, path, status, duration, sizeA URLSession swizzle, so requests are captured without you changing call sites
crashUncaught exceptions with their stackNSSetUncaughtExceptionHandler, chaining any handler already installed
rage_tap / dead_tapRepeated taps that did nothing — the friction signalThe same UIWindow.sendEvent swizzle as tap
rage_scroll / dead_scrollScroll thrash and scrolls that moved nothingThe same UIWindow.sendEvent swizzle as tap
app_foreground / app_backgroundLifecycle, which is what sessions are built fromUIApplication lifecycle notifications
identifyYour own user id, when you choose to send oneYour 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:

What the Drengr SDKs do not collect, and why that is structural
Not collectedHow you can tell
Screen recordings, screenshots or DOM snapshotsThere is no column for pixels or markup on the events table, and no SDK reads the framebuffer.
KeystrokesTap 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 bodiesWhat 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 keysMasked by name before capture, along with password, pin, cvv, ssn, otp and iban field names.
An IP address on the eventThe events table has no ip or geo column, so no stored event carries one.
Advertising or cross-app identifiersIdentity 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.

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.

Consent controls on the iOS SDK
Start pausedDrengr.start(startEnabled: false)
Opt outDrengr.optOut()
Opt back inDrengr.optIn()
Where the choice is storedUserDefaults, 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

The first five happen on every platform. The rest are specific to iOS.

Failure modes on the iOS SDK, with the cause and the fix
What you seeWhyFix
No events at all, and Overview never leaves its empty stateThe key belongs to a different organisation, so the writes are accepted against a tenant you are not looking at.Copy the key again from Settings in the organisation you have open in the console.
Events exist but your app is missing from the pickerapp_package does not match the scope you are viewing.Use your real bundle or package id, the same string on every launch. A display name will create a second app.
Events appear only after you close the appNot a bug. The queue batches in memory and flushes on background, which is what makes a dropped connection free.Background the app once, or wait for the next flush. Nothing is lost in the meantime.
Screens are empty but taps and network arriveScreen capture is the one signal that can be wired separately from start().Check the screen hook for your platform in the table above.
Nothing arrives after a user opted outWorking as intended. A persisted opt-out outranks the start argument and survives restarts.Call the opt-in method for your platform. See Privacy controls.
The earliest screens of a cold launch are missingstart() ran after those view controllers had already appeared.Call start() as early as you can, in application(_:didFinishLaunchingWithOptions:) or your App init.
Network is missing from a custom URLSessionCapture attaches through a URLSession hook. A session built with a configuration that bypasses it will not be seen.Use the default configuration, or route the traffic through a session created after start().

Where to go next

Once events are arriving, these are the things worth doing, in order.

Android

Published on Maven Central · dev.drengr:analytics-android.

Before you start

Requirements for the Android SDK
RuntimeminSdk 21 (Android 5.0) or newer, compiled against SDK 34.
Permissionsandroid.permission.INTERNET, declared by the SDK's own manifest and merged into yours. You do not need to add it.
Publishable keyConsole, under Settings. It is a publishable key, safe to ship in client code: it can only append events.

Install

kotlin
implementation("dev.drengr:analytics-android:0.3.0")

Start it

In Application.onCreate(). Your publishable key is safe to ship in client code: it can only write.

Application.onCreate()kotlin
// build.gradle.kts: implementation("dev.drengr:analytics-android:0.3.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.

Signals the Android SDK captures automatically, and the platform hook behind each
SignalWhat it recordsWhere it comes from
screen_viewEvery screen or route the user lands onApplication.registerActivityLifecycleCallbacks, on onActivityResumed. Jetpack Navigation and Navigation-Compose can be added with Drengr.trackNavigation()
tapTaps, with the element's own label when it has oneA wrapper around the activity's Window.Callback
net / net_failOutgoing requests: host, path, status, duration, sizeThe OkHttp Interceptor that start() returns, plus an HttpsURLConnection hook
crashUncaught exceptions with their stackThread.setDefaultUncaughtExceptionHandler, chaining the previous handler
rage_tap / dead_tapRepeated taps that did nothing — the friction signalThe same Window.Callback wrapper as tap
rage_scroll / dead_scrollScroll thrash and scrolls that moved nothingThe same Window.Callback wrapper as tap
app_foreground / app_backgroundLifecycle, which is what sessions are built fromApplication.registerActivityLifecycleCallbacks
identifyYour own user id, when you choose to send oneYour 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:

What the Drengr SDKs do not collect, and why that is structural
Not collectedHow you can tell
Screen recordings, screenshots or DOM snapshotsThere is no column for pixels or markup on the events table, and no SDK reads the framebuffer.
KeystrokesTap 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 bodiesWhat 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 keysMasked by name before capture, along with password, pin, cvv, ssn, otp and iban field names.
An IP address on the eventThe events table has no ip or geo column, so no stored event carries one.
Advertising or cross-app identifiersIdentity 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.

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.

Consent controls on the Android SDK
Start pausedDrengr.start(startEnabled = false)
Opt outDrengr.optOut()
Opt back inDrengr.optIn()
Where the choice is storedSharedPreferences 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

The first five happen on every platform. The rest are specific to Android.

Failure modes on the Android SDK, with the cause and the fix
What you seeWhyFix
No events at all, and Overview never leaves its empty stateThe key belongs to a different organisation, so the writes are accepted against a tenant you are not looking at.Copy the key again from Settings in the organisation you have open in the console.
Events exist but your app is missing from the pickerapp_package does not match the scope you are viewing.Use your real bundle or package id, the same string on every launch. A display name will create a second app.
Events appear only after you close the appNot a bug. The queue batches in memory and flushes on background, which is what makes a dropped connection free.Background the app once, or wait for the next flush. Nothing is lost in the meantime.
Screens are empty but taps and network arriveScreen capture is the one signal that can be wired separately from start().Check the screen hook for your platform in the table above.
Nothing arrives after a user opted outWorking as intended. A persisted opt-out outranks the start argument and survives restarts.Call the opt-in method for your platform. See Privacy controls.
Screens and taps arrive, network does notstart() returns an interceptor that has to be added to the OkHttp client your app actually uses.Add the returned DrengrInterceptor to every OkHttp client you build. Requests through other clients are not captured.
optOut() appeared to do nothingIt writes through the application context that start() captures, so it is a no-op before start() runs.Gate with start(startEnabled = false) and call optIn() once the user agrees.
Compose screens all report the same nameActivity lifecycle sees one Activity; Compose destinations need the navigation hook.Pass your NavController to Drengr.trackNavigation() to get per-destination screen names.

Where to go next

Once events are arriving, these are the things worth doing, in order.

Web

Published on npm · drengr-js.

Before you start

Requirements for the Web SDK
RuntimeAny browser with fetch and localStorage. Ships ESM and CommonJS with TypeScript types.
PermissionsNone. Runs client-side only.
Publishable keyConsole, under Settings. It is a publishable key, safe to ship in client code: it can only append events.

Install

bash
npm install drengr-js

Start it

In your app entry (or a root layout). Your publishable key is safe to ship in client code: it can only write.

your app entry (or a root layout)ts
// 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.

Signals the Web SDK captures automatically, and the platform hook behind each
SignalWhat it recordsWhere it comes from
screen_viewEvery screen or route the user lands onA History API patch on pushState and replaceState, plus popstate, hashchange and the initial load
tapTaps, with the element's own label when it has oneA capture-phase click listener on document
net / net_failOutgoing requests: host, path, status, duration, sizeA fetch wrapper plus XMLHttpRequest.prototype.open and send
crashUncaught exceptions with their stackwindow error and unhandledrejection listeners
rage_tap / dead_tapRepeated taps that did nothing — the friction signalThe same capture-phase click listener as tap
rage_scroll / dead_scrollScroll thrash and scrolls that moved nothingA passive capture-phase wheel listener on document
app_foreground / app_backgroundLifecycle, which is what sessions are built fromvisibilitychange and pagehide
identifyYour own user id, when you choose to send oneYour 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:

What the Drengr SDKs do not collect, and why that is structural
Not collectedHow you can tell
Screen recordings, screenshots or DOM snapshotsThere is no column for pixels or markup on the events table, and no SDK reads the framebuffer.
KeystrokesTap 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 bodiesWhat 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 keysMasked by name before capture, along with password, pin, cvv, ssn, otp and iban field names.
An IP address on the eventThe events table has no ip or geo column, so no stored event carries one.
Advertising or cross-app identifiersIdentity 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.

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.

Consent controls on the Web SDK
Start pausedstart({ enabled: false })
Opt outoptOut()
Opt back inoptIn()
Where the choice is storedlocalStorage, 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

The first five happen on every platform. The rest are specific to Web.

Failure modes on the Web SDK, with the cause and the fix
What you seeWhyFix
No events at all, and Overview never leaves its empty stateThe key belongs to a different organisation, so the writes are accepted against a tenant you are not looking at.Copy the key again from Settings in the organisation you have open in the console.
Events exist but your app is missing from the pickerapp_package does not match the scope you are viewing.Use your real bundle or package id, the same string on every launch. A display name will create a second app.
Events appear only after you close the appNot a bug. The queue batches in memory and flushes on background, which is what makes a dropped connection free.Background the app once, or wait for the next flush. Nothing is lost in the meantime.
Screens are empty but taps and network arriveScreen capture is the one signal that can be wired separately from start().Check the screen hook for your platform in the table above.
Nothing arrives after a user opted outWorking as intended. A persisted opt-out outranks the start argument and survives restarts.Call the opt-in method for your platform. See Privacy controls.
Nothing captured, and the console shows a server-render errorstart() touches window and localStorage, so it cannot run on the server.Call it from a client component. In Next.js that means a file with the "use client" directive.
An opted-out visitor still emitted one event on loadYou supplied an async storage adapter, so the opt-out check resolves after start() returns.Use synchronous storage (the default localStorage path) and the check runs before any capture begins.

Where to go next

Once events are arriving, these are the things worth doing, in order.