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 YourAppXcode 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
fiGitHub 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/:mapIdStorage
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).