Crash Reporting and Analytics Built Into Your App Backend
Crash reporting tells you which bugs your users hit, how often, in which release, and whether your fix worked. Analytics tells you what they do when nothing breaks. Most teams add two more vendors for this. StitchDB has both built into the backend: the same SDK that makes your app's API calls captures crashes and events, links them to the signed-in user, and stores them in your project's own database. Because the SDK sees the API traffic, every crash report shows the request that failed just before it, which is often the actual cause.
This guide sets up both for web, Flutter, Android and iOS, and covers grouping, regressions, source maps, privacy and limits.
#Why put this in the backend
A separate crash tool sees the stack trace. It does not see that, two seconds earlier, GET /rest/v1/orders returned 403 with code 42501 because a policy refused the row, and the screen then dereferenced undefined. With the reporting in the SDK that made that call, the report carries it:
| Part of a report | What it holds |
|---|---|
| Exception | Error type, message and stack, read into frames; your code is told apart from libraries |
| Breadcrumbs | The last 50 events: every API call (method, path, query parameter names, status, error code, duration), page changes on the web, and your own |
| The API call before the crash | The last failed call in the two minutes before the crash, otherwise the last call |
| Release, environment, device | What you passed to start(), plus OS, version, model and locale |
| User | The signed-in user, taken from their token |
The other reason is data location. Reports go to the _stitch_crashes schema and analytics events to _stitch_analytics, both in your project's database in the EU, not in a third-party service. You can query them with SQL and delete them like any other data.
#Set up crash reporting
Open Studio › Crashes and click Turn on crash reporting. Then, with the SDK installed, call crashes.start() once when the app starts, with your app version as the release. Nothing is captured before start(), so if you ask for consent, call it after the user agrees.
#Web and Node.js
import { createClient } from 'stitchdb-js'
const stitchdb = createClient(STITCHDB_URL, STITCHDB_ANON_KEY)
stitchdb.crashes.start({
release: '1.4.0', // your app's version
environment: 'production',
beforeSend: (report) => report, // change a report, or return null to drop it
})#Flutter
final crashes = Stitch.instance.client.crashes;
await crashes.start(release: '1.4.0', environment: 'production');
// FlutterError.onError and PlatformDispatcher.instance.onError are reported for you
// (the handlers you set before keep working)#Android and iOS
stitchdb.crashes.start(context, release = BuildConfig.VERSION_NAME, environment = "production")
// uncaught exceptions on any thread are written to disk and sent on the next startstitchdb.crashes.start(release: "1.4.0", environment: "production")
// uncaught NSExceptions are written to disk and sent on the next startOn mobile, a crash ends the process, so the report is written to disk and sent on the next start.
#Report handled errors
Not every bug crashes. Report the errors you catch, with context and, when the automatic grouping is wrong, your own fingerprint:
try {
await checkout(cart)
} catch (e) {
stitchdb.crashes.captureException(e, { context: { cart: cart.id }, fingerprint: ['checkout'] })
showError()
}
stitchdb.crashes.captureMessage('Payment sheet closed without a result')
stitchdb.crashes.addBreadcrumb({ type: 'ui', message: 'tap Checkout' })
await stitchdb.crashes.flush() // send now, such as before a serverless function returns#Issues, regressions and crash-free users
Reports of the same bug are grouped into one issue. Without your own fingerprint, grouping uses the error type and the three topmost frames of your own code, by function and file, without line numbers or build hashes. That is what keeps the same bug in the next build in the same issue, instead of opening a new one per deploy. Reports without a stack group by type and message, with numbers, ids and e-mail addresses taken out.
Three actions shape the workflow:
- Resolve an issue once the fix ships. If it happens again in a release that is new since then, it reopens as a regression and alerts. Events from the old release do not reopen it, so users who have not updated yet do not create noise.
- Ignore an issue to stop its alerts while still counting it.
- Crash-free users per release shows the share of installs that opened the app that day and did not crash. The SDK sends one small session a day per install and release for this.
New issues and regressions alert the project owner by e-mail, go to the project's webhooks as crash.issue.new and crash.issue.regressed, and can go to a Telegram chat. Only crashes alert by default; handled errors can be turned on. Alerts are capped at 20 per project an hour.
#A release gate in CI
The issues API makes a simple check after a staged rollout possible:
curl 'https://<ref>.p.stitchdb.com/crashes/v1/issues?status=open&release=1.4.0' \
-H "apikey: $SERVICE_KEY" -H "Authorization: Bearer $SERVICE_KEY"
# also: GET /crashes/v1/issues/<id>, PATCH /crashes/v1/issues/<id> {"status":"resolved"}, GET /crashes/v1/releasesFail the pipeline, or pause a feature flag rollout, when a new release has open crash issues.
#Source maps for JavaScript
Minified stacks point at app.3f9c2a1b.js:1:48213, which is useless. Upload the source maps of each release, and reports show the original file, line, column and function, with a few lines of source when the map has sourcesContent:
# the CLI 2.2 or newer
npm install -g https://stitchdb.com/cli/stitchdb.tgz
# after your build, in CI or by hand (the release is the one you pass to crashes.start)
stitchdb sourcemaps upload dist --release 1.4.0
stitchdb sourcemaps list --release 1.4.0
stitchdb sourcemaps delete --release 1.3.0Turn on source maps in your bundler (build.sourcemap: true, devtool: 'source-map' or similar). You do not need to publish the .map files on your site; they stay in your project. Upload them before the release goes out: stacks are mapped when a report arrives, before grouping, so reports that arrive first stay in the issue they were grouped into.
One gap to know before you choose: Android R8 mapping files, iOS dSYMs and native crashes (signals, the Android NDK) are not supported yet. Kotlin, Java and Dart stacks are read, but obfuscated Android builds will show obfuscated names, and crashes in native code are not captured. If your app depends on native libraries, a dedicated mobile crash tool may still be the better fit for now.
#Add product analytics
Open Studio › Analytics and click Turn on analytics. Then start it and track what matters:
stitchdb.analytics.start({ release: '1.4.0' }) // page views ($pageview) are tracked for you on the web
stitchdb.analytics.track('purchase', { plan: 'pro', amount: 29 })final analytics = Stitch.instance.client.analytics;
await analytics.start(release: '1.4.0');
analytics.track('purchase', {'plan': 'pro', 'amount': 29});Events are linked to the signed-in user. Before sign-in the SDK uses an anonymous id for the device, and after sign-in what the user did before is merged into them, so a funnel from landing page to purchase stays connected. Events are sent in batches in the background and kept while offline. Names are up to 64 characters; properties are a JSON object of up to 8 KB, and flat values are easiest to break down.
Studio then shows:
| View | What it answers |
|---|---|
| Active users | DAU, WAU, MAU, DAU/MAU and new users |
| Top events | What people do most, and how many users do it |
| Event over time | Trends by day, week or month, broken down by release, platform, country, OS or a property |
| Funnel | 2 to 6 steps in order, with the median time to each |
| Retention | Weekly cohorts, up to 12 weeks |
| One user | A user's latest events, by id |
With crash reporting on, crash-free users appear next to active users, so you can see whether a release that lost users also crashed more.
Because the events are in Postgres, you are not limited to the built-in views:
select name, count(*) events, count(distinct uid) users
from _stitch_analytics.events where at > now() - interval '7 days'
group by name order by events desc;Join them with your own tables to answer questions a separate analytics tool cannot, such as which features paying customers use. Server-side events, such as a renewal, can be sent with the service key and a user_id.
#Privacy by default
- Values of fields named like passwords, tokens, secrets, keys, cookies and authorization are replaced with
[scrubbed], and e-mail addresses and JWTs are removed from messages and URLs. Add your own field names in the settings. analytics.optOut()deletes the user's events and drops new ones;optIn()undoes it.- Apps cannot report or track as another user; the user comes from the token. Only the service key may name one.
- Nothing is collected before
start(), which makes consent flows straightforward.
#Limits per plan
| Plan | Crash reports / month | Kept | Source maps | Analytics events / month | Kept |
|---|---|---|---|---|---|
| Free | 5,000 | 7 days | 50 MB | 50,000 | 30 days |
| Pro | 100,000 | 30 days | 1 GB | 1,000,000 | 90 days |
| Scale | 1,000,000 | 90 days | 5 GB | 10,000,000 | 365 days |
| Enterprise | 10,000,000 | 90 days | 10 GB | 100,000,000 | 365 days |
Values from the plans API as of November 2026 (pricing). Over the crash limit, new reports still count on their issue but are not kept until the next month. Over the analytics limit, tracking answers 429 and the SDKs keep up to 1,000 events on the device until the next month. Daily active users and event counts are kept 400 days regardless. See crash reporting and analytics.
#FAQ
#Can I use this alongside Sentry or Firebase Crashlytics?
Yes. They are independent; you can run both during a trial and compare. Be aware that two crash handlers on mobile can interfere, so test the setup.
#Does analytics slow down my app?
Events are batched and sent in the background, up to 100 at a time, every 10 seconds or when 20 are waiting. On the web they are also sent when a page is hidden.
#Is there crash reporting for React Native?
The docs list web, Node.js, Flutter, Android and iOS. The JavaScript client runs in React Native, but crash capture there is not documented; see the React Native guide for what is.
#Can I gate a rollout on crash rates?
Combine the issues API with feature flags: roll out to 10%, check open issues for the new release, then raise the rollout.
Find out why your app crashed, with the API call that caused it: create a free project.