friction_sdk 0.0.4 copy "friction_sdk: ^0.0.4" to clipboard
friction_sdk: ^0.0.4 copied to clipboard

Zero-config friction capture, voice-AI feedback, and auto-ticketing for Flutter apps. The AI knows what broke because it watched it break.

0.0.4 — don't trigger on layout-overflow warnings #

  • Fix: layout-overflow assertions ("A RenderFlex overflowed by N pixels") and other debug-only framework diagnostics no longer pop the voice widget. Only genuine runtime exceptions count as friction now. These warnings still print to the console / red screen as usual.

0.0.3 — neutral default backend + router docs #

  • baseUrl now defaults to a neutral placeholder (your-backend.example.com) instead of a domain the package doesn't own — set it to your own backend.
  • Documented MaterialApp.router / go_router integration: attach FrictionRouteObserver to GoRouter(observers: [...]), not the app.

0.0.2 — docs #

  • README: dropped third-party URLs, corrected the install version, added author links (GitHub, LinkedIn).

0.0.1 — initial public release #

First release on pub.dev. Zero-config friction capture for Flutter: passive telemetry (widget tree, network, interactions, errors), an on-device trigger engine (API errors, uncaught exceptions, rage/dead clicks, form abandonment, slow requests), on-device PII redaction, an offline queue, and the floating voice-AI feedback widget that files developer-grade tickets from what it observed at runtime.

The entries below document the pre-publication development history.


0.5.0 — three new behavioral triggers #

The SDK now catches three classes of friction that no other RUM tool catches, because none of them involve an exception or HTTP error firing.

  • form_abandon — user filled a form ≥ N chars, then went idle / navigated away / backgrounded the app without submitting. New public API:

    late final FormTracker _t;
    @override void initState() {
      super.initState();
      _t = Friction.trackForm(
        name: 'CheckoutForm',
        controllers: [cardCtrl, cvcCtrl, nameCtrl],
        route: '/checkout',
      )!;
    }
    @override void dispose() { _t.dispose(); super.dispose(); }
    

    Multi-signal detector: idle threshold (default 45s, tunable), navigation-away (instant, via FrictionRouteObserver), app-backgrounded (instant, via a WidgetsBindingObserver), and unmount-without-submit (instant). Call _t.markSubmitted() from your submit handler so dispose doesn't false-positive.

  • duplicate_submit — user submitted the exact same form payload twice within 10s of a 4xx response. High-signal "user didn't understand the error message" trigger. New public API — call from your submit handler:

    final res = await http.post(url, body: payload);
    Friction.reportSubmit(
      target: 'SendReviewButton',
      payload: payload,
      status: res.statusCode,
      errorBody: res.body,
      url: url.toString(),
      method: 'POST',
    );
    
  • slow_api_no_ui — automatic, zero integration. An outbound request has been pending > 3s AND no loading widget is visible in the tree. The SDK walks the live widget tree at the threshold tick and matches against common loading-indicator types (CircularProgressIndicator, LinearProgressIndicator, RefreshProgressIndicator, CupertinoActivityIndicator, plus shimmer/skeletonizer packages).

All three trigger types are also recognized by the backend planner — your AI's first sentence will name what happened ("you bailed mid-form, file it?" / "you hit Send twice, the error wasn't clear — file it?" / "feed took 3s+ with no spinner. File it?").

0.4.0 — production-grade pass #

Real minor bump. Six focused enhancements that take the SDK from "evaluation toy" to "ready for a paying customer integration."

  • Offline queue + retry. Bundles that fail to POST (network blip, 5xx, cold offline) are now persisted to disk and replayed on the next launch with jittered exponential backoff. Previously a flaky cellular connection would silently lose the ticket — the worst possible failure mode for a "we catch what users won't tell you" product. New public API: Friction.flushOutbox() and Friction.outboxSize().
  • FrictionTheme widget theming. Pass a custom accent / surface / text color palette into Friction.init(theme: FrictionTheme(accent: ...)). Includes two named presets — FrictionTheme.dark() (the existing look, default) and FrictionTheme.harbour(). Hardcoded purple is gone from every widget. Customer-brandable in one constructor call.
  • FrictionLocalizations (EN + RU + ES). Every widget string now resolves through FrictionLocalizations.of(localeCode). Pass Friction.init(locale: 'ru') to switch languages. Adding more locales is one strings file each; no intl package required.
  • Friction.testTrigger() helper. Devs can fire a fake api_error / uncaught_exception / rage_click on demand to see the widget without breaking their app. Cuts integration debugging time from hours to minutes:
    await Friction.testTrigger(
      type: 'api_error',
      mockUrl: '/api/checkout/payment',
      mockStatus: 402,
      mockErrorBody: 'card_declined',
    );
    
  • 41 tests, up from 23. Added coverage for HttpTrigger, ErrorTrigger, TapTrigger, and the ContextBundle JSON round-trip used by the offline queue. OfflineQueue itself is covered by E2E (real device) — unit tests skipped pending a path_provider mock.
  • Polish: animated success row + mic-denied banner. The "✓ ticket on its way" row now does an elastic scale-in for the check icon and a delayed fade-in for the text. When the user previously denied microphone permission, the text fallback now shows a soft banner with an "Open Settings" button that calls permission_handler.openAppSettings().

0.3.8 #

  • Tickets no longer auto-file when the user dismisses the widget. Bundle ingest now CAPTURES runtime context but does not create a ticket — that only happens when the user explicitly engages (voice "done", or tap "Send report without talking"). Previously, every triggered bundle became a ticket regardless of whether the user closed the widget. The "Send report without talking" button now POSTs /v1/bundles/{id}/confirm before closing the widget so it still files for users who skip voice.

0.3.7 #

  • UTF-8 bundle uploads. FrictionClient.postBundle was using HttpClientRequest.write(String), which serialises via the request's encoding (Latin-1 by default). Any non-Latin-1 codepoint in the bundle — Arabic in a captured network errorBody, Cyrillic in a route name, an emoji in the user transcript — would throw "Invalid argument (string): Contains invalid characters" and the bundle would never reach the server. Fixed by explicitly utf8.encode-ing the JSON and sending bytes; Content-Type now advertises charset=utf-8 and Content-Length is set explicitly. No SDK consumer change required.

0.3.6 #

  • Two-pass PII redactor. Field-aware JSON walk (replaces values for 47 default sensitive keys — password, cvv, otp, accessToken, etc. case-insensitive, normalises _/-/space variants) runs before the regex sweep. Catches short passwords no regex would match, AND card numbers no field name would match. Customer-extensible via Redactor(sensitiveKeys: {...Redactor.allDefaultSensitiveKeys, 'myField'}).
  • URL query redaction. ?token=abc becomes ?token=<redacted> before the bundle's network event lands. Path preserved so the AI can still reason about which endpoint failed.
  • 23 redactor tests. Cover nested JSON, arrays, key variants, malformed URLs, header allowlist case-insensitivity, customer-extensible keys.

0.3.5 #

  • Help button: drag-then-tap fixed. After dragging the floating button to a new position, subsequent taps were silently ignored. Cleaned up the gesture detection — Flutter's built-in arbitration already distinguishes pan vs tap, so our extra _dragged flag was both unnecessary AND broken.
  • Smart route naming. The AI no longer says literal paths like "/farm/abc-123" or "/auth/otp/verify". Stub planner translates routes via a built-in map plus a generic fallback; OpenAI planner gets explicit instructions in the system prompt. "the home screen", "the farm details page", "signup verification", etc.
  • Loading state. New VoicePhase.preparing covers the gap between the user tapping Help and the greeting audio arriving (mic check + backend round-trip). Widget shows a spinner + "Just a sec…" instead of looking frozen.

0.3.4 #

0.3.3 #

  • Diagnostics: every step of the friction flow now logs to the Flutter console via debugPrint('[Friction] …'). You'll see bundle POST attempts, HTTP status, mic permission state, voice-turn response, playback start/end. Silent failures are now loud failures — vital for debugging on a real device where network and ATS issues used to disappear into the void.

0.3.2 #

  • Fix: TextField in FrictionWidget no longer crashes with "No Overlay widget found" when the user taps into the text input. EditableText requires an Overlay ancestor (for cursor + selection handles) — the user's MaterialApp provides one but it's our sibling in the Stack, not our ancestor. We now wrap FrictionWidget in its own Overlay defensively. Third (and hopefully final) wrap-order fix in this lineage, alongside the earlier Directionality and MaterialLocalizations defenses.

0.3.1 #

  • Fix: FrictionWidget's TextField and other Material widgets no longer crash with "No MaterialLocalizations found" when FrictionScope is the outermost widget. We now install DefaultMaterialLocalizations + DefaultWidgetsLocalizations inside the scope; your MaterialApp still installs its own copies for everything below it.
  • Draggable Help button: the floating button can now be dragged anywhere on screen. On release it snaps to the nearest horizontal edge (Messenger chat-head style) and remembers its position for the rest of the session. Quick taps still fire Friction.report — drags don't.
  • Help button bumped from 48px to 52px for a friendlier tap target.

0.3.0 #

Always-visible Help button with a spoken greeting.

  • New HelpButton — a 48x48 floating action button (bottom-right, brand purple, chat-bubble icon) that's always on screen. Tapping it manually fires a Friction.report(reason: 'manual'), opens the friction card, and starts the voice flow. Hides itself while the card is up via FrictionWidget.visibility. Accessibility label: "Get help".
  • FrictionScope.showHelpButton — new constructor arg (defaults to true). Set false to opt out (e.g. if your app already has its own help affordance). Only renders when enableWidget is also true.
  • Spoken greeting on manual taps. VoiceController.start gains a greet: true flag. When set, after the mic check the controller asks the backend for an opener via VoiceClient.greet(bundleId) — a zero-byte audio POST to /v1/voice/turn that the backend recognises as "widget just opened, no user input yet" and routes through the planner. The planner-generated greeting (which names the broken feature from the Context Bundle) is then played through TTS before the controller transitions to ready.
  • FrictionWidget.visibility — public ValueNotifier<bool> toggled open/closed with the card. The HelpButton listens to it; advanced users can also gate their own UI on it.
  • VoiceClient.greet({bundleId}) — public entry point for app-controlled voice sessions that want to fetch a greeting without recording.

Contract #

Empty audio + no prior turns on /v1/voice/turn → backend returns a planner-generated greeting (assistant turn stored at index 0, done=false). Empty audio mid-conversation still returns 400. The existing non-empty-audio flow is unchanged.

0.2.1 #

  • Fix: FrictionScope no longer crashes when wrapped around MaterialApp (the documented usage). The internal Stack used to overlay the floating widget needs a Directionality ancestor; we now install one if there isn't one yet, and respect the inherited one if there is.
  • FrictionScope now accepts a textDirection parameter for RTL apps that want the floating popup mirrored.

0.2.0 #

Voice in the widget.

  • Push-to-talk mic button in FrictionWidget. Records on tap, stops on second tap, uploads to POST /v1/voice/turn, plays the assistant's reply audio. Loops until the planner signals the conversation is done.
  • Live amplitude pulse while recording (mapped from record's amplitude stream).
  • Closed-caption display of both the user's transcribed turn and the assistant's reply text while audio plays.
  • Graceful fallback to text if microphone permission is denied or the platform has no audio capture. No change to the public API — the SDK detects and switches.
  • Friction.onBundleAccepted(bundleId, trigger) callback — fires once the backend has accepted a bundle and assigned it an id. The widget uses this to open a voice conversation tied to the right bundle.
  • Friction.buildVoiceClient() — public entry point to create a VoiceClient for app-controlled voice sessions outside the floating widget.
  • VoiceController / VoiceState / VoicePhase exported for custom UIs.
  • New runtime dependencies: record, audioplayers, permission_handler, path_provider, http, http_parser.

Platform setup #

iOS: add to Info.plist:

<key>NSMicrophoneUsageDescription</key>
<string>Friction asks for the microphone so you can tell us what went wrong using voice.</string>

Android: add to AndroidManifest.xml:

<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.INTERNET"/>

macOS (if targeting desktop): add com.apple.security.device.audio-input and com.apple.security.network.client to your entitlements.

Known limitations in 0.2.0 #

  • Push-to-talk only; streaming voice is 0.3.
  • Tap targets are still tagged by coordinate (not semantics) — same as 0.1.

0.1.0 #

Initial release.

  • Friction.init({appId, baseUrl}) top-level entry point.
  • Auto-capture of uncaught Flutter & isolate exceptions.
  • HTTP error capture via HttpOverrides — observes every dart:io request (which includes package:http and package:dio on mobile/desktop).
  • Rage-click detection via FrictionScope + tap listener.
  • FrictionScope widget — wraps your app and shows the floating Friction UI.
  • FrictionWidget — text-input feedback popup that opens on friction signals.
  • FrictionRouteObserver — feeds the current screen name into the bundle so the synthesized ticket can reference the right route.
  • On-device PII redaction: emails, JWT-shaped tokens, credit-card patterns, long hex strings, phone numbers. Allow-listed headers only.
  • Pure-Dart ContextBundle mirroring the backend wire format (schema 1.0).
  • Friction.report(...) for manual triggering.

Known limitations in 0.1.0 #

  • The floating widget is text-only. Voice input/output (Deepgram + ElevenLabs) arrives in 0.2.0.
  • Tap targets are tagged by coordinate rather than semantics — clustering by widget hierarchy comes in 0.2.0.
0
likes
140
points
118
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Zero-config friction capture, voice-AI feedback, and auto-ticketing for Flutter apps. The AI knows what broke because it watched it break.

Repository (GitHub)
View/report issues

Topics

#error-reporting #observability #telemetry #feedback #voice

License

MIT (license)

Dependencies

audioplayers, flutter, http, http_parser, path_provider, permission_handler, record

More

Packages that depend on friction_sdk