Make §45's logging rules true in a release build, and guard them #38

Closed
opened 2026-08-18 20:37:16 -05:00 by null · 1 comment
Owner

The app does not log health data today. Nothing stops it starting.

What is true now

A grep for Log., println and Timber across app, core and domain finds two hits, neither of them health data: PeriodApplication.kt:48 sets WorkManager's minimum logging level, and a comment in ReminderWorker.kt explains why a Log.d there would be a bad idea. So the rule currently holds by good behaviour rather than by construction.

The Batch 06 milestone lands only when "the release build has no cycle date reachable in any log or crash payload".

What it costs

A single Log.d("cycle", record.toString()) added while debugging a defect writes menstrual dates to logcat, where any app with log access on older Android versions, any bug report, and any crash reporter can read them. It is one line, it is invisible in review, and it is exactly the failure §45 is written about.

What to do, in order

  1. Write the guard first, and prove it fails by injecting a violation — docs/architecture/GUARDS.md requires this, and scripts/prove-guard.sh performs it. Three guards in this project have been green over the exact failure they claimed to catch; assume a new one is broken until watched failing.
  2. Fail the build on health-data logging: any android.util.Log or println reachable from the modules that see cycle data, and any toString() on a domain type that would render a date into a log.
  3. Strip remaining logging from release via R8 rules, and verify against the built artifact rather than the source.

Traps

  • data class toString() is the leak. PeriodRecord(startDate=2026-08-18) is a cycle date in a log, and nobody wrote a logging statement to produce it — an exception message or a crash payload will do it. Consider overriding toString() on the domain types that carry dates.
  • Guarding source is not guarding the APK. A dependency can log, and R8 rules can be wrong; QA pass H's "built artifact inspected" is the check that matters.
  • Crash reporting is not yet decided per docs/security/SECURITY.md — this guard is what makes adopting one safe later, so it is worth having before the decision rather than after.
  • The guard runs in ./gradlew check, which is what the hooks and any CI actually run.

Why filed and not fixed

It is real work with a proof step, not a tidy-up, and the R8 half cannot be checked without building and inspecting a release artifact.

Verify: ./gradlew check fails when a health-data log call is injected into a module that sees cycle data, and passes when it is removed — demonstrated with scripts/prove-guard.sh; and a release APK, driven through logging a period and receiving a reminder, produces no cycle date in logcat.

The app does not log health data today. Nothing stops it starting. ## What is true now A grep for `Log.`, `println` and Timber across `app`, `core` and `domain` finds two hits, neither of them health data: `PeriodApplication.kt:48` sets WorkManager's minimum logging level, and a comment in `ReminderWorker.kt` explains why a `Log.d` there would be a bad idea. So the rule currently holds by good behaviour rather than by construction. The Batch 06 milestone lands only when "the release build has no cycle date reachable in any log or crash payload". ## What it costs A single `Log.d("cycle", record.toString())` added while debugging a defect writes menstrual dates to logcat, where any app with log access on older Android versions, any bug report, and any crash reporter can read them. It is one line, it is invisible in review, and it is exactly the failure §45 is written about. ## What to do, in order 1. Write the guard first, and **prove it fails** by injecting a violation — `docs/architecture/GUARDS.md` requires this, and `scripts/prove-guard.sh` performs it. Three guards in this project have been green over the exact failure they claimed to catch; assume a new one is broken until watched failing. 2. Fail the build on health-data logging: any `android.util.Log` or `println` reachable from the modules that see cycle data, and any `toString()` on a domain type that would render a date into a log. 3. Strip remaining logging from release via R8 rules, and verify against the **built artifact** rather than the source. ## Traps - **`data class` `toString()` is the leak.** `PeriodRecord(startDate=2026-08-18)` is a cycle date in a log, and nobody wrote a logging statement to produce it — an exception message or a crash payload will do it. Consider overriding `toString()` on the domain types that carry dates. - **Guarding source is not guarding the APK.** A dependency can log, and R8 rules can be wrong; QA pass H's "built artifact inspected" is the check that matters. - Crash reporting is *not yet decided* per `docs/security/SECURITY.md` — this guard is what makes adopting one safe later, so it is worth having before the decision rather than after. - The guard runs in `./gradlew check`, which is what the hooks and any CI actually run. ## Why filed and not fixed It is real work with a proof step, not a tidy-up, and the R8 half cannot be checked without building and inspecting a release artifact. Verify: `./gradlew check` fails when a health-data log call is injected into a module that sees cycle data, and passes when it is removed — demonstrated with `scripts/prove-guard.sh`; and a release APK, driven through logging a period and receiving a reminder, produces no cycle date in logcat.
null added this to the Batch 06 — Privacy and Security milestone 2026-08-18 20:37:16 -05:00
null added the
P1
label 2026-08-18 20:37:16 -05:00
null closed this issue 2026-08-18 22:01:37 -05:00
Author
Owner

Shipped in a1efbde. checkNoHealthLogging in the root build.gradle.kts fails ./gradlew check on any logging call in a module that can see a cycle date; proved with scripts/prove-guard.sh — an injected Log.d in CycleRepository gave exactly one failure, and a comment containing Log.d(/println( stayed green, as does the android.util.Log.WARN constant in PeriodApplication. R8 -assumenosideeffects strips android.util.Log from release. Beyond the issue: Prediction's require messages were interpolating five predicted dates into an IllegalArgumentException, which §45 forbids in crash reports and no source guard could see — fixed, and PeriodRecord/SpottingRecord/CycleRecord/Prediction/NotYetObservation now override toString() so a data class cannot render dates into any string that touches it, pinned by NoDatesInDiagnosticsTest (7 cases, itself proved to fail). Verified on a real minified release APK signed with the debug key and driven from onboarding through logging a period: zero ISO dates and zero health words in logcat. 201 tests pass.

Shipped in a1efbde. checkNoHealthLogging in the root build.gradle.kts fails ./gradlew check on any logging call in a module that can see a cycle date; proved with scripts/prove-guard.sh — an injected Log.d in CycleRepository gave exactly one failure, and a comment containing Log.d(/println( stayed green, as does the android.util.Log.WARN constant in PeriodApplication. R8 -assumenosideeffects strips android.util.Log from release. Beyond the issue: Prediction's require messages were interpolating five predicted dates into an IllegalArgumentException, which §45 forbids in crash reports and no source guard could see — fixed, and PeriodRecord/SpottingRecord/CycleRecord/Prediction/NotYetObservation now override toString() so a data class cannot render dates into any string that touches it, pinned by NoDatesInDiagnosticsTest (7 cases, itself proved to fail). Verified on a real minified release APK signed with the debug key and driven from onboarding through logging a period: zero ISO dates and zero health words in logcat. 201 tests pass.
Sign in to join this conversation.
No Label
P0
P1
P2
release-blocker
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: null/Privacy-Period-Tracker#38
No description provided.