Crashes in TestFlight or Play but the codebase is AI built or unfamiliar? Reproduce reliably, read the top crash clusters, fix release-vs-debug gaps first, then stop the bleeding before you rewrite anything.

Published June 2026 by Batteries Included

You do not need to understand every file to make progress. Reproduce the crash reliably, read the top crash clusters, fix release-vs-debug gaps first, then stop the bleeding before you consider rewriting anything.

When this applies

  • App built with Cursor, Lovable, Bolt, Replit, or similar AI tools
  • Inherited repo from a contractor or co-founder you no longer have access to
  • “Works in debug / simulator, crashes in TestFlight or release”
  • Every AI fix introduces a new crash

If you are still on the pre-beta side of things (trying to harden the code before crashes happen), that is the demo-to-production hardening article. This article is for when crashes are already happening and you need to stop them.

Release-only vs debug-only crashes

This is the first question to answer because it changes where you look.

Release builds go through code optimization, minification (ProGuard/R8 on Android), and different environment config. Debug builds do not. A crash that only shows up in TestFlight or Play internal testing is often caused by one of three things:

  1. Optimization stripping code: a class or method referenced by reflection or dynamic dispatch gets removed.
  2. Missing or wrong environment config: the release build points at a different API URL, has a missing key, or skips initialization code that the debug flavor ran.
  3. Unhandled code paths: the happy path worked in the simulator, but release users hit edge cases the AI-generated code never exercised.

Our store release checklist covers release build mechanics, signing, and R8/ProGuard mapping files in detail. Come back to it when you need store-specific help; here we focus on finding and fixing the crashes themselves.

Step 1: Reproduce on purpose

“It crashed once” is not useful. A reproducible crash is a fixable crash.

  • Match the exact build variant. TestFlight crashes need a release build, not a debug build from Xcode.
  • Match the device and OS version from the report. A crash on iOS 16 on a real device may not appear in the iOS 18 simulator.
  • Write down the minimum steps: open app → log in → tap X → crash. If you cannot write it down, you have not reproduced it yet.
  • If the crash is intermittent, try different accounts, network conditions (throttled, offline mid-flow), and fresh installs. Background-to-foreground transitions and low-memory states are common triggers in AI-generated apps that never tested those paths.

A local release build on a physical device is usually the fastest way to reproduce a TestFlight crash without waiting for TestFlight distribution.

Step 2: Collect the right signals

iOS

  • Xcode Crashes Organizer: for TestFlight and App Store builds, open Xcode → Window → Organizer → Crashes. Xcode symbolically translates memory addresses into function names and line numbers automatically if you have the matching archive and dSYM files. Apple's symbolication guide explains what to do when symbolication is incomplete.
  • TestFlight feedback: testers can submit screenshots and comments; check App Store Connect under your build.
  • Device logs: for crashes on a connected device, Xcode → Window → Devices and Simulators → View Device Logs.

Keep every Xcode archive you distribute. Deleting archives means you lose the dSYM files needed to read crash stacks.

Android

  • Play Console → Android vitals → Crashes and ANRs: the platform groups crashes into clusters by stack trace and shows affected user counts. Google's Android vitals documentation explains the metrics. The key one for store discoverability is user-perceived crash rate, the percentage of daily active users who experienced at least one crash while the app was in the foreground. Google defines a bad-behavior threshold at 1.09% overall and 8% per device model; exceeding it reduces your app's visibility in Play search.
  • Upload your mapping file: if you use ProGuard or R8 (you probably do in release), Android vitals shows obfuscated stack traces until you upload the mapping.txt from your release build. Play Console → App → Android vitals → Upload deobfuscation file, or configure automatic upload in your build.
  • Logcat: for crashes you can reproduce locally, adb logcat captures the full stack at crash time, unobfuscated.

Third-party crash reporters (optional)

If the app already has Firebase Crashlytics or Sentry integrated, use it; they group crashes by stack, track affected user counts over time, and let you mark issues resolved. If nothing is integrated yet, the platform tools above are enough to start triage. Do not spend time adding a crash reporter before you have fixed the top clusters.

What any useful crash report needs: stack trace, app version, OS version, device model, and (ideally) breadcrumbs or logs for the last few actions before the crash.

Step 3: Rank crashes: fix order, not fix count

You will not fix everything. Rank by impact, then fix in order.

Fix first:

  • Crashes on your core flow (signup, login, checkout, or whatever the app's one job is). These block every user.
  • Crashes that appear across many users with the same stack trace (Play Console clusters them; Crashlytics groups by signature). One root cause, many affected users: high leverage.
  • Regressions: crashes that appeared after a specific AI commit or prompt session. Check git log and compare to your last known stable build.

Fix later (or not at all):

  • Edge screens rarely reached in normal use.
  • One-off crashes on obscure device/OS combinations with no reproducible stack.
  • Crashes from users on very old OS versions below your stated minimum.

User-perceived crash rate on Android is what Google measures for store visibility, so it is a good proxy for prioritization: crashes while the user is actively in the app count more than background crashes.

Step 4: Triage AI-generated code without reading all of it

The goal is to locate the broken module, not audit the whole codebase.

Use git bisect if you have history. If you know a build from two weeks ago was stable and today's is not, git bisect can binary-search commits to find the one that introduced the crash. This works even in code you did not write.

Search for the pattern in the stack, not the cause. Take the top frame from the crash stack and search the codebase for that function name or file. Ignore everything else for now.

Look for copy-pasted patterns in high-risk areas. AI tools generate repetitive code. Auth flows, API clients, and error handlers are often duplicated across screens with slight variations, and bugs in one copy often appear in all of them. A crash in one screen's auth check probably means the same check is broken on every other screen using the same pattern.

Find “touch once, break twice” modules. If the same module appears in every crash stack (a networking layer, a session manager, a shared ViewModel), that module is the boundary to fix first. It is also the boundary to freeze: once it is working, mark it hands-off in your next AI session.

Decide patch vs bounded rewrite. For a broken 40-line function, patch it. For an auth module that is fundamentally wrong and appears in every crash, a bounded rewrite of that module alone is often faster than patching individual call sites. Keep the rewrite scoped: one module, clear interface, testable.

Stop the prompt-and-patch spiral. The most common failure pattern in inherited AI codebases: reproduce a crash → ask AI to fix it → new crash → repeat. Before running another prompt, reproduce the crash, confirm you understand the stack, and write a minimal test or manual repro step that fails. Fix to make that repro pass. Then run the prompt if needed. This forces each fix to be verifiable.

Step 5: Know when you are stable enough

You are not aiming for zero crashes; you are aiming for stable enough to ship and iterate.

Stable enough means:

  • Core flow (login → main action → result) runs crash-free on a release build on a physical device.
  • No P0 clusters (crashes affecting a large share of your daily users) in Android vitals or your crash reporter.
  • TestFlight or Play internal testing feedback is not about crashes on the main path.

Once you are there, the next work is hardening (tests, types, boundaries) so the next AI session does not re-introduce what you just fixed. That is the demo-to-production hardening article. After hardening, the store release checklist covers signing, privacy manifests, and upload mechanics.

When self-serve is enough vs when to get help

Self-serve works if:

  • You can reproduce the crash locally on a release build.
  • The stack trace points at code you can find and read, even if you did not write it.
  • Single platform, limited crash clusters, and each fix does not create two new ones.

Get help if:

  • Release-only crashes you cannot reproduce locally; the build environment itself may be the issue.
  • Unsymbolicated stacks you cannot decode, or missing dSYMs you cannot recover.
  • Auth or data bugs where you cannot tell if user A can see user B's data.
  • The triage list grows faster than the fix list; every session produces more crashes than it closes.

The two-to-three day MVP Unblock Triage on Vibe coded app rescue is designed for exactly this: we go through the crash clusters, identify root causes, and hand you a prioritized fix list, either to execute yourself or continue with us through Launch Rescue (two to four weeks, hands-on through TestFlight or Play).

Ready to stop the crash spiral?

Tell us the platform, the crash patterns, and whether you can reproduce them locally. We will tell you if MVP Unblock Triage (2–3 days) or Launch Rescue fits, or point you back to the self-serve guides below.

Sources