← Articles

Using Sentry in Flutter

By Ann Tech · 31 October 2025

Sentry catches errors in production and gives you the context to reproduce and fix them: stack trace, device info, OS version, recent user actions, and custom context you attach. Here is how to integrate it properly in a Flutter app.

Setup

dependencies:
  sentry_flutter: ^8.0.0

Initialization

Wrap runApp with Sentry's initializer:

import 'package:sentry_flutter/sentry_flutter.dart';

Future<void> main() async {
  await SentryFlutter.init(
    (options) {
      options.dsn = 'https://[email protected]/project-id';
      options.tracesSampleRate = 0.2;   // 20% of transactions for performance
      options.profilesSampleRate = 0.1; // 10% of traces for profiling
      options.environment = const String.fromEnvironment('APP_ENV', defaultValue: 'production');
      options.release = '${packageInfo.appName}@${packageInfo.version}+${packageInfo.buildNumber}';

      // Attach screenshots on error (iOS/Android only)
      options.attachScreenshot = true;

      // Don't send events in debug mode
      options.beforeSend = (event, hint) {
        if (event.level == SentryLevel.debug) return null;
        return event;
      };
    },
    appRunner: () => runApp(const MyApp()),
  );
}

Catching uncaught errors

Sentry's init wrapper catches most errors automatically. Add explicit handlers for the remaining cases:

// Flutter framework errors
FlutterError.onError = (details) {
  FlutterError.presentError(details);
  Sentry.captureException(
    details.exception,
    stackTrace: details.stack,
  );
};

// Async errors not caught by Flutter framework
PlatformDispatcher.instance.onError = (error, stack) {
  Sentry.captureException(error, stackTrace: stack);
  return true;
};

Capturing exceptions manually

try {
  final order = await orderRepo.createOrder(request);
  // ...
} catch (exception, stackTrace) {
  await Sentry.captureException(
    exception,
    stackTrace: stackTrace,
    hint: Hint.withMap({'order_request': request.toJson()}),
  );
  // Show user-facing error
}

Adding context with scope

Context makes errors actionable:

// Set user context (cleared on sign out)
Sentry.configureScope((scope) {
  scope.setUser(SentryUser(
    id: user.id,
    email: user.email,
    data: {'plan': user.plan, 'region': user.region},
  ));
});

// Add tags for filtering in Sentry dashboard
Sentry.configureScope((scope) {
  scope.setTag('flavor', AppFlavor.current.name);
  scope.setTag('api_env', ApiConfig.baseUrl);
});

// Add extra context
Sentry.configureScope((scope) {
  scope.setExtra('cart_item_count', cart.items.length);
  scope.setExtra('feature_flags', remoteConfig.activeFlags);
});

Breadcrumbs trace what the user did before the crash:

// Automatic breadcrumbs: navigation, network requests, console output
// Manual breadcrumbs:
Sentry.addBreadcrumb(Breadcrumb(
  message: 'User tapped checkout',
  category: 'user.action',
  level: SentryLevel.info,
  data: {'cart_total': cart.total.toString()},
));

// Navigation breadcrumbs with go_router:
GoRouter(
  observers: [SentryNavigatorObserver()],
  routes: [...],
)

Performance monitoring

Track slow operations:

Future<Order> placeOrder(OrderRequest request) async {
  final transaction = Sentry.startTransaction(
    'order.place',
    'task',
    bindToScope: true,
  );

  try {
    final span = transaction.startChild('order.validate');
    await validateOrder(request);
    await span.finish();

    final apiSpan = transaction.startChild('order.api.create');
    final order = await api.createOrder(request);
    await apiSpan.finish();

    await transaction.finish(status: const SpanStatus.ok());
    return order;
  } catch (e, stack) {
    await transaction.finish(status: const SpanStatus.internalError());
    rethrow;
  }
}

Source maps for release builds

In release mode, Dart is compiled to native code and stack traces are symbolicated. Upload debug symbols to Sentry:

# Install Sentry CLI
npm install -g @sentry/cli

# Upload Android symbols
flutter build apk --obfuscate --split-debug-info=./debug-info
sentry-cli upload-dif ./debug-info --org your-org --project your-project

# iOS symbols are uploaded via Xcode build phase or Fastlane

Environment-based DSN

Use different Sentry projects for dev/staging/production:

const dsn = String.fromEnvironment('SENTRY_DSN', defaultValue: '');

options.dsn = dsn.isEmpty ? '' : dsn; // Empty DSN disables Sentry

In CI:

- name: Build
  run: |
    flutter build apk \
      --dart-define=SENTRY_DSN=${{ secrets.SENTRY_DSN_PROD }} \
      --dart-define=APP_ENV=production

Common pitfalls

Not scrubbing PII. Sentry captures request data, user input, and screenshots. Configure beforeSend to remove sensitive fields:

options.beforeSend = (event, hint) {
  // Remove sensitive headers
  event.request?.headers?.remove('Authorization');
  return event;
};

100% traces sample rate in production. Performance monitoring has overhead. Start at 10-20% for production. Use tracesSampler for dynamic rates (higher for critical flows, lower for background tasks).

Not clearing the user on sign-out. Sentry persists user context across sessions. Call Sentry.configureScope((s) => s.setUser(null)) when the user signs out.

Sign in to like, dislike, or report.

Using Sentry in Flutter — ANN Tech