Add tracker/company reverse lookup, methodology page, sitemap and social metadata - #16
Conversation
|
CI was red on the Pushed a lockfile-only fix in a separate commit (671e791):
Both are patch bumps inside the existing semver ranges ( 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 |
…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
671e791 to
104811d
Compare
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.
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.
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.jsbuilds an inverted index — tracker → apps and company → apps — from the stored analyses. It is normalised (app metadata stored once, referenced by bundle ID), cached underCACHE_DIRin 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:
/trackersand/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/:slugand/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.comvsMob 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.isSystemSignatureis exported for that.Methodology page
/methodologydocuments what a figure on this site means:Linked from the nav and from
/about.Discoverability
/sitemap.xmlre-enabled (was commented out) and extended to lookup and reference pages, with reallastmodtimestamps instead of "now" for every URL./robots.txtpointing at the sitemap.SITE_URLpins the public origin so these URLs arehttps://behind a TLS-terminating proxy; it falls back to the request otherwise.Incidental
jurisdiction.resolveTrackerNameis 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
/statisticson 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.Generated by Claude Code