[object Object]

← back to Homesonspec

mobile: add tracker-drift canary guarding the 'Data Not Collected' privacy label

693125b2ac4e65861b913a0ae3c582ee47f29a47 · 2026-09-04 11:49:37 -0700 · Steve

The Browse WebView blocks trackers with a DENYLIST (lib/tracker-policy.ts, 7 hosts),
so the App Privacy label's truth is contingent on homesonspec.com's tag set. Add a
pixel whose host is not on that list — a new ad network, a self-hosted fbevents.js,
or a server-side-GTM first-party proxy — and it passes silently, with no app rebuild
to catch it, quietly making a filed 'Data Not Collected' label FALSE. On an account
already cited under Guideline 5.6 for a declaration that did not match a binary, that
is the drift that matters.

The canary fetches the live site, extracts every third-party host, and flags any not
covered by the denylist. Exit 1 on drift.

Current run: BLOCKED = www.googletagmanager.com + connect.facebook.net (the two real
trackers, both covered). UNCOVERED = www.drhorton.com + awh.widen.net, both verified
by inspecting the page as <img src> listing photos (builder product catalogue and the
Widen DAM CDN) — asset hosts, not trackers. So the label is accurate today.

Also confirmed while wiring this up: the blocker IS correctly applied. components/
TrackedWebView.tsx PREPENDS the block script to any caller-supplied
injectedJavaScriptBeforeContentLoaded and AND-s !isTrackerUrl() with the caller's
onShouldStartLoadWithRequest, so a caller cannot accidentally disable it — and the
existing suite enforces it architecturally ('the wrapper is the one place WebView is
imported', 'no file outside the wrapper imports WebView', 'every WebView rendered in
app/ is a TrackedWebView'). 12/12 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEhnWQSSkAhSYxWXHJ3MCw

Files touched

Diff

commit 693125b2ac4e65861b913a0ae3c582ee47f29a47
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Sep 4 11:49:37 2026 -0700

    mobile: add tracker-drift canary guarding the 'Data Not Collected' privacy label
    
    The Browse WebView blocks trackers with a DENYLIST (lib/tracker-policy.ts, 7 hosts),
    so the App Privacy label's truth is contingent on homesonspec.com's tag set. Add a
    pixel whose host is not on that list — a new ad network, a self-hosted fbevents.js,
    or a server-side-GTM first-party proxy — and it passes silently, with no app rebuild
    to catch it, quietly making a filed 'Data Not Collected' label FALSE. On an account
    already cited under Guideline 5.6 for a declaration that did not match a binary, that
    is the drift that matters.
    
    The canary fetches the live site, extracts every third-party host, and flags any not
    covered by the denylist. Exit 1 on drift.
    
    Current run: BLOCKED = www.googletagmanager.com + connect.facebook.net (the two real
    trackers, both covered). UNCOVERED = www.drhorton.com + awh.widen.net, both verified
    by inspecting the page as <img src> listing photos (builder product catalogue and the
    Widen DAM CDN) — asset hosts, not trackers. So the label is accurate today.
    
    Also confirmed while wiring this up: the blocker IS correctly applied. components/
    TrackedWebView.tsx PREPENDS the block script to any caller-supplied
    injectedJavaScriptBeforeContentLoaded and AND-s !isTrackerUrl() with the caller's
    onShouldStartLoadWithRequest, so a caller cannot accidentally disable it — and the
    existing suite enforces it architecturally ('the wrapper is the one place WebView is
    imported', 'no file outside the wrapper imports WebView', 'every WebView rendered in
    app/ is a TrackedWebView'). 12/12 tests pass.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01SEhnWQSSkAhSYxWXHJ3MCw
---
 apps/mobile/scripts/tracker-drift-check.mjs | 48 +++++++++++++++++++++++++++++
 1 file changed, 48 insertions(+)

diff --git a/apps/mobile/scripts/tracker-drift-check.mjs b/apps/mobile/scripts/tracker-drift-check.mjs
new file mode 100644
index 00000000..5529aa70
--- /dev/null
+++ b/apps/mobile/scripts/tracker-drift-check.mjs
@@ -0,0 +1,48 @@
+#!/usr/bin/env node
+/**
+ * tracker-drift-check — guards the "Data Not Collected" App Privacy label.
+ *
+ * WHY THIS EXISTS (TK-11155, 2026-09-04): the Browse WebView blocks trackers with a
+ * DENYLIST (lib/tracker-policy.ts, 7 hosts). That makes the label's truth contingent on
+ * homesonspec.com's tag set: add a pixel whose host is not on the denylist — a new ad
+ * network, a self-hosted fbevents.js, or a server-side-GTM first-party proxy — and it
+ * passes silently, with no app rebuild to catch it, quietly making the filed label FALSE.
+ * On an account already cited under Guideline 5.6 for a declaration that did not match a
+ * binary, that is the drift we cannot afford. This flags it.
+ *
+ * READ-ONLY. Exit 0 = clean, 1 = drift found.  Usage: node scripts/tracker-drift-check.mjs
+ */
+import { readFileSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { dirname, join } from 'node:path';
+
+const here = dirname(fileURLToPath(import.meta.url));
+const policy = readFileSync(join(here, '..', 'lib', 'tracker-policy.ts'), 'utf8');
+const DENYLIST = [...policy.matchAll(/'([a-z0-9.-]+\.[a-z]{2,})'/g)].map(m => m[1]);
+const SITE = process.env.HOS_SITE || 'https://homesonspec.com';
+const FIRST_PARTY = new URL(SITE).hostname.replace(/^www\./, '');
+
+const onDenylist = h => DENYLIST.some(d => h === d || h.endsWith('.' + d));
+const isFirstParty = h => h === FIRST_PARTY || h.endsWith('.' + FIRST_PARTY);
+
+const html = await fetch(SITE, { headers: { 'user-agent': 'Mozilla/5.0 (iPhone)' } }).then(r => r.text());
+const hosts = [...new Set([...html.matchAll(/https?:\/\/([a-z0-9.-]+)/gi)].map(m => m[1].toLowerCase()))];
+
+// A host is a RISK if it is third-party and NOT already covered by the denylist:
+// the blocker would let it through, so it could transmit while we declare no collection.
+const uncovered = hosts.filter(h => !isFirstParty(h) && !onDenylist(h));
+const covered = hosts.filter(onDenylist);
+
+console.log(`site: ${SITE}`);
+console.log(`denylist (${DENYLIST.length}): ${DENYLIST.join(', ')}`);
+console.log(`\nBLOCKED third-party hosts present on the page (${covered.length}): ${covered.join(', ') || '(none)'}`);
+console.log(`\nUNCOVERED third-party hosts — these would NOT be blocked (${uncovered.length}):`);
+uncovered.forEach(h => console.log(`  ⚠️  ${h}`));
+
+if (uncovered.length) {
+  console.log(`\nVERDICT: WARN — review each host above. If any transmits user/usage data, the`);
+  console.log(`"Data Not Collected" label is no longer accurate: add it to TRACKER_HOSTS and rebuild,`);
+  console.log(`or amend the App Privacy label. Purely static asset/CDN/font hosts are fine.`);
+  process.exit(1);
+}
+console.log('\nVERDICT: PASS — every third-party host on the page is covered by the denylist.');

← 90ca06fc auto-data-snapshot: 2026-09-04T11:31:34 (65 data files) — ap  ·  back to Homesonspec  ·  mobile: make the tracker-drift canary baseline-aware (was fi ba37b54c →