Your app is on the App Store. You check the reports: zero crashes. You tell yourself everything is fine, your app is perfect!
But does that mean it never crashes… or simply that you cannot see the crashes?
Imagine it crashes three times a day on your users’ devices. You see nothing, so you fix nothing. And the person using it will not necessarily send you an email. They try again, it crashes again, and eventually they uninstall it.
Today, we will look at how to collect this information with Sentry, integrate it into a real app, and, above all, learn to read a report to understand what crashed and where to look in your code.

When crashes happen on a user's device, the developer needs visibility.
How does that work?

When your app crashes during development, you have Xcode right in front of you. You know what you just did, you can look at where things went wrong, and you can try to reproduce the problem.
But once your app is on someone else’s device, you are no longer there to see what happens.
They might tell you, “It crashes when I open this page.” OK. But which app version? On which iPhone? And, above all, what crashed in the code? Because when you open the same page on your own device, everything works.
What we need, then, is evidence we can use to investigate, even when we were not there when the crash happened.\
That is where Sentry comes in. I already use it in Keepio, but today we will add it together to Caffio, another of my iOS apps.
The goal is not just to install it and say, “Well, that’s done.” We will trigger a test crash, see what gets reported, and learn to read the report. Having an error in a dashboard is one thing. Understanding what it tells us so we can fix it is what matters.
What a crash report gives you
A crash report provides evidence from a process you were not watching: the failure, its stack trace, the app build, and device context. Depending on the configuration, breadcrumbs can also show some of the actions leading up to it.
An event is one occurrence. An issue groups related events. Ten events do not necessarily mean ten different bugs, and an event count is not the same thing as a count of affected people. Sentry’s guide to capturing events
That distinction matters when deciding what to fix first. A launch crash affecting new users can be more urgent than a larger number of development-only errors.
Reporting also has limits. The SDK must be initialized, the event must be captured and delivered, and the project must accept it. An empty dashboard is not proof that every user had a crash-free experience.
Create a project for the app
In Sentry, create a project for your app (mine's gonna be Caffio) and select iOS as the platform. Choose the team that should have access and configure the notifications you want to receive.
1. Open the project list
Open Settings → Projects, or use your organization’s menu to reach Projects.

From the project list, choose Create Project.

2. Choose the platform and project settings
Connecting a repository is optional for this setup.

Select iOS as the platform.

The products panel describes the available features. In these screenshots, error monitoring is marked Always on; this panel does not require selecting a checkbox.

Name the project with your app name and choose the team that should have access.

Configure alerts and their delivery channel. The source walkthrough uses High Priority Only and Email; choose the settings that suit your project, then create it.

3. Open manual configuration and find the DSN
Sentry offers a setup wizard. For this walkthrough, open Manual Configuration so we can add the SDK and configure it ourselves.

The exact onboarding labels can change. The important outputs are the project and its DSN, which tells the SDK where to send events. Copy the DSN from your project’s setup instructions or Client Keys settings rather than constructing it yourself.

Keep the distinction between capture and notification in mind: an event appearing in Sentry and an email being sent are separate steps. Alert rules can filter or delay notifications.
We will use a DSN in the app and an authentication token for uploading debug symbols. They serve different purposes; the upload token belongs in your developer or CI environment, not in the application binary.
Okay, now that we have our project, let's get back to Xcode.
Add the Apple SDK through Swift Package Manager
In Xcode, open File → Add Package Dependencies.

Enter the official package URL:
https://github.com/getsentry/sentry-cocoa.gitAdd the Sentry product to the main application target. Adding it to the app does not automatically instrument a widget or another extension.

Check the SDK’s compatibility with your deployment target before selecting a version. Sentry’s manual setup currently documents iOS 15 or later for Cocoa SDK 9.x, and SDK 8.x for iOS 11–14. Manual setup
Initialize it early
My app initializer creates a SwiftData ModelContainer. If the cloud-backed configuration fails, it attempts a local configuration. If both attempts fail, the current code calls fatalError.
That is a useful reminder about initialization order: starting crash reporting when the first view appears would miss the opportunity to observe failures earlier in startup.
The following is a partial initializer. Add it at the beginning of the existing app initialization and preserve the rest of your setup:
import Sentry
init() {
SentrySDK.start { options in
options.dsn = "YOUR_PROJECT_DSN"
options.sendDefaultPii = false
#if DEBUG
options.environment = "development"
options.debug = true
#else
options.environment = "production"
#endif
}
// Continue the existing notification and ModelContainer setup here.
}Initialize on the main thread as early as practical. In a SwiftUI app, Sentry documents using the App initializer. Its debug option prints SDK diagnostics; it does not switch crash reporting on. Initialization guidance
The environment names above are a simple starting point. A Release build can also be a staging or TestFlight build, so use an explicit build configuration if those need separate environments.
Performance tracing and session replay are separate features. This guide focuses on crash reporting, so it does not enable them as part of the setup.
Give Sentry the matching debug symbols
Compiled code does not give you the source-level view you expect in Xcode. Debug symbols allow a crash address to be mapped back to a function and, when available, a file and line. This process is called symbolication.
The binary and its companion dSYM must have matching build UUIDs. The same app version number is not enough. The main executable, frameworks, and extensions can each have their own symbols. Apple’s debugging information guide
In the target’s Build Settings, check Debug Information Format. For the configuration you are testing or distributing, use DWARF with dSYM File if you need a separate dSYM to upload. Debug normally places symbols in the binary; Release normally produces the companion file.
Your app target: Build Settings → Debug Information Format

Add the temporary crash button from the next section before building, so its code belongs to the binary whose symbols you upload.
For a development build, use Product → Build, find the app in Products, and choose Show in Finder. Locate its companion dSYM in the build products. For a distribution build, retain the Xcode archive and its dSYMs.
Install the symbol-upload utility and authenticate:
brew install getsentry/tools/sentry-cli
sentry-cli loginThese commands use sentry-cli, the debug-file upload utility, rather than the separate sentry developer CLI. Installation, authentication
Upload the symbols for that exact build:
sentry-cli debug-files upload \
--org YOUR_ORGANIZATION_SLUG \
--project YOUR_PROJECT\
"/path/to/YOUR_APP.app.dSYM"Use the actual organization and project slugs. Check the command result and the project’s Debug Files settings. Do not assume that generating a dSYM also uploaded it. Sentry’s Apple symbol-upload guide
If a report stays partially symbolicated, compare the required UUID with the dSYM you uploaded:
dwarfdump --uuid "/path/to/YOUR_APP.app.dSYM"Apple explains how to locate and compare the corresponding binary UUID in its missing-symbol guide.
For ongoing releases, automate symbol uploads in CI or your build workflow and retain the archives. Rebuilding the app can produce a different binary and matching dSYM pair.
Verify capture with a deliberate crash
Adding the package and getting a successful build are not evidence that a crash reaches the dashboard.
Temporarily place this button in a development-only screen that is reachable after launch:
#if DEBUG
Button("Test crash reporting") {
SentrySDK.crash()
}
#endifThe screen also needs the usual SwiftUI and Sentry imports. Keep the crash behind a deliberate action. Putting it in the app initializer creates a launch crash loop and can prevent the next launch from delivering the saved report.
To test:
Build and install the app with the temporary button included.
Stop the Xcode run session.
Open the installed app from the device’s home screen, without the debugger attached.
Tap the button and let the app terminate.
Reopen the app so the SDK can send the saved crash report.
Check the correct project and environment in Sentry and allow time for processing.
Sentry specifically requires testing crashes without an attached debugger. A captured message or handled error can test event delivery, but it does not prove that the crash handler works. Verification guidance
Remove the temporary button when finished. It has no place in a shipping build.
Read the report as an investigation
Start with the event’s context: the release and build, environment, device, operating system, and when it happened. Confirm that you are inspecting your test rather than an unrelated event.
Then look at the failure and crashed thread. Find the first relevant frame in your own code and follow the callers around it. A system frame at the top does not automatically mean the operating system caused the bug.
Use breadcrumbs as supporting context when available. They are a partial trail of recorded activity, not a complete recording of the user’s session.
Finally, look across events in the issue. Is it limited to a release or OS version? Does it affect a particular path? The stack trace helps locate the failure; the surrounding evidence helps formulate a reproduction.
If the report is unreadable, fix symbolication first. Guessing from hexadecimal addresses is a poor substitute for supplying the matching symbols.
Use an agent to help with the investigation
Once an issue exists, an agent can help gather the evidence and inspect the relevant code. Sentry’s hosted MCP server supports OAuth and can be scoped to a project:
<https://mcp.sentry.dev/mcp/YOUR_ORGANIZATION_SLUG/YOUR_APP>Add it through your client’s supported MCP settings and complete authentication. Follow the current client instructions rather than guessing an endpoint from the app’s DSN region. Official Sentry MCP setup
For example, ask the agent:
Inspect this <your_app> issue. Identify the release, failure, and relevant application frames. Compare the report with the source code, propose a reproduction, and explain the smallest correction. Separate observed facts from hypotheses.
A Notion connection can help record the issue URL, reproduction, diagnosis, fix, and validation. Access to Sentry alone does not give the agent access to Notion (and that's only an advice I like to give, since it's my workflow!)
Review the proposed change and test the reproduction. Resolving a ticket or issue changes its tracking state; it does not deploy a corrected app to users.
Keep crash context useful and limited
A crash report should help diagnose a failure without becoming a copy of a your users data.
Disabling default PII collection is a starting point, not a guarantee that custom breadcrumbs or error messages contain no sensitive data. Review what you attach. Sentry supports beforeSend and beforeBreadcrumb hooks for filtering event and breadcrumb data. Data-scrubbing guidance
Useful context can often be expressed as a state such as “empty summary” rather than raw records. If you want a reliable affected-user count, define an appropriate identification strategy; do not invent a user count from the number of events.
Make the workflow repeatable
For each release, keep the chain intact: initialize reporting early, identify the environment and build, upload matching symbols, and verify the event path. When an issue arrives, use its evidence to reproduce, fix, test, and follow the result after the updated app is released.
Sentry does not correct the bug for you. It gives you a better starting point than “it crashed on someone else’s phone.”
That is the part I want from crash reporting: enough context to turn a vague symptom into a concrete investigation.
That's it for now, have fun debugging!



