Guide · System design
Frontend Error Monitoring: Catching Crashes on Millions of Phones
26 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- Frontend error monitoring means catching errors where they happen, inside the app on users' phones and browsers, and sending them to your servers. Server logs never see a crash that happens before a request leaves the device.
- An SDK (a small ready-made library inside your app) catches each error and saves it as a report. On a phone, a crash report is usually sent on the next launch.
- Release builds usually shrink names, so a report's stack trace (the list of functions that were running) is unreadable. A decoding file saved per release fixes it: a mapping file on Android, a source map on the web.
- A fingerprint, a short code built from the error type and your own code's top functions, groups millions of reports into a few issues, one per bug.
- Beginners can stop after the section on how reports reach your servers. Engineers then get tested material: a source map decode with expected output, a grouping function, caps for report floods and Android's published crash-rate threshold.
What goes wrong when you cannot see your users' crashes?
You learn about crashes from one-star reviews instead of from your own tools. A crash on a phone happens inside the app, often before any request leaves the device. So your server logs look clean while users fail to pay.
Picture a common setup. A shopping app sells handloom sarees and towels, with about 30 lakh (3 million) Android installs and a mobile website. The setup is made up, but the problem is real.
Two days into a year-end sale, the store rating starts to fall. The new reviews all say the same thing: the app closes when they tap Pay. The team tries the payment flow on four test phones, and it works every time.
The server logs show nothing unusual, because the crash happens before the app sends the payment request. The team cannot answer four basic questions.
- Which phones crash, and on which Android versions?
- Which line of code fails?
- Is it one bug, or five bugs that look alike?
- Did yesterday's release cause it?
After four days, someone finds the cause by luck on an old phone with little memory. This guide builds the system that would have shown it within minutes.
What is frontend error monitoring?
Frontend error monitoring is a system that catches errors inside apps and web pages on users' devices, sends them to a central service and groups them. Engineers then fix the bugs that hurt the most users first.
It has two halves. The client half is an SDK, short for software development kit: ready-made code you add to your app. The server half receives reports, decodes them, groups them, stores them and raises alerts.
Four words come up again and again, so here they are once.
- Stack trace: the list of functions that were running when the error happened, newest first. Each line is a frame, naming one function, file and line.
- Error report, often called an event: one record of one failure on one device, with the stack trace, app version, phone model and recent user actions.
- Issue: a group of error reports that share one cause. If 40,000 phones hit the same bug, you see one issue with a count of 40,000.
- Release: one version of your app or website, such as 5.12.0. Every report carries its release name.
Think of a water purifier company whose technicians visit thousands of homes. Each technician writes a slip with the fault code from the panel and the purifier model. Head office counts slips per fault code and spots the code that jumped after a new filter shipped.
Map it part by part. Each home is a phone, each slip is an error report, each fault code is an issue, and head office is the server half.
Where the picture breaks: a purifier shows its fault code on the panel. An app never names its own bug, so the server has to compute the code from the stack trace.
How does an app catch errors on a user's phone or browser?
An app catches errors by registering a last-chance handler with the platform, which runs when an error escapes all other code. The SDK also records errors the app catches itself, and watches for freezes.
On Android
Android's documentation says an app crashes on an unexpected exit caused by an unhandled exception or signal. Java and Kotlin code (the two main languages for Android apps) crashes on an exception nothing catches. C and C++ code crashes on a signal such as SIGSEGV, a bad memory access.
Java lets a program set a default uncaught exception handler with Thread.setDefaultUncaughtExceptionHandler. It runs only for threads that have no handler of their own. A careful SDK saves the handler that was already there, puts itself in that slot, and writes its report first.
Then it calls the old handler, so the app still closes the normal way. We ran this chain on plain Java 21: the SDK's handler ran, then the old one. A thread with its own handler skipped both.
A freeze is different. Android raises an ANR, short for Application Not Responding, when the main thread (the UI thread that draws the screen and handles taps) is blocked too long. One documented trigger: the app has not responded to a tap or key press within 5 seconds.
In a browser
In a browser, the window error event fires when a script throws an error that nothing catches. Its handler gets the message, the script's address, the line, the column and the error object. Errors thrown inside timer callbacks reach it too.
A promise is a JavaScript object for work that finishes later, like a network call. When a promise fails and nothing handles that failure, the browser fires a separate event, unhandledrejection, instead of the error event.
That includes an error thrown inside an async function (a function that waits on promises) and a failed fetch call (the browser's network request) with no catch. An SDK must listen to both events, or it misses every promise failure nobody handled.
You can check this yourself. Open a new blank tab, open the browser's developer console, and paste these four lines. Chrome blocks pasting for people new to its console: type allow pasting, press Enter, then paste again.
addEventListener("error", (e) => console.log("caught:", e.message));
addEventListener("unhandledrejection", (e) => console.log("rejected:", e.reason.message));
setTimeout(() => { null.total; }, 0);
Promise.reject(new Error("payment API said 502"));The console also prints its own red error lines. Among them, in Chromium 141 (the open-source base of Chrome), you should see caught: Uncaught TypeError: Cannot read properties of null (reading 'total'). You should also see rejected: payment API said 502.
Notice one more thing: the page keeps running after the error. On the web, an uncaught error usually breaks one button or one feature, not the whole tab. That matters for how reports get sent, as the next section shows.
| Kind of error | What the user sees | How the SDK catches it |
|---|---|---|
| Uncaught exception (Android, Java or Kotlin) | The app closes | The default uncaught exception handler |
| Native crash (C or C++ code) | The app closes suddenly | A signal handler in native code |
| App not responding (freeze) | The screen stops reacting; Android offers to close the app | A watchdog: a background check that notices when the main thread stays stuck |
| Handled exception | Often nothing, or an error message | The app's own code calls the SDK |
| Uncaught script error (web) | A button or feature stops working | The window error event |
| Unhandled promise rejection (web) | A spinner that never stops | The unhandledrejection event |
A stack trace alone rarely explains a bug. So the SDK also keeps breadcrumbs: a short rolling list of recent events, such as screens opened, buttons tapped and network calls made. Sentry, a widely used error monitoring tool, keeps the last 100 by default in its JavaScript SDK.
We tested both halves of that advice in Chromium 141. With the attribute and the header, the full message, file, line and column came through. With the attribute but no header, the script did not load at all, so add the header first. MDN, the Mozilla-run web reference, adds that promise rejections from cross-origin scripts do not fire unhandledrejection at all.
How do error reports get from a phone to your servers?
On a phone, the SDK writes each crash report to the device's storage first and uploads it later, usually when the app next starts. A process that is crashing should not wait on a slow network call.
Sentry's Android SDK documents exactly this. It stores events on the device's disk before sending them, and events left unsent at shutdown go out on the next app start.
The web is different. The tab usually survives the error, so the SDK can send the report right away. The risky moment is the user leaving the page. MDN recommends calling navigator.sendBeacon when the page's visibilitychange event reports that the page is hidden. It sends a small request, up to 64 KiB, without delaying the page unload. MDN calls the unload, beforeunload and pagehide events unreliable for this.
Follow the arrows below from the crash to the deleted file. Notice that the report only leaves the phone after the app starts again.
One crash report's trip off the phone
Drag to exploreOn Android, the report is saved during the crash and uploaded on the next launch. The local file is deleted only after the server accepts it.
Diagram connections:
- App crashes on Pay to Handler saves report
- Handler saves report to User opens app again
- User opens app again to Upload saved reports
- Upload saved reports to Server accepted it?
- Server accepted it? to Delete local file (Yes)
- Server accepted it? to Keep file, retry later (No)
- Keep file, retry later to User opens app again (Next launch)
- App crashes on Pay: Uncaught exception in the payment screen
- Handler saves report: Small file in app storage, then the app exits
- User opens app again: Minutes or days later
- Upload saved reports: Batched and compressed, sent in background
- Server accepted it?: The SDK checks the reply code
- Delete local file: Report is safe on the server
- Keep file, retry later: Waits longer each time; drops after 7 days
A few rules protect the user's battery, storage and data pack. The exact numbers below are the team's own choices, not a standard.
- Cap the local store, for example at 50 reports or 2 MB, and drop the oldest when it fills up.
- Batch several reports into one compressed request instead of one request each.
- Retry with growing waits plus a little randomness, so a million phones do not all retry at the same second.
- Delete reports older than a week. A month-old crash from an old version rarely helps.
One bug needs special care: the crash loop, where the app crashes at every start. If the upload runs a few seconds after start, it never gets the chance, and the worst bug sends no reports at all.
Some SDKs handle this by treating early crashes as launch crashes. One iOS SDK, BugSnag, counts crashes in the first 5 seconds after start. On the next start it blocks for up to 2 seconds to send that report first.
How do you turn an unreadable stack trace back into real code?
You keep a decoding file for every release, and the server uses it to turn shortened names back into real ones. On Android that file is the mapping file. On the web it is the source map.
Android: R8 and mapping.txt
Release builds are usually shrunk to make the download smaller. On Android you turn this on in the release build script with isMinifyEnabled = true. As of 19 October 2026, Android Gradle plugin (the Android build tool) 9.3 and later use an optimization block instead, and Android recommends always turning it on. The R8 tool then renames classes and methods to short names, so a class such as CheckoutPresenter may become a.b.a in the app users install. That is good for size and bad for reading crashes.
The build writes a file called mapping.txt that lists each original name and its short name. Android's documentation warns that the mapping file is overwritten every time you build. So save a copy for every release you publish.
To decode a trace by hand, Android's retrace command-line tool takes the mapping file and a file holding the stack trace. As of 19 October 2026, the docs add that Logcat, Android Studio's log viewer, decodes traces automatically from Android Studio Otter 3 Feature Drop with Android Gradle plugin 9.0.
Web: minifiers and source maps
Web code is shrunk too, by a tool called a minifier. The shop's website ships one long line of code with names like n and t. A source map is a JSON (structured text) file that links positions in that shrunk code back to the original file, line and column.
Source maps are now a formal standard, ECMA-426, whose first edition Ecma International published in December 2024. Its mappings field packs the positions into a compact text code called Base64 VLQ.
To decode a position, a tool finds the last mapping at or before that spot on the line and returns its original location. We checked this rule in the source-map library: a column between two mappings resolved to the earlier one.
On the server, this step is called symbolication or deobfuscation. A worker reads the report's release, fetches the matching mapping file or source map, and rewrites every frame. The team then sees CheckoutPresenter.onPayTapped instead of a.b.a.
Build the release
The build creates the app and its mapping file, or the website and its source maps, all tagged with one release name, such as 5.12.0.
Upload the decoding files
The release pipeline uploads those files to the monitoring service before the release goes out, so the first crash can already be decoded.
Tag every report
The SDK adds the release name to each report, so the server knows which decoding file to use.
Decode on arrival
A worker loads the file for that release, rewrites each frame, and caches the parsed file, because thousands of reports need the same one.
Try it: decode a minified stack trace yourself
This small file has a bug: it reads cart.items without checking that the cart has items. Save it as checkout.js in an empty folder.
function cartTotal(cart) {
let total = 0;
for (const item of cart.items) {
total += item.price * item.qty;
}
return total;
}
function onPayTapped(cart) {
const amount = cartTotal(cart);
return "Paying " + amount;
}
module.exports = { onPayTapped };Now save this decoder as decode.mjs in the same folder. It takes one line:column position from a stack trace and prints the original file, line, column and name.
import fs from "node:fs";
import { SourceMapConsumer } from "source-map";
const map = JSON.parse(fs.readFileSync("checkout.min.js.map", "utf8"));
const [line, col] = process.argv[2].split(":").map(Number);
await SourceMapConsumer.with(map, null, (consumer) => {
// Stack traces count columns from 1; this library counts from 0
const pos = consumer.originalPositionFor({ line, column: col - 1 });
console.log(pos.source + ":" + pos.line + ":" + (pos.column + 1), pos.name);
});Run these five commands in that folder on Node 22. npm, Node's package installer, adds terser (a popular minifier) and source-map (a library that reads source maps). Then npx runs terser to minify the file, and node crashes it once and decodes two stack positions.
npm install [email protected] [email protected]
npx terser checkout.js --mangle toplevel --source-map "url=checkout.min.js.map" -o checkout.min.js
node -e 'require("./checkout.min.js").onPayTapped({})'
node decode.mjs 1:40
node decode.mjs 1:95The third command crashes on purpose and prints TypeError: n.items is not iterable, at n and at Object.t, with positions 1:40 and 1:95. The two functions are now called n and t, and everything sits on line 1.
With terser 5.51.2 and source-map 0.8.0 on Node 22, you get checkout.js:3:27 items, then checkout.js:10:18 cartTotal. Line 3, column 27 is exactly where cart.items is read.
What just happened? The file, line and column came back exactly. The name did not: it is the name of the code at that spot, items, not the function the frame was in.
Naming the function each frame was in needs a separate step, such as finding which function's code contains that position. Check that your decoder does it.
How are millions of error reports grouped into a few issues?
Reports are grouped by a fingerprint: a short code computed from the parts of a report that identify its cause. Reports with the same fingerprint join the same issue, like slips sharing one fault code.
The hard part is choosing what goes into the fingerprint. Put in too much and one bug splits into thousands of issues. Put in too little and different bugs merge into one.
- Use the error type, like NullPointerException. It rarely changes for the same bug.
- Use the top few frames from your own code, after decoding. Frames from libraries add noise.
- Use function names, not line numbers. Lines shift whenever someone edits the file above.
- Leave out the error message. Messages often carry changing values, such as order IDs or prices.
Sentry documents a similar order as of 19 October 2026. A custom fingerprint wins first, then the stack trace using only your application's frames, then the exception type and value, then the message.
Here is a small fingerprint function in Python. Each report is a dictionary that has already been decoded. Every frame has a module (the package and class) and a function name, and the frames list starts with the newest frame.
import hashlib
APP_PREFIX = "com.example.shop."
def fingerprint(report):
frames = report["frames"] # newest frame first
own = [f for f in frames if f["module"].startswith(APP_PREFIX)]
top = (own or frames)[:5]
parts = [report["error_type"]]
parts += [f["module"] + "." + f["function"] for f in top]
key = "|".join(parts)
return hashlib.sha256(key.encode()).hexdigest()[:16]The own list keeps only the app's frames, so a crash deep inside an Android library still groups by the app code that called it. If no frame belongs to the app, the expression own or frames falls back to all frames.
What just happened? We ran it on Python 3.13 with six sample reports. A new message and new line numbers gave the same 16-character fingerprint. So did a different library frame on top.
A different error type gave a new fingerprint, as it should. Order matters too: the same two functions in a different order gave a new fingerprint, because that is a different call path.
Look at the diagram below and notice where the alert fires: only when an issue is new.
From raw report to issue
Drag to exploreDecode first, then fingerprint, then count. Only a fingerprint never seen before creates an issue and raises an alert.
Diagram connections:
- Raw report from queue to Decode the frames
- Decode the frames to Compute fingerprint
- Compute fingerprint to Fingerprint seen before?
- Fingerprint seen before? to Add to existing issue (Yes)
- Fingerprint seen before? to Create a new issue (No)
- Create a new issue to Alert the mobile team
- Raw report from queue: Shortened names, release 5.12.0
- Decode the frames: Uses mapping.txt for that release
- Compute fingerprint: Error type plus top app frames
- Fingerprint seen before?: Lookup in the issue table
- Add to existing issue: Count, last seen, affected users
- Create a new issue: First seen time and release
- Alert the mobile team: New crash in release 5.12.0
No fingerprint is perfect, so the dashboard should let a person merge two issues that are the same bug. After that, reports with either fingerprint count toward one issue.
What does the server side of an error monitoring system look like?
The server side has four parts. An ingest service accepts reports quickly, and a queue (a waiting line for reports) absorbs spikes. Workers decode and group, and separate stores keep issues and raw reports. The front door must stay fast when a bad release floods it.
Start with a rough size, using made-up numbers. Say 1 percent of the shop's 12 lakh (1.2 million) daily sessions hit some error. That is 12,000 reports on a normal day, and a bad release could multiply it by 50 within an hour.
Follow the diagram from the phones to the dashboard, and notice the queue in the middle. Ingest only checks the report and queues it, so slow decoding never makes phones wait.
The error monitoring backend
Drag to exploreFast ingest, a queue for spikes, then decoding and grouping off to the side. Volumes and retention are made up.
Diagram connections:
- Phones and browsers to Load balancer
- Load balancer to Ingest service
- Ingest service to Report queue
- Report queue to Processing workers
- Processing workers to Symbol store (Fetch decoding file)
- Processing workers to Raw report store (Save full report)
- Processing workers to Issue database (Update issue counts)
- Issue database to Dashboard and alerts
- Phones and browsers: About 12,000 reports on a normal day
- Load balancer: Spreads uploads across ingest servers
- Ingest service: Checks key, size and quota, then replies
- Report queue: Absorbs spikes after a bad release
- Processing workers: Scrub, decode, fingerprint, count
- Symbol store: Mapping files and source maps per release
- Raw report store: Full reports, kept 30 days
- Issue database: One row per fingerprint with counts
- Dashboard and alerts: What the mobile and web teams read
Why two stores? The issue database is small and busy: one row per fingerprint, updated with every report. Raw reports are large and rarely read, so cheap object storage (storage for files, read by name) suits them.
The SDK also needs a key to send reports, and that key ships inside the app where anyone can find it. Sentry's documentation says its key only allows sending new events, not reading data. The ingest service must still enforce size limits and quotas (caps on how many reports each app may send in a period).
How do you stop a crash storm from flooding the system?
You stop a crash storm with limits at every layer: sampling on the device, rate limits and quotas at ingest, and per-issue caps in the workers. A rate limit caps how many reports arrive per second. The goal is to keep enough reports to debug, not every single one.
Sampling means sending only a share of events. Sentry's JavaScript SDK, for example, has a sampleRate option for error events, from 0.0 to 1.0, with a default of 1.0, which sends everything.
The shop keeps every crash, because crashes are rare and serious. It sends only 10 percent of handled errors, because some screens log the same harmless error thousands of times. Remember to multiply those counts by 10 when you read them.
Workers add one more guard. For each issue, they store the first 100 full reports every minute and only count the rest. The count stays exact, and storage stays flat during a storm.
import time
from collections import defaultdict
FULL_PER_MINUTE = 100
seen = defaultdict(int) # (issue_id, minute) -> reports so far
def handle(issue_id, report, now=None):
now = time.time() if now is None else now
minute = int(now // 60)
seen[(issue_id, minute)] += 1
increment_issue_count(issue_id)
if seen[(issue_id, minute)] <= FULL_PER_MINUTE:
save_full_report(issue_id, report)The key (issue_id, minute) gives every issue a fresh budget each minute. The helpers increment_issue_count and save_full_report stand in for the database and object storage calls.
What just happened? We fed it 250 reports for one issue within a minute, using stand-in helpers. It counted 250 and saved 100. Thirty more reports the next minute were all saved, because the budget reset.
In production, the counters would live in a shared cache (fast in-memory storage that every worker can reach) with a short expiry, not in one worker's memory. Otherwise every worker keeps its own budget, and old minutes pile up forever.
What should an error report never contain?
An error report should never contain passwords, card details, OTPs (one-time passwords) or other personal data the team does not need to fix the bug. Reports travel far and stay long, so anything in them can leak widely.
The risk hides in breadcrumbs and messages. A breadcrumb may log an address the user typed. An error message may include a phone number from a failed lookup. So scrub in two places, and one missed rule does not leak data.
- On the device: drop form field values, and record only that a field changed, not what was typed.
- On the device: strip query strings (the part of an address after the question mark) from logged addresses, since they often carry tokens.
- On the server: replace anything shaped like a phone number, email or card number with a placeholder.
- Count affected users with an anonymous ID, not the user's name or phone number.
If a user turns off diagnostics in the app's settings, the SDK must stop sending reports from that moment.
How do you know if a new release made the app worse?
You compare crash numbers per release, mainly the crash-free users rate: the share of users who saw no crash in a time window. If the new version's rate falls below the old one's, the release is the likely cause.
Raw crash counts mislead, because a new version may simply have more users. Dividing by users fixes that. Sentry's release health page defines crash-free users this way, and Google Play's Android vitals uses a close cousin.
| Number | What it means | Published threshold |
|---|---|---|
| Crash-free users (Sentry) | Share of distinct users with no crash in the chosen time period | None; you pick a target |
| User-perceived crash rate (Android vitals) | Share of daily active users who had at least one crash while actively using the app | 1.09% overall, or 8% on a single device model, counts as bad behaviour |
| User-perceived ANR rate (Android vitals) | Share of daily active users who had at least one user-perceived ANR; only Input dispatching timed out ANRs count for now | 0.47% overall, or 8% on a single device model |
On Google Play, Android releases can go out in stages, to a percentage of users first. Google Play's help says that halting a staged rollout stops new users getting the version. Users who already have it stay on it, so the fix still has to ship.
Alert on change, not on every error. An alert for every single error trains people to ignore alerts.
- New issue: a fingerprint never seen before appears in the latest release.
- Regression: an issue marked fixed in 5.11.0 shows up again in 5.12.0.
- Spike: an old issue suddenly gets five times its usual hourly count.
Here is how the sale bug would look with this system. Within 20 minutes of a 5 percent rollout, a new issue appears: OutOfMemoryError in the code that loads saree photos on the payment screen.
Its reports come only from phones with 2 GB of memory or less. The team halts the rollout and fixes the image loading the same day. Those timings are made up, but each step uses a tool from this guide.
How do you explain frontend error monitoring in a system design interview?
Explain it as a pipeline from the device to a grouped issue, and give one reason for each stage. Keep the client half and the server half separate in your answer.
Ask about scale and platforms
Ask how many devices, which platforms, and what counts as an error. Crashes, freezes, handled errors and web errors need different capture.
Design the client SDK
Describe the last-chance handlers, breadcrumbs, saving to disk, and upload on next launch with batching, retries and launch-crash handling.
Design fast ingest
Show a load balancer (a server that spreads requests across servers), a thin ingest service that checks key, size and quota, and a queue that absorbs spikes.
Decode and group
Explain release-tagged mapping files and source maps, then a fingerprint from the error type and the top app frames.
Store and alert
Split a small issue database from cheap raw storage. Alert on new issues, regressions and spikes, and track crash-free users per release.
Cover limits and privacy
Mention sampling, per-issue caps, scrubbing on the device and the server, and an SDK that can never crash the app.
Questions people ask
- What is the difference between error monitoring and logging?
- Logging records events your own code chooses to write, usually on servers. Error monitoring catches failures automatically on users' devices, adds context such as the phone model and release, and groups them into issues. Server logs miss client crashes that happen before any request is sent.
- Why do crash reports arrive late?
- On phones, many SDKs save the report during the crash and send it on the next launch, because a dying app should not wait on the network. If the user never opens the app again, that report never arrives, so crash counts run a little low.
- What is a source map in error monitoring?
- A source map is a JSON file that links positions in shrunk website code back to the original file, line and column. It follows the ECMA-426 standard. The monitoring server uses it to make browser stack traces readable, so upload one for every release.
- Why can't I read my Android crash stack traces?
- Release builds are often shrunk by R8, which renames classes and methods to short names like a.b.a. Keep the mapping.txt file from each release build, because the next build overwrites it. Android's retrace tool, or your crash service, uses it to decode the trace.
- Why does my error tool only show Script error?
- The script that failed came from another domain without permission to share its error details. Add crossorigin="anonymous" to its script tag and make its server send an Access-Control-Allow-Origin header. Add the header first, or the script stops loading.
- Should I send every error from every user?
- No. Send every crash, because crashes are rare and serious, but sample noisy handled errors and scale their counts back up. On the server, keep exact counts while storing only a capped number of full reports per issue. That keeps cost flat during a crash storm.
Sources and further reading
- Crashes (definition, user-perceived crash rate, thresholds), Android developer documentation
- ANRs (UI thread blocked, 5-second input timeout), Android developer documentation
- ANRs in Android vitals (user-perceived ANR rate, thresholds), Android developer documentation
- Enable app optimization (isMinifyEnabled, optimization block from AGP 9.3), Android developer documentation
- Test and troubleshoot app optimization: mapping.txt and retrace, Android developer documentation
- Thread.setDefaultUncaughtExceptionHandler, Java SE 21 API documentation
- Window: error event, MDN Web Docs
- Window: unhandledrejection event, MDN Web Docs
- The script element, crossorigin attribute and error logging, MDN Web Docs
- Navigator: sendBeacon() method (64 KiB limit, send on visibilitychange), MDN Web Docs
- Self-XSS warning: pasting into the DevTools Console and typing allow pasting, browser developer blog
- ECMA-426 Source map format specification, 1st edition, Ecma International
- ECMA-426 current draft (mappings encoding and position lookup), TC39
- Event grouping order (fingerprint, stack trace, exception, message), error monitoring SDK documentation
- Shutdown and draining on Android (events stored on disk, sent on next start), error monitoring SDK documentation
- JavaScript configuration options (sampleRate, maxBreadcrumbs), error monitoring SDK documentation
- DSN explainer (keys only allow sending events), error monitoring SDK documentation
- Release health (crash-free users and sessions), error monitoring SDK documentation
- Identifying crashes at launch on iOS (5-second window, 2-second send), crash reporting SDK documentation
- Release app updates with staged rollouts (halting a rollout), app store publishing help