Observabilidade no Flutter: um pacote unificado para logs, erros e performance

Observabilidade não é mais um luxo, é um requisito básico para apps Flutter que precisam escalar, diagnosticar problemas rapidamente e entender o comportamento real do usuário. Pensando nisso, criei o Observability Flutter, um pacote que centraliza logs, erros, transações e breadcrumbs em uma API simples e extensível.

👉 Quer testar agora? O pacote já está disponível no pub.dev e pode ser usado em qualquer projeto Flutter:

🔗 https://pub.dev/packages/observability_flutter

Esse é um pacote que eu mesmo desenvolvi e já utilizo em projetos reais. Ele está em constante evolução, e fico totalmente aberto a dicas, sugestões de melhoria, novos providers ou feedbacks de uso no dia a dia

A ideia é clara: escrever observabilidade uma única vez e enviar para quantos providers você quiser.

O problema da observabilidade fragmentada

Em projetos reais, é comum acabar com:

  • print espalhado pelo código
  • Logs em um provider
  • Erros em outro (Sentry, Firebase, etc.)
  • Métricas de performance em um terceiro

Isso gera acoplamento, duplicação de código e dificulta trocar ou adicionar ferramentas no futuro.

O Observability Flutter resolve isso criando uma camada de abstração única, desacoplando sua aplicação das ferramentas de observabilidade.

O que o Observability Flutter entrega

Principais features do pacote:

  • 📝 Logs com níveis (debuginfowarningerror)
  • 🔴 Captura de erros e exceções
  • 📊 Transações para medir performance
  • 🍞 Breadcrumbs para rastrear ações do usuário
  • 🔌 Suporte a múltiplos providers simultâneos
  • 🏭 Registry para auto-discovery de providers

Tudo isso exposto por uma única interface.

Instalação

Adicione ao seu pubspec.yaml:

flutter pub add observability_flutter

Inicialização básica

Você pode inicializar o manager passando diretamente os providers desejados:

import 'package:observability_flutter/observability_flutter.dart';

await ObservabilityManager.instance.initialize(
  providers: [
    DebugObservabilityService(),
    MyCustomObservabilityService(),
  ],
);

ObservabilityManager.instance.logMessage('Hello!');
ObservabilityManager.instance.recordError(error, stackTrace: stackTrace);

Criando um provider customizado (exemplo real com Sentry)

A seguir, um exemplo real de provider customizado usando Sentry, implementando a interface ObservabilityService.

import 'package:flutter/foundation.dart';
import 'package:observability_flutter/observability_flutter.dart';
import 'package:sentry_flutter/sentry_flutter.dart';

/// Implementação de Observabilidade para Sentry
class SentryObservabilityService implements ObservabilityService {
  final String dns;
  final Map<String, dynamic>? config;

  SentryObservabilityService({required this.dns, this.config});

  @override
  Future<void> initialize() async {
    await SentryFlutter.init((options) {
      options.dsn = dns;
      options.tracesSampleRate = (config?['tracesSampleRate'] ?? 1.0) as double;
      options.environment = config?['environment'] as String?;
      options.release = config?['release'] as String?;
    });
  }

  @override
  void logMessage(
    String message, {
    LogLevel level = LogLevel.info,
    Map<String, dynamic>? data,
    String? category,
  }) {
    Sentry.addBreadcrumb(
      Breadcrumb(
        message: message,
        category: category ?? 'log',
        level: _mapLevel(level),
        data: data,
      ),
    );
  }

  @override
  void recordError(
    dynamic error, {
    StackTrace? stackTrace,
    Map<String, dynamic>? data,
    String? reason,
  }) {
    Sentry.captureException(
      error,
      stackTrace: stackTrace,
      withScope: (scope) {
        if (data != null) {
          data.forEach(scope.setExtra);
        }
        if (reason != null) {
          scope.setTag('reason', reason);
        }
      },
    );
  }

  @override
  Future<T> startTransaction<T>(
    String name,
    Future<T> Function() operation, {
    Map<String, dynamic>? data,
  }) async {
    final transaction = Sentry.startTransaction(name, 'task');
    try {
      final result = await operation();
      transaction.finish(status: const SpanStatus.ok());
      return result;
    } catch (e, s) {
      transaction.throwable = e;
      transaction.finish(status: const SpanStatus.internalError());
      rethrow;
    }
  }

  @override
  void addBreadcrumb(
    String message, {
    Map<String, dynamic>? data,
    String? category,
  }) {
    Sentry.addBreadcrumb(
      Breadcrumb(
        message: message,
        category: category ?? 'breadcrumb',
        data: data,
      ),
    );
  }

  SentryLevel _mapLevel(LogLevel level) {
    switch (level) {
      case LogLevel.debug:
        return SentryLevel.debug;
      case LogLevel.info:
        return SentryLevel.info;
      case LogLevel.warning:
        return SentryLevel.warning;
      case LogLevel.error:
        return SentryLevel.error;
    }
  }
}

Esse provider pode ser usado isoladamente ou em conjunto com outros (ex: Debug, Firebase, Datadog, etc.).

Benefícios dessa abordagem

  • 🔁 Troca de provider sem refatorar a aplicação
  • 🧪 Facilidade para mockar observabilidade em testes
  • 📦 Providers reutilizáveis entre projetos
  • 🧱 Arquitetura limpa e desacoplada
  • 🚀 Escala natural conforme o app cresce

Quando usar esse padrão

Esse pacote é ideal para:

  • Apps de médio e grande porte
  • Projetos white-label
  • Apps corporativos
  • Times que valorizam Clean Architecture
  • Projetos que precisam trocar ferramentas sem dor