39 // journal

Crash reporting without a surveillance SDK: what each option actually collects

engineering · 2026-11 · 7 min read · engineering · privacy · mobile

journal/crash-reporting-surveillance-sdk
Crash reporting without a surveillance SDK: what each option actually collects

fig.01 · what leaves the device: the contents of a crash report and who signs for it

An app that crashes silently is an app that gets uninstalled. So the crash reporter is non-negotiable: someone has to receive the stack trace. The question is who receives it, what else rides along, and whose name is on the store declaration when it does. That declaration is yours either way. Google is explicit: "You alone are responsible for making complete and accurate declarations in your app's store listing on Google Play," including "data collected and handled through any third-party libraries or SDKs used in their apps."

The constraint

A crash report is a snapshot of a dying process. Google's own disclosure of what Crashlytics sends shows the shape: stack traces and app state at the moment of the crash, device metadata, and a "Crashlytics installation UUID" generated to count affected users. Firebase's privacy page itemizes further: every frame's instruction pointer, the device model, CPU architecture, RAM, whether the device is jailbroken or rooted, the binary images loaded in memory, and the Firebase Installation ID of the app instance.

None of that is sinister; a stack trace is what you asked for. The problem is the delivery vehicle. Crashlytics drags the Firebase installations SDK into your binary transitively, and Google's data-safety definitions name "Firebase installation ID" as a declarable "Device or other IDs" entry. So a crash reporter quietly buys you two declaration rows, "Crash logs" and "Device or other IDs," plus whatever custom keys and user IDs your team attaches. The constraint to hold: the SDK you ship collects under your signature, not the vendor's.

The options

Crashlytics. The default answer because it is genuinely good and free: Crashlytics is a no-cost Firebase product on both the Spark and Blaze plans. The price is not money, it is the data path. Reports go to Google, automatic collection is on by default for all users, and Apple's privacy details page is blunt about what that means: "even if you collect the data for reasons other than analytics or advertising, it still needs to be declared." You can flip to opt-in collection, but Google's own docs warn it reduces the accuracy of your crash-free metrics.

Sentry SaaS. Better tooling, real cost. Sentry is volume-priced: a free developer tier, then paid plans with an error quota and pay-as-you-go rates above it. The data path is the same shape as Crashlytics: your users' crash payloads land on Sentry's servers. Sentry does run server-side scrubbing by default, filtering values that look like credit cards and keys whose names contain "password," "secret," "api_key," "token," and friends. But scrubbing happens after the bytes leave the device, it is "generally not retroactive for events that have already reached Sentry," and the server still sees the connecting IP.

Sentry self-hosted. The only popular option where no crash byte leaves your infrastructure. The catch is honest: this is an operations commitment, not a download. Sentry's published minimums and CPU requirements are steep enough that they belong in the numbers section below, and the errors-only profile exists but is exactly that: Issues, Alerts, Integrations, Dashboards, Releases, and Errors. No traces, profiles, replays, or crons.

No SDK at all: MetricKit. On iOS, the OS itself is the collector. MetricKit is a first-party framework that aggregates performance and diagnostic data on-device, then hands your app a daily metrics payload plus diagnostic reports, crash reports included. Nothing is embedded from a third party, nothing leaves the device unless your own code uploads it, and there is nothing new to declare that you did not already declare. The limitation is the mirror of its strength: Apple-only, daily cadence on metrics, and you build your own backend to receive whatever you forward.

The mechanism

The declaration trail is the mechanism teams miss. It runs SDK, store form, your signature, in that order, and the store asks about the SDK's behavior, not yours:

COLLECT
crashlytics: stack, app state, device metadata, install uuid
+ firebase installations sdk rides along transitively

+ your custom keys, logs, user ids, non-fatal events
DECLARE
play data safety: "crash logs" + "device or other ids"
app store privacy label: diagnostics + device identifiers

signed by you, "you alone are responsible" per google
COST
crashlytics: $0 product, data path to google
sentry saas: quota-priced, payload + client ip at sentry

sentry self-hosted: a vm you operate end to end

metrickit: $0, ios only, daily payloads, your own backend

The other mechanism worth knowing is opt-in collection. Crashlytics can store crash reports on-device when collection is disabled, and your code can check for them with checkForUnsentReports and transmit them with sendUnsentReports. That is the privacy-respecting pattern Google itself documents: collect with consent, hold locally, transmit on approval.

The same forms audit the rest of the dependency tree too; we walked the SDK-by-SDK declaration work in Apple wants a privacy manifest for every SDK you ship.

Where it bites

Self-hosted Sentry bites hardest. Sixteen gigabytes of minimum RAM, another sixteen of swap, and a ClickHouse that will not boot on older CPUs means your "free" crash reporter is a VM, a backup plan, a version-upgrade schedule, and an on-call rotation. For a two-person studio shipping one app, that is a server you now run forever to save a privacy declaration row.

SaaS scrubbing bites differently: it creates a false sense of hygiene. The payload reaches the vendor first and gets cleaned second, so anything the scrubber misses is already collected. And MetricKit bites by omission: it gives you Apple-platform crashes on Apple's cadence, no breadcrumbs, no custom context, no dashboard, and nothing for Android, where you are back to the same decision with no equivalent escape hatch.

The numbers

  • Sentry self-hosted minimums: 4 cores, 16 GB RAM + 16 GB swap, 20 GB disk; errors-only profile installs at 2 cores and 7 GB. ClickHouse 26.6+ requires x86-64-v3 (AVX2).
  • Sentry SaaS: free developer tier, then Team from $26/mo and Business from $80/mo billed annually, with pay-as-you-go event rates above the included quota (50K errors included on Team).
  • Crashlytics: no-cost on Spark and Blaze plans; custom logging capped at 64 kB per report.
  • Google Play data safety defines the declaration: "Crash log data from your app. For example, the number of times your app has crashed, stack traces, or other information directly related to a crash," with "Device or other IDs" covering identifiers like the Firebase installation ID.

Our position

Default to Crashlytics or Sentry SaaS and declare honestly. For the apps we ship, the privacy cost of a mainstream SDK is smaller than the engineering cost of self-hosting a crash pipeline, and the declaration rows are what the forms exist for. Reserve self-hosted Sentry for clients whose regulators or contracts say crash bytes cannot leave their infrastructure; that is a real requirement some buyers have, and the errors-only profile makes it survivable. Treat MetricKit as a supplement on iOS, not a substitute. Whatever you pick, audit what you attach: the custom key that logs a user's email into every crash report is your privacy problem, not the SDK's.

The takeaway

PICK
crashlytics or sentry saas as default; declare both rows
SELFHOST
only when data-residency is contractual: errors-only profile
budget 2c/7gb minimum, 4c/16gb+swap for feature-complete
METRICKIT
free first-party supplement on ios; daily, apple-only
SCRUB
vendor scrubbing is post-arrival; strip pii in the sdk
OPT-IN
crashlytics can queue reports on-device until consent
DECLARE
"crash logs" + "device or other ids" on play
diagnostics + identifiers on the app store label

your signature, not the vendor's

The declaration work is the part we handle in support engagements and on our own apps like CashTaq: same SDK, same forms, same signature. If your store listing's data-safety section was last edited by whoever shipped the app in 2023, send us the package name and we will reconcile it against what the binary actually sends.

// next step

We work this way on client projects too.

Tell us what you are making. We reply with a plan, a timeline, and a price.

← the journal