docs(legal): a privacy policy that matches the code, one retention per store - #11217
Merged
Conversation
…r store The privacy section had drifted behind a year of features. It promised "no personal data (names, emails, etc.)" while the feedback widget asks for exactly that and keeps it; it named no legal basis and no jurisdiction; its rights list stopped at access/rectify/erase/export; and nothing said that Cloudflare processes the visitor's IP at the edge. Each store now carries the retention that is true for it — Cloud Logging's 30 days, a feedback entry until it is deleted by hand — rather than one blanket figure. Plausible is described by the property that matters (no cookies, no cross-site identifier, no IP stored, the browser talking only to our domain) instead of the false claim that the script is not a third party's. The crawler body in api/routers/seo.py mirrored the same wrong sentence and is corrected with it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The policy still omits stored feedback identifiers and retains an inaccurate personal-data claim.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Updates privacy disclosures to match current analytics, feedback, hosting, and retention behavior.
Changes:
- Expands the interactive privacy policy.
- Synchronizes crawler-facing legal content.
- Adds regression tests and a changelog fragment.
File summaries
| File | Description |
|---|---|
app/src/pages/LegalPage.tsx |
Revises privacy disclosures and rights. |
app/src/pages/LegalPage.test.tsx |
Tests key legal claims and retention rules. |
api/routers/seo.py |
Updates crawler-facing privacy content. |
tests/unit/api/test_routers.py |
Tests crawler legal output. |
changelog.d/legal-privacy-audit.md |
Records the policy audit. |
Review details
Suppressed comments (2)
app/src/pages/LegalPage.tsx:255
- This remains a blanket false claim: the policy itself says that server logs collect IP addresses and user agents, Cloudflare processes IP addresses, and feedback automatically stores an IP hash and session identifier. Replace this with the narrower name/email claim, and update the test at
LegalPage.test.tsx:81, which currently pins the inaccurate wording.
• no personal data unless you type it — the optional contact field in the feedback
widget is the only one that asks for a name or an email
app/src/pages/LegalPage.tsx:258
- The localStorage inventory omits
anyplot_feedback_session, whichFeedbackWidget.tsx:25,144-149persists and sends with feedback. Add this random feedback session identifier to the list so the newly sharpened storage claim is complete.
• no cookies at all (localStorage holds UI preferences — theme, image size — and a
cached release number)
- Files reviewed: 5/5 changed files
- Comments generated: 2
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…data line Four findings from the Copilot review, all of them right: * The feedback paragraph read as an exhaustive list but left out the window size and the random session id the widget sends with every submission (FeedbackWidget.tsx:213-214). Both are named now. * "no personal data unless you type it" was the old blanket claim in new words. An IP address, its hash and a persistent random id are personal data whether or not anyone typed them, and the same page says three paragraphs earlier that all three are stored. The bullet now promises only what it can: no name or email unless you type one. * The localStorage inventory listed preferences and a cached release number, but not `anyplot_feedback_session` — a persistent random identifier, which is the one entry a reader would want listed. * The crawler-body test claimed to pin both retentions and asserted only the 30 days. It now asserts the feedback one too. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
api/routers/seo.pycarried the same wrong sentence ("no personal data collected") and is corrected in the same commit — that string is anyplot's prerender for/legal, and its own comment asks for the two to stay in sync.What was added
data controller: an operator's legitimate interest in protecting the site and knowing how often it is read; Swiss data protection law, GDPR on top for EU visitors. No article numbers, no EU-representative claim.feedback.paragraph: every field the widget stores (message, reaction, contact, page, window size, user agent, submission id, IP hash), admin-only, no timerthe edge.paragraph: Cloudflare processes the IP for rate limiting, bot filtering (which can show a check page) and DDoSWhat was wrong and is now fixed
Each claim with the evidence that decided it:
app/src/components/FeedbackWidget.tsx:485placeholder is literallyName or email (optional); stored atcore/database/models.py:250(contact), alongsidemessage,viewport,user_agent,session_id,ip_hash(models.py:248–259). The promise is deliberately only name/email: an IP address, its hash and the feedback session id are personal data whether or not anyone typed them, so any broader "no personal data" wording would be the same false claim again (Copilot review)app/nginx.conf:321–323proxies/js/script.js,:335proxies/api/event;app/index.html:143,145api/cloudbuild.yaml:4_REGION: europe-west4,:66anyplot:europe-west4:anyplot-db;gcloud storage buckets list→anyplot-images,anyplot-staticbothEUROPE-WEST4copy_code, download_image, external_link, feedback_opened, feedback_submitted, internal_link, library_click, library_filter, map_*, nav_click, page_not_found, plot_rotate, report_issue, search_no_results, stats_top_impl_click, suggest_spec, tag_click, theme_toggle— no banner event existsexportwas the wrong rightSet-Cookiefromanyplot.aiorapi.anyplot.ai(curl, 2026-09-03). localStorage keys:theme(useThemeMode.ts:3),imageSize(PlotsPage.tsx:100),anyplot:latest-release(useLatestRelease.ts:4) andanyplot_feedback_session(FeedbackWidget.tsx:25,144). The last two are not preferences — one is a cached release number, the other a persistent random identifier — so both are named (Copilot review)scripts/evaluate-plot.py:500(CI/generation time).grep anthropic api/ core/finds only the unusedcore/config.py:79key — nothing on a request path. The bullet now says soapi/routers/seo.py)What was checked and left alone
gcloud logging buckets describe _Default --location=global --project=anyplot→retentionDays: 30. This is the configured value (and also GCP's default), so the sentence stands as written.europe-west4; Cloudflare global with EU data centres; Plausible EU + proxied). Only the sentence introducing it was over-broad.https://plausible.io/anyplot.aianswers 200.plots/*/specification.yamlcarriessuggested: <github handle>. Paragraph kept unchanged.app/security-headers.conf:28sets noreport-uri/report-to, and the onlyreport-toheader on the live site is Cloudflare's own NEL endpoint, which reports to Cloudflare, not to us. Nothing was written about it, since there is nothing to write. (The sibling repo does have one; this is the asymmetry, not an omission.)ip_hash(api/routers/feedback.py:96–101), so it is covered by the feedback paragraph rather than getting one of its own.For Markus — please confirm
_DefaultCloud Logging bucket, not from any file in the repo. If a sink or a bucket policy is ever changed in the console, this sentence goes stale silently — there is no test that can catch it./debugroutes (api/routers/debug.py:547,560,599, allrequire_admin). The page now says so honestly, but if you would rather the field stopped asking for an email, or entries got an automatic expiry, that is a code change and a separate PR.Decisions taken here
data controller·what we collect·what we do not collect·hosting & third parties·your rights) rather than adding sub-headings — the brief asked for a focused update, not a new page.data controllerinstead of giving it its own heading; one sentence did not earn one.Review round
One Copilot review, four findings, all legitimate and all applied in
6f6b610: the feedback field list was missing the window size and the submission id; "no personal data unless you type it" was the old blanket claim in new words (an IP address, its hash and a persistent random id are personal data whether or not anyone typed them, and the page says so three paragraphs earlier); the localStorage inventory omittedanyplot_feedback_session; and the crawler-body test claimed to pin both retentions while asserting only the 30 days. Both threads answered and resolved.Test plan
yarn type-check(tsc + tsconfig.test.json) — cleanyarn lint— cleanyarn fm:check— cleanyarn test— 70 files, 626 tests pass;LegalPage.test.tsxgrew from 9 to 12 cases, pinning the legal basis + jurisdiction, the per-store retentions, and the corrected claims (including the two feedback identifiers, so a later trim cannot drop them)uv run pytest -k seo_legal— passes; the test now also asserts the old wrong sentence is goneuv run ruff check+ruff format --checkon the touched Python — cleanuv run python -m tools.changelog check --base origin/main— fragment well-formed🤖 Generated with Claude Code
https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3