SpitfireLoad Testing & Observabilityv1.1.0 · What's new

Version: 1.1.0This documentation is for Spitfire 1.1.0.

Connecting a Flutter app with OTel

Flutter (Android, iOS, web, desktop): HTTP calls as traces, linked to your backend with traceparent. Values: Connecting services with OTel: overview.

Version table

Flutter Dart Package Version
3.22 and later 3.4 and later dartastic_opentelemetry 0.12.0
3.22 and later 3.4 and later http 1.6.0
3.19 and earlier 3.3 and earlier not supported upgrade to Flutter 3.22

Find your version: flutter --version

OpenTelemetry has no official Dart SDK. dartastic_opentelemetry is a community package (Apache-2.0, being donated to the CNCF); pin its version. Traces only.

Verified: 2026-11-04 (code checked with Flutter 3.47.5, Dart 3.13.4). Source: pub.dev dartastic_opentelemetry, source and README, OTel blog: Dart and Flutter, pub.dev http, Flutter --dart-define-from-file.

What you need

  • Spitfire address https://<spitfire> (HTTPS: Android and iOS refuse plain HTTP)
  • Service name <service-name>, version <service-version>, environment <environment>
  • The app folder (here /srv/app, where pubspec.yaml is)

Flutter

  1. Create the key: Observability → Browser and app key; Allowed origins: the web build's address, empty for Android and iOS only; copy sfrum_… (Observability).

  2. Add the packages:

bash
cd /srv/app && flutter pub add 'dartastic_opentelemetry:0.12.0' 'http:1.6.0'
  1. Create /srv/app/lib/otel.dart and paste:
dart
// IS_OTEL_ENABLED=true: OTel on; any other value: off.
import 'package:dartastic_opentelemetry/dartastic_opentelemetry.dart';
import 'package:http/http.dart' as http;

const otelOn = String.fromEnvironment('IS_OTEL_ENABLED') == 'true';

Future<void> initOtel() async {
  if (!otelOn) return;
  await OTel.initialize(
    serviceName: const String.fromEnvironment('OTEL_SERVICE_NAME'),
    serviceVersion: const String.fromEnvironment('OTEL_SERVICE_VERSION'),
    resourceAttributes: OTel.attributesFromMap({'deployment.environment': const String.fromEnvironment('OTEL_ENVIRONMENT')}),
    spanKind: SpanKind.client,
    sampler: ParentBasedSampler(TraceIdRatioSampler(double.parse(const String.fromEnvironment('OTEL_TRACES_SAMPLER_ARG', defaultValue: '0.1')))),
    spanProcessor: BatchSpanProcessor(OtlpHttpSpanExporter(OtlpHttpExporterConfig(
      endpoint: const String.fromEnvironment('OTEL_TRACES_ENDPOINT'),
      headers: {'authorization': 'Bearer ${const String.fromEnvironment('OTEL_KEY')}'},
    ))),
    detectPlatformResources: false,
    enableMetrics: false,
    enableLogs: false,
  );
}

/// An http.Client whose requests are client spans carrying traceparent.
class OtelHttpClient extends http.BaseClient {
  final http.Client _inner = http.Client();
  final _propagator = W3CTraceContextPropagator();

  @override
  Future<http.StreamedResponse> send(http.BaseRequest request) async {
    if (!otelOn) return _inner.send(request);
    final span = OTel.tracer().startSpan('HTTP ${request.method}', kind: SpanKind.client,
        attributes: OTel.attributesFromMap({'http.request.method': request.method, 'url.full': request.url.toString()}));
    try {
      return await OTel.withSpanAsync(span, () async {
        _propagator.inject(Context.current, request.headers, _HeaderSetter(request.headers));
        final res = await _inner.send(request);
        span.addAttributes(OTel.attributesFromMap({'http.response.status_code': res.statusCode}));
        return res;
      });
    } catch (e, st) {
      span.recordException(e, stackTrace: st);
      span.setStatus(SpanStatusCode.Error, e.toString());
      rethrow;
    } finally {
      span.end();
    }
  }
}

class _HeaderSetter implements TextMapSetter<String> {
  _HeaderSetter(this._headers);
  final Map<String, String> _headers;

  @override
  void set(String key, String value) => _headers[key] = value;
}
  1. In /srv/app/lib/main.dart, make main async and start it first:
dart
import 'otel.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initOtel();
  runApp(const MyApp());
}
  1. Send your API calls with the traced client:
dart
final api = OtelHttpClient();
final res = await api.get(Uri.parse('https://api.example.com/orders'));
  1. Create /srv/app/otel.json and paste:
json
{
  "IS_OTEL_ENABLED": "true",
  "OTEL_SERVICE_NAME": "<service-name>",
  "OTEL_SERVICE_VERSION": "<service-version>",
  "OTEL_ENVIRONMENT": "<environment>",
  "OTEL_TRACES_ENDPOINT": "https://<spitfire>/otlp/v1/traces",
  "OTEL_KEY": "sfrum_…",
  "OTEL_TRACES_SAMPLER_ARG": "0.1"
}
  1. Build with it (iOS: ipa; web: web):
bash
cd /srv/app && flutter build apk --release --dart-define-from-file=otel.json

Verify

  1. Spitfire takes the key from an app (no Origin):
bash
curl -s -o /dev/null -w '%{http_code}' -X POST https://<spitfire>/otlp/v1/traces -H 'Authorization: Bearer sfrum_…' -H 'Content-Type: application/json' -d '{}' | grep -q '^200
  

 && echo OK
  1. The code compiles:
bash
cd /srv/app && flutter analyze lib/otel.dart lib/main.dart && echo OK

Turning off and removing

  1. Turn off: in otel.json write "IS_OTEL_ENABLED": "false", then build again.
  2. Remove:
bash
cd /srv/app && flutter pub remove dartastic_opentelemetry && rm -f lib/otel.dart otel.json

Common problems

Symptom Fix
No spans Build with --dart-define-from-file=otel.json; flutter run needs it too
401 The key is sfrum_…, enabled; OTEL_KEY holds it without Bearer
Web build: 403 origin not allowed Add the web build's address to Allowed origins
429 Raise the key's Exports per minute or lower OTEL_TRACES_SAMPLER_ARG
version solving failed Flutter 3.22 or later
App and backend spans are not one trace Send the calls with OtelHttpClient