epok

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: hello@getepok.dev.

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:

bash
# 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.

swift
// 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.

kotlin
// 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

  1. Run the app (simulator/emulator is fine) and trigger an instrumented operation.
  2. Open Traces in the app — your span appears under the service.name you set.
  3. 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.