Where the action is
The contribution areas with the highest leverage and the lowest barrier:Privacy-label parser fixes
When Apple changes their HTML, the parser breaks. These fixes are usually 5-10 lines in
lib/scraper.ts plus a regression test against a captured payload.New locale
Translations round-trip through Crowdin. A new language is one Crowdin add + one config row + one placeholder JSON. See Translations.
Dashboard widgets / charts
app/components/charts/ is self-contained. Most charts read from /api/stats/* and follow the same data-fetch + ECharts render pattern.Documentation
This site! Pages live in the docs repo (separate from the main codebase). Improvements to clarity, examples, screenshots, or new troubleshooting entries are very welcome.
Setup
1
Fork and clone
Fork github.com/privacykey/privacytracker, then:
2
Install Node 24 + dependencies
npm install fails.3
Create a branch
Branch names follow
<type>/<short-description>:<type> is one of fix, feat, chore, docs, test, refactor.What to run before opening a PR
The full pre-flight is fast — run all four:next.config.js, tsconfig.json, package.json):
scripts/ios-app-import/:
src-tauri/ (the Rust desktop shell):
What makes a good PR
Three things, in priority order. The change should be small enough to review in one sitting. If your branch touches a parser, a UI component, a chart, and a migration, that’s four PRs. Reviewers approve smaller PRs faster, and bisecting later is easier. Tests for parser changes are non-negotiable. Apple’s HTML is volatile. The pattern intests/app/scraper-fixture.test.ts is to stub global.fetch so it returns a captured App Store payload from an in-file helper, then assert against the parsed output stored in the DB. When you fix a parser bug, capture the payload that was breaking, add it the same way, and assert on it — that prevents regression next time Apple iterates. The sibling scraper-shoebox, scraper-lookup, scraper-advanced and scraper-related-shelves tests cover the other parser eras and endpoints.
Database changes need both CREATE TABLE and migrations updates. Forgetting one breaks upgrade paths for existing installs. The pattern is in lib/db.ts; copy it.
Picking an issue
Issues labelledgood first issue are scoped for someone new to the codebase. help wanted is broader — anything we’d merge but haven’t gotten to.
If you want to propose something not in an existing issue, open a discussion first for anything beyond a small fix. Saves you the disappointment of a no-merge after the work.
Privacy-label parser fixes
Most parser breakage takes one of two forms:
Always include the failing App Store URL in the PR description so reviewers can verify the fix without a separate hunt.
Dashboard widgets and charts
app/components/charts/ follows a consistent pattern:
- Component takes typed props derived from a
/api/stats/*shape. - Data fetch happens in the parent (server component) and is passed in via props — no client-side fetch.
- Theme tokens (severity colours, focus states) come from
lib/privacy-meta.ts— not from inline hex codes. - ECharts options are constructed in a memoised helper for testability.
/api/stats/<name> route (thin wrapper over a lib/stats/<name>.ts helper), add the component under app/components/charts/, register it in the dashboard layout under the correct feature flag.
Adding a locale
Most of the work is in Crowdin, not the repo. Editing translations directly is fine but the flow that scales is:1
Add the language in Crowdin
Project Settings → Target Languages → add. Wait for the next workflow run to upload
locales/en.json so the language appears in the project tree.2
Add the bundle row
crowdin.yml → languages_mapping.two_letters_code: add the row only if Crowdin’s ID doesn’t match your bundle filename (e.g. pt-BR: pt-br for pt-br.json; nothing needed for plain es: es).3
Register in next-intl
Add the new locale to the supported list in
i18n.ts so the bundle gets loaded.4
Drop a placeholder JSON
locales/<new>.json containing {} — next-intl needs the file to exist before its dynamic import resolves. The first build will use English fallbacks for every key until Crowdin populates the bundle.Code style
A few conventions to know before you open a PR:- No CSS-in-JS or styled-components. Tokens and severity styling live in
app/globals.cssandlib/privacy-meta.ts. Reuse those rather than inlining new colours. @/*is the path alias. It maps to the repo root. Use it instead of../../../lib/foo.- Server-only modules go in
lib/with the'server-only'import where they must not be bundled. Examples:lib/feature-flags-server.ts. Importing one of these from a Client Component fails the build. - All DB writes through transactions. Multi-step writes use
db.transaction(() => { … })(). Don’t open ad-hoc connections. - Tests use
node:testplustsx. Run vianpm test. Look attests/helpers/setup-env.tsfor the standard env shape. - Don’t add new top-level dependencies casually. Privacy is the value prop; every dep is a supply-chain question. Justify in the PR description.
Reporting issues vs. opening PRs
Bug reports are valuable on their own — you don’t have to fix what you found.- Bugs and feature requests: github.com/privacykey/privacytracker/issues,
bug_report.ymltemplate. Paste your support bundle (Settings → Admin → Deployment Diagnostics → Copy support bundle) — it has system info, app version, sync state, and the most recent activity log without any of your tracked apps’ data. - Security findings: GitHub Private Vulnerability Reporting. Don’t open a public issue for security.