Instrument your mobile app
Your iOS or Android app can ship telemetry to Epok today with the standard OpenTelemetry SDKs — opentelemetry-swift and opentelemetry-android. Point them at Epok's OTLP endpoints and you get hand-instrumented spans, error events, and custom metrics from the app, in the same views as your backend — and when a mobile request hits your instrumented backend, the two sides join into one trace.
Time to first span: ~15 min · Trial: 14 days, no card · API key: app.getepok.dev → Settings → API Keys
What this is — and isn't
This recipe uses the community OpenTelemetry SDKs, not a native Epok mobile SDK (there isn't one yet). You get everything you instrument by hand: spans, error events, custom metrics, and backend trace correlation. What you do not get today:
- No automatic crash or ANR capture — an uncaught crash that kills the process is not reported unless you record it yourself.
- No dSYM / ProGuard symbolication — stack traces from release builds arrive obfuscated and stay that way.
- No mobile session replay.
Native mobile SDK, crash reporting, symbolication, and mobile replay are on the roadmap. If mobile is your primary platform, tell us — it moves the priority: [email protected].
The two endpoints
Mobile SDKs configure exporters in code rather than via environment variables. Both signals go to Epok's ingest over OTLP/HTTP with your API key as a header:
# Traces (spans, error events)
https://ingest.getepok.dev/v1/traces
# Metrics (counters, histograms, gauges)
https://ingest.getepok.dev/v1/metrics
# Auth — send on every request:
x-api-key: YOUR_API_KEY
# Protocol: OTLP over HTTP (http/protobuf). There is no gRPC (:4317) endpoint —
# a default gRPC exporter will silently fail.iOS (Swift)
Add opentelemetry-swift via Swift Package Manager. One-time exporter setup, then instrument the operations you care about. The SDK's APIs shift between releases — pin a version and check its examples if a symbol has moved.
// Package.swift / Xcode: add https://github.com/open-telemetry/opentelemetry-swift
// products: OpenTelemetryApi, OpenTelemetrySdk, OpenTelemetryProtocolExporterHttp
import OpenTelemetryApi
import OpenTelemetrySdk
import OpenTelemetryProtocolExporterHttp
// ── One-time setup (e.g. in AppDelegate) ────────────────────────
let headers = [("x-api-key", "YOUR_API_KEY")]
let resource = Resource(attributes: [
"service.name": AttributeValue.string("my-ios-app"),
"app.version": AttributeValue.string("2.4.1"),
])
let traceExporter = OtlpHttpTraceExporter(
endpoint: URL(string: "https://ingest.getepok.dev/v1/traces")!,
config: OtlpConfiguration(headers: headers)
)
OpenTelemetry.registerTracerProvider(tracerProvider:
TracerProviderBuilder()
.add(spanProcessor: BatchSpanProcessor(spanExporter: traceExporter))
.with(resource: resource)
.build()
)
// Metrics: see "App metrics from mobile" below — the mobile OTLP metric
// exporters emit protobuf, which Epok's direct metrics endpoint doesn't
// accept; route metrics via a small collector instead.
// ── A hand-instrumented operation ───────────────────────────────
let tracer = OpenTelemetry.instance.tracerProvider
.get(instrumentationName: "checkout", instrumentationVersion: "1.0")
let span = tracer.spanBuilder(spanName: "checkout.submit").startSpan()
span.setAttribute(key: "cart.items", value: 3)
do {
try submitOrder()
} catch {
// error event → shows up as an error span in Epok
span.addEvent(name: "exception", attributes: [
"exception.type": AttributeValue.string(String(describing: type(of: error))),
"exception.message": AttributeValue.string(error.localizedDescription),
])
span.status = .error(description: error.localizedDescription)
}
span.end()
// ── A custom metric ─────────────────────────────────────────────
let meter = OpenTelemetry.instance.meterProvider
.get(instrumentationName: "checkout", instrumentationVersion: "1.0")
var attempts = meter.createIntCounter(name: "checkout_attempts_total")
attempts.add(value: 1, labels: ["result": "ok"])Optional: URLSessionInstrumentation(in the same package) auto-creates client spans for network calls and propagates trace context — that's what powers the backend correlation below.
Android (Kotlin)
Use opentelemetry-android (wraps the Java SDK with app-lifecycle awareness) plus the OTLP exporter. Same pattern: exporters once, then hand-instrumented spans, error events, and metrics. Pin versions — the RUM builder API is still evolving.
// build.gradle.kts
// implementation("io.opentelemetry.android:core:<latest>")
// implementation("io.opentelemetry:opentelemetry-exporter-otlp:<latest>")
// ── One-time setup (Application.onCreate) ───────────────────────
val otelRum = OpenTelemetryRum.builder(this, OtelRumConfig())
.addSpanExporterCustomizer {
OtlpHttpSpanExporter.builder()
.setEndpoint("https://ingest.getepok.dev/v1/traces")
.addHeader("x-api-key", "YOUR_API_KEY")
.build()
}
// Metrics: see "App metrics from mobile" below — the mobile OTLP metric
// exporters emit protobuf, which Epok's direct metrics endpoint doesn't
// accept; route metrics via a small collector instead.
.build()
// ── A hand-instrumented operation ───────────────────────────────
val tracer = otelRum.openTelemetry.getTracer("checkout")
val span = tracer.spanBuilder("checkout.submit").startSpan()
span.setAttribute("cart.items", 3L)
try {
span.makeCurrent().use { submitOrder() }
} catch (e: Exception) {
// error event → shows up as an error span in Epok
span.recordException(e)
span.setStatus(StatusCode.ERROR, e.message ?: "checkout failed")
throw e
} finally {
span.end()
}
// ── A custom metric ─────────────────────────────────────────────
val meter = otelRum.openTelemetry.getMeter("checkout")
val attempts = meter.counterBuilder("checkout_attempts_total").build()
attempts.add(1, Attributes.of(AttributeKey.stringKey("result"), "ok"))Optional: the project's OkHttp instrumentation auto-creates client spans and injects trace context into outgoing requests.
The payoff: one trace, app to backend
When your app's HTTP client is instrumented (URLSession / OkHttp above), it injects the W3C traceparent header into every request. If your backend is instrumented for tracing, its spans carry the same trace_id— so "checkout is slow on the app" opens as a single trace: the tap in the app, the network call, and every backend span it caused, in one tree. A mobile error event and the backend exception it triggered land in the same place instead of two disconnected tools.
Verify it's working
- Run the app (simulator/emulator is fine) and trigger an instrumented operation.
- Open Traces in the app — your span appears under the
service.nameyou set. - Open Explore — hand-instrumented error spans appear alongside your backend traces (shared trace IDs correlate them automatically).
Seeing no data? The usual causes: a gRPC exporter pointed at the HTTP endpoint (use the OTLP HTTP exporter classes shown above); a missing or wrong x-api-keyheader; the batch processor not flushing before the app is killed (foreground the app for ~30s, or call the SDK's force-flush in testing); or no service.name resource attribute (data arrives but groups under unknown_service).
App metrics from mobile
The mobile OTel SDKs' HTTP metric exporters emit OTLP protobuf, and Epok's direct /v1/metrics endpoint accepts OTLP JSON only — pointing those exporters at it returns a 400. Traces are unaffected (the trace endpoint accepts protobuf, which is why the snippets above work as shown).
To ship app metrics, run a small OpenTelemetry Collector your apps export to, with its otlphttp exporter set to encoding: json and pointed at https://ingest.getepok.dev — or start with traces and errors only; they cover most mobile debugging and correlate with your backend out of the box.