outcode_bug_reporter 2.0.0 copy "outcode_bug_reporter: ^2.0.0" to clipboard
outcode_bug_reporter: ^2.0.0 copied to clipboard

In-app bug reporter for Flutter: a draggable floating button that captures the screen, lets users annotate and redact it, set severity and type, and file a detailed report to ClickUp or any custom backend.

outcode_bug_reporter #

pub version pub likes license: MIT Flutter

In-app bug reporter for Flutter. A draggable floating button captures the current screen, lets users annotate it (draw, arrow, box, redact, text), set a severity and type, and file a report to ClickUp or any custom backend.

Part of the OutCode Bug Reporter monorepo. For the web (React, Angular, Vue, Svelte, Solid, vanilla), see @outcode/bug-reporter-web; for React Native, @outcode/bug-reporter-native. All of them file the same report format, so tickets look identical whichever client they came from.

Features #

  • 🐛 Draggable floating button that snaps to the screen edge
  • ✏️ Annotate — pen, arrow, box, opaque redaction, and text on the captured screenshot
  • 📸 No plugins — capture is Flutter's own RepaintBoundary, so there is nothing to configure
  • 🪟 Works over dialogs and bottom sheets, and the system back gesture steps back through the flow
  • 🧭 Auto-captured context — route, platform, viewport, screen, text scale, orientation, locale, timezone, console/network summaries… consent-free
  • 🎨 Themeable — Indigo / Noir / Mint presets or your own tokens
  • 📡 Offline retry queue, breadcrumb capture, screenshot size guard
  • 🧩 Pluggable backends — ClickUp out of the box, or any repository / HTTP endpoint

Install #

dependencies:
  outcode_bug_reporter: ^2.0.0

No platform setup, no native peers: everything is pure Dart plus http.

Platform views don't capture. A native map, webview, or camera preview composites outside the Flutter layer tree and comes back blank or black in the screenshot. Everything Flutter draws itself captures faithfully.

Quick start #

Wrap your app in MaterialApp.builder so the reporter sits above the Navigator:

import 'package:flutter/material.dart';
import 'package:outcode_bug_reporter/outcode_bug_reporter.dart';

MaterialApp(
  home: const HomePage(),
  // Fills the "Route" row in the captured context (optional).
  navigatorObservers: [BugReporterNavigatorObserver()],
  builder: (context, child) => BugReporter(
    config: BugReporterConfig(
      appName: 'My Flutter App',
      appVersion: '1.0.0',
      theme: BugReporterTheme.indigo,
      repository: ClickUpBugReporterRepository(
        apiKey: const String.fromEnvironment('CLICKUP_API_KEY'),
        problemListId: problemListId,
        suggestionListId: suggestionListId,
      ),
    ),
    child: child!,
  ),
);

builder is the right mount point for two reasons: the button floats over every route without being rebuilt on navigation, and only child ends up inside the capture boundary — so the button never appears in its own screenshots. See example-flutter/ for a runnable demo.

Reporting a bug while a dialog or bottom sheet is open #

This just works, and it's the one place Flutter has it easier than React Native. Dialogs and sheets are routes inside the Navigator, and the button is mounted above it, so it stays tappable; the report flow is then pushed as a route of its own, landing on top of whatever the user was stuck on. The screenshot contains the dialog, because capture snapshots the whole widget tree rather than one view. The system back gesture steps back through the flow — annotate → close, form → annotate — and never pops the app's screen out from under it.

One consequence of mounting above the Navigator: the reporter cannot use Navigator.of(context) to present the flow, because that searches ancestors and the navigator is a descendant. It locates the navigator by descending from its own element, which needs nothing from you. If you already keep a navigator key, passing it skips the search:

BugReporter(config: config, navigatorKey: myNavigatorKey, child: child!)

Opening it yourself, and turning it off #

final reporter = BugReporterController();

BugReporter(config: config, controller: reporter, child: child!)
// …anywhere: reporter.open() / reporter.close() / reporter.isOpen

// Ship release builds without it:
BugReporter(enabled: kDebugMode, config: config, child: child!)

enabled: false builds child and nothing else — no button, no capture boundary, no queue flush, and no running animation (the pulsing halo is a repeating AnimationController, so leaving it going would keep the app rendering frames for nothing).

To present the flow from a shake gesture or debug menu instead of the button, push it yourself from a context that has a navigator:

Navigator.of(context).push(ReportFlowRoute(config: config, screenshot: await captureBoundary(key)));

Device info and network type (optional) #

The package takes no plugin dependencies, so it doesn't guess at your device model or connectivity. Inject whatever you already collect — statically via deviceInfo / packageInfo, or per-report via collectContext:

final info = await DeviceInfoPlugin().androidInfo;   // device_info_plus
final pkg = await PackageInfo.fromPlatform();        // package_info_plus

BugReporterConfig(
  appName: pkg.appName,
  appVersion: pkg.version,
  deviceInfo: {'model': info.model, 'osVersion': info.version.release},
  packageInfo: {'packageName': pkg.packageName, 'buildNumber': pkg.buildNumber},
  collectContext: () async {
    final network = await Connectivity().checkConnectivity();   // connectivity_plus
    return ReportContextRow.fromMap({'Network': network.first.name});
  },
);
late final BugReporterLogCapture capture;

void main() {
  capture = BugReporterLogCapture.install();   // Flutter errors + debugPrint
  runApp(const MyApp());
}

BugReporterConfig(appName: 'My App', collectDiagnostics: capture.collect);

install() chains to the error handlers already in place, so it composes with Crashlytics or Sentry rather than replacing them. Route requests through BugReporterHttpClient(capture) to also record failed calls. Reports then carry a Recent Logs block and, when relevant, a Last Failed API Call.

Offline retry queue #

Pass any key/value store — shared_preferences, secure storage, a file — and failed submits are persisted, then replayed the next time the reporter mounts. A queued report shows "Saved for later" instead of a ticket id.

class PrefsStorage implements ReportQueueStorage {
  @override
  Future<String?> getItem(String key) async =>
      (await SharedPreferences.getInstance()).getString(key);
  @override
  Future<void> setItem(String key, String value) async =>
      (await SharedPreferences.getInstance()).setString(key, value);
  @override
  Future<void> removeItem(String key) async =>
      (await SharedPreferences.getInstance()).remove(key);
}

BugReporterConfig(appName: 'My App', storage: PrefsStorage());

At most 10 reports are kept; if a write fails because the screenshots are too large for your store, the queue retries without them rather than losing the text. InMemoryReportQueueStorage is bundled for tests and demos.

Configuration #

BugReporterConfig mirrors the shared config documented in the root README, with Dart types:

Option Type
appName (required) / appVersion String / String?
repository BugReporterRepository? — when set, used instead of apiUrl
apiUrl / headers String? / Map<String, String>?
theme BugReporterTheme — a preset, or preset.copyWith(...)
backendName / label String — form footer text / button semantics label
defaultPriority ReportPriority?
screenshot ScreenshotOptionsmaxWidth (default 1280) or an exact pixelRatio
storage ReportQueueStorage? — enables the offline queue
collectDiagnostics Map<String, Object?>? Function()?
collectContext FutureOr<List<ReportContextRow>> Function()?
deviceInfo / packageInfo Map<String, Object?>?

BugReporter itself also takes controller, enabled, and navigatorKey.

Severity → backend priority: critical→urgent, high→high, medium→normal, low→low. Type → a backend tag.

Two Flutter-specific notes on screenshot: maxWidth is applied as a capture pixel ratio rather than a post-hoc resize, and output is always PNG (dart:ui encodes nothing else), so there is no quality knob — lower maxWidth to shrink a report. Annotations are flattened at the resolution of the original capture, not the on-screen preview.

The Redact tool paints an opaque block, not a blur. A blur can sometimes be inverted; a password or a customer's name in a screenshot deserves better than that.

Custom backends #

class MyRepository implements BugReporterRepository {
  @override
  Future<BugReportResponse> createReport(CreateReportParams params) async {
    // params.title, .description, .screenshotBase64, .severity, .type,
    // .context, .deviceInfo, .packageInfo, .diagnostics
    return const BugReportResponse(success: true, id: 'ISSUE-123');
  }
}

Return success: false rather than throwing for expected failures — that's what lets the reporter queue the report for retry. With no repository at all, reports are POSTed to apiUrl as JSON with snake_case keys (screenshot_base64, metadata.app_name), identical to the web and React Native packages, so one endpoint serves all three.

⚠️ Never hardcode a ClickUp API key. Inject it with --dart-define. The key ships inside your app binary, so for public releases prefer the apiUrl path behind a server proxy and reserve direct-to-ClickUp for internal builds.

Platform support #

Pure Dart with no conditional imports (dart:async, dart:convert, dart:math, dart:typed_data, dart:ui only), so it compiles everywhere Flutter does, web and Wasm included. The flow has been driven end-to-end on a physical Android device; other platforms are verified to compile and share the same engine capture path, but haven't been hand-tested — please open an issue if you hit something.

License #

MIT © OutCode Software

0
likes
0
points
154
downloads

Publisher

unverified uploader

Weekly Downloads

In-app bug reporter for Flutter: a draggable floating button that captures the screen, lets users annotate and redact it, set severity and type, and file a detailed report to ClickUp or any custom backend.

Homepage
Repository (GitHub)
View/report issues

Topics

#bug-report #feedback #screenshot #annotation #clickup

License

unknown (license)

Dependencies

flutter, http, http_parser

More

Packages that depend on outcode_bug_reporter