appInitErrorHandling function

Future<void> appInitErrorHandling()

Implementation

Future<void> appInitErrorHandling() async {
  final config = errorReportingConfig;

  // Set the error reporting configuration in Logger
  Logger.setErrorReportingConfig(config);

  // Check for master kill switch first - this disables ALL error reporting
  // regardless of other settings. Use for live Firebase dev projects.
  if (AppConfigBase.doDisableErrorReporting) {
    debugPrint('Error Reporting DISABLED via DO_DISABLE_ERROR_REPORTING');
    _setupMinimalErrorHandlers();
    return;
  }

  // Determine if error reporting is blocked by emulator mode
  // Can be overridden with DO_FORCE_ERROR_REPORTING for testing
  final isBlockedByEmulator =
      AppConfigBase.doUseBackendEmulator && !AppConfigBase.doForceErrorReporting;

  // Determine if we should use error reporting
  // This must be checked BEFORE initializing reporters to prevent
  // Sentry/Crashlytics from capturing errors when running in emulator
  final shouldUseErrorReporting = !isBlockedByEmulator &&
      (config.enableInDebug || !kDebugMode) &&
      (config.enableOnWeb || !kIsWeb);

  // Custom reporter follows the same rules as main error reporting
  // The enableOnWeb/enableInDebug flags allow reporting on those platforms,
  // but only when not blocked by emulator mode or master kill switch
  final shouldUseCustomReporter = config.customReporter != null &&
      !isBlockedByEmulator &&
      ((config.enableInDebug || !kDebugMode) && (config.enableOnWeb || !kIsWeb));

  // Firebase Crashlytics requires Firebase to be initialized and doesn't support web
  final canUseFirebaseCrashlytics = config.useFirebaseCrashlytics &&
      AppConfigBase.isFirebaseInitialized &&
      !kIsWeb;

  debugPrint('Error Reporting Configuration: '
      'useFirebaseCrashlytics=${config.useFirebaseCrashlytics}, '
      'customReporter=${config.customReporter != null}, '
      'customReporterManagesErrorHandlers=${config.customReporterManagesErrorHandlers}, '
      'enableInDebug=${config.enableInDebug}, '
      'enableOnWeb=${config.enableOnWeb}, '
      'doUseBackendEmulator=${AppConfigBase.doUseBackendEmulator}, '
      'doForceErrorReporting=${AppConfigBase.doForceErrorReporting}, '
      'isBlockedByEmulator=$isBlockedByEmulator, '
      'shouldUseErrorReporting=$shouldUseErrorReporting, '
      'shouldUseCustomReporter=$shouldUseCustomReporter, '
      'environmentType=${AppConfigBase.environmentType.value}');

  // Initialize custom reporter ONLY if it should be used
  // This prevents Sentry/etc from setting up internal error handlers
  // when running in emulator mode
  if (shouldUseCustomReporter) {
    await config.customReporter!.initialize();
    // Set the custom error reporter in Logger for crash reporting
    Logger.setCustomErrorReporter(config.customReporter);
  }

  // Disable analytics and crashlytics for web or emulator (unless configured otherwise)
  if (!shouldUseErrorReporting) {
    FlutterError.onError = (details) {
      loge(details.stack ?? StackTrace.current, details.exceptionAsString());

      // Still report to custom reporter if it was initialized (enabled on web/debug)
      if (shouldUseCustomReporter) {
        config.customReporter!.recordFlutterError(details);
      }
    };

    PlatformDispatcher.instance.onError = (exception, stackTrace) {
      loge(stackTrace, exception.toString());

      // Still report to custom reporter if it was initialized (enabled on web/debug)
      if (shouldUseCustomReporter) {
        config.customReporter!.recordError(exception, stackTrace);
      }
      return true;
    };
  } else {
    // Setup error handlers for production
    // Note: If using Firebase Crashlytics, Firebase must be initialized first
    // via appInitFirebase() before calling appInitErrorHandling()

    // Only set up error handlers if the custom reporter doesn't manage them
    // (e.g., Sentry with SentryFlutter.init sets up its own handlers)
    if (!config.customReporterManagesErrorHandlers) {
      // Setup Flutter error handler for non-async exceptions
      FlutterError.onError = (FlutterErrorDetails details) {
        if (canUseFirebaseCrashlytics) {
          FirebaseCrashlytics.instance.recordFlutterError(details);
        }
        if (shouldUseCustomReporter) {
          config.customReporter!.recordFlutterError(details);
        }
      };

      // Setup async error handler
      PlatformDispatcher.instance.onError = (error, stack) {
        if (canUseFirebaseCrashlytics) {
          FirebaseCrashlytics.instance.recordError(error, stack);
        }
        if (shouldUseCustomReporter) {
          config.customReporter!.recordError(error, stack);
        }
        return true;
      };
    } else {
      // Custom reporter manages error handlers, but we still need to
      // chain Firebase Crashlytics if enabled
      if (canUseFirebaseCrashlytics) {
        // Save the custom reporter's error handler
        final originalFlutterErrorHandler = FlutterError.onError;
        final originalPlatformErrorHandler = PlatformDispatcher.instance.onError;

        // Set up handlers that report to both Firebase and the custom reporter
        FlutterError.onError = (FlutterErrorDetails details) {
          FirebaseCrashlytics.instance.recordFlutterError(details);
          // Call the custom reporter's handler if it was set
          originalFlutterErrorHandler?.call(details);
        };

        PlatformDispatcher.instance.onError = (error, stack) {
          FirebaseCrashlytics.instance.recordError(error, stack);
          // Call the custom reporter's handler if it exists
          return originalPlatformErrorHandler?.call(error, stack) ?? true;
        };
      }
    }

    // Catch errors outside of the Flutter framework (isolates)
    // Only for non-web platforms
    // Note: When customReporter manages error handlers (e.g., Sentry's init),
    // they typically set up their own isolate error listeners
    if (!kIsWeb) {
      if (!config.customReporterManagesErrorHandlers) {
        // Standard approach: we set up the isolate listener
        Isolate.current.addErrorListener(
          RawReceivePort((pair) async {
            final List<dynamic> errorAndStacktrace = pair;
            final error = errorAndStacktrace.first;
            final stackTrace = errorAndStacktrace.last as StackTrace;

            if (canUseFirebaseCrashlytics) {
              await FirebaseCrashlytics.instance.recordError(error, stackTrace);
            }
            if (shouldUseCustomReporter) {
              config.customReporter!.recordError(error, stackTrace);
            }
          }).sendPort,
        );
      } else if (canUseFirebaseCrashlytics) {
        // Custom reporter manages handlers, but we need Firebase to catch isolate errors
        // Chain with any existing listener that the custom reporter may have set
        Isolate.current.addErrorListener(
          RawReceivePort((pair) async {
            final List<dynamic> errorAndStacktrace = pair;
            final error = errorAndStacktrace.first;
            final stackTrace = errorAndStacktrace.last as StackTrace;

            // Report to Firebase
            await FirebaseCrashlytics.instance.recordError(error, stackTrace);

            // Note: Custom reporter's isolate listener (if any) will also catch this
            // since multiple listeners can be added to the same isolate
          }).sendPort,
        );
      }
    }
  }
}