import 'dart:async'; import 'package:sysfarma_mobile/src/core/models/tenant_session.dart'; import 'package:flutter/foundation.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; /// Observabilidad de crashes/errores de la app móvil (Sentry). /// /// El DSN se inyecta en build con `--dart-define=SENTRY_DSN=...`. Si no se /// provee DSN, Sentry queda deshabilitado y la app corre normal (solo logs por /// consola en debug). Así no se versiona ningún secreto en el repo. /// /// Build con observabilidad: /// ``` /// flutter build appbundle \ /// --dart-define=SENTRY_DSN=https://xxxx@oXXXX.ingest.sentry.io/XXXX \ /// --dart-define=SENTRY_ENVIRONMENT=production /// ``` class Observability { Observability._(); static const String dsn = String.fromEnvironment('SENTRY_DSN', defaultValue: ''); static const String environment = String.fromEnvironment('SENTRY_ENVIRONMENT', defaultValue: 'production'); static const double _tracesSampleRate = 0.2; /// Debe coincidir con `version` de pubspec.yaml. static const String release = 'sysfarma-mobile@5.161.118+20260514'; static bool get enabled => dsn.isNotEmpty; /// Inicializa Sentry (si hay DSN) y arranca la app dentro de su zona para /// capturar errores no manejados. Sin DSN instala handlers locales que solo /// imprimen en consola (debug) y corre la app directo. static Future bootstrap(FutureOr Function() runner) async { if (!enabled) { _installLocalHandlers(); await runner(); return; } await SentryFlutter.init( (options) { options.dsn = dsn; options.environment = environment; options.release = release; options.tracesSampleRate = _tracesSampleRate; options.enableAutoSessionTracking = true; options.attachStacktrace = true; options.debug = kDebugMode; }, appRunner: () => runner(), ); } static void _installLocalHandlers() { FlutterError.onError = (FlutterErrorDetails details) { if (kDebugMode) { debugPrint('[SysFarma] FLUTTER ERROR: ${details.exceptionAsString()}'); debugPrint('[SysFarma] STACK: ${details.stack}'); } FlutterError.presentError(details); }; PlatformDispatcher.instance.onError = (error, stack) { if (kDebugMode) { debugPrint('[SysFarma] PLATFORM ERROR: $error'); debugPrint('[SysFarma] STACK: $stack'); } return true; }; } /// Asocia el usuario/tenant activo a los eventos de Sentry. No-op sin DSN. static Future setSessionContext(TenantSession session) async { if (!enabled) return; await Sentry.configureScope((scope) { scope.setUser(SentryUser( username: session.companyName, data: { 'ruc': session.ruc, 'user_type': session.userType, 'establishment_id': session.activeEstablishmentId, }, )); scope.setTag('tenant', session.ruc ?? 'unknown'); scope.setTag('base_url', session.baseUrl); }); } /// Limpia el usuario del scope (logout). No-op sin DSN. static Future clearSessionContext() async { if (!enabled) return; await Sentry.configureScope((scope) => scope.setUser(null)); } /// Reporta una excepción manejada. En debug además la imprime. static Future captureException(Object error, [StackTrace? stack]) async { if (kDebugMode) debugPrint('[SysFarma] captured: $error'); if (!enabled) return; await Sentry.captureException(error, stackTrace: stack); } }