Skip to main content

flog_flow 0.7.2 copy "flog_flow: ^0.7.2" to clipboard
flog_flow: ^0.7.2 copied to clipboard

Records the user journey in a Flutter app (screens, taps, network, logs, errors) and exports it as one self-contained HTML file to replay in any browser.

flog_flow #

Record what a user does in your Flutter app and get one self-contained .html file that replays the whole session in any browser: screens, taps, network requests, errors and logs on a single timeline. Attach it to a ticket, send it over Slack, open it with a double click. No server, no account, no internet.

🤖 Install with an AI assistant #

Paste this prompt into your coding assistant (Claude Code, Cursor, Copilot, Gemini…) from the root of your Flutter project:

Add the Flutter package flog_flow to this project so test sessions can be recorded and exported as a self-contained HTML report. Adapt every step to the existing code: keep the current architecture, chain existing handlers instead of replacing them, and don't change app behavior in production.

1. Dependencies: run `flutter pub add flog_flow`. If the project uses Dio, also run `flutter pub add flog_flow_dio`. If it uses package:http (directly or through another package), also run `flutter pub add flog_flow_http`.

2. Root widget (MaterialApp, MaterialApp.router, CupertinoApp, GetMaterialApp…):
   - Wrap the app with `FlogFlogScope` through `builder: (context, child) => FlogFlogScope(child: child!)`. If a `builder` already exists, wrap what it returns instead of replacing it.
   - Register `FlogFlow.instance.navigatorObserver` in `navigatorObservers` (with go_router: `GoRouter(observers: [...])`). For every additional Navigator (ShellRoute, nested navigators, tabs with their own Navigator) use `FlogFlow.instance.newNavigatorObserver()`; never share one observer instance between two navigators.

3. Network: add `FlogDioInterceptor()` to the interceptors of every Dio instance (`import 'package:flog_flow_dio/flog_flow_dio.dart'`). For package:http, use `FlogHttpClient()` wherever an `http.Client` is created, or wrap the app in main with `http.runWithClient(() => runApp(...), () => FlogHttpClient())` (`import 'package:flog_flow_http/flog_flow_http.dart'`).

4. Errors: in main(), after `WidgetsFlutterBinding.ensureInitialized()`, chain the global handlers so uncaught errors are also recorded, keeping any existing crash reporter (Crashlytics, Sentry…) working:
   - `FlutterError.onError`: call the previous handler, then `FlogFlow.instance.recordError(details.exception, stackTrace: details.stack, context: 'FlutterError')`.
   - `PlatformDispatcher.instance.onError`: call `FlogFlow.instance.recordError(error, stackTrace: stack)` and return the previous handler's result (or false).
   Inside existing try/catch blocks that show an error to the user, add `FlogFlow.instance.recordError(e, stackTrace: st, context: '<where>')`.

5. Start recording in main(), before runApp, only in non-production builds (use the project's flavor or environment flag if it has one, otherwise `!kReleaseMode`):
   `await FlogFlow.instance.start(appName: '<app name>', appVersion: '<version if available>');`
   If the app has authentication, call `FlogFlow.instance.setUser('<user id>')` after login and `FlogFlow.instance.setUser(null)` on logout.

6. Export: add a discreet way to export the session, available only in non-production builds (an item in an existing debug or settings menu; otherwise a small floating button shown only when recording). It must run `final file = await FlogFlow.instance.export();` and share the file with share_plus if the project already uses it; otherwise show the file path in a dialog and print it.

7. Privacy: password fields are masked in screenshots automatically. Wrap any other sensitive widget (card numbers, balances, personal data) in `RedactedZone(child: …)`. `CaptureConfig.offlineMode` is on by default and records full headers and bodies. Set `offlineMode: false` if the team does not need this detail.

8. Run `flutter pub get` and `flutter analyze`, fix anything you introduced, and finish with a short summary: files changed and how to export a session.

Quick start #

flutter pub add flog_flow
flutter pub add flog_flow_dio    # if you use Dio
flutter pub add flog_flow_http   # if you use package:http
void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  if (!kReleaseMode) await FlogFlow.instance.start(appName: 'MyApp');
  runApp(const MyApp());
}

MaterialApp(
  navigatorObservers: [FlogFlow.instance.navigatorObserver],
  builder: (context, child) => FlogFlogScope(child: child!),
  // ...
);

// When the tester is done (e.g. from a debug menu):
final file = await FlogFlow.instance.export(); // flow_<session>.html

Features #

One HTML file, nothing else #

The whole session (timeline, screenshots and viewer) lives in a single .html that opens offline in Chrome, Safari, Edge or Firefox. Anyone can open it: no SDK, no dashboard, no login. Data never leaves the device unless someone shares the file.

Self-contained HTML report opened in the browser

Step-by-step replay #

Every screen and every interaction, in order: the exact point of each tap, the direction of each scroll, the navigation it triggered and what the screen looked like at that moment. Play it like a video or move with the arrow keys, and copy any screenshot (with the tap marker) in one click.

Stepping through scrolling, taps, navigation and an error

Network traffic #

Each request appears on the timeline next to the tap that caused it: method, URL, status and duration, plus headers and bodies with a JSON viewer. Copiar cURL (copy as cURL) repeats the exact request from a terminal. Works with Dio (flog_flow_dio), package:http (flog_flow_http) or any client through FlogFlow.instance.recordHttpCall.

dio.interceptors.add(FlogDioInterceptor());                               // Dio
http.runWithClient(() => runApp(const MyApp()), () => FlogHttpClient());  // package:http

Request headers, response JSON and copy as cURL

This request is from the recorded session. Its authorization header was redacted before generating documentation media.

Errors with context #

Every error is stored with its stack trace and a screenshot of the screen it happened on, in the same timeline as the taps and requests that led to it. Connect the global handlers once and nothing escapes.

final onFlutterError = FlutterError.onError;
FlutterError.onError = (details) {
  FlogFlow.instance.recordError(details.exception, stackTrace: details.stack);
  onFlutterError?.call(details);
};
final onPlatformError = PlatformDispatcher.instance.onError;
PlatformDispatcher.instance.onError = (error, stack) {
  FlogFlow.instance.recordError(error, stackTrace: stack);
  return onPlatformError?.call(error, stack) ?? false;
};

Recorded error with stack trace and the screen where it occurred

Export for AI #

One click builds a configurable .zip for analysis in Claude, ChatGPT or Gemini: a readable session.md, a session.json and the key screenshots. It starts with automatic findings: failed, slow or repeated requests, identical responses, unlabeled taps, frozen UI and connection drops. Secrets (tokens, cards, passwords) are hidden by default.

AI export options, completed ZIP and generated session summary

Read the generated session summary.

Export to video #

Turns the session (or a selected range) into an MP4, rendered in the browser: animated taps and scrolls, screen transitions, and a side panel with the requests and notes as they happen. Horizontal 16:9 or vertical 9:16. Evidence for the ticket without having recorded the screen.

Preview of the generated session video

Watch or download the generated MP4 — 97 seconds, 1280 × 720, generated by the viewer from this session.

Replay of a recorded iOS session

Real IMDUMB session on iOS: 16 screens, 39 interactions, 31 requests and two recorded errors. Documentation copies have authorization headers removed. The reviewer note shown below was added for this demonstration.

Steps to reproduce #

Select a range of the timeline and get the bug report written: environment, precondition, numbered steps (in Spanish, like the viewer: "Toca «Pagar» → se abre «Checkout»"), observed result (errors, failed requests) and expected result. Markdown for Jira, GitHub or Slack, editable before copying.

Generated steps to reproduce for the selected error sequence

Read the generated reproduction steps.

Notes on any action #

Testers write a note on the exact action where something went wrong. Notes show up in the video, the steps to reproduce and the AI export, and Descargar copia con notas (download copy with notes) saves them inside the same .html for the next person.

Adding a reviewer note and downloading an HTML copy with the note

The note is a demonstration annotation added to the recorded error.

Search everything #

⌘K / Ctrl+K searches URLs, request and response bodies, headers, logs, errors and notes. Opening a result jumps to that action and highlights every match.

Searching Spider-Man in response bodies and jumping to a highlighted match

Flow map #

All the screens visited as a map, with the transitions between them and the action that triggered each one. The fastest way to see the path a user took.

Recorded screens connected by navigation and interaction arrows

System signals #

The timeline also shows what the app went through, not only what the user tapped:

  • app sent to background and back (and for how long);
  • connection changes: wifi, mobile data, none;
  • keyboard opened and closed;
  • which text fields were edited (never what was typed);
  • slow frames and UI freezes (profile/release builds).

App background and foreground events in the recorded timeline

This session includes a 26-second background interval and field-edit events. It contains no connectivity changes, keyboard events or slow-frame events.

Private by default #

  • Password fields are masked in screenshots automatically; mask every text field with CaptureConfig(maskTextFields: true).
  • Wrap anything else sensitive in RedactedZone.
  • Nothing stays on disk between sessions.
  • Full headers and bodies are enabled by default with offlineMode: true. Set it to false to omit headers and use body previews. Raw HTML exports can contain authorization tokens, cookies and personal data.
  • Need a protected file? exportFlowx(passphrase: …) writes an AES-256 encrypted .flowx that opens from the Abrir button of any report.

Masked name field in a recorded screen

The name field is masked in the original capture. This session does not contain a password field or an encrypted-file export.

Logs and breadcrumbs #

Capture debugPrint output (and print, through FlogLogCapture.zoneSpec(); opt-in), add your own breadcrumbs, and label the action that triggers a navigation so the flow reads like a story.

FlogLogCapture.installDebugPrintCapture();   // with CaptureConfig(captureLogs: true)
FlogFlow.instance.breadcrumb('Cart updated', data: {'items': 3});
FlogFlow.instance.tag('User clicks "Pay"');  // right before Navigator.push

Field-edit breadcrumbs without the text entered

These two recorded breadcrumbs identify edited fields without their values. This session contains no captured print or debugPrint logs.

Keep only the sessions that fail #

Record always but keep the flow only when something breaks, and export it the moment the error happens.

await FlogFlow.instance.start(
  appName: 'MyApp',
  config: const CaptureConfig(recordOnErrorOnly: true),
);
FlogFlow.instance.onErrorRecorded = () async {
  final file = await FlogFlow.instance.exportIfError();
  // upload or share `file`
};

The two recorded errors filtered in the events view

The session illustrates recorded errors; the retention policy is configured in the SDK with the code above.

Light on the app #

Screenshots are taken outside animations and encoded during idle time, kept on disk instead of in memory; exports run in an isolate, and the expensive options are opt-in. Everything is tunable in CaptureConfig.

For a lighter recording, reduce screenshot size and disable body capture:

const CaptureConfig(
  offlineMode: false,
  captureNetworkBodies: false,
  framePerInteraction: false,
  pixelRatio: 0.75,
  jpegQuality: 55,
);

The media above demonstrate the exported session; they do not measure recording overhead.

Configuration #

FlogFlow.instance.start(appName: …, config: CaptureConfig(…)). The most used options:

Option Default What it does
captureNetwork true Records requests from a network adapter
captureNetworkBodies true Adds the full URL and body previews
offlineMode true Full bodies and headers (enables Copiar cURL); sensitive
captureLogs false Records debugPrint/print output
framePerInteraction true One screenshot per tap, frozen at the moment of the tap
maskPasswordFields true Masks password fields in screenshots
maskTextFields false Masks every text field
recordOnErrorOnly false Keeps the session only if an error happens
recordAppLifecycle, recordConnectivity, recordKeyboard, recordFieldEdits, recordSlowFrames true System signals on the timeline
pixelRatio / jpegQuality 1.0 / 65 Screenshot sharpness and size

Companion packages #

Package Use it when
flog_flow_dio Your app uses Dio
flog_flow_http Your app uses package:http (or a package built on it)

Platforms #

Android, iOS, macOS, Windows and Linux (Flutter web is not supported: the recorder uses dart:io). The viewer inside the HTML is in Spanish.

11
likes
150
points
411
downloads

Documentation

API reference

Publisher

verified publisheralecodeando.dev

Weekly Downloads

Records the user journey in a Flutter app (screens, taps, network, logs, errors) and exports it as one self-contained HTML file to replay in any browser.

Topics

#debugging #session-replay #testing #network

License

MIT (license)

Dependencies

archive, cryptography, flutter, image, path, path_provider

More

Packages that depend on flog_flow