flawsnap
In-app bug reporting for Flutter apps. Users shake the phone or tap a floating
button, mark up a screenshot of the app, and send. Reports land in the same
Snaplog inbox as your website's, tagged ios or android.
Setup
Requires Dart 3.13 / Flutter 3.47 or newer.
-
In the Snaplog portal, open your project's Security settings and turn on Accept reports from mobile apps. Copy the project key.
-
Add the package:
dependencies: flawsnap: ^0.0.2 -
Initialise and wrap your app:
import 'package:flutter/material.dart'; import 'package:flawsnap/flawsnap.dart'; Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await Snaplog.init(apiUrl: 'https://api.example.com/api/v1', projectKey: 'pk_...'); runApp(Snaplog.wrap(const MyApp())); } -
Optional extras:
MaterialApp(navigatorObservers: [Snaplog.navigatorObserver]) // report the current screen final client = Snaplog.httpClient(); // record network calls Snaplog.identify(name: user.name, email: user.email); // prefill the reporter Snaplog.log('Payment failed', level: 'error'); // custom log lines Snaplog.setMetadata({'plan': 'pro'}); // extra details Snaplog.show(); // open the reporter yourselfChoose triggers with
Snaplog.init(..., triggers: {SnaplogTrigger.button})(default: shake and button). Shake needs a physical device.Admins can hide the floating button from the project's settings in the portal, or set its position (left/right) and colour; the label set there is used as the button's accessibility label. Shake and
Snaplog.show()keep working when the button is hidden.
Team sign-in
When the project allows it (portal: project settings, "Let team members sign
in from the widget and app"), the report screen shows Team member? Sign
in. The SDK shows a code like ABCD-EFGH and opens Snaplog's approval page
in the browser. The member signs in there (or on another device, with the
code) and presses Allow. From then on their reports go in under their
Snaplog account ("Reporting as Jane · Sign out"), without the name and email
fields. Everyone else reports anonymously as before.
Snaplog.signIn()/Snaplog.signOut()do the same from your own UI.- The app gets a reporter token, not a Snaplog session. It can only send reports to this project and lasts 30 days. If the API rejects it (expired, revoked, or the setting turned off), the name and email fields come back.
- The token is kept in
shared_preferences. To keep it encrypted at rest, implementReporterTokenStorewith flutter_secure_storage and pass it asSnaplog.init(..., tokenStore: MyStore()). - On Android, auto backup copies
shared_preferencesto the user's cloud backup by default. To keep the token out of it, excludeFlutterSharedPreferences.xmlin your backup rules (android:fullBackupContent/android:dataExtractionRules), setandroid:allowBackup="false", or use the secure-storage option above.
Insights (user flow, API timing and errors)
Snaplog Insights draws your app's user flow: which screens people open, in what order, where they go back or leave, how long they stay, and which API calls each screen makes (and which are slow). It also groups Dart errors into issues. It's off unless both of these are on:
-
The project's Insights switch in the portal (Insights → Setup).
-
insights: truein your app, plus the navigator observer:await Snaplog.init( apiUrl: 'https://api.example.com/api/v1', projectKey: 'pk_...', insights: true, ); // ... MaterialApp(navigatorObservers: [Snaplog.navigatorObserver], ...)
Screen names
Screens are named after their route's RouteSettings.name, so give every
page route a name; unnamed routes show up as (unnamed). Only full-screen
page routes count; dialogs and bottom sheets don't.
-
Named routes /
Navigator.pushNamed: names come from yourroutes:table oronGenerateRoute. ForMaterialPageRoute, passsettings: const RouteSettings(name: 'Cart'). -
GoRouter: add the observer and give each route a
name:GoRouter( observers: [Snaplog.navigatorObserver], routes: [GoRoute(name: 'Cart', path: '/cart', builder: ...)], )If your routes use
pageBuilder, pass the name on the page too:MaterialPage(name: state.name, child: ...). AShellRoute's nested navigator needs its own observer, and one observer can only watch one navigator, so inside shells callSnaplog.screen('Orders')from the screen instead. -
auto_route: pass the observer to the router config; screens are named after your route classes (
CartRoute):MaterialApp.router( routerConfig: _appRouter.config( navigatorObservers: () => [Snaplog.navigatorObserver], ), ) -
Anything else (tabs, page views, custom navigation):
Snaplog.screen('Settings')names the current screen from your code.
What's sent:
- Screens: each screen view with how the user got there (opened, pushed, went back, replaced), the previous screen, and time on screen while the app is in the foreground. A visit ends when the app is closed or after 30 minutes in the background.
- API calls made through
Snaplog.httpClient()(method, URL without query string or fragment, status, duration), each tied to the screen it was made on. Use it as yourpackage:httpclient, or wrap your own:Snaplog.httpClient(myClient). Calls through other clients (e.g. Dio) aren't seen yet. - Errors caught by
FlutterError.onErrorandPlatformDispatcher.onError(type, message up to 500 characters, top 30 stack frames). Native iOS/Android crashes aren't included. Stacks are as compiled; obfuscated builds show obfuscated names. - With each batch: your app version,
iosorandroid, and a random visit id. The visit id isn't tied to the device or user, and every launch starts a new one; it's saved only alongside events waiting to be sent, and deleted with them once they're sent. - The user id, only when the project recognises returning users (below). Unlike the visit id, it stays on the device across launches.
Never sent: request or response bodies, headers, query strings, your app's
account ids, or anything given to Snaplog.identify().
URLs inside error messages and stacks lose their query strings, and email
addresses are masked, before anything is saved or sent. Screen names are
masked the same way (ids, UUIDs, emails).
Events go in batches (every 15 s, at 20 events, and when the app goes to the
background). Unsent events (at most 200, about 256 KB) wait in
shared_preferences and go on the next launch; when Insights is off or
consent is withdrawn, that buffer is cleared.
Returning users and actions
When the project also turns on Recognise returning visitors (Insights →
Setup), the SDK keeps a random id (u_ + 26 characters) in
shared_preferences (snaplog.uid.<project key>) and sends it with
Insights events, so
one person's sessions can be followed as a journey. It isn't your account id
and isn't linked to a name or email: Snaplog.identify() stays for bug
reports only and never reaches Insights. A reinstall gives a new id, and
turning the setting off deletes it at the next launch.
Snaplog.track('signup_completed')records an action on the current screen. Names are lowercase letters, digits and_ . : -(1 to 64); no properties. Invalid names are ignored, including names with a part that looks like an id (6+ digits in a row, a long hex id, a UUID). At most 200 actions are sent per launch (20 of the same name).Snaplog.resetUser()deletes the id (say, on sign-out); the rest of this launch is anonymous and a new id is made on the next launch.Snaplog.consent(false)deletes the id.consent(true)after a withdrawal starts with a fresh one; callingconsent(true)again while allowed keeps the current id.- Consent first (as in the EU, where storing this id needs consent):
Snaplog.init(..., insights: true, consentPending: true). Insights then sends nothing and keeps no id until your consent flow callsSnaplog.consent(true); a returning user keeps their id.
Consent
Snaplog.consent(false) stops Insights and deletes everything not sent yet,
in memory and on the device. Snaplog.consent(true) turns it back on with a
new visit; nothing captured in between is sent. It's safe to call before
Snaplog.init, and the choice isn't remembered, so call it from your consent
flow on every launch (or only pass insights: true once the user agreed).
App store privacy labels
If you turn Insights on, declare this data. Snaplog collects it on your behalf, it isn't linked to the user's identity (the user id is a random id, not your account id), and it isn't used for tracking.
- App Store (App Privacy):
- Diagnostics → Crash Data: app functionality, not linked, no tracking.
- Diagnostics → Performance Data: app functionality / analytics, not linked, no tracking.
- Usage Data → Product Interaction (screens opened and time on them): analytics / app functionality, not linked, no tracking.
- With Recognise returning visitors on: Identifiers → User ID (the random Snaplog id): analytics, not linked to the user's identity, not used for tracking.
- Google Play (Data safety):
- App info and performance → Crash logs and Diagnostics: collected, not shared, for app functionality / analytics, processed ephemerally: no.
- App activity → App interactions (screens opened and time on them): collected, not shared, for analytics.
- With Recognise returning visitors on: Device or other IDs (the random Snaplog id): collected, not shared, for analytics.
- Data is encrypted in transit (HTTPS). Users can opt out if you wire
Snaplog.consent(false)to a setting or consent prompt.
What a report includes
- Screenshot of the app with the user's marks (box, draw, five colours).
- Title, description, priority, and "Who should look?" from your project's categories.
- Device model, OS and version, app name, version and build, screen size, pixel ratio, locale, light/dark mode.
- The current route (with
navigatorObserver). - The last 100 log lines: Flutter errors, uncaught exceptions,
debugPrintoutput andSnaplog.logcalls. - The last 100 requests made with
Snaplog.httpClient()(method, URL, status, timing; bodies are never recorded).
If the key is wrong or mobile reporting is off for the project, init logs why
and the SDK stays hidden; your app is unaffected.
Local development
The example app talks to a local API:
cd example
cp dart_defines.example.env dart_defines.env # then put in your project key
flutter run --dart-define-from-file=dart_defines.env
The iOS example allows plain HTTP to local addresses (NSAllowsLocalNetworking)
for development. Production apps should use an https API URL. The Android
emulator reaches your machine at http://10.0.2.2:8080, and needs a cleartext
network-security exception for HTTP.
Tests
flutter test
Libraries
- flawsnap
- Flawsnap: in-app bug reporting for Flutter, powered by Snaplog.