Logging is the last thing anyone reviews before a release and the first place personal data leaks. It happens quietly: a developer adds a log line to debug a login flow, the line prints the whole response object, the response contains a token and an email address, and that line ships.
Short answer
Device logs are not private. Android's log information disclosure guidance documents the risk directly: information written to system logs can be exposed to other parties, and sensitive data should not be logged. Apple's unified logging treats this as a first-class design concern, redacting dynamic strings by default unless a format specifier marks them public. The safe assumption is that anything you log may be read by someone other than you.
Who can actually read your logs
The threat model changed over the years and the folklore has not kept up, so it is worth being precise about who reads what.
On a normal Android device, an ordinary app cannot read another app's log output. What can read it is anyone with a debug connection to the device, anyone with a device dump or bug report, and any diagnostic tooling the user has installed with the right permissions. Rooted devices remove the boundary entirely.
On iOS, unified logging is accessible through the Console app to anyone who can connect the device to a Mac, and sysdiagnose archives collected for support routinely contain log content.
Then there is the second-order path that catches more teams than the first: logs collected off the device. Crash reporters, analytics SDKs and observability tooling frequently capture recent log lines as context, which turns a local debug statement into data sitting in a third-party service under a different jurisdiction and retention policy.
That last route is why "it is only a debug log" is rarely an accurate description of the exposure.
What ends up in logs and should not
The same short list appears again and again.
Authentication material. Tokens, refresh tokens, session identifiers, API keys, and the full request or response bodies that contain them. Logging an entire HTTP response is the single most productive source of leaked credentials in mobile code.
Direct identifiers. Email addresses, phone numbers, full names, postal addresses, government identifiers, and device identifiers used for tracking.
Special category data under privacy law. Health measurements, location traces, payment details, anything about children, and anything revealing beliefs or orientation.
Internal structure that helps an attacker. Full stack traces with file paths, internal hostnames, database queries, and detailed error messages describing why authentication failed.
That last group is worth calling out separately because it feels harmless. An error message that distinguishes "no such account" from "wrong password" is an account enumeration primitive, and putting it in a log while also showing it to the user compounds the problem.
The patterns that leak without anyone noticing
Object dumps. Logging a model object calls its string conversion, and generated conversions print every field. A user object logged for debugging prints the email, the identifier and often the token.
Network interceptors. HTTP logging interceptors set to a body-level verbosity are a deliberate development convenience and a production disaster, and they are usually enabled by a flag that someone forgets to condition on the build type.
Third-party SDK verbosity. Many SDKs log at a level you did not choose, and some print request payloads at their default setting. This is not visible in your code, only in the device output.
Exception handling that logs the exception and its cause chain, which frequently carries the request that failed, including its headers.
The through line is that none of these are decisions to log sensitive data. They are decisions to log something convenient that happens to contain it.
Building a policy that survives a deadline
Rules that depend on remembering do not hold up in the week before a release, so the useful controls are structural.
Strip or disable logging in release builds rather than relying on log levels. On Android, R8 keep rules can remove log calls entirely; on iOS, compile-time conditionals achieve the same thing. A log call that does not exist in the release binary cannot leak.
Log identifiers rather than values. A user reference, a request identifier, a correlation identifier: all of these let you debug a report without the log containing the person. This one change removes most of the exposure at almost no cost to debuggability.
Never log whole objects or whole payloads. Log the two or three fields you actually need, named explicitly. This is more typing and it is the difference between a log line you can defend and one you cannot.
Treat log capture by third-party SDKs as a data flow, not a debug feature. If your crash reporter attaches log context, that is a transfer of whatever is in those logs to that vendor, and it belongs in your privacy documentation.
The MASVS privacy category and the storage category both cover this ground, and the practical version is simpler than the standards make it look: decide what may leave the device, and make everything else structurally impossible to log.
Checking what your build actually prints
Reading the code is a weak check here, because most leakage comes from third-party components and from string conversions you did not write.
The strong check is to run the release build and watch the device output. Exercise login, a failed login, a network error, a payment path and a background sync, then read what appeared. Teams doing this for the first time usually find at least one thing they did not expect, and it is frequently from a dependency.
Do the same for whatever your crash reporter uploads, using a deliberately triggered crash, so you see the payload as the vendor receives it rather than as you assume it looks.
An automated pass over the artifact helps with the parts that are visible statically, and a scanner such as PTKD.com will surface verbose logging configuration, debuggable builds and secrets in the bundle. The runtime half, what a specific flow prints on a real device, still needs someone to look at the console once per release.
The regulatory angle, briefly
Logs are records, and privacy law does not treat them as a special case exempt from the rules that cover any other store of personal data.
If your logs contain personal data and leave the device, that is a transfer to whoever receives them, with the same obligations as any other transfer: a lawful basis, a retention period, a location, and a place in whatever documentation you maintain about your data flows.
The awkward part is that log retention is rarely deliberate. A crash reporter's default retention becomes your retention. A support engineer's local copy of a sysdiagnose becomes a copy of production personal data on a laptop. A debug archive attached to a support ticket becomes personal data inside your helpdesk.
None of that requires a breach to become a problem. A subject access request that has to include log content is a considerably harder request to answer than one that does not, and the cheapest way to make it easy is to have logged identifiers rather than people.
Redaction, and why it usually fails
The instinct when this is raised is to add a redaction layer that strips sensitive values on the way out. It is a reasonable instinct with a poor track record.
Pattern-based redaction catches the formats you thought of. Tokens come in formats you did not, and the first unusual one passes through untouched. A redactor tuned for email addresses does nothing for a phone number in an unexpected format or an identifier embedded in a URL path.
Redaction also runs in your code, which means it protects only what your code logs. The SDK printing a request payload at its own default verbosity never passes through your redactor.
And redaction creates a false sense of coverage that discourages the structural fix. A team that believes logs are redacted stops treating log content as sensitive, which is the opposite of the behaviour you want.
Where redaction earns its place is as a second layer under a policy of not logging values at all, and as a stopgap on a legacy codebase too large to fix at once. As a primary control it is weak.
A short pre-release checklist
Five checks, each a few minutes, and together they catch most of what ships.
Confirm network logging interceptors are conditioned on build type rather than on a manual flag.
Search the codebase for logging calls that pass an object rather than named fields, which is where string conversions leak whole records.
Run the release build through login, failed login, network error and payment, reading the console as you go.
Trigger a crash and inspect what the reporter uploaded, including any attached log context.
List the SDKs that write to the log at their default level, which is only visible by watching the device rather than by reading your source.
Run those five against a build you are about to submit rather than against a development branch, since the difference between the two is precisely where the problem lives. A debug build that logs verbosely is doing its job; the question is only ever what the shipped artifact does.
Logging that is actually useful
Removing sensitive values does not mean removing the ability to debug, and teams resist this change because they expect it to.
The substitute for logging a value is logging a stable reference to it. A user identifier that means nothing outside your database, a request identifier propagated from the client to the server, and a correlation identifier that ties a session's events together give you everything a support investigation needs.
Structured logging helps more here than it does anywhere else, because named fields make it obvious what a line contains. A line that logs event=login_failed reason=invalid_credentials user_ref=8f21 is readable, greppable and contains no person.
Log state transitions rather than payloads. Which step the flow reached, what the server returned as a status, how long it took. Almost every debugging session that people believe requires the payload is actually answered by knowing where the flow stopped.
And keep the loud logging for local development behind a build-type condition, where it costs nothing and reaches nobody.
The objection to all of this is that a support case will eventually need the exact value, and occasionally that is true. The answer is to fetch it deliberately from the system that is supposed to hold it, with the access controls and audit trail that system provides, rather than to have it sitting in a log file because it might one day be useful. A value retrieved on purpose is a very different artifact from a value logged by default.
What to take away
Assume device logs will be read by someone who is not you, including by any SDK that collects log context off the device, and including by whoever handles a support archive months from now.
Log identifiers, never values, and never whole objects or payloads, because a string conversion you did not write is what turns a harmless line into a record of a person.
Remove logging from release builds structurally instead of relying on levels, because levels depend on somebody remembering.
Audit third-party SDK output at their default verbosity, since it is not visible in your source, and treat any SDK that collects log context as a data processor rather than as a debugging tool.
And check by running the release build and reading the console once before you ship, because that is the only method that catches what you did not write.
The reason this is worth a recurring slot rather than a one-off cleanup is that logging drifts back. Every debugging session adds a line, most of those lines are removed, and the ones that survive are exactly the ones added under time pressure. A five minute console read per release keeps that drift visible instead of letting it accumulate for a year.



