# Reproduce a bug that only happens on first launch

> A crash or blank screen on a fresh install that never recurs comes from empty first-run state that vanishes the instant you relaunch and the app writes it.

*Answering the search: "reproduce a bug that only happens on first launch"*

## Evidence

Two grounding screenshots from the run timeline, before and after the event: the populated screen comes back empty.

A bug that fires only on the very first launch is a code path that assumed state which does not exist yet: an empty cache, a missing keychain token, a one-time migration, or an unanswered permission prompt. It vanishes the moment you launch again, and the empty-to-populated write that triggered it is already gone, so the only way to observe it is a captured record of the clean boot itself.

## It breaks once, then works on every launch after

You install a fresh build and the app crashes on the splash screen, lands on a blank view, or skips a step in onboarding. You relaunch to grab a screenshot and everything is fine. Deleting the app and reinstalling reproduces it on some tries and not others. Attach a debugger and it refuses to fire, because the run you are inspecting is no longer a first launch.

## First launch runs against state that is not there yet

Four mechanisms cover most of these.

Empty defaults and caches. On a clean install, UserDefaults returns nil, false, or 0 for every key you have never set, the caches directory is empty, and the Core Data store has zero rows. A guard let that assumed a seeded value takes its failing branch, or a force-unwrap on the first row of an empty result crashes, only when there is nothing there.

Missing keychain entry. Your auth layer reads a token from the keychain and the item does not exist yet. Keychain items live outside the app sandbox and survive app deletion on iOS, so "delete and reinstall" does not give you a clean auth state. That is exactly why deleting and reinstalling from Xcode leaves the old token in place and fails to reproduce it, even though it hits every first-time user.

A migration that runs once. Core Data lightweight migration executes on the first launch after a model version bump, and hand-rolled migrations are usually gated behind a schemaVersion flag in UserDefaults. If the migration is wrong, it runs, sets the flag, and never runs again. The window to observe it is a single launch.

The permission prompt race. On first run, authorization status is .notDetermined, so requestAuthorization presents the system modal and calls back asynchronously on a queue you do not control. Code that reads authorizationStatus synchronously, or updates UI before the completion handler returns, behaves differently only on the one launch where the prompt appears. (React Native and Expo repeat the same shapes: AsyncStorage and expo-secure-store are empty on first run, SecureStore persists across reinstall like the keychain, and Expo permission modules carry the identical first-run callback timing.)

## How you'd catch it

You read the boot from the outside, off the artifacts the run leaves behind. The video is the full replay, so it shows exactly what a first-time user sees: a crash frame on the splash, a blank view where content should be, an onboarding step that gets skipped, or the system permission alert appearing mid-flow. The timeline is the step-by-step agent trajectory, and every step carries a grounding screenshot plus the action it took, so you get the screen right before and right after the step that ran against empty state and stop guessing which interaction broke.

The device logs carry the failure itself. A force-unwrap on a nil keychain read or a missing UserDefaults value lands here as a crash stack, a runtime error the app printed shows up color-coded by severity, and if the process was killed the termination is marked on the performance chart. For the permission race the video shows the system modal sliding up while the timeline screenshots show the view mutating a frame before the callback would have returned, which is the ordering bug made visible without reading a single variable. If the empty state was a missing token, the network waterfall shows the first-launch request firing without auth and coming back rejected, so the request table tells you the token was not there yet. Nothing here reaches into your app's memory or its stored files; it infers the empty-state cause from what the run visibly did.

Your own machine stops short here. A full simctl erase and re-provision does return the simulator to a clean state, so you can make the crash fire again. But that clean state is your simulator's, on whatever model and iOS release you happen to have checked out. Permission prompts and system behavior shift between OS versions, so if a user hit this on a model or release you are not running, a local erase reproduces nothing at all. And you can break at launch and step through the empty state, but a stepping session is not a record you can replay or send, and stepping through an async first-run callback perturbs the very timing you are trying to catch. Seeing the failure reliably means recording the boot as it runs and reading the report once the run is over.

## Reproduce it on a cloud device that boots clean

Revyl boots the device fresh, installs your build, and records the whole run, so the point is running that boot on the model and iOS release where the bug was actually reported rather than the one on your desk. Aim it at that target, install the build, and drive the first-launch flow. Because each run starts from a fresh boot and a clean install, the flow meets the empty state a first-time user gets, with no manual erase between attempts, and the boot is captured as video, device logs, and a step-by-step timeline you can scrub back through. Running it this way gives you the same fresh start every time, recorded so the one-shot failure outlives the run that produced it. Vault, a SwiftUI Revolut-style banking app, is a good worked example. Its onboarding walks account signup through KYC identity verification before it opens the account, so the first-run empty state that trips these bugs is exactly the trajectory you exercise from a clean install. Poke it live to feel the crash, then replay the same path as an agent test and read the report: the video of the empty-state screen, the device logs with the crash, and the timeline screenshots on either side of the step that failed.

```bash
revyl auth login
revyl build --platform ios
revyl device start --platform ios                 # poke the clean first-launch flow live
revyl test run vault-onboarding --build-id <id>   # replay it as an agent test with a full report
```

## Technical sources

- [Apple: UserDefaults](https://developer.apple.com/documentation/foundation/userdefaults)
- [Apple: Migrating your data model automatically](https://developer.apple.com/documentation/coredata/migrating-your-data-model-automatically)
- [Apple: Keychain Services](https://developer.apple.com/documentation/security/keychain-services)

## Common questions

**Why does my app only crash on the first launch after install?**

First launch runs against state that does not exist yet: UserDefaults returns nil for keys you never set, the caches directory is empty, the keychain has no token, and any one-time migration has not run. A guard or force-unwrap that assumed a seeded value takes the failing branch only when there is nothing there. Run the app once and the state is written, so it never happens again.

**How do I reproduce a first-launch bug after I've already opened the app?**

You need a clean state on every attempt, because after one run the container is populated and the failing branch is gone. A full simctl erase resets your simulator so the crash fires again, but you have to erase and re-provision before every try, on whatever model and OS you happen to have checked out. A cloud device that boots fresh and installs your build each run gives you a first launch every time on the model and OS the user hit, and records the run as video, device logs, and a step-by-step timeline of screenshots.

**Why can't I inspect a first-launch bug on my own simulator?**

You can make it fire with a full simctl erase and re-provision before every attempt, and you can even break at launch and step through the empty state. Two things still get in the way. Permission prompts and system behavior differ across iOS versions, so if a user hit it on a model or release you are not running, a local erase reproduces nothing. And a stepping session is not a record you can replay or send, while stepping perturbs the timing of any async first-run callback you are chasing. Recording the clean boot on the model and release the user actually hit, as video, device logs, and step-level timeline screenshots, is what shows the failure.

---

Reproduce it on a cloud device with Revyl: https://revyl.com/use-cases/bug-only-on-first-launch/ · https://app.revyl.ai/signup
