# Flutter

> Instrument your Flutter application with OpenTelemetry to send distributed traces to Last9 using the dio HTTP client

Source: https://last9.io/docs/integrations/frameworks/dart/flutter/

Use OpenTelemetry to instrument your Flutter application and send distributed traces to Last9. This guide covers two approaches: header-only trace propagation using `dio`, and full SDK instrumentation with the `opentelemetry` Dart package.

## Prerequisites

Before setting up Flutter instrumentation, ensure you have:

- **Flutter**: 3.x with Dart 3.x
- **dio**: ^5.0.0 (HTTP client)
- **Last9 Account**: With OpenTelemetry integration credentials (only for Approach B)

## Approach A: Traceparent Header Injection (Recommended)

No SDK dependency needed. A single dio interceptor generates a W3C `traceparent` header on every outbound request. Downstream services pick it up and continue the same trace.

```
Flutter app           Backend Service A        Backend Service B
    │                      │                        │
    │──traceparent: 00-{same-trace-id}──────────────▶│
    │                      │                        │
                [single trace visible in Last9 across all services]
```

1. **Create the interceptor**

   Create `lib/telemetry/traceparent_interceptor.dart`:

   ```dart
   import 'dart:math';
   import 'package:dio/dio.dart';

   /// Injects a W3C traceparent header into every outbound HTTP request.
   /// Downstream services will continue the same trace automatically.
   class TraceparentInterceptor extends Interceptor {
     final Random _rng = Random.secure();

     String _randomHex(int bytes) => List.generate(
           bytes,
           (_) => _rng.nextInt(256).toRadixString(16).padLeft(2, '0'),
         ).join();

     @override
     void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
       // Format: 00-{16-byte trace-id}-{8-byte span-id}-01
       // 01 = sampled flag (tell downstream to record this trace)
       final traceId = _randomHex(16); // 32 hex chars
       final spanId  = _randomHex(8);  // 16 hex chars
       options.headers['traceparent'] = '00-$traceId-$spanId-01';
       handler.next(options);
     }
   }
   ```

2. **Register the interceptor**

   Add it to your Dio instance. If your app uses a single shared `Dio` instance (common with providers or service classes), add the interceptor there:

   ```dart
   import 'package:dio/dio.dart';
   import 'telemetry/traceparent_interceptor.dart';

   final dio = Dio();
   dio.interceptors.add(TraceparentInterceptor());
   ```

   :::caution[Interceptor Order]
   Add `TraceparentInterceptor` **before** any retry or logging interceptors. The `traceparent` header must be set before the request is sent or retried.
   :::

3. **Verify**

   After making a request, check Last9 Traces. Look for the trace ID in downstream service spans — the backend span should show `traceparent` in its incoming HTTP attributes, and all services will share the same trace ID.

## Approach B: Full OTel SDK

Use this when you need visibility inside the Flutter app itself: HTTP call durations, error details, retry counts, or user-journey traces.

:::note[SDK Status]
The [`opentelemetry` Dart package](https://pub.dev/packages/opentelemetry) (v0.18.x by Workiva) supports traces (beta) and metrics (alpha). Logs are not yet implemented.
:::

1. **Install dependencies**

   ```yaml
   # pubspec.yaml
   dependencies:
     opentelemetry: ^0.18.10
     dio: ^5.0.0
   ```

2. **Initialize OpenTelemetry**

   Create `lib/telemetry/otel_setup.dart`:

   ```dart
   import 'package:opentelemetry/api.dart';
   import 'package:opentelemetry/sdk.dart';

   void initTelemetry({
     required Uri otlpEndpoint,
     required String authHeader,
     String serviceName = 'your-flutter-app',
     String environment = 'production',
   }) {
     final exporter = CollectorExporter(
       otlpEndpoint,
       headers: {'authorization': authHeader},
     );

     final provider = TracerProviderBase(
       processors: [BatchSpanProcessor(exporter)],
       resource: Resource([
         Attribute.fromString('service.name', serviceName),
         Attribute.fromString('deployment.environment', environment),
       ]),
     );

     registerGlobalTracerProvider(provider);
   }
   ```

   Call it from `main.dart` before `runApp`:

   ```dart
   void main() {
     WidgetsFlutterBinding.ensureInitialized();
     initTelemetry(
       otlpEndpoint: Uri.parse('https://otlp.last9.io'),
       authHeader: 'Basic YOUR_BASE64_CREDENTIALS',
     );
     runApp(const MyApp());
   }
   ```

3. **Create the instrumented dio interceptor**

   Create `lib/telemetry/otel_dio_interceptor.dart`:

   ```dart
   import 'package:dio/dio.dart';
   import 'package:opentelemetry/api.dart';

   /// TextMapSetter implementation for dio headers.
   class _DioHeaderSetter implements TextMapSetter<Map<String, dynamic>> {
     @override
     void set(Map<String, dynamic> carrier, String key, String value) {
       carrier[key] = value;
     }
   }

   class OtelDioInterceptor extends Interceptor {
     final Tracer _tracer = globalTracerProvider.getTracer('flutter-http');
     final Map<RequestOptions, Span> _spans = {};
     final _setter = _DioHeaderSetter();

     @override
     void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
       final span = _tracer.startSpan(
         '${options.method} ${options.path}',
         kind: SpanKind.client,
         attributes: [
           Attribute.fromString('http.method', options.method),
           Attribute.fromString('http.url', options.uri.toString()),
           Attribute.fromString('http.host', options.uri.host),
         ],
       );

       // Inject W3C traceparent for downstream propagation
       final ctx = contextWithSpan(Context.current, span);
       W3CTraceContextPropagator().inject(ctx, options.headers, _setter);

       _spans[options] = span;
       handler.next(options);
     }

     @override
     void onResponse(Response response, ResponseInterceptorHandler handler) {
       final span = _spans.remove(response.requestOptions);
       if (span != null) {
         span.setAttribute(
             Attribute.fromInt('http.status_code', response.statusCode ?? 0));
         span.end();
       }
       handler.next(response);
     }

     @override
     void onError(DioException err, ErrorInterceptorHandler handler) {
       final span = _spans.remove(err.requestOptions);
       if (span != null) {
         span.setAttribute(
             Attribute.fromString('error.message', err.message ?? 'unknown'));
         if (err.response?.statusCode != null) {
           span.setAttribute(
               Attribute.fromInt('http.status_code', err.response!.statusCode!));
         }
         span.setStatus(StatusCode.error, err.message ?? '');
         span.end();
       }
       handler.next(err);
     }
   }
   ```

4. **Register the interceptor**

   ```dart
   dio.interceptors.add(OtelDioInterceptor());
   ```

5. **Verify**

   In Last9 Traces, filter by `service.name = your-flutter-app`. You should see spans for each HTTP call with method, URL, status code, and duration. Downstream services will share the same trace ID.

## Choosing Between the Two Approaches

**Use Approach A** (traceparent injection) when you need distributed traces across Flutter and backend services without adding new dependencies.

**Use Approach B** (full OTel SDK) when you need spans from inside the Flutter app — HTTP call durations, error details, retry visibility, or user-journey tracing.

## Troubleshooting

### Traces Not Connecting Across Services

**traceparent header not reaching backend:**

If your backend is behind a reverse proxy or load balancer, ensure it forwards custom HTTP headers. Some proxies strip unknown headers by default — check your proxy's header passthrough configuration.

**Traces appear disconnected:**

Ensure the `traceparent` header format is correct: `00-{32 hex}-{16 hex}-01`. The version must be `00`, and the flags byte `01` means "sampled". If flags is `00`, downstream services may not record the trace.

### Full SDK Issues (Approach B)

**Export fails on mobile:**

The `CollectorExporter` uses HTTP/protobuf which works on most mobile networks. If you see connection timeouts, check that your app has internet permissions and the endpoint is reachable from the device network.

**High memory usage from span tracking:**

The `_spans` map in `OtelDioInterceptor` grows if responses are never received (e.g., cancelled requests). Add cleanup logic for stale spans or use `dio`'s cancel token to clean up properly.

## Production Considerations

### Crash Recording

Wrap your app entry point to capture unhandled exceptions as spans:

```dart
void main() {
  runZonedGuarded(() async {
    WidgetsFlutterBinding.ensureInitialized();
    await initTelemetry(); // Approach B only
    runApp(const MyApp());
  }, (error, stack) {
    // Record crash as a span (Approach B)
    final tracer = globalTracerProvider.getTracer('flutter-app');
    final span = tracer.startSpan('app.crash');
    span.setStatus(StatusCode.error, error.toString());
    span.setAttribute(Attribute.fromString('error.stack', stack.toString()));
    span.end();
  });
}
```

### Flush on Background

Ensure telemetry is flushed before the app goes to the background. Undelivered spans are lost if the OS kills the app:

```dart
class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) {
      // Force flush pending spans before backgrounding
      (globalTracerProvider as TracerProviderBase).forceFlush();
    }
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }
}
```

## Need Help?

If you encounter any issues or have questions:

- Join our [Discord community](https://discord.com/channels/652153247672729619/652153247672729621) for real-time support
- Contact our support team at [support@last9.io](mailto:support@last9.io)
