The Flutter tester app has two coloured tiles. The green one is public. The magenta one is wrapped in EverframeSensitive. This is what the dashboard received when we filed a report from it:

The screenshot attached to an Everframe report from a Flutter app. One tile is replaced by a solid black rectangle.
The report as it arrived. The magenta tile is a black rectangle. It was masked on the device, before the PNG was encoded.

We have just released everframe_flutter. This page is one run through it on 1 October 2026: Flutter 3.47.6, Dart 3.13.5, an Android 15 emulator, and a local Everframe dashboard. It covers what we wired, what arrived, and what the plugin does not do for you.

What the plugin is

A bridge. The Dart side captures Flutter pixels and passes them, with whatever context you hand it, to the native Everframe SDK on Android or iOS. The native SDK owns the reporter UI, the encrypted outbox and delivery. On Flutter web a separate EverframeWebBridge, with EverframeWebCapture on the same boundary, hands off to the Web SDK.

That split explains most of what follows. Things the native layer can see, such as taps, lifecycle and the device, are recorded for you. Things only Dart knows, such as your screen names, your exceptions and your HTTP calls, are explicit calls.

Step 1. Create the integration

In the dashboard: Add integration, pick Flutter, name it. The setup page comes back with the app ID and SDK key already filled into the snippets.

The Everframe setup page for a Flutter integration, with the pub add command and a start() snippet carrying the app ID and SDK key.
The setup page for a new Flutter integration. The key in this capture is masked.
flutter pub add everframe_flutter

The plugin needs Flutter 3.47+ and Dart 3.13+, with Android API 24+ or iOS 15+. On Android the host Activity must be a FlutterFragmentActivity, because the reporter is Compose, and release builds need the INTERNET permission.

import 'package:everframe_flutter/everframe_flutter.dart';

final everframe = EverframeNativeBridge();
await everframe.start(
  appId: '<Flutter integration app ID>',
  sdkKey: '<Flutter integration SDK key>',
  environment: 'production',
);

There is no endpoint argument. The published native SDK owns its ingest endpoint and uses the production one.

Step 2. Draw the capture boundary

If you have ever needed a Flutter screenshot of a widget, you know the mechanism: a RepaintBoundary with a GlobalKey, and its render object turned into an image. The plugin uses the same boundary, with one addition. Every private widget is registered in a SensitiveRegionRegistry, and those rectangles are blacked out before anything is encoded.

This is the tester app’s screen, trimmed:

final boundaryKey = GlobalKey();
final sensitiveRegions = SensitiveRegionRegistry();

RepaintBoundary(
  key: boundaryKey,
  child: Column(
    children: [
      Container(height: 70, width: 160, color: const Color(0xFF00CC00)),
      EverframeSensitive(
        registry: sensitiveRegions,
        child: const SizedBox(
          height: 70,
          width: 160,
          child: ColoredBox(color: Color(0xFFFF00FF)),
        ),
      ),
      // ...buttons
    ],
  ),
)

Two properties of this are worth stating plainly. The capture fails closed: if the masking geometry is unavailable, there is no screenshot, not an unmasked one. And the boundary is the limit of what is captured. A native platform view outside it, such as a map or a web view, is not in the picture.

Step 3. Give it the context only Dart has

The tester app’s “Record Dart context” button does two things:

await everframe.recordNetwork(
  method: 'GET',
  url: Uri.parse('https://api.example.com/orders/123?token=private'),
  statusCode: 503,
  durationMs: 93,
);

final accepted = await everframe.captureException(
  StateError('probe checkout failure'),
  stackTrace: StackTrace.current,
);

Note the URL: a path and a token query parameter. And accepted is true when the error was stored in the native outbox on the device. It confirms local storage, not delivery.

Screen changes are a third call, recordScreen('Screen B'), made wherever your router knows the name.

Step 4. Open the reporter

final outcome = await everframe.openReporter(
  boundaryKey: boundaryKey,
  sensitiveRegions: sensitiveRegions,
);

You own the trigger: a button, a shake handler, a debug menu. On the emulator it went like this.

Three Android screens: the Flutter tester app with a green and a magenta tile, the native Report a bug sheet with the captured screenshot, and the same sheet with a title typed in.
The tester app, the native reporter over it, and a title typed in. The status line read Reporter: submitted after Send.

The reporter is native UI, not a Flutter route. The person filing can annotate the capture, delete it, add a title and a description. outcome.status tells your code how it ended.

What arrived

The report at the top of this page is the result. Beside the masked screenshot, the trail began:

LIFE  foreground
NAV   MainActivity → Screen A
TAP   tap FlutterSurfaceView
NAV   Screen A → Screen B
TAP   tap FlutterSurfaceView
NET   GET https://api.example.com 503        93 ms

Three things in there are worth a second look.

The network row is origin only. We recorded https://api.example.com/orders/123?token=private. What crossed the bridge was https://api.example.com, the method, the status and the duration. Paths, queries, headers and bodies do not cross it.

Taps are named after the surface. The native layer sees one FlutterSurfaceView, so every tap carries that name, not the widget’s. The screen names next to them come from your recordScreen calls, which is why those are worth making.

Device and app context needed no code. The Context tab had it:

FieldValue
OSAndroid 15
Modelsdk_gphone64_arm64
Screen393 × 750 · portrait
Locale / timezoneen-US · Europe/Vilnius
Appdev.everframe.everframe_flutter_android_probe, v1.0.0, build 1
SDKeverframe-flutter
Platformandroid · phone

The handled error landed separately, under Errors, as its own group.

Everframe error group for a Dart StateError, Bad state: probe checkout failure, with one stack frame at main.dart line 87 and the trail leading up to it.
The StateError from captureException, grouped, with the trail of the session it happened in.

The stack has one frame, _FlutterAndroidProbeAppState._recordContext at main.dart:87:32, which is the line that called captureException. That is the Dart stack as the VM printed it, from a debug build. A release build compiled with --obfuscate or --split-debug-info will not print readable frames, and we have not tested one.

Turning on Flutter session replay

The first report had one attachment, the screenshot. Replay is off for a new integration. A dashboard switch turns it on, and the recorder below has to be wired in Dart.

The Session replay control in the Everframe SDK configuration: an enabled switch, a buffer duration of 15, 30 or 60 seconds, and a capture coverage slider.
SDK configuration for the integration, with session replay switched on.

On the Dart side the recorder is explicit. It samples frames that are already masked, into a buffer held in memory and bounded in Dart, at 30 seconds by default:

final replay = SafeReplayBuffer();
late final recorder = SafeReplayRecorder(
  capture: () => captureRegisteredFrameAfterFrame(boundaryKey, sensitiveRegions),
  buffer: replay,
);

await recorder.start();          // after everframe.start()
await recorder.sampleNow();      // e.g. after a screen change

// when the user opens the reporter
recorder.freeze();
await everframe.openReporter(
  boundaryKey: boundaryKey,
  sensitiveRegions: sensitiveRegions,
  replayBuffer: replay,
);

We flipped the switch, restarted the app so the SDK picked up the new configuration, and filed a second report.

An Everframe report with a 31-second replay of a Flutter app. The timeline has lanes for taps, screens and network, and the playhead sits on the failed request at 21.3 seconds.
The second report. Clicking the 503 row in the trail moved the replay to 00:21.3, the moment of the request.

It carried a replay of 31.4 seconds with lanes for taps, screens and network. The sensitive tile is black in every frame, for the same reason it is black in the screenshot: the frames are sampled from the masked boundary.

It is image replay, a sequence of frames. It is not a video of the device screen, it needs no screen-recording permission, and nothing leaves the device until someone files a report.

What it does not do

The plugin has no automatic Dart error hook, no HTTP interception and no Dart console capture. If you want uncaught errors under Errors, you can forward them from FlutterError.onError or a zone handler to captureException. They are recorded through the handled-error path, and we have not tested that wiring. If you want requests in the trail, call recordNetwork from your HTTP wrapper.

It is also not crash analytics. If the question is “how many sessions crashed on the new release”, Firebase Crashlytics answers that and this does not. This is for the report a person files, with the evidence attached, and the two run side by side.

What this run did not verify:

  1. A physical device. Everything here is an Android emulator. iOS has its own simulator probe, which we did not run for this page.
  2. The published build end to end. The tester app compiles against local source and a local dashboard in the development environment. Your app installs from pub.dev and uses the production endpoint.
  3. Offline retry across a restart. The outbox is native and encrypted, and we have not yet verified a report surviving a process restart on this bridge.
  4. Embedded platform views. They are not guaranteed to appear in the capture, and we have not tested a screen built around a map or a web view.

If you are adding it this week

  1. Create a Flutter integration and run flutter pub add everframe_flutter. Install has the host requirements.
  2. Put one RepaintBoundary around the screen body and register every private widget before you take a single screenshot.
  3. Call recordScreen from your router observer. It is the difference between a trail of anonymous taps and one you can read.
  4. File one report and look at it. Check the masked regions with your own eyes. Then decide on replay. Configuration covers the recorder.

The plugin is MIT-licensed. The complete tester apps are in the repository for Android and iOS, and the Flutter SDK page lists what is captured.