| name | distributed-tracing-mobile |
| description | Instrument mobile apps with OpenTelemetry client SDKs so traces start on the device and propagate to the backend. Use when wiring up tracing on Android, iOS, Flutter, or React Native. |
Distributed Tracing on Mobile
Instructions
A mobile trace is only useful if it connects to the backend trace. Use OpenTelemetry, start the root span on the client for the user-visible operation, and propagate via W3C headers.
1. Model: the Root Span Lives on the Client
For any user-initiated flow (tap -> network -> render), the root span starts on the device. Backend spans become children via the propagated context.
ui.load or ui.action is the root.
- Each outbound HTTP call is a child
http.client span.
- Backend spans become children of those
http.client spans.
Do not start the root span in the backend and try to "attach" the client later; the client view is half of the latency that matters.
2. Android (Kotlin)
val tracerProvider = SdkTracerProvider.builder()
.addSpanProcessor(
BatchSpanProcessor.builder(
OtlpHttpSpanExporter.builder()
.setEndpoint("https://otel.example.com/v1/traces")
.addHeader("Authorization", "Bearer ${BuildConfig.OTEL_TOKEN}")
.build()
).build()
)
.setResource(Resource.create(Attributes.of(
AttributeKey.stringKey("service.name"), "my-app-android",
AttributeKey.stringKey("service.version"), BuildConfig.VERSION_NAME,
AttributeKey.stringKey("deployment.environment"), BuildConfig.FLAVOR,
AttributeKey.stringKey("device.model"), Build.MODEL
)))
.setSampler(Sampler.parentBased(Sampler.traceIdRatioBased(0.15)))
.build()
val otel = OpenTelemetrySdk.builder()
.setTracerProvider(tracerProvider)
.setPropagators(ContextPropagators.create(W3CTraceContextPropagator.getInstance()))
.buildAndRegisterGlobal()
Consider io.opentelemetry.android:android-agent for auto-instrumentation of activity lifecycle, network, and crashes.
3. iOS (Swift)
let exporter = OtlpHttpTraceExporter(endpoint: URL(string: "https://otel.example.com/v1/traces")!,
headers: ["Authorization": "Bearer \(otelToken)"])
let processor = BatchSpanProcessor(spanExporter: exporter,
scheduleDelay: 30,
maxQueueSize: 2048,
maxExportBatchSize: 512)
let provider = TracerProviderBuilder()
.add(spanProcessor: processor)
.with(sampler: ParentBasedSampler(root: TraceIdRatioBasedSampler(ratio: 0.15)))
.with(resource: Resource(attributes: [
"service.name": .string("my-app-ios"),
"service.version": .string(Bundle.main.version),
"deployment.environment": .string(env),
"device.model": .string(UIDevice.current.modelIdentifier)
]))
.build()
OpenTelemetry.registerTracerProvider(tracerProvider: provider)
OpenTelemetry.registerPropagators(textPropagators: [W3CTraceContextPropagator()], baggagePropagator: W3CBaggagePropagator())
4. Flutter (Dart)
The official opentelemetry package works in Dart; for HTTP exporter use opentelemetry_sdk + opentelemetry_exporter_otlp_http. A lighter alternative is Sentry's performance SDK which exposes OTel-compatible APIs.
final provider = TracerProviderBase(
resource: Resource([
Attribute.fromString('service.name', 'my-app-flutter'),
Attribute.fromString('service.version', appVersion),
Attribute.fromString('deployment.environment', env),
]),
sampler: ParentBasedSampler(TraceIdRatioBasedSampler(0.15)),
processors: [BatchSpanProcessor(OtlpHttpSpanExporter(
uri: Uri.parse('https://otel.example.com/v1/traces'),
headers: {'Authorization': 'Bearer $token'}))],
);
registerGlobalTracerProvider(provider);
5. React Native (TypeScript)
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { W3CTraceContextPropagator } from '@opentelemetry/core';
import { Resource } from '@opentelemetry/resources';
const provider = new WebTracerProvider({
resource: new Resource({
'service.name': 'my-app-rn',
'service.version': pkg.version,
'deployment.environment': Config.ENV,
}),
});
provider.addSpanProcessor(new BatchSpanProcessor(new OTLPTraceExporter({
url: 'https://otel.example.com/v1/traces',
headers: { Authorization: `Bearer ${Config.OTEL_TOKEN}` },
})));
provider.register({ propagator: new W3CTraceContextPropagator() });
Patch global.fetch and XMLHttpRequest via the OTel instrumentations so HTTP calls are traced automatically.
6. Start Spans Around User Intents
val tracer = GlobalOpenTelemetry.getTracer("my-app")
suspend fun loadHome() {
val span = tracer.spanBuilder("home.load").setSpanKind(SpanKind.INTERNAL).startSpan()
try {
withContext(span.asContextElement()) {
val feed = feedApi.fetch()
homeView.render(feed)
span.addEvent("first_frame_rendered")
}
} catch (t: Throwable) {
span.recordException(t); span.setStatus(StatusCode.ERROR); throw t
} finally { span.end() }
}
7. Sampling
ParentBased(TraceIdRatioBased(0.15)) in production, AlwaysOn in debug.
- Override to 100% for transactions that end in error: use a
TailSampler-style composite, or mark the transaction sampled = true manually when catching an exception.
- Fresh-release boost: 100% for the first 24 hours after a release, then taper.
8. Resource Attributes (mandatory)
Every span must carry:
service.name, service.version, deployment.environment
telemetry.sdk.name, telemetry.sdk.version (auto)
device.model, os.name, os.version
app.release (same string as Sentry/Crashlytics release)
9. Cost Control
- Spans are the most expensive telemetry. Cap spans per transaction to a reasonable number (~50) and use span events for finer granularity.
- Use
SpanLimits to cap attribute count (32), attribute length (1 KB), and events (128).
- Drop spans shorter than 1 ms in chatty instrumentations (this is usually wall-clock noise).
10. Verification
- A manual golden flow (launch -> search -> checkout) produces one trace that spans client + backend with the same
trace_id.
- In the vendor UI the trace waterfall shows both client and backend spans, ordered correctly.
Checklist