- Published on
The Fragility of Negative Claims: Why "The App Doesn't Embed X" Quietly Becomes a Lie With One Dependency Change
- Authors

- Name
- Jack Qin
There's a particularly dangerous form of compliance claim: the negative claim — "This app doesn't embed Sentry, Firebase, or any third-party analytics/advertising/behavioral-tracking SDK." It's dangerous not because it's written wrong, but because its truth value isn't determined by itself — it's determined by code elsewhere. Add one dependency on the code side, change not a single word on the claim side, and it flips from "true" to "false" — and that flip is completely invisible from the side making the change.
This post isn't about remediating one compliance incident; it's about dissecting a structural ledger: why a negative claim's truth value is anchored to the code, why "code changed, claim didn't" is this kind of claim's default failure path rather than accidental oversight, and why the only reliable countermeasure is to bind the code change and the claim change into an atomic unit that can't be partially deployed. Understand this ledger and you're facing not just one privacy policy, but a whole class of "claim truth value determined by state elsewhere" compliance/documentation problems.
The symptom: one dependency makes one claim a lie
The mobile privacy policy page once carried this literal claim:
The App does not embed Sentry, Firebase, or any third-party analytics, advertising or behavioural-tracking SDK.
Then in some task, a mobile-only diff installed @sentry/react-native. The problem took shape right there: once this diff ships, the sentence above becomes false on the spot — and this contradiction is completely invisible from the mobile review's perspective, surfacing only if the reviewer happens to remember to flip to the privacy policy page.
Note the asymmetry of this failure: the action that makes the claim false (adding the dependency) and the claim itself sit in two different files, two different review fields of view. Whoever changes package.json sees the dependency list, not that sentence; whoever wrote the claim couldn't have foreseen that some future dependency change would overturn it. This isn't anyone's carelessness — it's structural to negative claims, whose truth value is anchored to an external state it can neither observe nor constrain (the dependency graph).
Why this is a real problem, not just "a contradiction"
The risk isn't just at the "the claim doesn't match" layer; it has three independent downstream consequences:
- The App Store "App Privacy" answers and the Google Play "Data Safety" form are both transcribed from the published privacy policy. Apple's review will, and does, cross-check the claimed data flow against the live policy URL — an inconsistency is a documented rejection reason.
- Australian Privacy Principle APP 8 (cross-border disclosure of personal information): the moment any personal information is routed to an overseas processor, it triggers an active disclosure obligation. Discovering this obligation after release is irreversible — the data has already flowed out.
- Negative-list claims silently expire. The failure of a claim like "the app doesn't embed X" raises no error and triggers no alert — it just quietly becomes a lie after some dependency change.
The second point especially highlights why timing matters: a compliance obligation comes into being the moment the data flow actually happens, not the moment you remember to update the docs. Once the SDK is wired up and the build runs, the data flow is real — whether the policy kept up doesn't change the fact that the obligation already triggered.
Trigger condition: when this ledger must be reckoned
Not every dependency change triggers it. The decisive criterion is "does it introduce or expand the processing and outbound transfer of user/device data?":
- Adding to
apps/mobile/package.jsonan SDK that establishes a network connection to a third-party source and processes user/device data (Sentry, the Firebase suite, PostHog, Mixpanel, Amplitude, Segment, Branch, AppsFlyer, OneSignal, Adjust, LogRocket…); - A material expansion of an existing SDK's data scope (turning on
sendDefaultPii, addingSentry.setUser, enabling Replay, adding attribution events); - An existing processor's data residency/region changed;
- Adding an EAS secret /
EXPO_PUBLIC_*value that gates an SDK's initialization.
Does not trigger: backend-only SDK changes (apps/api has its own disclosure surface), web-only SDK changes (the web policy covers it separately), local libraries that never make network requests (date-fns, lodash, theme libraries, etc.). This boundary is itself the core of the criterion — whether it triggers depends on whether data actually goes out, not on package size or importance.
The countermeasure: bind code and claim into an atomic unit that can't be partially deployed
Since the failure path is "code changed, claim didn't keep up," the countermeasure must mechanically eliminate the possibility of "changing only half." The means: stuff all related changes into the same PR — because a PR can't be partially deployed, this is the simplest atomicity enforcement. The same PR must cover:
apps/web/src/app/(others)/legal/privacy/index.tsx(web policy) — update the diagnostic-data entry, add the processor to the third-party processor list, add it to the cross-border list if it's transmitted out of Australia;apps/web/src/app/(others)/legal/privacy/mobile/index.tsx(mobile-specific policy) — must be changed even though it looks "generic," because that negative-list claim lives right here and quietly expires;docs/legal/privacy-web.mdanddocs/legal/privacy-mobile.md— the markdown mirrors for legal review;apps/web/src/app/(others)/legal/legal-pages.test.tsx— assert the new processor name + each new disclosure clause appears in the rendered page.
Each processor's disclosure must also spell out: the operator's legal name (e.g. "Functional Software, Inc. (Sentry)," not just the brand name), the categories of data transmitted, the purpose, data residency/region, whether personal account information is transmitted (name the fields explicitly, e.g. "mobile does not transmit name, email, or user identifier"), and the cross-border notice (APP 8, if the data leaves Australia).
Timing constraint: the claim must go live before the data flow
The atomic PR solves "changing it incompletely," but there's still a timing problem: the claim's live-visibility time must be earlier than the actual occurrence of the data flow.
The web deploy (Cloudflare Pages, auto-triggered after main merge) must precede the SDK-enabled mobile build being pushed to any test channel (TestFlight, Google Play Closed Testing, production). Bundling the changes into one PR is the cleanest enforcement precisely because it solves atomicity and timing at once — one PR can't be partially deployed, so it can't let the SDK go live ahead of the policy.
If the work absolutely must be split across multiple PRs, the timing has to be guarded manually: the privacy policy PR ships and deploys first, and verify the live URL really reflects the new disclosure; the mobile SDK PR ships later, and the build containing that SDK must not start its EAS Build until the live URL reflects the disclosure.
This also directly defines a set of anti-patterns, all of which share breaking atomicity or timing:
- "Put the privacy update in a follow-up PR" — before the follow-up PR lands, the mobile build is already in TestFlight and the policy is misaligned the whole time;
- Updating only the web policy page — the one lying is precisely the mobile policy page;
- Changing JSX but not the markdown mirror — legal reads the markdown, and without sync they drift;
- Skipping the test assertion — with no test, the next refactor silently regresses the disclosure;
- Adding the EAS secret + plugin config first and deferring the policy change — once the SDK is wired up and the build runs, the data flow is real.
The transferable layer
Set aside the specific privacy-policy and App Store processes, and there are two transferable lessons.
A negative claim ("the system doesn't do X") has a truth value determined by external state, so it silently fails with any change to that state. Its fragility is asymmetric to a positive claim ("the system does Y"): with a positive claim you at least know where to maintain it, while a negative claim's killer is a distant change with no explicit link to the claim at all. A code comment's "this will never be null," a doc's "this service doesn't store PII," a config's "no telemetry enabled" — all the same class of time bomb. For this kind of claim, the only reliable guard is to mechanically couple "the action that makes it false" and "the claim itself" at the tooling level: a test assertion, an atomic commit, a CI check — so the claim can't sit safely in the docs once its premise has been overturned.
When an obligation comes into being the instant the "state actually happens," the docs and the state must change atomically and in the correct order. Once the data goes out, the compliance obligation is in force whether or not the docs kept up. Binding the code change and the claim change into a unit that can't be partially deployed is essentially using an engineering means to guarantee "the claim never lags behind reality." Ask yourself: if someone overturned the premise of this claim, is there a mechanism (not just a memory) that would immediately make the claim follow?