OpenTelemetry bridge for
package:firebase_crashlytics,
built on the
Dartastic OpenTelemetry SDK.
Records crashes both to Crashlytics and as OTel exception
events on the currently active span — so the same exception that
lands in Crashlytics also lands in the trace, in the span where
it happened. One call, two destinations.
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:otel_firebase_crashlytics/otel_firebase_crashlytics.dart';
final crashlytics = FirebaseCrashlytics.instance;
try {
await checkout();
} catch (e, st) {
await tracedRecordError(
e, st,
crashlytics: crashlytics,
reason: 'cart validation',
fatal: false,
);
}
await tracedLog('user confirmed cart', crashlytics: crashlytics);
await tracedSetUserIdentifier(user.uid, crashlytics: crashlytics);When called inside an active span (tracer.startActiveSpan or
similar):
| Action | OTel span | Crashlytics |
|---|---|---|
tracedRecordError (non-fatal) |
exception event with type/message/stack; error.type, error.fatal=false, error.reason attributes |
recordError(fatal: false) |
tracedRecordError (fatal) |
same + span status Error |
recordError(fatal: true) |
tracedRecordFlutterError |
same as recordError, with reason derived from details.context |
recordFlutterError(fatal:) |
tracedLog |
crashlytics.log event with log.message attribute |
log(message) |
tracedSetUserIdentifier |
enduser.id attribute on the active span (opt-out via recordOnSpan: false) |
setUserIdentifier(id) |
When no active span is present, the OTel side becomes a no-op — Crashlytics still receives the call. Helpers degrade gracefully so you can sprinkle them anywhere without worrying about whether you're inside a traced scope.
await runWithoutCrashlyticsInstrumentationAsync(() async {
await tracedRecordError(e, st, crashlytics: crashlytics);
});Inside the helper's zone the OTel side becomes a passthrough; Crashlytics still records normally. Use this if you have a span processor that's emitting Crashlytics signals on its own (e.g., to avoid double-counting in dashboards).
The helpers are top-level functions taking crashlytics: as a
named parameter rather than extension methods on
FirebaseCrashlytics. Reasons:
- It makes the dependency explicit at the call site — no implicit
reliance on
FirebaseCrashlytics.instanceyou can't inject in tests. - The behavior is genuinely two-sided (OTel side + Crashlytics side), not a wrapped call. Top-level is clearer about that.
- Easier to unit-test with a
FirebaseCrashlyticsfake (see this package's tests for an example).
- The wrapper calls
Context.current.spanonce at the top of each helper; if the active span changes after that (which would be unusual inside a synchronous frame), the recorded events stay on the original span. That's intentional — you're recording the error in the context where it was caught. - This package does NOT install a
SpanProcessorthat automatically forwards every OTel exception to Crashlytics. That would be a different package; this one is the user-driven bridge. - The wrapper does not call
getTracer— it doesn't open new spans, it decorates the existing one.OTel.initialize()must still have run (soContext.currentis meaningful).
Apache 2.0 — see LICENSE.