Skip to content

Repository files navigation

RN-OTel Symbolication Middleware

Sits between a React Native OpenTelemetry client and OpenObserve: ingests OTLP/JSON, symbolicates minified JS stack traces using uploaded source maps, and forwards the enriched telemetry downstream.

[React Native App] --OTLP/JSON--> [Bun Middleware] --OTLP/JSON--> [OpenObserve]
                                        ^
                            (source maps) |
                                   [CI/CD Pipeline]

How it works

  1. On release, CI calls POST /api/upload-url (OIDC-authenticated) for a presigned URL, then PUTs the Metro source map straight to object storage as [app]/[platform]/[version].map. The map bytes never transit the middleware.
  2. The RN app points its OTLP exporter here instead of OpenObserve, attaching service.name, app.version, and os.platform resource attributes plus its own OpenObserve Authorization header.
  3. On ingest, the service loads the matching source map (LRU-cached), rewrites each exception.stacktrace frame to original source coordinates, and forwards the payload — passing the client's Authorization header straight through.

Telemetry is never dropped: if a map is missing or symbolication fails, the original payload is forwarded unchanged.

Authentication

  • Upload-URL requests use asymmetric OIDC/JWT: the middleware verifies the bearer token against the issuer's JWKS and checks audience + repository/owner allowlists before returning a short-lived presigned PUT URL. Nothing secret is stored server-side. At least one allowlist is required or the service refuses to start. Defaults target GitHub Actions; override OIDC_* for any issuer.
  • Ingestion forwards the client's own Authorization header downstream, so the middleware never holds OpenObserve credentials.

Setup

bun install
cp .env.example .env   # then edit values
bun run dev            # hot-reload, or: bun run start
Env var Purpose
PORT Listen port (default 4318).
CACHE_MAX Max parsed maps kept in memory (default 32).
CACHE_TTL Cached map lifetime before re-read, seconds (default 300).
UPLOAD_URL_TTL Presigned upload URL lifetime, seconds (default 300).
S3_BUCKET Source-map bucket. Required — the service is stateless.
S3_PREFIX Key prefix (default maps/).
S3_REGION / S3_ENDPOINT Region and (for MinIO/R2/Spaces) custom endpoint.
S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY S3 credentials (fall back to AWS env).
OIDC_ISSUER Token issuer (default GitHub Actions).
OIDC_JWKS_URI JWKS URL (default ${issuer}/.well-known/jwks).
OIDC_AUDIENCE Expected aud claim (default rn-otel-map-upload).
OIDC_ALLOWED_REPOSITORIES / OIDC_ALLOWED_OWNERS / OIDC_ALLOWED_SUBJECTS Upload allowlists (comma-sep). At least one required.
OPENOBSERVE_URL Downstream base URL, e.g. .../api/default. Required.

API

  • POST /api/upload-url (CI/CD only) — OIDC-authenticated. Headers x-app-name, x-app-version, x-app-platform (ios/android). Returns a presigned PUT URL; CI then PUTs the .map bytes directly to it. From GitHub Actions, use the bundled .github/actions/upload-map action (see .github/workflows/upload-map.example.yml).
  • POST /v1/logs · POST /v1/traces — public OTLP/JSON ingestion. Returns 202 with { "symbolicated": <n> }.
  • GET /health — liveness probe.

Test & deploy

bun test
docker build -t rn-otel -f Containerfile .
docker run -p 4318:4318 --env-file .env rn-otel

Pushes to main build and publish a multi-arch (amd64/arm64) image to ghcr.io via .github/workflows/trunk.yml.

About

Symbolication middleware for Open Telemetry events from React Native apps.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages