brevwick 0.4.1
brevwick: ^0.4.1 copied to clipboard
Brevwick SDK for Flutter — send QA feedback from your app in one widget wrap.
Changelog #
All notable changes to the brevwick Flutter SDK are documented here.
The format follows Keep a Changelog;
the project follows SemVer (pre-1.0: minor for any
public-API addition, patch for bug fixes / internal refactors).
0.4.1 — 2026-05-02 #
Fixed #
- Screenshot pre-capture (race-free frame). The "Attach screenshot"
flow now captures the underlying app frame BEFORE the bottom-sheet
is pushed (in
BrevwickOverlay._openComposer). Previously the capture ran from inside the sheet, so the captured frame included the modal barrier scrim and the sheet UI itself. The captured screenshot is now exactly the page state the user wanted to report on; subsequent "Attach screenshot" taps within the same sheet are no-ops (the captured frame is the moment the FAB was tapped). - Attachment-cap race. Two concurrent attach actions (e.g. tapping
"Attach screenshot" while a previous "Attach file" is still
resolving) could both pass the pre-flight cap-check and double-append
past the 5-attachment cap. Both attach buttons now disable while
any attach action is in flight (
_attachInFlight); the cap is also re-checked inside_tryAddAttachmentas defence-in-depth. file_pickerunbounded memory.defaultBrevwickPickFilesnow usesallowMultiple: false. Multi-select withwithData: truematerialises every selected file's bytes before the overlay can apply its caps, so a user picking 100 × 10 MB files would allocate 1 GB before any gate ran. Bounding to one file per tap keeps the per-tap memory footprint at the documented 10 MB cap; multi-attach is N taps instead of one batch.- Single-place cap-check.
_tryAddAttachmentnow owns all three validations (count cap, size cap, MIME allow-list). The duplicate cap-check inside_attachFile's loop body is gone; the helper's comment block matches the code. - AI toggle accessibility. The
Format with AIrow was aRowwith aTextlabel and a separateSwitch, which exposed two semantic nodes — TalkBack and VoiceOver read the switch as unlabeled and the label was not tappable. Now rendered asSwitchListTile.adaptive: merged semantics, tappable label, iOS / macOS get a Cupertino switch automatically.
Tests #
- New:
Pre-capture happens BEFORE the sheet opensconfirms the screenshot hook fires once on FAB tap, BEFORE any sheet-side interaction. - New:
Race-safe: two concurrent attach actions never exceed the cappins the disabled-during-in-flight contract. - New:
Attach screenshot at-cap surfaces the cap SnackBarcovers the early-return path when the screenshot future would otherwise be awaited unnecessarily. - New:
Screenshot can only be attached once per sheetpins the pre-capture latch semantics. - New:
AI toggle: tapping the label (not the switch) toggles the valuepins the merged-semantics behaviour of the SwitchListTile. - Hardened
defaultBrevwickPickFilestest-seam restoration:FilePicker.platformis now restored in every group'stearDownso a fake leaks no further than the test that installed it. - The
_resolveHooksproduction fall-through test no longer makes a real network call: a fake_ConfigOkAdapterresolves the project-config GET andsubmit_seam.submitDioOverridecovers the submit boundary.
0.4.0 — 2026-05-02 #
Added #
BrevwickOverlay— themed FAB + modal bottom-sheet composer that mirrors the React widget UX (description, expected/actual collapsible, attach screenshot/file, AI toggle, send). Wrap the app once afterBrevwick.install(...)and the FAB stacks overchild. The FAB itself is wrapped inBrevwickSkipso it never appears in captured screenshots.BrevwickOverlay.position—Alignmentcontrolling where the FAB sits inside the overlay'sStack. Defaults toAlignment.bottomRight. Replaces the placeholderOverlayPositionenum from the original SDD contract —Alignmentis the existing Flutter primitive for this and removes the need for a one-off enum.BrevwickOverlay.hidden—boolflag that unmounts the FAB without unmountingchild. Toggle from ancestor state to suppress the FAB during tutorials, onboarding, or full-screen takeovers.BrevwickOverlay.debugHooks—@visibleForTestingstatic seam for widget tests. Production callers leave itnull; the overlay then binds against the liveBrevwick.instanceplusFilePicker.platformon first use.BrevwickOverlayHooks+BrevwickPickedFile— test-seam types inlib/src/overlay_testing.dart. Deliberately not exported from the public barrel (lib/brevwick.dart); tests reach them via directimport 'package:brevwick/src/overlay_testing.dart'. Mirrors the established pattern fromsubmit_test_seam.dart.file_picker: ^10.0.0runtime dependency for the "Attach file" button. Supports Android, iOS, macOS, Linux, Windows, and web. Adds the third runtime dep on the SDK afterdioandscreenshot(plus thecrypto/device_info_plus/package_info_pluscontextual deps); justified by the cross-platform native picker UX and bounded by the existing 10 MB / 5-attachment cap. Recorded in SDD § 12 dependency-discipline block.
Changed #
BrevwickOverlayis aStatelessWidget(the original SDD draft declaredStatefulWidget; the shipped widget holds no mutable state on the outer wrapper). The composer sheet itself remainsStatefulWidgetbecause it owns text controllers + attachment list- submit lifecycle.
AI toggle behaviour (intentional JS divergence) #
- The Flutter overlay defaults the "Format with AI" switch to off
and omits
use_aifrom the submittedFeedbackInputuntil the submitter explicitly toggles it. The React widget defaults the switch to on and always attachesuse_ai. This is a deliberate UX divergence — issue #8 acceptance criteria call for opt-in semantics on Flutter. Server-side default applies whenuse_aiis omitted, making the wire-level behaviour semantically equivalent for tenants whose admin-default is "on". Documented inline onBrevwickOverlayand_AiToggleRowdartdocs.
0.3.0 — 2026-05-02 #
Added #
Brevwick.send(input)— staticsubmit(input)entry point that preserves the never-throws contract even beforeinstall()has run (returnsSubmitFailed(ingestRejected, 'Brevwick.install(...) must be called first')pre-install). Use this when the call site cannot be certainBrevwick.instanceis live.Brevwick.instance.submit(input)— full submit pipeline implementation (wasUnimplementedErrorin 0.2.0). Validates attachments, presigns each, PUTs bytes to the returned URL, then POSTs/v1/ingest/issueswith retries on the final POST only (matches JS SDK byte-for-byte). Returns the sealedSubmitResultunion; never throws.Brevwick.clearRings()— drains every ring buffer. Called by the submit pipeline after aSubmitOkso a successful report does not leak context into the next submission.Brevwick.currentRoutePath()— live top-of-stack route path captured by the route observer.kSubmitTotalBudget(default 30 s) andkSubmitBackoffs([250 ms, 1 s]) public constants documenting the submit-pipeline contract.crypto: ^3.0.6runtime dependency for SHA-256 attachment digests (sent in the presign body so the server signs R2 PUTs with a matchingx-amz-checksum-sha256, and on eachattachments[*].sha256for ingest cross-checking).characters: ^1.4.0runtime dependency for grapheme-aware title truncation — the previousString.substring(0, 120)would have split surrogate pairs (e.g. emoji landing exactly across the 120-codepoint boundary), emitting malformed UTF-16 / UTF-8 on the wire.ProjectConfigvalue class (ai_enabled,ai_submitter_choice_allowed) — drives whether the overlay renders the "Format with AI" toggle. Both flags must betruefor the toggle to be shown.Brevwick.projectConfig—ValueListenable<ProjectConfig?>exposing the cached tenant config. Seedednulland staysnulluntilensureProjectConfig()resolves; after first resolution the notifier always holds a non-nullProjectConfig(failure modes writeProjectConfig(false, false)rather than reverting tonull).Brevwick.ensureProjectConfig()— best-effort, idempotent fetch againstGET {endpoint}/v1/ingest/config. Concurrent calls share the in-flight Future so the network request is performed exactly once perinstall(). Never throws — non-2xx, malformed JSON, empty body (204), and any thrown exception all resolve to theProjectConfig(false, false)fallback plus a warn-level entry in the console ring (when the console ring is installed).
Changed #
- Wire shape:
usernow lives INSIDEuser_contextalongside the mergeduserContext()extras. This matches the canonical SDD § 12 line 1562 and the JS SDK'scomposePayload(submit.ts:489-495,514). The previous top-leveluserplacement was a divergence from both sources of truth. - Wire field name: the network ring's snapshot ships under
network_errorsper SDD § 7 line 642 + § 12 line 1569. The JS SDK is tracked for alignment in a follow-up cross-repo PR (it currently emitsnetwork_calls); server ingest readsnetwork_errors. - Presign retry removed —
presignOneis now single-shot per attachment, matching the JS source. Retrying a transient 5xx three times burned 3× the presign quota for one blip; only the final/v1/ingest/issuesPOST retries (1 + 2 backoffs = 3 total attempts). - Server-rejection redaction is now redact-then-cap, not cap-then-redact. A bearer token at offset > 256 in a 5 MB server-echo body could previously slip past the cap unredacted; the new ordering collapses secrets first so the cap can never sever a token mid-pattern.
SubmitErrorCode.ingestRejectedis now used for non-retryable Dio failures (badCertificate,unknown) — they were never retried, so labelling themingestRetryExhaustedlied about the failure shape.BrevwickUser.idruns throughredact()at the boundary as defence-in-depth; the SDD documentsidas opaque, but consumers in practice stuff in email-shaped strings, JWTs, etc._raceWithCancelrewritten on top ofCompleterso the losing-sidewhenCancel.thencontinuation cannot throw_CancelledByBudgetinto the unhandled-error zone after the task resolved (previously crashedflutter_testand any caller installing a zone error handler).
Security #
- Every payload context field (
title,description,expected,actual,route_path,device_context,user_context,BrevwickUser.traits) runs throughredact()before transmission; the test suite pins each path with a redaction test. userContext()callback exceptions have their message run throughredact()before landing in the console-ring warn entry. A consumer exception could otherwise carry the very PII the boundary scrubber is meant to remove (e.g.StateError("user 'jane@acme.com' missing flag")).- Project-config fetch failures route through the console ring's
record(), which runsredact()over the message at the boundary. ABearer pk_…substring accidentally surfaced through a transport error'stoString()collapses toBearer [redacted]before reaching the captured warn entry — pinned by a redaction test.
Internal #
- New
SubmitContextinterface decouplesSubmitterfrom the liveBrevwicksingleton so the submit pipeline is no longer in a circular import with the client. Tests can constructSubmitterwith a hand-rolledSubmitContextfake. submitDioOverridetest seam moved tolib/src/submit_test_seam.dart(NOT re-exported from the public barrel) — pub.dev consumers cannot reach the override without an explicitpackage:brevwick/src/...import that signals "I am reaching into internals".lib/brevwick.dartbarrel now usesshow Brevwickto narrow the exported surface — internal helpers (Submitter, the submit-test seam, etc.) stay invisible to consumers.
Notes #
- Project-config failure-mode divergence from JS. JS
getConfig()returnsPromise<ProjectConfig | null>; the Flutter SDK exposes aValueListenable<ProjectConfig?>that, after first resolution, holdsProjectConfig(false, false)rather than reverting tonull. Both surfaces encode the same "no AI toggle, no submitter choice" outcome. The Flutter shape lets consumer widgets render the AI toggle unconditionally without a null-state arm. Documented in brevwick-ops SDD § 12.
0.2.0 — 2026-05-01 #
Added #
BrevwickScreenshotScopewidget — wraps the app once, owns the SDK'sScreenshotController, and exposes an opaqueBrevwickScreenshotScopeHandleto descendants viaBrevwickScreenshotScope.maybeOf(context). Closes #7.BrevwickSkipwidget — wraps any subtree that must NOT appear in the captured PNG (passwords, the SDK FAB itself, tenant-flagged sensitive UI). Mirrors the JS SDK[data-brevwick-skip]semantics. Outside a scope it still wraps in a permanently-visibleVisibility(maintainSize: true)so layout never reflows when a scope is added or removed.BrevwickScreenshotScopeHandle— opaque capture handle with a reference-countedcapture(pixelRatio:, onCaptureWarn:, timeout:)method. Concurrent captures cannot strand the UI hidden; nested captures only restore visibility when the outermost finalises. Default 5 s timeout — a wedged render tree resolves tonullplus awarnconsole-ring entry rather than hanging the SDK's submit pipeline indefinitely.BrevwickRouteObserver— aNavigatorObserverthat captures{path, timestamp}records onto the route ring (cap 20, FIFO) and exposes the live top-of-stack path viaValueListenable<String?> routePath. Wire intoMaterialApp.navigatorObserversorGoRouter.observers. Path is run through the placeholder pass (:token/:auth/:key) and the globalredact()sweep so resolved emails / JWTs / bearer tokens / blobs collapse to coarse tokens before reaching the ring.Brevwick.routeObserver()andBrevwick.goRouterObserver()— lazy-construct (and reuse) the boundBrevwickRouteObserver. The observer is disposed onuninstall()so dangling navigator callbacks become no-ops.Brevwick.snapshotRoute()— FIFO defensive snapshot of the route ring.- Console ring (
lib/src/rings/console.dart, internal) — chainsFlutterError.onError+PlatformDispatcher.instance.onErrorpreserving previously-installed handlers by reference, dedups on(message, firstFrame)within 500 ms via aMap<key, entry>(mirroring JS canonical, including the opportunistic prune past size 32), trims stacks to top 20 frames, and redacts message + stack at the boundary. Brevwick.recordConsoleEvent({level, message, stack?})— manual seam for non-error logs and screenshot-ring warnings; honours the ring's dedup + redact + trim. No-op whenconfig.rings.console = false.Brevwick.snapshotConsole()— FIFO defensive snapshot of the console ring (entries are live references the dedup path mutates).Brevwick.runGuarded(body)— staticrunZonedGuardedwrapper that routes uncaught zone errors into the console ring. Safe to call beforeinstall()(escapes are silently dropped, matching the JS contract).
Changed #
Brevwick.captureScreenshot()now takes aBuildContextand an optionalpixelRatio(default2.0); the previous signature was a Phase-3 stub. ReturnsUint8List?—nullwhen the SDK is disabled, when noBrevwickScreenshotScopeis mounted (awarnconsole-ring entry is recorded so triagers see why the submit payload had no attachment), or when the underlying capture throws (the throw is caught + forwarded to the console ring as awarnentry; the never-throws contract holds for the public surface).
Security #
- Route paths run through
redact()before landing in the ring, so a consumer who pushes/users/jane@acme.com/editwrites/users/[email]/edit— and theroutePathlistenable, which submit reads to populate top-levelroute_path, reflects the same redacted form. - Console messages and trimmed stacks run through
redact()at the ring boundary so bearer tokens / JWTs / emails surfaced throughFlutterErrororPlatformDispatcherexceptions never leave the device raw. - The internal "no scope" warn message emitted by
captureScreenshotcontains no PII — just the diagnostic string.
0.1.0 — 2026-05-01 #
Added #
Brevwicksingleton facade withinstall/uninstall/instancelifecycle — the one entry point pub.dev consumers touch.BrevwickConfigvalidated configuration contract (mirrors the JS SDK byte-for-byte:projectKey,endpoint,environment,enabled,buildSha,release,user,userContext,rings,fingerprintOptOut).BrevwickUseropaque user identity carrier.FeedbackInput/FeedbackAttachmentsubmission contracts.SubmitResultsealed union (SubmitOk/SubmitFailed) withSubmitErrorCodeenum.DeviceContext/SdkInfo/Viewportdevice-context value objects.SdkInfo.versionis now sourced frompackage_info_plusso the wire payload trackspubspec.yamlautomatically.DeviceContextCollectorwith cached plugin reads +BuildContextviewport capture.BrevwickDioInterceptorDio network ring with header allow-list, query-param redaction, body caps (2 kB request / 4 kB response), loop-avoidance matching JSmakeLoopGuard, and per-request redaction.Brevwick.recordNetworkEventmanual seam for non-Dio HTTP clients; every entry runs throughredactValueat the boundary.Brevwick.snapshotNetworkfor FIFO-ordered defensive ring snapshots.redact/redactValuepublic redaction helpers.
Changed #
- N/A (initial public release).
Security #
- Network ring strips
userInfo(https://user:pass@host/...) from every captured URL unconditionally — accidental credentials embedded in a URL never reach the wire payload. DioException.messageflows throughredact()before reaching the ring so PII leaked into Dio error messages cannot escape the device.