{
  "$schema": "https://drengr.dev/docs/events.json",
  "version": 1,
  "description": "Signals every Drengr Analytics SDK captures without instrumentation, the platform hook behind each, and what is never collected.",
  "source": "Generated from the same data that renders https://drengr.dev/docs",
  "retention": {
    "window_days": 180,
    "counted_from": "ingested_at",
    "note": "Server receipt, not client event time, so a backfilled row is never born expired."
  },
  "deletion": {
    "endpoint": "/functions/v1/forget-events",
    "method": "POST",
    "selectors": [
      "install_id",
      "subject_hash"
    ],
    "note": "Exactly one selector per call. Tenant is taken from your credentials, never the body."
  },
  "signals": [
    {
      "kind": "screen_view",
      "description": "Every screen or route the user lands on",
      "hooks": {
        "flutter": "DrengrNavigatorObserver, a RouteObserver on didPush and didPop",
        "ios": "A swizzle of UIViewController.viewDidAppear(_:)",
        "android": "Application.registerActivityLifecycleCallbacks, on onActivityResumed. Jetpack Navigation and Navigation-Compose can be added with Drengr.trackNavigation()",
        "web": "A History API patch on pushState and replaceState, plus popstate, hashchange and the initial load"
      }
    },
    {
      "kind": "tap",
      "description": "Taps, with the element's own label when it has one",
      "hooks": {
        "flutter": "GestureBinding.instance.pointerRouter.addGlobalRoute",
        "ios": "A swizzle of UIWindow.sendEvent(_:)",
        "android": "A wrapper around the activity's Window.Callback",
        "web": "A capture-phase click listener on document"
      }
    },
    {
      "kind": "net / net_fail",
      "description": "Outgoing requests: host, path, status, duration, size",
      "hooks": {
        "flutter": "DrengrHttpOverrides, an HttpOverrides subclass that chains your existing one",
        "ios": "A URLSession swizzle, so requests are captured without you changing call sites",
        "android": "The OkHttp Interceptor that start() returns, plus an HttpsURLConnection hook",
        "web": "A fetch wrapper plus XMLHttpRequest.prototype.open and send"
      }
    },
    {
      "kind": "crash",
      "description": "Uncaught exceptions with their stack",
      "hooks": {
        "flutter": "FlutterError.onError plus PlatformDispatcher.instance.onError",
        "ios": "NSSetUncaughtExceptionHandler, chaining any handler already installed",
        "android": "Thread.setDefaultUncaughtExceptionHandler, chaining the previous handler",
        "web": "window error and unhandledrejection listeners"
      }
    },
    {
      "kind": "rage_tap / dead_tap",
      "description": "Repeated taps that did nothing — the friction signal",
      "hooks": {
        "flutter": "The same global pointer route as tap",
        "ios": "The same UIWindow.sendEvent swizzle as tap",
        "android": "The same Window.Callback wrapper as tap",
        "web": "The same capture-phase click listener as tap"
      }
    },
    {
      "kind": "rage_scroll / dead_scroll",
      "description": "Scroll thrash and scrolls that moved nothing",
      "hooks": {
        "flutter": "The same global pointer route as tap",
        "ios": "The same UIWindow.sendEvent swizzle as tap",
        "android": "The same Window.Callback wrapper as tap",
        "web": "A passive capture-phase wheel listener on document"
      }
    },
    {
      "kind": "app_foreground / app_background",
      "description": "Lifecycle, which is what sessions are built from",
      "hooks": {
        "flutter": "WidgetsBindingObserver.didChangeAppLifecycleState",
        "ios": "UIApplication lifecycle notifications",
        "android": "Application.registerActivityLifecycleCallbacks",
        "web": "visibilitychange and pagehide"
      }
    },
    {
      "kind": "identify",
      "description": "Your own user id, when you choose to send one",
      "hooks": {
        "flutter": "Your own call to Drengr.identify()",
        "ios": "Your own call to Drengr.identify()",
        "android": "Your own call to Drengr.identify()",
        "web": "Your own call to identify()"
      }
    }
  ],
  "not_collected": [
    {
      "what": "Screen recordings, screenshots or DOM snapshots",
      "how_you_can_tell": "There is no column for pixels or markup on the events table, and no SDK reads the framebuffer."
    },
    {
      "what": "Keystrokes",
      "how_you_can_tell": "Tap capture records that a tap happened and the element's own accessibility label. No SDK attaches a key or text listener."
    },
    {
      "what": "Raw request and response bodies",
      "how_you_can_tell": "What is stored is a safe dotted-path projection, redacted and capped at 64 KiB on the device before anything is sent."
    },
    {
      "what": "Authorization headers, cookies and API keys",
      "how_you_can_tell": "Masked by name before capture, along with password, pin, cvv, ssn, otp and iban field names."
    },
    {
      "what": "An IP address on the event",
      "how_you_can_tell": "The events table has no ip or geo column, so no stored event carries one."
    },
    {
      "what": "Advertising or cross-app identifiers",
      "how_you_can_tell": "Identity is a random install_id the SDK generates. No IDFA, no GAID, and no device fingerprint is used for identity."
    }
  ],
  "platforms": [
    {
      "key": "flutter",
      "label": "Flutter",
      "registry": "pub.dev · drengr_flutter_sdk",
      "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.",
      "consent": {
        "start_paused": "Drengr.start(enabled: false)",
        "opt_out": "Drengr.optOut()",
        "opt_in": "Drengr.optIn()",
        "persistence": "SharedPreferences (drengr.opt_out), mirrored to a temp marker for the synchronous check at start",
        "caveat": null
      }
    },
    {
      "key": "ios",
      "label": "iOS",
      "registry": "Swift Package Manager · CocoaPods",
      "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.",
      "consent": {
        "start_paused": "Drengr.start(startEnabled: false)",
        "opt_out": "Drengr.optOut()",
        "opt_in": "Drengr.optIn()",
        "persistence": "UserDefaults, key dev.drengr.opt_out",
        "caveat": null
      }
    },
    {
      "key": "android",
      "label": "Android",
      "registry": "Maven Central · dev.drengr:analytics-android",
      "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.",
      "consent": {
        "start_paused": "Drengr.start(startEnabled = false)",
        "opt_out": "Drengr.optOut()",
        "opt_in": "Drengr.optIn()",
        "persistence": "SharedPreferences file drengr_sdk, key opt_out",
        "caveat": "optOut() writes through the application context that start() captures, so calling it before start() is a no-op. If you gate on consent, prefer start(startEnabled = false) and call optIn() once the user agrees."
      }
    },
    {
      "key": "web",
      "label": "Web",
      "registry": "npm · drengr-js",
      "runtime": "Any browser with fetch and localStorage. Ships ESM and CommonJS with TypeScript types.",
      "permissions": "None. Runs client-side only.",
      "consent": {
        "start_paused": "start({ enabled: false })",
        "opt_out": "optOut()",
        "opt_in": "optIn()",
        "persistence": "localStorage, key drengr_opt_out",
        "caveat": "With synchronous storage the opt-out is read before any capture begins, so an opted-out visitor emits nothing at all on the next load. If you supply an async storage adapter, start() begins paused and resumes only once the check resolves."
      }
    }
  ],
  "honours_do_not_track": false,
  "honours_global_privacy_control": false
}