captureException static method

Future<BloomTelemetryEvent?> captureException(
  1. dynamic exception, {
  2. dynamic stackTrace,
  3. Map<String, dynamic>? context,
  4. List<String>? fingerprint,
  5. String? exceptionType,
  6. BloomErrorLevel level = BloomErrorLevel.error,
})

Captures a handled or unhandled exception with context and breadcrumbs.

Implementation

static Future<BloomTelemetryEvent?> captureException(
  dynamic exception, {
  dynamic stackTrace,
  Map<String, dynamic>? context,
  List<String>? fingerprint,
  String? exceptionType,
  BloomErrorLevel level = BloomErrorLevel.error,
}) async {
  if (!_isInitialized || !_config.enabled) return null;

  // Apply client-side sample rate evaluation
  if (_config.sampleRate < 1.0 && _random.nextDouble() > _config.sampleRate) {
    return null;
  }

  final effectiveType = exceptionType ?? exception.runtimeType.toString();
  final message = exception.toString();
  final stackStr = stackTrace?.toString() ?? StackTrace.current.toString();

  // Compute deterministic crash fingerprint
  final computedFingerprint = BloomCrashFingerprint.compute(
    exceptionType: effectiveType,
    message: message,
    stackTrace: stackStr,
    customFingerprint: fingerprint,
  );
  final fingerprintHash = BloomCrashFingerprint.hashTokens(computedFingerprint);

  _eventCounter++;
  final eventId = 'err_${DateTime.now().millisecondsSinceEpoch}_$_eventCounter';

  // Assemble metadata payloads
  final appPayload = {
    'name': _config.appInfo['name'] ?? 'bloom_app',
    'version': _config.bloomVersion ?? _config.appInfo['version'] ?? '1.0.0',
    'buildNumber': _config.buildNumber ?? _config.appInfo['buildNumber'] ?? '1',
    ..._config.appInfo,
  };

  final runtimePayload = {
    'bloomVersion': _config.bloomVersion ??
        _config.appInfo['bloomVersion'] ??
        _config.appInfo['version'] ??
        '1.0.0',
    'dartVersion': kIsWeb ? 'web' : getDartSdkVersion(),
    'flutterVersion': _config.flutterVersion ?? _config.appInfo['flutterVersion'] ?? '3.27.0',
    'runtimeFingerprint': _config.runtimeFingerprint ??
        BloomRuntimeFingerprint.current().computeHash(),
    'channel': _config.channel ?? _config.appInfo['channel'] ?? 'production',
    if (_config.activePatchId != null || _config.appInfo['activePatchId'] != null)
      'activePatchId': _config.activePatchId ?? _config.appInfo['activePatchId'],
  };

  final devicePayload = {
    'isWeb': kIsWeb,
    'platform': kIsWeb ? 'web' : getOperatingSystem(),
    'osVersion': kIsWeb ? 'browser' : getOperatingSystemVersion(),
  };

  final eventContext = {
    ..._config.tags,
    if (context != null) ...context,
  };

  var event = BloomTelemetryEvent(
    eventId: eventId,
    timestamp: DateTime.now().toUtc(),
    level: level,
    exceptionType: effectiveType,
    message: message,
    stackTrace: stackStr,
    fingerprint: computedFingerprint,
    fingerprintHash: fingerprintHash,
    context: eventContext,
    breadcrumbs: _ringBuffer.toList(),
    app: appPayload,
    runtime: runtimePayload,
    device: devicePayload,
  );

  // Apply beforeSend mutation/filter hook
  if (_config.beforeSend != null) {
    final modifiedEvent = _config.beforeSend!(event);
    if (modifiedEvent == null) {
      logger.debug('BloomObservability: Event $eventId dropped by beforeSend filter.');
      return null;
    }
    event = modifiedEvent;
  }

  // Transmit via configured transport
  try {
    await _config.transport.send(event);
    logger.info(
        'BloomObservability: Captured $effectiveType [$eventId] (${event.breadcrumbs.length} breadcrumbs)');
  } catch (e, stack) {
    logger.error('BloomObservability: Failed to transmit event: $e', stack);
  }

  return event;
}