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();
    // (b) Kill-switch path — reporting is disabled, so the early buffer cannot
    // be flushed anywhere; drop+clear it (a no-op when the installer was never
    // called). MUST run before the early `return`, or the buffer leaks (Issue 35).
    _logAndClearEarlyErrorBuffer('error reporting disabled (kill switch)');
    // Reporting is off — no chokepoint target, so `_recordErrorSafe` (e.g. a
    // re-routed isolate error) buffers (and that buffer is dropped next attach).
    _activeReporter = null;
    // Reporting is off — drop any deferred bootstrap diagnostics and mark
    // attached so later ones short-circuit (loge no-ops under the kill switch).
    _errorHandlingAttached = true;
    _dropDeferredBootstrapReports();
    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));

  debugPrint('Error Reporting Configuration: '
      'customReporter=${config.customReporter != null}, '
      'customReporterManagesErrorHandlers=${config.customReporterManagesErrorHandlers}, '
      'reporterRequiresFirebase=${config.reporterRequiresFirebase}, '
      '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);
    // Attach the chokepoint target so `_recordErrorSafe` (the isolate listener,
    // plus the Phase-4 zone/web-JS surfaces) forwards instead of buffering
    // (ERH-011/021). Set here so it is live BEFORE the buffer flush below.
    _activeReporter = config.customReporter;
  } else {
    // No active reporter on this path — `_recordErrorSafe` falls back to
    // buffering (its buffer is dropped on the disabled/blocked branch below).
    _activeReporter = null;
  }

  // Disable analytics and crashlytics for web or emulator (unless configured otherwise)
  if (!shouldUseErrorReporting) {
    FlutterError.onError = (details) {
      _presentErrorInDebugConsole(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;
    };

    // (c) Emulator-blocked / debug-no-reporting branch. The minimal handlers
    // above replaced the early buffering handlers; reporting is suppressed on
    // this path, so drop+clear the early buffer (a no-op when the installer was
    // never called). This branch does NOT early-return — the buffer would
    // otherwise leak (Issue 35).
    _logAndClearEarlyErrorBuffer('error reporting blocked (emulator/debug)');
    // Reporting suppressed on this path — drop deferred bootstrap diagnostics
    // and mark attached (later ones short-circuit; loge no-ops here).
    _errorHandlingAttached = true;
    _dropDeferredBootstrapReports();
  } else {
    // Production branch. dreamic ships no built-in reporter, so the handlers
    // route to the single registered reporter (if any).

    // Only install handlers if the reporter doesn't manage its own (e.g. Sentry
    // via SentryFlutter.init with appRunner). When it does, dreamic leaves
    // FlutterError.onError / PlatformDispatcher.onError / the isolate listener to
    // the reporter and just wires loge() + flushes the buffers below.
    if (!config.customReporterManagesErrorHandlers) {
      // Flutter-framework (non-async) errors.
      FlutterError.onError = (FlutterErrorDetails details) {
        _presentErrorInDebugConsole(details);
        if (shouldUseCustomReporter) {
          config.customReporter!.recordFlutterError(details);
        }
      };

      // Async / platform errors.
      PlatformDispatcher.instance.onError = (error, stack) {
        if (shouldUseCustomReporter) {
          config.customReporter!.recordError(error, stack);
        }
        return true;
      };

      // Errors outside the Flutter framework (isolates), non-web only.
      // Apply-once across gate-retry re-runs: `addErrorListener` allows multiple
      // listeners, so an unguarded re-run would accumulate them (Issue 31).
      //
      // Registered AT attach and routed through `_recordErrorSafe` (ERH-021), so
      // isolate errors get the same re-entrancy guard, cross-surface dedup, and
      // redaction as every other surface (BEH-1/9/11). Pre-attach uncaught main-
      // isolate errors are already covered by the early `FlutterError.onError` /
      // `PlatformDispatcher.onError` handlers (which buffer) — this listener does
      // NOT buffer pre-attach isolate errors (ERH-029).
      if (!kIsWeb && !_isolateErrorListenerAdded) {
        _isolateErrorListenerAdded = true;
        Isolate.current.addErrorListener(
          RawReceivePort((pair) {
            final List<dynamic> errorAndStacktrace = pair;
            final error = errorAndStacktrace.first as Object;
            final stackTrace = errorAndStacktrace.last as StackTrace;
            _recordErrorSafe(error, stackTrace);
          }).sendPort,
        );
      }
    }

    // (a) Production branch — the reporter is now attached. Flush the early
    // buffers in BREADCRUMBS-then-ERRORS order (ERH-032 / BEH-12) so each
    // buffered early error is reported with its preceding breadcrumb trail
    // already in place. Flushed breadcrumbs were redacted at emit time and are
    // NOT re-redacted. Both flushes are safe no-ops when their buffers are empty
    // (e.g. the installer was never called — Issue 36).
    if (_activeReporter != null) {
      _flushAndClearEarlyBreadcrumbs(_activeReporter!);
    }
    _flushAndClearEarlyErrorBuffer();
    // Reporter attached: deliver any deferred bootstrap diagnostics (e.g. the
    // appInitFirebase recovery, which fired at the Firebase step before this
    // attach), then route future ones immediately.
    _errorHandlingAttached = true;
    _flushDeferredBootstrapReports();
  }
}