[object Object]

← back to Homesonspec

Homes on Spec: actually test the tracker-blocking script (TK-10387)

f2de197d2bf221e7dfced05f65b7c4900ee79e03 · 2026-09-04 08:17:39 -0700 · Steve

The App Store 'Data Not Collected' claim rests on the injected TRACKER_BLOCK script
working. Until now only isTrackerUrl() — the native-side URL filter — had tests. The
script doing most of the work (neutering gtag/fbq/dataLayer and intercepting
fetch/XHR/Image/sendBeacon inside the page) had none, so 'it looks right' was the only
evidence behind a claim we make to Apple.

- lib/tracker-block.ts now exports buildTrackerBlock(hosts) and has ZERO imports, so
  node --test can load it directly (Node's ESM resolver requires explicit extensions,
  which the TS/Metro resolver does not use). TrackedWebView composes it from the real
  TRACKER_HOSTS, so the shipped behaviour is unchanged.
- lib/tracker-block.test.mjs runs the REAL script text, built from the REAL host list,
  in a vm sandbox with a fake window/document, and asserts observable behaviour rather
  than string matching: gtag/fbq/dataLayer.push are no-ops (and dataLayer does not
  accumulate), tracker fetch short-circuits to an empty 204 while first-party fetch
  reaches the real implementation, GA4's sendBeacon transport is refused, tracker XHR
  never sends, the Facebook tr pixel never gets a real src, subdomains of tracker hosts
  are blocked, and a lookalike domain is NOT blocked so the filter cannot over-reach
  and break the site.

12/12 tests pass; tsc --noEmit clean. This verifies the MECHANISM only — the panel's
binding condition still stands: do not file 'Data Not Collected' until a full-device
proxy capture across all four tabs is clean.

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

Files touched

Diff

commit f2de197d2bf221e7dfced05f65b7c4900ee79e03
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Sep 4 08:17:39 2026 -0700

    Homes on Spec: actually test the tracker-blocking script (TK-10387)
    
    The App Store 'Data Not Collected' claim rests on the injected TRACKER_BLOCK script
    working. Until now only isTrackerUrl() — the native-side URL filter — had tests. The
    script doing most of the work (neutering gtag/fbq/dataLayer and intercepting
    fetch/XHR/Image/sendBeacon inside the page) had none, so 'it looks right' was the only
    evidence behind a claim we make to Apple.
    
    - lib/tracker-block.ts now exports buildTrackerBlock(hosts) and has ZERO imports, so
      node --test can load it directly (Node's ESM resolver requires explicit extensions,
      which the TS/Metro resolver does not use). TrackedWebView composes it from the real
      TRACKER_HOSTS, so the shipped behaviour is unchanged.
    - lib/tracker-block.test.mjs runs the REAL script text, built from the REAL host list,
      in a vm sandbox with a fake window/document, and asserts observable behaviour rather
      than string matching: gtag/fbq/dataLayer.push are no-ops (and dataLayer does not
      accumulate), tracker fetch short-circuits to an empty 204 while first-party fetch
      reaches the real implementation, GA4's sendBeacon transport is refused, tracker XHR
      never sends, the Facebook tr pixel never gets a real src, subdomains of tracker hosts
      are blocked, and a lookalike domain is NOT blocked so the filter cannot over-reach
      and break the site.
    
    12/12 tests pass; tsc --noEmit clean. This verifies the MECHANISM only — the panel's
    binding condition still stands: do not file 'Data Not Collected' until a full-device
    proxy capture across all four tabs is clean.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01QfGYEoLBywwJD1nfrHe1on
---
 apps/mobile/components/TrackedWebView.tsx          | 122 +-----------------
 .../1-browse.png                                   | Bin 0 -> 1894668 bytes
 .../BUILD3-stills-2026-09-04T13-10-12Z/2-saved.png | Bin 0 -> 145762 bytes
 .../BUILD3-stills-2026-09-04T13-10-12Z/3-map.png   | Bin 0 -> 1657914 bytes
 .../4-alerts.png                                   | Bin 0 -> 303248 bytes
 .../5-browse-return.png                            | Bin 0 -> 1894668 bytes
 ...-DEVICE-journey-BUILD3-2026-09-04T13-10-12Z.mov | Bin 0 -> 1746252 bytes
 .../privacy-sweep-BUILD3-2026-09-04T13-10-12Z.jpg  | Bin 0 -> 195531 bytes
 apps/mobile/lib/tracker-block.test.mjs             | 137 +++++++++++++++++++++
 apps/mobile/lib/tracker-block.ts                   | 129 +++++++++++++++++++
 10 files changed, 268 insertions(+), 120 deletions(-)

diff --git a/apps/mobile/components/TrackedWebView.tsx b/apps/mobile/components/TrackedWebView.tsx
index 3a6d187e..f6de610e 100644
--- a/apps/mobile/components/TrackedWebView.tsx
+++ b/apps/mobile/components/TrackedWebView.tsx
@@ -21,128 +21,10 @@
 import { WebView, WebViewProps } from 'react-native-webview';
 import { forwardRef } from 'react';
 import { isTrackerUrl, TRACKER_HOSTS } from '../lib/tracker-policy';
+import { buildTrackerBlock } from '../lib/tracker-block';
 
-/**
- * Runs BEFORE any page script. Neutralizes analytics/ad trackers two ways so
- * the app genuinely transmits nothing:
- *   1. Stub the JS APIs (gtag / dataLayer / fbq / google_tag_manager) into no-ops.
- *   2. Intercept every network primitive WKWebView exposes to page JS —
- *      script injection (createElement), Image()/img.src pixels (the Facebook
- *      <noscript> <img> fallback path too), fetch, XMLHttpRequest, and
- *      navigator.sendBeacon (GA4's default transport) — and drop any request
- *      whose URL matches a tracker host.
- * onShouldStartLoadWithRequest handles main-frame/navigation-level blocks; this
- * handles the sub-resource requests WKWebView never surfaces to native.
- */
-const TRACKER_BLOCK = `
-(function() {
-  try {
-    var HOSTS = ${JSON.stringify(TRACKER_HOSTS)};
-    function blocked(u) {
-      try {
-        var hostname = new URL(String(u), window.location.href).hostname.toLowerCase();
-        for (var i = 0; i < HOSTS.length; i++) {
-          if (hostname === HOSTS[i] || hostname.slice(-(HOSTS[i].length + 1)) === '.' + HOSTS[i]) return true;
-        }
-      } catch (e) {}
-      return false;
-    }
-
-    // 1. Stub tracker JS APIs
-    var noop = function() {};
-    window.dataLayer = window.dataLayer || [];
-    window.dataLayer.push = noop;
-    window.gtag = noop;
-    window.ga = noop;
-    window.google_tag_manager = {};
-    window.fbq = function() {}; window.fbq.queue = []; window.fbq.loaded = true;
-    window._fbq = window.fbq;
-
-    // 2a. Block <script>/<img>/<iframe> pointed at a tracker host
-    var _createElement = document.createElement.bind(document);
-    document.createElement = function(tag) {
-      var el = _createElement(tag);
-      var t = String(tag || '').toLowerCase();
-      if (t === 'script' || t === 'img' || t === 'iframe') {
-        try {
-          var proto = Object.getPrototypeOf(el);
-          var desc = Object.getOwnPropertyDescriptor(proto, 'src')
-                  || Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'src');
-          if (desc && desc.set) {
-            Object.defineProperty(el, 'src', {
-              configurable: true,
-              get: function() { return desc.get ? desc.get.call(el) : ''; },
-              set: function(v) { if (!blocked(v)) desc.set.call(el, v); }
-            });
-          }
-        } catch (e) {}
-      }
-      return el;
-    };
-
-    // 2b. Block Image()/new Image().src pixels (Facebook tr pixel, GA hits)
-    var _Image = window.Image;
-    if (_Image) {
-      window.Image = function() {
-        var img = new _Image();
-        try {
-          var d = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src');
-          if (d && d.set) {
-            Object.defineProperty(img, 'src', {
-              configurable: true,
-              get: function() { return d.get.call(img); },
-              set: function(v) { if (!blocked(v)) d.set.call(img, v); }
-            });
-          }
-        } catch (e) {}
-        return img;
-      };
-    }
-
-    // 2c. Block fetch
-    var _fetch = window.fetch;
-    if (_fetch) {
-      window.fetch = function(input) {
-        var url = (input && input.url) ? input.url : input;
-        if (blocked(url)) return Promise.resolve(new Response('', { status: 204 }));
-        return _fetch.apply(this, arguments);
-      };
-    }
-
-    // 2d. Block XHR
-    var _open = XMLHttpRequest.prototype.open;
-    XMLHttpRequest.prototype.open = function(method, url) {
-      this.__blocked = blocked(url);
-      return _open.apply(this, arguments);
-    };
-    var _send = XMLHttpRequest.prototype.send;
-    XMLHttpRequest.prototype.send = function() { if (this.__blocked) return; return _send.apply(this, arguments); };
-
-    // 2e. Block sendBeacon (GA4 default transport)
-    if (navigator.sendBeacon) {
-      var _beacon = navigator.sendBeacon.bind(navigator);
-      navigator.sendBeacon = function(url) { if (blocked(url)) return false; return _beacon.apply(this, arguments); };
-    }
+const TRACKER_BLOCK = buildTrackerBlock(TRACKER_HOSTS);
 
-    // 2f. Strip any tracker <img>/<script> that slips into the DOM (e.g. <noscript> fallback)
-    try {
-      var obs = new MutationObserver(function(muts) {
-        muts.forEach(function(m) {
-          for (var i = 0; i < m.addedNodes.length; i++) {
-            var n = m.addedNodes[i];
-            if (n && n.tagName && (n.tagName === 'IMG' || n.tagName === 'SCRIPT' || n.tagName === 'IFRAME')) {
-              var s = n.getAttribute && n.getAttribute('src');
-              if (s && blocked(s)) { n.setAttribute('src', ''); if (n.parentNode) n.parentNode.removeChild(n); }
-            }
-          }
-        });
-      });
-      obs.observe(document.documentElement || document, { childList: true, subtree: true });
-    } catch (e) {}
-  } catch (e) {}
-  true;
-})();
-`;
 export type TrackedWebViewProps = WebViewProps;
 
 const TrackedWebView = forwardRef<WebView, TrackedWebViewProps>(function TrackedWebView(
diff --git a/apps/mobile/device-proof-evidence/BUILD3-stills-2026-09-04T13-10-12Z/1-browse.png b/apps/mobile/device-proof-evidence/BUILD3-stills-2026-09-04T13-10-12Z/1-browse.png
new file mode 100644
index 00000000..fedd318f
Binary files /dev/null and b/apps/mobile/device-proof-evidence/BUILD3-stills-2026-09-04T13-10-12Z/1-browse.png differ
diff --git a/apps/mobile/device-proof-evidence/BUILD3-stills-2026-09-04T13-10-12Z/2-saved.png b/apps/mobile/device-proof-evidence/BUILD3-stills-2026-09-04T13-10-12Z/2-saved.png
new file mode 100644
index 00000000..6c5a03ee
Binary files /dev/null and b/apps/mobile/device-proof-evidence/BUILD3-stills-2026-09-04T13-10-12Z/2-saved.png differ
diff --git a/apps/mobile/device-proof-evidence/BUILD3-stills-2026-09-04T13-10-12Z/3-map.png b/apps/mobile/device-proof-evidence/BUILD3-stills-2026-09-04T13-10-12Z/3-map.png
new file mode 100644
index 00000000..28e8ca77
Binary files /dev/null and b/apps/mobile/device-proof-evidence/BUILD3-stills-2026-09-04T13-10-12Z/3-map.png differ
diff --git a/apps/mobile/device-proof-evidence/BUILD3-stills-2026-09-04T13-10-12Z/4-alerts.png b/apps/mobile/device-proof-evidence/BUILD3-stills-2026-09-04T13-10-12Z/4-alerts.png
new file mode 100644
index 00000000..53715f25
Binary files /dev/null and b/apps/mobile/device-proof-evidence/BUILD3-stills-2026-09-04T13-10-12Z/4-alerts.png differ
diff --git a/apps/mobile/device-proof-evidence/BUILD3-stills-2026-09-04T13-10-12Z/5-browse-return.png b/apps/mobile/device-proof-evidence/BUILD3-stills-2026-09-04T13-10-12Z/5-browse-return.png
new file mode 100644
index 00000000..fedd318f
Binary files /dev/null and b/apps/mobile/device-proof-evidence/BUILD3-stills-2026-09-04T13-10-12Z/5-browse-return.png differ
diff --git a/apps/mobile/device-proof-evidence/homesonspec-DEVICE-journey-BUILD3-2026-09-04T13-10-12Z.mov b/apps/mobile/device-proof-evidence/homesonspec-DEVICE-journey-BUILD3-2026-09-04T13-10-12Z.mov
new file mode 100644
index 00000000..0d8738a6
Binary files /dev/null and b/apps/mobile/device-proof-evidence/homesonspec-DEVICE-journey-BUILD3-2026-09-04T13-10-12Z.mov differ
diff --git a/apps/mobile/device-proof-evidence/privacy-sweep-BUILD3-2026-09-04T13-10-12Z.jpg b/apps/mobile/device-proof-evidence/privacy-sweep-BUILD3-2026-09-04T13-10-12Z.jpg
new file mode 100644
index 00000000..2d953ff1
Binary files /dev/null and b/apps/mobile/device-proof-evidence/privacy-sweep-BUILD3-2026-09-04T13-10-12Z.jpg differ
diff --git a/apps/mobile/lib/tracker-block.test.mjs b/apps/mobile/lib/tracker-block.test.mjs
new file mode 100644
index 00000000..4cfe4e5e
--- /dev/null
+++ b/apps/mobile/lib/tracker-block.test.mjs
@@ -0,0 +1,137 @@
+/**
+ * Behavioural tests for TRACKER_BLOCK — the script actually injected into every WebView.
+ *
+ * `tracker-policy.test.mjs` only covers isTrackerUrl(), which is the NATIVE-side URL
+ * filter. That left the thing doing most of the work — the injected page script that
+ * neuters gtag/fbq and intercepts fetch/XHR/Image/sendBeacon — with no test at all.
+ * Since the App Store "Data Not Collected" claim rests on this script actually working,
+ * "it looks right" was not good enough.
+ *
+ * These tests run the real script text in a sandbox with a fake window/document and
+ * assert observable behaviour: tracker requests are dropped, first-party requests are
+ * untouched.
+ */
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import vm from 'node:vm';
+
+import { buildTrackerBlock } from './tracker-block.ts';
+import { TRACKER_HOSTS } from './tracker-policy.ts';
+
+// Test the script exactly as shipped: built from the REAL host list.
+const TRACKER_BLOCK = buildTrackerBlock(TRACKER_HOSTS);
+
+const TRACKER = 'https://www.googletagmanager.com/gtag/js?id=G-ZGFNZ3RQ6S';
+const PIXEL = 'https://connect.facebook.net/en_US/fbevents.js';
+const FIRST_PARTY = 'https://homesonspec.com/api/listings';
+
+/** Build a minimal browser-ish sandbox, run TRACKER_BLOCK in it, return the context. */
+function runBlock() {
+  const calls = { fetch: [], beacon: [], xhrSend: [], imgSrc: [] };
+
+  class FakeXHR {
+    open(method, url) { this.__url = url; }
+    send() { calls.xhrSend.push(this.__url); }
+  }
+
+  class FakeImage {
+    constructor() { this._src = ''; }
+    set src(v) { this._src = v; calls.imgSrc.push(v); }
+    get src() { return this._src; }
+  }
+
+  const el = () => ({ _attrs: {}, setAttribute(k, v) { this._attrs[k] = v; }, getAttribute(k) { return this._attrs[k]; } });
+
+  const sandbox = {
+    calls,
+    Response: class { constructor(body, init) { this.body = body; this.status = init?.status ?? 200; } },
+    Promise,
+    URL,
+    XMLHttpRequest: FakeXHR,
+    MutationObserver: class { observe() {} },
+    console,
+    document: {
+      documentElement: {},
+      createElement: (tag) => ({ tagName: String(tag).toUpperCase(), ...el() }),
+    },
+  };
+  sandbox.window = sandbox;
+  sandbox.Image = FakeImage;
+  sandbox.HTMLImageElement = FakeImage;
+  sandbox.HTMLElement = function () {};
+  sandbox.location = { href: 'https://homesonspec.com/' };
+  sandbox.navigator = { sendBeacon: (url) => { calls.beacon.push(url); return true; } };
+  sandbox.fetch = (input) => { calls.fetch.push(input?.url ?? input); return Promise.resolve('REAL'); };
+
+  vm.createContext(sandbox);
+  vm.runInContext(TRACKER_BLOCK, sandbox);
+  return sandbox;
+}
+
+test('gtag / fbq / dataLayer are neutered into no-ops', () => {
+  const w = runBlock();
+  assert.equal(typeof w.gtag, 'function');
+  assert.equal(typeof w.fbq, 'function');
+  assert.doesNotThrow(() => w.gtag('event', 'page_view', { send_to: 'G-XXXX' }));
+  assert.doesNotThrow(() => w.fbq('track', 'PageView'));
+  assert.doesNotThrow(() => w.dataLayer.push({ event: 'x' }));
+  // dataLayer.push must not actually accumulate — a real GTM would drain it.
+  assert.equal(w.dataLayer.length, 0, 'dataLayer.push should be a no-op, not a real push');
+});
+
+test('fetch to a tracker host is dropped; first-party fetch passes through', async () => {
+  const w = runBlock();
+  const blocked = await w.fetch(TRACKER);
+  assert.equal(blocked.status, 204, 'tracker fetch should be short-circuited with an empty 204');
+  assert.deepEqual(w.calls.fetch, [], 'the real fetch must never see a tracker URL');
+
+  const real = await w.fetch(FIRST_PARTY);
+  assert.equal(real, 'REAL', 'first-party fetch must reach the real implementation');
+  assert.deepEqual(w.calls.fetch, [FIRST_PARTY]);
+});
+
+test('sendBeacon (GA4 default transport) is dropped for trackers only', () => {
+  const w = runBlock();
+  assert.equal(w.navigator.sendBeacon('https://region1.google-analytics.com/g/collect'), false);
+  assert.deepEqual(w.calls.beacon, [], 'no tracker beacon may reach the real sendBeacon');
+
+  assert.equal(w.navigator.sendBeacon(FIRST_PARTY), true);
+  assert.deepEqual(w.calls.beacon, [FIRST_PARTY]);
+});
+
+test('XHR to a tracker never sends; first-party XHR does', () => {
+  const w = runBlock();
+  const bad = new w.XMLHttpRequest();
+  bad.open('POST', PIXEL);
+  bad.send();
+  assert.deepEqual(w.calls.xhrSend, [], 'tracker XHR must not send');
+
+  const good = new w.XMLHttpRequest();
+  good.open('GET', FIRST_PARTY);
+  good.send();
+  assert.deepEqual(w.calls.xhrSend, [FIRST_PARTY]);
+});
+
+test('Image() pixel to a tracker is dropped; a first-party image still loads', () => {
+  const w = runBlock();
+  const px = new w.Image();
+  px.src = 'https://www.facebook.com/tr?id=123&ev=PageView';
+  assert.deepEqual(w.calls.imgSrc, [], 'the Facebook tr pixel must never get a real src');
+
+  const img = new w.Image();
+  img.src = 'https://homesonspec.com/hero.jpg';
+  assert.deepEqual(w.calls.imgSrc, ['https://homesonspec.com/hero.jpg']);
+});
+
+test('a subdomain of a tracker host is blocked too (not just the bare host)', async () => {
+  const w = runBlock();
+  const r = await w.fetch('https://analytics.google-analytics.com/g/collect?v=2');
+  assert.equal(r.status, 204);
+  assert.deepEqual(w.calls.fetch, []);
+});
+
+test('a lookalike domain is NOT blocked (the filter must not over-reach)', async () => {
+  const w = runBlock();
+  const r = await w.fetch('https://notgoogletagmanager.com/thing.js');
+  assert.equal(r, 'REAL', 'a lookalike host must pass through — over-blocking breaks real sites');
+});
diff --git a/apps/mobile/lib/tracker-block.ts b/apps/mobile/lib/tracker-block.ts
new file mode 100644
index 00000000..8ef053f6
--- /dev/null
+++ b/apps/mobile/lib/tracker-block.ts
@@ -0,0 +1,129 @@
+/**
+ * Runs BEFORE any page script. Neutralizes analytics/ad trackers two ways so
+ * the app genuinely transmits nothing:
+ *   1. Stub the JS APIs (gtag / dataLayer / fbq / google_tag_manager) into no-ops.
+ *   2. Intercept every network primitive WKWebView exposes to page JS —
+ *      script injection (createElement), Image()/img.src pixels (the Facebook
+ *      <noscript> <img> fallback path too), fetch, XMLHttpRequest, and
+ *      navigator.sendBeacon (GA4's default transport) — and drop any request
+ *      whose URL matches a tracker host.
+ * onShouldStartLoadWithRequest handles main-frame/navigation-level blocks; this
+ * handles the sub-resource requests WKWebView never surfaces to native.
+ */
+/**
+ * Built as a FUNCTION of the host list rather than importing it, so this module has
+ * zero imports and can be loaded directly by `node --test` (Node's ESM resolver wants
+ * explicit file extensions, which the TS/Metro resolver does not use). That is what
+ * lets `tracker-block.test.mjs` execute the real script text in a sandbox.
+ */
+export function buildTrackerBlock(hosts: readonly string[]): string {
+  return `
+(function() {
+  try {
+    var HOSTS = ${JSON.stringify(hosts)};
+    function blocked(u) {
+      try {
+        var hostname = new URL(String(u), window.location.href).hostname.toLowerCase();
+        for (var i = 0; i < HOSTS.length; i++) {
+          if (hostname === HOSTS[i] || hostname.slice(-(HOSTS[i].length + 1)) === '.' + HOSTS[i]) return true;
+        }
+      } catch (e) {}
+      return false;
+    }
+
+    // 1. Stub tracker JS APIs
+    var noop = function() {};
+    window.dataLayer = window.dataLayer || [];
+    window.dataLayer.push = noop;
+    window.gtag = noop;
+    window.ga = noop;
+    window.google_tag_manager = {};
+    window.fbq = function() {}; window.fbq.queue = []; window.fbq.loaded = true;
+    window._fbq = window.fbq;
+
+    // 2a. Block <script>/<img>/<iframe> pointed at a tracker host
+    var _createElement = document.createElement.bind(document);
+    document.createElement = function(tag) {
+      var el = _createElement(tag);
+      var t = String(tag || '').toLowerCase();
+      if (t === 'script' || t === 'img' || t === 'iframe') {
+        try {
+          var proto = Object.getPrototypeOf(el);
+          var desc = Object.getOwnPropertyDescriptor(proto, 'src')
+                  || Object.getOwnPropertyDescriptor(HTMLElement.prototype, 'src');
+          if (desc && desc.set) {
+            Object.defineProperty(el, 'src', {
+              configurable: true,
+              get: function() { return desc.get ? desc.get.call(el) : ''; },
+              set: function(v) { if (!blocked(v)) desc.set.call(el, v); }
+            });
+          }
+        } catch (e) {}
+      }
+      return el;
+    };
+
+    // 2b. Block Image()/new Image().src pixels (Facebook tr pixel, GA hits)
+    var _Image = window.Image;
+    if (_Image) {
+      window.Image = function() {
+        var img = new _Image();
+        try {
+          var d = Object.getOwnPropertyDescriptor(HTMLImageElement.prototype, 'src');
+          if (d && d.set) {
+            Object.defineProperty(img, 'src', {
+              configurable: true,
+              get: function() { return d.get.call(img); },
+              set: function(v) { if (!blocked(v)) d.set.call(img, v); }
+            });
+          }
+        } catch (e) {}
+        return img;
+      };
+    }
+
+    // 2c. Block fetch
+    var _fetch = window.fetch;
+    if (_fetch) {
+      window.fetch = function(input) {
+        var url = (input && input.url) ? input.url : input;
+        if (blocked(url)) return Promise.resolve(new Response('', { status: 204 }));
+        return _fetch.apply(this, arguments);
+      };
+    }
+
+    // 2d. Block XHR
+    var _open = XMLHttpRequest.prototype.open;
+    XMLHttpRequest.prototype.open = function(method, url) {
+      this.__blocked = blocked(url);
+      return _open.apply(this, arguments);
+    };
+    var _send = XMLHttpRequest.prototype.send;
+    XMLHttpRequest.prototype.send = function() { if (this.__blocked) return; return _send.apply(this, arguments); };
+
+    // 2e. Block sendBeacon (GA4 default transport)
+    if (navigator.sendBeacon) {
+      var _beacon = navigator.sendBeacon.bind(navigator);
+      navigator.sendBeacon = function(url) { if (blocked(url)) return false; return _beacon.apply(this, arguments); };
+    }
+
+    // 2f. Strip any tracker <img>/<script> that slips into the DOM (e.g. <noscript> fallback)
+    try {
+      var obs = new MutationObserver(function(muts) {
+        muts.forEach(function(m) {
+          for (var i = 0; i < m.addedNodes.length; i++) {
+            var n = m.addedNodes[i];
+            if (n && n.tagName && (n.tagName === 'IMG' || n.tagName === 'SCRIPT' || n.tagName === 'IFRAME')) {
+              var s = n.getAttribute && n.getAttribute('src');
+              if (s && blocked(s)) { n.setAttribute('src', ''); if (n.parentNode) n.parentNode.removeChild(n); }
+            }
+          }
+        });
+      });
+      obs.observe(document.documentElement || document, { childList: true, subtree: true });
+    } catch (e) {}
+  } catch (e) {}
+  true;
+})();
+`;
+}

← 16d8e5af auto-data-snapshot: 2026-09-04T06:32:13 (1 data files) — app  ·  back to Homesonspec  ·  mobile: cap rendered map markers (120, densest-first) + lowe 4df903f1 →