DOCS · SYMBOLICATION

Make stack frames readable.

Three upload paths — JavaScript source maps, Android ProGuard mappings, iOS dSYMs. All three resolve to the same source-of-truth: a row in source_maps keyed by (project, release, filename or UUID).

JavaScript source maps

For React Native, the bundler emits a main.jsbundle.map alongside the bundle.

# Build the release bundle (RN ≥ 0.73)
npx react-native bundle \
  --platform ios \
  --dev false \
  --entry-file index.js \
  --bundle-output main.jsbundle \
  --sourcemap-output main.jsbundle.map

# Upload
curl -X POST 'https://app.tillpulse.io/api/projects/<id>/source-maps' \
  -H 'Authorization: Bearer tp_sk_...' \
  -F 'file=@main.jsbundle.map' \
  -F 'release=1.4.0' \
  -F 'filename=main.jsbundle.map' \
  -F 'symbolication_type=js_source_map'

Symbolication runs once per issue group on first creation. Frames are cached in an LRU keyed by release. The dashboard shows a small symbolicatedbadge on resolved frames.

Android ProGuard / R8

The ProGuard mapping file is at build/outputs/mapping/release/mapping.txt:

curl -X POST 'https://app.tillpulse.io/api/projects/<id>/source-maps' \
  -H 'Authorization: Bearer tp_sk_...' \
  -F 'file=@app/build/outputs/mapping/release/mapping.txt' \
  -F 'release=1.4.0' \
  -F 'filename=mapping.txt' \
  -F 'symbolication_type=proguard'

The parser is pure-JS (no JVM). When a frame's function looks Android-shaped (short class.method tokens), the dispatcher routes through ProGuard; otherwise through SourceMapConsumer. Line-range entries are picked first; first-entry fallback otherwise.

iOS dSYM

Upload the inner DWARF Mach-O — not the .dSYM bundle:

# Inside the bundle
# YourApp.app.dSYM/Contents/Resources/DWARF/YourApp

curl -X POST 'https://app.tillpulse.io/api/projects/<id>/dsyms' \
  -H 'Authorization: Bearer tp_sk_...' \
  -F 'file=@YourApp.app.dSYM/Contents/Resources/DWARF/YourApp' \
  -F 'release=1.4.0' \
  -F 'filename=YourApp'

TillPulse parses the Mach-O LC_UUID load command (thin and fat binaries supported) and inserts one row per slice in source_maps.binary_uuid. Crash images carry their image UUIDs; the resolver matches incoming frames to the right slice.

Address-to-symbol resolution for dSYM currently requires a Mac-side helper (atos / dwarfdump) — the dSYM is stored and indexed, and frames render in the dashboard with the binary slice annotated. Full in-cluster lookup is on the roadmap.

CLI

For CI pipelines, use the tillpulse CLI (binary in the apps/cli workspace). Login once with tillpulse login; the token is stored at ~/.tillpulse/config.json mode 0600.

tillpulse login
tillpulse releases create --project pay --version 1.4.0 --env production
tillpulse source-maps upload --project pay --release 1.4.0 main.jsbundle.map
tillpulse proguard upload --project pay --release 1.4.0 mapping.txt
tillpulse dsym upload --project pay --release 1.4.0 YourApp

Xcode build phase (recommended)

Add a Run Script phase after Archive:

if [ -n "$DWARF_DSYM_FOLDER_PATH" ]; then
  for path in "$DWARF_DSYM_FOLDER_PATH"/*.dSYM; do
    name=$(basename "$path" .app.dSYM)
    tillpulse dsym upload \
      --project "$TILLPULSE_PROJECT" \
      --release "$MARKETING_VERSION" \
      "$path/Contents/Resources/DWARF/$name"
  done
fi

GitHub Actions (Android)

- name: Upload ProGuard mapping
  if: success()
  run: |
    npm i -g @tillpulse/cli
    tillpulse proguard upload \
      --project "$\{\{ vars.TILLPULSE_PROJECT }}" \
      --release "$\{\{ github.sha }}" \
      app/build/outputs/mapping/release/mapping.txt
  env:
    TILLPULSE_TOKEN: $\{\{ secrets.TILLPULSE_TOKEN }}

Listing & deleting

GET    /api/projects/:id/source-maps
GET    /api/projects/:id/dsyms
DELETE /api/projects/:id/source-maps/:mapId

Storage

On a self-hosted ingest API, source maps are stored in SOURCEMAPS_DIR(defaults to /tmp/tillpulse-sourcemaps). Cloudflare Workers cannot write the local FS — for production on Cloudflare, point SOURCEMAPS_DIRat a mounted R2 bucket via the dashboard binding (R2 adapter on the roadmap).