# Upload dSYMs

> Name crash stacks in Tapll by uploading each build’s dSYMs from Xcode Cloud, fastlane or an Xcode build phase.

Source: https://tapll.com/docs/dsyms · Updated 2026-10-23

Crash stacks arrive as addresses. Upload each build’s dSYMs from CI and Tapll names every frame of your own code — in the stack, in the issue title, in alerts.

## What an upload does

The Tapll SDK sends each frame as a binary UUID and an offset. With the dSYM for that UUID, Tapll shows the function instead: `CheckoutViewModel.submit() + 44` rather than `MyApp +0x223f4`. Issues that already exist are renamed when their dSYM arrives, so an upload that lands a day after the crash still helps.

An upload **does not merge issues across builds**. Tapll groups a crash by where it happened in one build, and that stays true with names — so an issue still covers one build, and a rebuilt app with the same bug opens a new one. Function names today; file and line come later from the same upload, without asking you to upload again.

## Get a server key

In your project, open **Settings → Server keys** and create a key (`sk_…`). Store it in your CI’s secret store as `TAPLL_SERVER_KEY`.

> **The app’s client key is refused**
> The key in your app build (`av_…`) ships inside every copy of the app. Accepting it here would let anyone who unpacked your IPA rename your stack traces, so uploads need a server key.

## Xcode Cloud

Add `ci_scripts/ci_post_xcodebuild.sh` next to your project and make it executable (`chmod +x`). Add TAPLL_SERVER_KEY to the workflow as a secret environment variable. Runs after every archive action.

```sh
#!/bin/sh
set -e
if [ "$CI_XCODEBUILD_ACTION" = "archive" ]; then
  curl -fsSL https://tapll.com/dsyms/v3.sh -o "/tmp/tapll-dsyms.sh" \
    && echo "45588f5ee591290d86e5e82a02f56e47c7256a66f303580e13e2f4caa137af8d  /tmp/tapll-dsyms.sh" | shasum -a 256 -c - \
    && bash "/tmp/tapll-dsyms.sh" "$CI_ARCHIVE_PATH"
fi
```

## fastlane

After build_app (gym), which leaves the zipped dSYMs at DSYM_OUTPUT_PATH. Set TAPLL_SERVER_KEY in your CI secrets.

```ruby
lane :release do
  build_app(scheme: "MyApp")
  sh(<<~SH)
    curl -fsSL https://tapll.com/dsyms/v3.sh -o "/tmp/tapll-dsyms.sh" \
      && echo "45588f5ee591290d86e5e82a02f56e47c7256a66f303580e13e2f4caa137af8d  /tmp/tapll-dsyms.sh" | shasum -a 256 -c - \
      && bash "/tmp/tapll-dsyms.sh" "#{lane_context[SharedValues::DSYM_OUTPUT_PATH]}"
  SH
  upload_to_testflight
end
```

## Xcode build phase

Add a **Run Script** phase as the last phase of the app target, with `${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}` as an input file. Release builds only. Turn off User Script Sandboxing for the target (the script reads the dSYM and uses the network), and provide TAPLL_SERVER_KEY from the environment — never paste it into the project.

```sh
[ "$CONFIGURATION" = "Release" ] || exit 0
curl -fsSL https://tapll.com/dsyms/v3.sh -o "$DERIVED_FILE_DIR/tapll-dsyms.sh" \
  && echo "45588f5ee591290d86e5e82a02f56e47c7256a66f303580e13e2f4caa137af8d  $DERIVED_FILE_DIR/tapll-dsyms.sh" | shasum -a 256 -c - \
  && bash "$DERIVED_FILE_DIR/tapll-dsyms.sh" "$DWARF_DSYM_FOLDER_PATH"
```

## By hand

For a build that shipped before you set this up, run the script on the archive yourself. Xcode keeps archives in **Window → Organizer**; right-click one and choose Show in Finder.

```sh
TAPLL_SERVER_KEY=sk_your_server_key \
  bash v2.sh ~/Library/Developer/Xcode/Archives/2026-09-28/MyApp.xcarchive
```

Or let the script find it. **Settings → Debug symbols → Missing dSYMs** lists the binary UUIDs your crashes are in that no upload names yet, and gives one command carrying all of them. With `--uuid` and no path, the script looks each one up through Spotlight, then in Xcode’s Archives and DerivedData, uploads what it finds and names what it could not. With a path, it uploads only the dSYMs in it that hold those UUIDs.

```sh
TAPLL_SERVER_KEY=sk_your_server_key \
  bash v2.sh --uuid 6474C08F92D03410BFAA668957E3F700
```

> **A lost dSYM cannot be rebuilt**
> Only the dSYM of the exact build that shipped matches. Rebuilding the same commit produces new UUIDs, so a dSYM that is gone cannot be recreated — crashes in that build keep their addresses.

## The script

[https://tapll.com/dsyms/v3.sh](https://tapll.com/dsyms/v3.sh) runs on the Mac that built your app. For each binary in each dSYM it reads the function table with Apple’s own tools (`dwarfdump`, `nm`, `swift-demangle`), uploads it, and archives the dSYM itself. Binaries Tapll already has are skipped, so running it on every build costs one request per binary.

The file is versioned: `v2.sh` never changes behaviour, so what runs in your pipeline is what you reviewed (`v1.sh` stays published for pipelines that pinned it; v2 adds `--uuid` and is otherwise the same). Every snippet above checks it against its SHA-256 before running it:

```text
45588f5ee591290d86e5e82a02f56e47c7256a66f303580e13e2f4caa137af8d
```

| Option | What it does |
| --- | --- |
| `--uuid` | Upload only the dSYMs holding this binary UUID (repeatable). Without a path, find them on this Mac. |
| `--no-raw` | Upload the function tables only, not the dSYM itself. File and line will not be available for these builds later. |
| `--strict` | Exit non-zero when an upload fails. Without it, a failure is a warning and never fails your build. |
| `--dry-run` | Extract and write the tables to the current folder without uploading. Needs no key. |

## Check it worked

**Settings → Debug symbols** lists every binary Tapll has, with its UUID. A crash is named only when its binary carries exactly that UUID — a dSYM from a different build of the same version will not match, and the crash page says which UUID it is waiting for.
