Skip to content

Add tracker/company reverse lookup, methodology page, sitemap and social metadata - #16

Merged
kasnder merged 10 commits into
mainfrom
claude/website-journalists-usefulness-yxhq1y
Aug 12, 2026
Merged

Add tracker/company reverse lookup, methodology page, sitemap and social metadata#16
kasnder merged 10 commits into
mainfrom
claude/website-journalists-usefulness-yxhq1y

Conversation

@kasnder

@kasnder kasnder commented Aug 11, 2026

Copy link
Copy Markdown
Member

Makes the site usable by journalists and researchers, who arrive with the opposite question to the one the site answered ("which apps contain this tracker?", not "what is in this app?") and who need to check how a number was produced before quoting it.

Reverse lookup

lib/reverseIndex.js builds an inverted index — tracker → apps and company → apps — from the stored analyses. It is normalised (app metadata stored once, referenced by bundle ID), cached under CACHE_DIR in its own entry, and rebuilt on the same database signature as the aggregate site data, so requests do no extra work and the homepage cache stays small.

New pages:

  • /trackers and /companies — directories of everything seen in an analysed app, with a client-side filter (public/js/filter.js, external file so it passes the existing CSP).
  • /tracker/:slug and /company/:slug — the apps a tracker or company was found in, most reviewed first, 50 per page.

Slugs are derived from names, deduplicated deterministically so a collision (Mob.com vs Mob com) resolves the same way on every rebuild. Lookups use own-property checks, so /tracker/__proto__ 404s rather than reaching an inherited property.

App reports and the statistics tables now link into these pages, so a reader can go from an app to a tracker to every other app carrying it.

Apple system APIs the analyser reports (AdID access, Get device information) are labelled as system APIs rather than being listed as unattributed trackers; jurisdiction.isSystemSignature is exported for that.

Methodology page

/methodology documents what a figure on this site means:

  • The sample is free apps on the UK App Store, queued on demand and ordered by review count — explicitly not a random sample, which is the caveat most likely to be lost in a story.
  • Detection is static: a code signature is present in the binary. That is not evidence of transmission, and an empty result is not evidence of no tracking.
  • Jurisdiction classification rules, counting rules, limitations (substring name matching, ageing ownership data, staleness, storefront limits), what the data does and does not support, verification contact, and how to cite.

Linked from the nav and from /about.

Discoverability

  • Canonical links plus Open Graph and Twitter card metadata on every page, with the App Store icon as the card image on report pages.
  • /sitemap.xml re-enabled (was commented out) and extended to lookup and reference pages, with real lastmod timestamps instead of "now" for every URL.
  • /robots.txt pointing at the sitemap.
  • SITE_URL pins the public origin so these URLs are https:// behind a TLS-terminating proxy; it falls back to the request otherwise.

Incidental

jurisdiction.resolveTrackerName is memoised. Its substring scan over the company database now runs once per distinct tracker name instead of once per app occurrence, which is what makes building the index over the whole corpus cheap.

Testing

npm test — 57 tests pass (was 35).

  • test/reverseIndex.test.js: counting and denominators, ordering by popularity, company grouping, system-signature flagging, slug collisions, rejection of invalid and inherited-property slugs, JSON cache round-trip, pagination clamping.
  • test/lookupPages.test.js: boots the server against a stubbed database and asserts the new pages render, unknown slugs 404, trackers link from app reports and statistics, and the sitemap, robots.txt and social metadata contain what they should.

Both suites run without a database. Rendered output was also checked by hand for markup and spacing.

Notes for review

  • Nav now has five items (Home / Trackers / Statistics / Methodology / About); worth a look on a narrow viewport.
  • /statistics on a cold cache builds both the site data and the reverse index, so it reads all apps twice once. Both are cached afterwards. Say the word if you would rather they share one read.
  • The methodology text makes factual claims about the pipeline and about the operator; please check the wording before this leaves draft.

Generated by Claude Code

kasnder commented Aug 11, 2026

Copy link
Copy Markdown
Member Author

CI was red on the npm audit step, not on the tests — all 57 tests passed. The failure was not caused by this branch: its package.json and package-lock.json were byte-identical to main, and main's last CI run was on 13 July, before these two advisories were published. npm audit queries the live advisory database, so main would fail the same way if re-run today.

Pushed a lockfile-only fix in a separate commit (671e791):

  • body-parser 1.20.5 → 1.20.6 — GHSA-v422-hmwv-36x6, invalid limit values silently disabling size enforcement. Worth having here, since body size limits are configured from environment variables.
  • brace-expansion 5.0.6 → 5.0.9 — GHSA-3jxr-9vmj-r5cp and two related, transitive.

Both are patch bumps inside the existing semver ranges (express depends on body-parser@~1.20.5), so package.json is unchanged. Afterwards npm audit reports no vulnerabilities and the full suite still passes, including the body-limit tests that exercise the changed body-parser behaviour.

It is a separate commit so it can be dropped cleanly if Dependabot is already handling these, or if you would rather keep dependency changes out of a feature PR.


Generated by Claude Code

claude added 2 commits August 11, 2026 13:15
…ial metadata

The site could only answer "what is in this app?". Reporters and researchers
normally arrive with the opposite question, and with a need to check how a
number was produced before quoting it.

Reverse lookup: lib/reverseIndex.js builds an inverted tracker -> apps and
company -> apps index from the stored analyses, cached and rebuilt on the same
signature as the aggregate site data so requests do no extra work. New pages
/trackers, /companies, /tracker/:slug and /company/:slug list every tracker and
company with the apps they were found in, most reviewed first and paginated.
App reports and the statistics tables now link into them.

Methodology: /methodology documents the sample (free UK App Store apps, queued
on demand and ordered by popularity, so not a random sample), how detection
works and what a detection does and does not mean, the jurisdiction
classification rules, counting rules, limitations and citation guidance.

Discoverability: canonical links, Open Graph and Twitter card metadata per
page, with the app icon as the card image on reports; /sitemap.xml re-enabled
and extended to the lookup and reference pages with real lastmod timestamps;
/robots.txt points at it. SITE_URL pins the public origin behind a TLS proxy.

Also memoises jurisdiction.resolveTrackerName, whose substring scan over the
company database now runs once per distinct tracker name instead of once per
app occurrence.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011VVgEEw2J1LensebivHrwW
CI runs `npm audit` as a hard gate. Two advisories published since main last
ran CI now fail it on any branch, including this one, whose dependency files
are otherwise identical to main:

- body-parser <1.20.6 (GHSA-v422-hmwv-36x6), reachable here because the app
  configures body size limits from environment variables
- brace-expansion 3.0.0-5.0.8 (GHSA-3jxr-9vmj-r5cp and two related), transitive

Both are patch bumps within the existing semver ranges (express depends on
body-parser ~1.20.5), so package.json is unchanged. `npm audit` reports no
vulnerabilities afterwards and the full suite still passes, including the body
limit tests that exercise the affected body-parser behaviour.

Kept as a separate commit so it can be dropped if these are handled elsewhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011VVgEEw2J1LensebivHrwW
@kasnder
kasnder force-pushed the claude/website-journalists-usefulness-yxhq1y branch from 671e791 to 104811d Compare August 11, 2026 11:16
Resolve routes/index.js against the Turnstile and storefront metadata
work on main:

- keep both import groups (analysisPolicy, turnstile, appMetadata)
- keep main's GET /analysis/:appId shape: requireValidAppId, the
  database-only lookup that renders request-analysis for unknown apps,
  and buildReportMetadata
- reapply the third-party tracker filter to the tracker summary line
- derive the page title, description and og:image from reportMetadata so
  the social card follows the same storefront precedence as the report
CACHE_DIR is a persistent volume, so entries survive a deploy. Both
consumers detect new data through a signature of appCount and the latest
analysis timestamp, which cannot see a change in how the cached data is
derived from that data. Filtering Apple system signatures out of the
tracker counts in buildSiteData is exactly such a change: the signature
still matches the entry written before the deploy, so the pre-filter
figures would be served on the homepage and statistics page until the
next analysis lands.

Stamp a schema version on every entry and treat a mismatch as a miss.
Four issues where this branch and the Turnstile/storefront work on main
met without being reconciled:

SITE_URL was read from routing middleware that runs before every route.
Unset in production, the throw became a 500 on every path including
/healthz, from a process that had started cleanly. Move the resolution
into lib/siteUrl.js and assert it at startup next to the Turnstile
check, so the deployment fails where a missing variable is visible.

One rate-limit budget covered both page views and form submissions.
sitemap.xml now points crawlers at every app, tracker and company URL,
and 100 requests per 5 minutes is 0.33/s — low enough that a single
crawler's discovery pass collects 429s. Budget them separately: cached
page views get a larger allowance, the forms that reach the App Store
get a smaller one than they had.

getSiteDataSignature aggregates over every stored app and now runs on
the report page too, since the tracker links need the reverse index —
twice per /statistics request. Memoise it briefly; writes clear it.

getAllApps selected apps.* only, so the directories, lookup pages,
sitemap and homepage rendered the queue-time snapshot while the report
page rendered the refreshed storefront row: the same app under two
titles. Join the cache row, resolve display metadata once via
buildListingDetails, and add the storefront generation to the cache
signature — without it a metadata refresh changes nothing the signature
can see, and the persisted cache would never rebuild.
Main moved bot protection from an in-page Turnstile widget to Cloudflare
WAF rules, which changed the shape of the two endpoints this branch has
to account for. Drop the Turnstile wiring and reconcile:

Search became GET /search and the analysis request gained a GET
/request/:appId confirmation step, both GETs so a Cloudflare challenge
can replay them. The rate-limit split added here keyed "cheap" off the
method, which would have handed the two paths that reach the App Store
the larger page-view budget. Budget them by cost instead of by method.

For the same reason both are now reachable by a crawler, where a form
post was not, so robots.txt withholds them: following either spends an
App Store call and returns a challenge interstitial rather than content.
The sitemap never listed them, and a test now holds that line.

Keep the startup assertion for SITE_URL alongside main's new one for
CLOUDFLARE_ORIGIN_SECRET.
@kasnder
kasnder marked this pull request as ready for review August 12, 2026 13:38
claude added 3 commits August 12, 2026 15:34
Main reworked the origin gate after this branch forked, touching the two
files this branch also rewrites.

index.js: main replaced the hard boot failure on a missing
CLOUDFLARE_ORIGIN_SECRET with a warning, so take that posture for the
origin secret. SITE_URL keeps its hard failure — it is a correctness
requirement rather than a security one, since every route builds
canonical and Open Graph URLs from it and would otherwise derive them
from an untrusted Host header.

server.js: purely additive. Main's originGate() middleware and this
branch's rate-limit path predicates landed at the same point in the file;
both are kept, predicates first. Main's 1c52cf2 had already dropped the
analyser exemption from the gate, so nothing needed reconciling there.

npm test: 112 pass, 0 fail. npm audit: 0 vulnerabilities.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Feui1nyEwyTRJ9iEaHXEEx
The methodology page repeated most of About and read as technical
documentation rather than an explanation for a general reader: refresh
intervals, analysis versions, database sizes, counting rules, and
citation guidance. It also described how the analysis reaches an app's
code in more detail than we want to publish.

Drop the page and keep the part About was missing — what a report does
and does not mean — in plainer language, carried by visuals rather than
prose so About stays short:

- A four-step diagram of how an app is analysed.
- Side-by-side cards for what a report can and cannot show, which is the
  distinction most easily lost when a result is quoted.
- The jurisdiction labels as a key, rendered from the same metadata the
  reports use, replacing the longer jurisdiction prose.

The sampling caveat moves into an alert under the diagram. Nav, the
directory, lookup, report and statistics pages, the sitemap and the
README now point at /about and its section anchors.
Pug joins sibling piped-text lines with a newline, but concatenates a
sibling tag with no whitespace at all, so a text line that has lost its
trailing space renders straight into the tag that follows it. Three
places on the app report did: "your usages.Learn more...", "Based
onXray Tracker Database", and "on your phone.Learn more...".

Use inline interpolation instead of a bare tag line, so the space is
written in the template rather than carried by trailing whitespace that
an editor or a reformat can quietly strip.

Add a test that renders every public page and fails on text sitting
directly against a single inline tag. A run of several tags is two
elements next to each other, which is a layout decision, so only single
tags count.
@kasnder
kasnder merged commit 2485c18 into main Aug 12, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants