nohmo 0.6.0 copy "nohmo: ^0.6.0" to clipboard
nohmo: ^0.6.0 copied to clipboard

Official Nohmo analytics SDK for Flutter — device tracking, session journeys, install attribution, deep linking, crash reporting and event batching for iOS and Android.

0.6.0 #

A full audit of 0.5.0 against real user behaviour — what a person actually does with a phone — rather than against the code. Two of these stopped iOS working outright; the rest were wrong data that looked like right data.

Fixed — iOS #

  • Deep linking was completely dead on Flutter 3.47+. The plugin registered only for UIApplicationDelegate callbacks. Once a host app adopts the UIScene lifecycle — Flutter's migration is enabled by default from 3.47 and rewrites the template AppDelegate automatically — UIKit stops calling application:openURL:options: and application:continueUserActivity: entirely, and per Flutter's migration guide the launchOptions passed to didFinishLaunchingWithOptions become nil. All three link entry points went quiet at once: no direct opens, no Universal Links, no deferred deep linking and no launch-URL attribution, on every iOS install, with no error anywhere. The plugin now also registers as a scene delegate and implements scene:willConnectToSession:options:, scene:openURLContexts: and scene:continueUserActivity:, while keeping the application-delegate path for apps that have not migrated. Both return NO so the host app's own URL handling still runs.
  • Added the privacy manifest. PrivacyInfo.xcprivacy now ships through both CocoaPods and Swift Package Manager, declaring the required-reason APIs the SDK calls (NSUserDefaults, file timestamps) and the data it collects. Without it an App Store upload comes back with ITMS-91053, because an app cannot declare on an SDK's behalf what it does not know the SDK calls.
  • A transient Keychain failure no longer destroys the stable device id. Any non-success from SecItemCopyMatching fell through to delete-and- regenerate — including errSecInteractionNotAllowed, which is what a launch into the background before the device's first unlock returns. That threw away the real reinstall-matching identity and split one person into two devices, over a failure that would have resolved itself on the next launch. Only a genuinely absent or unreadable item is replaced now.
  • Tapping the same link twice did nothing the second time. Destinations were de-duplicated by value for the lifetime of the process, and the value was restored from storage, so across launches too. Two friends sharing one product URL, or re-opening a saved message, produced no event and no stream emission, and the app never navigated. Only a redelivery of one tap by the platform, within a second, is collapsed now.
  • A direct open was reported to the app as deferred. The replay to a late subscriber hardcoded the source — and because a link resolves inside init(), before any app widget exists, the replay is the only way an app ever sees a cold-start link. Anything branching on source did the wrong thing on every cold start. The source is carried and persisted with the destination.
  • Launch-URL attribution now survives a restart. Session attribution was memory-only, so session 1 of a campaign install was attributed and session 2 looked organic.
  • Added Nohmo.consumeDeepLink(). The replay is per-subscriber and was otherwise unbounded, so an app subscribing from a screen that remounts kept being handed a destination it had already navigated to.

Fixed — sessions and screens #

  • A two-second app switch no longer splits the session. Every foreground minted a new session id, so reading an OTP, approving a payment or picking a photo ended one session and started another. Checkout funnels spanned two sessions and session counts were inflated. New sessionTimeout option, 30 minutes by default (Duration.zero restores the old behaviour).
  • The screen the user backgrounds from now records TIME_SPENT. It only ever fired on a screen change, so the last screen of every session — usually checkout, the paywall or the signup form — recorded no time at all. Time spent in the background is no longer billed to the screen either.
  • Back-navigation no longer corrupts the screen name. didPop named the route underneath from RouteSettings.name, so a tab labelled by a NohmoScreen became / on the way back — and where the route had no name, nothing was reported and the current screen stayed stuck on the page the user had just left, stamping every later event with it. A visible NohmoScreen now claims its route, and the observer prefers that name.

Fixed — delivery and resilience #

  • A transient 4xx no longer destroys the batch. Every non-5xx response dropped the events, so one 400 from a bad rolling deploy, a WAF or a captive portal lost the batch permanently — including the APP_INSTALL in it, which is never re-sent because the first-open flag is already written. 408, 425 and 429 are retried with 5xx; 400 and rejected credentials are still dropped rather than retried forever.
  • Retries back off exponentially from 5s to a 5-minute ceiling, with jitter. A device offline for hours was re-POSTing on every flush interval. An explicit flush(), including the one on backgrounding, ignores the backoff.
  • A NohmoStorage that throws can no longer kill the SDK. One failed read aborted init before the device id was assigned, and every event afterwards buffered into an uncapped in-memory list that nothing ever drained — silently, permanently, for the life of the process. Reads are individually guarded, the device id is always assigned, and the buffer is capped.
  • A push token registered while offline is retried on the next start until the server confirms it, like linkUser already was. Uninstall detection depends on that token arriving.
  • No more silent trimming between two caps. The persisted tail was half the in-memory queue, so events between the two vanished on process death without a word. Both are 1000 now, dropping is reported, and persisting throttles itself when the queue is deep so a large write does not risk an ANR.
  • install_utm is no longer sent on every event. The install's utm_* values were stamped onto every SCREEN_VIEW, PRESS and CONVERSION for the life of the install, and ingestion never read the field — it is not referenced anywhere in the backend or the dashboard, and is not even stored, since only data is persisted as JSON. Nothing is lost: the attribution is still reported as the INSTALL_ATTRIBUTED event (which the dashboard does render) and is durable server-side in InstallAttribution, keyed to the device.
  • Event names, user ids and session ids are clipped to 255 characters. Ingestion stores all three in 255-char columns with no truncation of its own (tracker/models.py:199-201), and one over-long value fails the bulk_create for the entire batch, which then retries and fails identically — so a single long custom event name from the host app could silently destroy every event batched alongside it.
  • Device and session ids come from Random.secure().
  • The Play Install Referrer service connection is closed on the timeout path too.
  • Android's cold-start deep link is no longer dropped when a cached FlutterEngine outlives the activity.

Fixed — autocapture #

  • Personal data in a tap label is redacted by default. text is whatever the control displays, and controls display user data: PRESS "priya@gmail.com" was leaving the device because an app turned autocapture on. Emails, phone numbers, card- and account-length digit runs and currency amounts become placeholders, keeping the wording that makes a report readable. New NohmoRedacted widget for what shape-matching cannot catch — names, addresses — and redactText: false to opt out.
  • A control behind IgnorePointer or AbsorbPointer no longer reports a press. It cannot be tapped, so reporting it manufactured dead presses that never happened — the pattern apps use to disable a form mid-submit.
  • Text fields report a tap, so a form funnel has its "tapped into the email field" step. The value is never read; only the field's own hint or label.

Changed — verified against ingestion #

  • APP_BACKGROUND can now fire more than once per sessionId, which follows directly from sessionTimeout. Checked against the backend before shipping: sessionDurationSecs is never read — nothing in the repo references it — and APP_BACKGROUND itself is stored but read by no dashboard, rollup, metric or webhook. Session duration is computed as SUM(TIME_SPENT.data.seconds) per session (tracker/tasks.py:396-419), so the extra events are inert and the sessionDurationSecs field is carried for parity only.
  • Session durations will go UP and session counts will go DOWN, both because the numbers were wrong before. Duration rises because TIME_SPENT now fires on backgrounding, and the final screen of every session — previously never measured at all — is now included in the sum the backend takes. Counts fall because one visit interrupted by an OTP or a payment redirect is now one session rather than three. Expect a visible step change in Avg session and Sessions on the dashboard the day this ships.
  • A session the user starts themselves is now attributed as organic. Session attribution is cleared when a new session begins (campaign credit for the install itself lives in InstallAttribution, which is unaffected). The backend gives a session with no utm blank source fields and no lookback (tracker/tasks.py:424-450), so returning visits count as organic — which is the standard per-session meaning, and what the Traffic page's organic/UTM split already assumes.

Added #

  • flushInterval: Duration.zero disables the periodic timer, so the SDK can be initialised inside a testWidgets test. The live Timer.periodic otherwise outlives the widget tree and trips Flutter's '!timersPending' assertion before tearDown can shut it down.
  • iosPasteboardAttribution option. The first-launch pasteboard read shows iOS 14+'s "Pasted from …" banner for most users, not only those who tapped a Nohmo link — a comment in the source claimed otherwise, and was wrong.

Fixed — release hygiene #

  • kNohmoSdkVersion matches the package again. 0.5.0 shipped stamping 0.4.1 onto every device record, where it cannot be backfilled, so there was no way to tell whether 0.5.0's fixes had reached anyone. tool/verify.sh and a new CI job now assert that the four version declarations agree, that the privacy manifest exists and is wired into both dependency managers, and that the scene-lifecycle registration is present.

0.5.0 #

Fixed #

  • A rejected call is no longer reported as success. Every endpoint except /track read the response body without ever checking the HTTP status, so a wrong API key, an unknown project or a device the server had never registered came back 4xx with parseable JSON and the SDK logged success. linkUser() even emitted USER_LINKED for a link that never happened. Failures are now reported with debugPrint, and rejected credentials produce one clear message naming projectId, apiKey and host rather than a line per call.

  • A refused batch of events says so. /track checked only for 5xx, so a 4xx — including a rejected key, and /track authenticates by body rather than by header so it can fail on its own — dropped the batch and reported nothing. The drop is still correct (retrying will not fix a refused key) but it is now reported; 5xx still re-queues, unchanged.

  • A failed linkUser() is retried instead of lost. The SDK now records which userId the server actually confirmed and re-sends it on the next start, so someone who logged in while offline is linked as soon as the app reaches the network. A link already confirmed costs no request on later launches.

  • A reinstall no longer creates a new anonymous device. The device id lives in SharedPreferences/NSUserDefaults, which an uninstall wipes, and the stableId the backend matches returning devices on was never sent from Flutter — so every reinstall counted a fresh install and detached a logged-in user from their history. The plugin now supplies one: a Keychain UUID on iOS (ThisDeviceOnly, so it never syncs across devices) and a SHA-256 of ANDROID_ID salted with the package name on Android. Where the platform has none, no stableId is sent rather than a guessed fingerprint, which would collide across identical handsets and merge two people into one device.

    This is native code: do a clean build after upgrading.

0.4.1 #

Initial release of the Nohmo Flutter SDK — feature parity with the React Native SDK at the same version.

  • Device identity, sessions and batched event delivery with a queue that survives the process being killed.
  • Automatic screen tracking via NohmoNavigatorObserver (SCREEN_VIEW, TIME_SPENT), plus NohmoScreen for apps that do not route through a Navigator.
  • Tap autocapture via NohmoAutocapture (PRESS, LONG_PRESS, RAGE_CLICK) — no build-time codegen required.
  • App lifecycle events (APP_INSTALL, APP_OPEN, APP_BACKGROUND).
  • Install attribution: Play Install Referrer on Android, pasteboard click token on iOS, probabilistic fallback on both.
  • Smart Links: direct and deferred deep linking through Nohmo.deepLinks.
  • Crash reporting: Flutter framework and uncaught Dart errors as JS_ERROR; native Android/iOS crashes (including Swift fatalError and signals) as APP_CRASH, attributed to the session they happened in.
  • Invite links (buildInviteLink), conversions, user linking, push-token registration for uninstall detection.
  • Injectable httpClient so apps can assert on exactly what the SDK sends.
  • Swift Package Manager support alongside CocoaPods — both read the same sources under ios/nohmo/Sources/nohmo, so CocoaPods users and older Flutter versions are unaffected.

Verified on Flutter stable 3.47.1 (Dart 3.13.1) and beta 3.48.0 (Dart 3.14.0): flutter analyze clean, 29 tests passing, and a real flutter build apk on both the KGP and Built-in Kotlin Gradle paths (AGP 9.1 / Kotlin 2.4) with the plugin's classes asserted present in the dex. iOS sources compile against the iOS 26 SDK for device and simulator, and a real flutter build ios links NohmoPlugin/NohmoCrash into the app through both CocoaPods and Swift Package Manager.

Additionally run on a physical Android device (Android 16 / API 36, arm64), confirming install attribution, autocapture, rage clicks, session lifecycle, crash-surviving queue persistence, and native crash capture at runtime.

Fixed during that device testing: Android crash records were written correctly but never drained. The crash store resolved its directory from a Context that was only captured when the crash handler was installed, while Dart drains the previous run's crashes before installing this run's handler — so the read always returned empty and records accumulated on disk, never reported. readAndClear now takes the Context directly, removing the ordering dependency.

tool/verify.sh re-runs all of that in one command, and .github/workflows/flutter.yml runs it against stable and beta on every push and weekly on a schedule.

0
likes
160
points
169
downloads

Documentation

Documentation
API reference

Publisher

verified publishernohmo.in

Weekly Downloads

Official Nohmo analytics SDK for Flutter — device tracking, session journeys, install attribution, deep linking, crash reporting and event batching for iOS and Android.

Homepage
View/report issues

Topics

#analytics #attribution #tracking #deeplink #crash-reporting

License

MIT (license)

Dependencies

flutter, http

More

Packages that depend on nohmo

Packages that implement nohmo