---
name: asc-crash-logs
category: asc
description: Pull TestFlight crash feedback and full crash logs via the App Store Connect API when Xcode Organizer can't. Use when the user says "pull the crash log", "TestFlight crash", "crash feedback", "get the .ips", or a beta tester reports a crash and you need the stack trace without a device.
invoke: "/asc-crash-logs [app-id] — List TestFlight crash feedback and fetch full crash logs via the ASC API."
---

# ASC Crash Logs

**Retrieve tester-reported crash feedback and fully-symbolicated crash logs straight from the App Store Connect API — no device, no Xcode Organizer, no waiting for the owner to AirDrop an `.ips` file.**

## When to Use

- A TestFlight tester (or the owner) reports a crash and you need the stack trace now.
- Xcode Organizer fails with "Xcode failed to locate iOS App with App Store Identifier …" — the Organizer only resolves apps it has a local archive/project association for, which breaks on shared or freshly-migrated checkouts. The API route bypasses that entirely.
- You want to confirm whether a new crash is a known signature or a regression across builds.

**Don't use** for crashes that never went through TestFlight (local debug runs) — those are on the device or in Console.

## The two-endpoint reality (verified 2026-08-18)

| Endpoint | Status |
|---|---|
| `GET /v1/apps/{appId}/betaFeedbackCrashSubmissions` | **Works.** Lists tester crash feedback (comment, build, device, OS, uptime). |
| `GET /v1/betaFeedbackCrashSubmissions/{id}/crashLog` | **Works.** Returns the full crash log as `attributes.logText`. |
| `GET /v1/betaFeedbackCrashSubmissions` (top level) | **403** — collection fetch is forbidden; you must go through the app relationship. |
| `GET /v1/builds/{id}/diagnosticSignatures?filter[diagnosticType]=CRASH` | **400** — the filter only accepts `DISK_WRITES`, `HANGS`, `LAUNCHES`. Aggregate crash signatures are NOT in the public API. |

## Recipe

### 1. Auth: ES256 JWT from the ASC API key

Use the bundled `asc_jwt.rb` (requires the `jwt` gem — present in any repo with a Fastlane `Gemfile`). The script reads three env vars; inject them however the current repo manages secrets:

```bash
# Aether family repos: Doppler project "apple-dev", config "dev"
TOKEN=$(doppler run --project apple-dev --config dev -- \
  ruby "$(dirname "$0")/asc_jwt.rb" 2>/dev/null | tail -1)
```

Required env vars: `APP_STORE_CONNECT_API_KEY_KEY_CONTENT` (the .p8 contents),
`APP_STORE_CONNECT_API_KEY_ISSUER_ID`, `APP_STORE_CONNECT_API_KEY_KEY_ID`.

### 2. List crash feedback for the app

```bash
curl -gsS -H "Authorization: Bearer $TOKEN" \
  "https://api.appstoreconnect.apple.com/v1/apps/APP_ID/betaFeedbackCrashSubmissions?limit=10"
```

**The `-g` is required** — query strings contain `[brackets]` that curl would otherwise treat as glob patterns.

Each entry gives you `id`, `createdDate`, the tester's `comment`, `deviceModel`, `osVersion`, and `appUptimeInMilliseconds`. There is no `filter[build]` on this endpoint — identify the crash you want by date/comment/build-comment correlation.

### 3. Fetch the crash log

```bash
curl -gsS -H "Authorization: Bearer $TOKEN" \
  "https://api.appstoreconnect.apple.com/v1/betaFeedbackCrashSubmissions/SUBMISSION_ID/crashLog" \
  | python3 -c "import json,sys; print(json.load(sys.stdin)['data']['attributes']['logText'])" \
  > /tmp/app-crash.ips
```

The result is a standard `.ips` crash report — same format as device exports, symbolicated against the uploaded build.

## Reading the log: signature → root-cause class

| Signature in the crashed thread | Root cause class |
|---|---|
| `EXC_BREAKPOINT (SIGTRAP)` + `_dispatch_assert_queue_fail` ← `_swift_task_checkIsolatedSwift` ← **your closure** ← framework callback frame | Swift 6 isolation trap: a closure formed in a `@MainActor` context inherited that isolation, and the framework invoked it on another thread/queue. See `swift6-concurrency` §10. The closure body touching only `Sendable` values does NOT save you — the isolation rides with the closure itself. |
| `EXC_CRASH (SIGABRT)` + `NSException` from `AVAudioInputNode` / `installTap` / `outputFormat` | AVAudioEngine setup-order bug: `outputFormat`/`installTap` before `prepare()`. Uncatchable from Swift — fix the order, don't wrap in `do/catch`. |
| `EXC_CRASH (SIGKILL)` + `Termination Reason: WATCHDOG` | Launch/hang timeout, not a code exception. |
| `EXC_RESOURCE` / `MEMORY` | Jetsam. Check memory growth, not logic. |

A crash "right after the user grants a permission" is usually NOT the permission dialog — it is the work that starts once permission arrives (audio engine start, analyzer start). Old knowledge base misdiagnosis note: this was repeatedly blamed on dialog timing until someone granted permissions manually in Settings and it still crashed.

## Fallbacks (when the API isn't enough)

1. **On the device:** Settings → Privacy & Security → Analytics & Improvements → Analytics Data → `<AppName>-YYYY-MM-DD-*.ips` → share to the Mac. Immediate, no sync delay.
2. **Xcode → Window → Organizer → Crashes** — works only when the local checkout has an archive/project association for the app; expect "failed to locate iOS App" on shared machines.
3. **Reproduce attached:** device plugged in, Xcode Devices window or Console.app streaming.

## Notes

- Crash feedback appears in the API quickly (the tester's device uploads on next launch with connectivity); the ASC web TestFlight → Feedback → Crashes page shows the same submissions.
- `DELETE /v1/betaFeedbackCrashSubmissions/{id}` is allowed if you want to clear handled feedback — confirm with the owner before deleting.
- App IDs worth keeping in repo docs: agents shouldn't have to rediscover them. (Aether family example: Dial = `6762118139`.)
