friction_sdk 0.0.4
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 #
baseUrlnow 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: attachFrictionRouteObservertoGoRouter(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 aWidgetsBindingObserver), 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()andFriction.outboxSize(). FrictionThemewidget theming. Pass a custom accent / surface / text color palette intoFriction.init(theme: FrictionTheme(accent: ...)). Includes two named presets —FrictionTheme.dark()(the existing look, default) andFrictionTheme.harbour(). Hardcoded purple is gone from every widget. Customer-brandable in one constructor call.FrictionLocalizations(EN + RU + ES). Every widget string now resolves throughFrictionLocalizations.of(localeCode). PassFriction.init(locale: 'ru')to switch languages. Adding more locales is one strings file each; nointlpackage required.Friction.testTrigger()helper. Devs can fire a fakeapi_error/uncaught_exception/rage_clickon 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 theContextBundleJSON round-trip used by the offline queue.OfflineQueueitself is covered by E2E (real device) — unit tests skipped pending apath_providermock. - 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}/confirmbefore closing the widget so it still files for users who skip voice.
0.3.7 #
- UTF-8 bundle uploads.
FrictionClient.postBundlewas usingHttpClientRequest.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 explicitlyutf8.encode-ing the JSON and sending bytes; Content-Type now advertisescharset=utf-8andContent-Lengthis 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 viaRedactor(sensitiveKeys: {...Redactor.allDefaultSensitiveKeys, 'myField'}). - URL query redaction.
?token=abcbecomes?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
_draggedflag 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.preparingcovers 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:
TextFieldinFrictionWidgetno longer crashes with "No Overlay widget found" when the user taps into the text input.EditableTextrequires anOverlayancestor (for cursor + selection handles) — the user'sMaterialAppprovides one but it's our sibling in the Stack, not our ancestor. We now wrapFrictionWidgetin its ownOverlaydefensively. Third (and hopefully final) wrap-order fix in this lineage, alongside the earlierDirectionalityandMaterialLocalizationsdefenses.
0.3.1 #
- Fix:
FrictionWidget'sTextFieldand other Material widgets no longer crash with "No MaterialLocalizations found" whenFrictionScopeis the outermost widget. We now installDefaultMaterialLocalizations+DefaultWidgetsLocalizationsinside the scope; yourMaterialAppstill 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 aFriction.report(reason: 'manual'), opens the friction card, and starts the voice flow. Hides itself while the card is up viaFrictionWidget.visibility. Accessibility label: "Get help". FrictionScope.showHelpButton— new constructor arg (defaults totrue). Set false to opt out (e.g. if your app already has its own help affordance). Only renders whenenableWidgetis also true.- Spoken greeting on manual taps.
VoiceController.startgains agreet: trueflag. When set, after the mic check the controller asks the backend for an opener viaVoiceClient.greet(bundleId)— a zero-byte audio POST to/v1/voice/turnthat 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 toready. FrictionWidget.visibility— publicValueNotifier<bool>toggled open/closed with the card. TheHelpButtonlistens 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:
FrictionScopeno longer crashes when wrapped aroundMaterialApp(the documented usage). The internalStackused to overlay the floating widget needs aDirectionalityancestor; we now install one if there isn't one yet, and respect the inherited one if there is. FrictionScopenow accepts atextDirectionparameter 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 toPOST /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 aVoiceClientfor app-controlled voice sessions outside the floating widget.VoiceController/VoiceState/VoicePhaseexported 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 includespackage:httpandpackage:dioon mobile/desktop). - Rage-click detection via
FrictionScope+ tap listener. FrictionScopewidget — 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
ContextBundlemirroring the backend wire format (schema1.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.