| name | trace-propagation |
| description | Propagate W3C traceparent and baggage headers from mobile HTTP clients (OkHttp, URLSession, Dio, fetch) to backend services. Use when adding network instrumentation or debugging why client traces don't connect to backend spans. |
W3C Trace Propagation for Mobile HTTP
Instructions
Distributed traces only work if the outbound request carries the context. Use W3C traceparent + baggage (RFC 9110 / W3C Trace Context Level 2), not B3 or Jaeger-proprietary headers, unless the backend forces otherwise.
1. Headers to Send
traceparent: 00-<trace-id>-<span-id>-<flags> -- mandatory.
tracestate: <vendor-state> -- optional, vendor-specific.
baggage: key1=value1,key2=value2 -- optional, for non-identifying propagation (experiment id, release, user bucket).
Never put PII in baggage -- it is passed through every downstream service and often logged.
2. Android -- OkHttp Interceptor
Use io.opentelemetry.instrumentation:opentelemetry-okhttp-3.0 to auto-instrument, or write the interceptor yourself:
class TracePropagationInterceptor(private val otel: OpenTelemetry) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val req = chain.request()
val tracer = otel.getTracer("http.client")
val span = tracer.spanBuilder("${req.method} ${endpointGroup(req.url)}")
.setSpanKind(SpanKind.CLIENT)
.setAttribute("http.method", req.method)
.setAttribute("http.url", req.url.toString())
.setAttribute("endpoint_group", endpointGroup(req.url))
.startSpan()
val builder = req.newBuilder()
otel.propagators.textMapPropagator.inject(
Context.current().with(span), builder
) { carrier, key, value -> carrier?.header(key, value) }
return try {
val resp = chain.proceed(builder.build())
span.setAttribute("http.status_code", resp.code.toLong())
if (resp.code >= 400) span.setStatus(StatusCode.ERROR)
resp
} catch (t: Throwable) {
span.recordException(t); span.setStatus(StatusCode.ERROR); throw t
} finally { span.end() }
}
}
Register on your OkHttpClient.Builder() before shipping.
3. iOS -- URLSession Swizzling or Wrapper
OTel Swift ships URLSessionInstrumentation. Minimal config:
let config = URLSessionInstrumentationConfiguration(
shouldInjectTracingHeaders: { _ in true },
createdRequest: { request, span in
span.setAttribute(key: "endpoint_group",
value: endpointGroup(request.url))
}
)
URLSessionInstrumentation(configuration: config)
If you use Alamofire, add a RequestAdapter that calls propagators.textMapPropagator.inject into the URLRequest.allHTTPHeaderFields.
4. Flutter -- Dio Interceptor
class TraceInterceptor extends Interceptor {
final Tracer tracer;
TraceInterceptor(this.tracer);
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final span = tracer.startSpan('${options.method} ${endpointGroup(options.uri)}',
kind: SpanKind.client);
final carrier = <String, String>{};
globalPropagators().inject(Context.current.withSpan(span), carrier,
(c, k, v) => c[k] = v);
options.headers.addAll(carrier);
options.extra['otel_span'] = span;
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
final span = response.requestOptions.extra['otel_span'] as Span?;
span?..setAttribute('http.status_code', response.statusCode ?? 0)..end();
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
final span = err.requestOptions.extra['otel_span'] as Span?;
span?..recordException(err)..setStatus(StatusCanonicalCode.error)..end();
handler.next(err);
}
}
5. React Native -- fetch / XHR
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
registerInstrumentations({
instrumentations: [
new FetchInstrumentation({
propagateTraceHeaderCorsUrls: [/.*\.example\.com\/.*/],
clearTimingResources: true,
applyCustomAttributesOnSpan: (span, request) => {
span.setAttribute('endpoint_group', endpointGroup(request.url));
},
}),
new XMLHttpRequestInstrumentation({
propagateTraceHeaderCorsUrls: [/.*\.example\.com\/.*/],
}),
],
});
6. endpoint_group: the Most Important Attribute
Do not use raw URL as a label. Normalize:
/users/123/orders/456 -> /users/:id/orders/:id.
/v1/search?q=... -> /v1/search (drop query string unless it's a bounded enum).
Keep an allow-list of endpoint groups per backend service so a new unknown path falls into endpoint_group="other" rather than exploding cardinality.
7. Backend Side
The backend must read traceparent as the parent. If your services use a different propagator (B3, Jaeger), enable multi-propagator handling at the ingress:
propagators: [tracecontext, baggage, b3]
Keep the client on W3C and let the backend translate.
8. Baggage Discipline
Allow-list baggage keys. Good examples:
experiment.id
release
user.tier (enum: free / pro / enterprise)
device.class (low / mid / high)
Forbidden in baggage:
user.id, session.id, auth.token
- Any PII
- Values longer than 256 bytes (propagation will be truncated silently by some proxies)
9. CORS and Cross-Origin
For webviews and RN web bridges, the server must include traceparent, tracestate, baggage in Access-Control-Allow-Headers or the browser will drop them. Verify with a dev-tools network inspection.
10. Verification
- Fire a request from the app and inspect the ingress log on the backend: the
traceparent header must be present and well-formed.
- In the backend APM, the span must show the client
trace_id and client span as the parent.
- In the mobile APM, the waterfall shows backend children under the client
http.client span.
Checklist