[object Object]

← back to Interiordesignershowroom

add tracked-link coverage canary: samples live /go redirects, alerts on worsening CJ-tracking coverage (CNCP), daily 3:15am

325e3999d24740d6243cc62133dc295114afd0ed · 2026-08-02 00:46:18 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 325e3999d24740d6243cc62133dc295114afd0ed
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun Aug 2 00:46:18 2026 -0700

    add tracked-link coverage canary: samples live /go redirects, alerts on worsening CJ-tracking coverage (CNCP), daily 3:15am
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 scripts/tracked-coverage-canary.js | 91 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 91 insertions(+)

diff --git a/scripts/tracked-coverage-canary.js b/scripts/tracked-coverage-canary.js
new file mode 100644
index 0000000..5be3442
--- /dev/null
+++ b/scripts/tracked-coverage-canary.js
@@ -0,0 +1,91 @@
+#!/usr/bin/env node
+// Tracked-link coverage canary for Interior Designer's Showroom.
+// The whole affiliate model earns $0 if /go/:id redirects to RAW merchant URLs
+// instead of CJ tracked click-links (which only mint once advertisers are JOINED
+// in CJ). This canary samples the LIVE site's /go redirects and measures what
+// fraction land on a CJ tracked domain. HTTP-only, read-only — no DB, no tunnel.
+//
+// Classifies OK (>=50% tracked) / WARN (1-49%) / CRITICAL (0%). Alerts (CNCP
+// parking-lot card) only on a worsening transition, never on steady-state or
+// recovery. Writes data/coverage-canary.json every run (heartbeat + last state).
+'use strict';
+const https = require('https');
+const fs = require('fs');
+const path = require('path');
+
+const SITE = 'https://interiordesignershowroom.com';
+const CNCP = 'http://127.0.0.1:3333/api/parking-lot';
+const STATE = path.join(__dirname, '..', 'data', 'coverage-canary.json');
+const CJ_TRACKED = /dpbolvw|anrdoezrs|kqzyfj|tkqlhce|jdoqocy|cj\.com|emjcd|ftjcd|awltovhc/i;
+const SAMPLE = 20;
+
+function get(url, { method = 'GET', followRedirect = true } = {}) {
+  return new Promise((resolve) => {
+    const req = https.request(url, { method, timeout: 10000 }, (res) => {
+      let body = '';
+      res.on('data', (c) => { body += c; });
+      res.on('end', () => resolve({ status: res.statusCode, headers: res.headers, body }));
+    });
+    req.on('error', () => resolve({ status: 0, headers: {}, body: '' }));
+    req.on('timeout', () => { req.destroy(); resolve({ status: 0, headers: {}, body: '' }); });
+    req.end();
+  });
+}
+
+async function main() {
+  // 1. harvest product ids off the live /shop grid
+  const shop = await get(`${SITE}/shop`);
+  const ids = [...new Set((shop.body.match(/data-id="(\d+)"/g) || [])
+    .map((m) => m.match(/\d+/)[0]))];
+  if (!ids.length) { return finish('UNKNOWN', 0, 0, 'could not read product ids from /shop'); }
+
+  // 2. sample /go/:id redirects, inspect the Location header (302 -> merchant)
+  const pick = ids.sort(() => 0.5 - Math.random()).slice(0, SAMPLE);
+  let tracked = 0, checked = 0;
+  for (const id of pick) {
+    const r = await get(`${SITE}/go/${id}`, { method: 'HEAD' });
+    const loc = r.headers.location || '';
+    if (!loc) continue;
+    checked++;
+    if (CJ_TRACKED.test(loc)) tracked++;
+  }
+  if (!checked) return finish('UNKNOWN', 0, 0, 'no /go redirects resolved');
+
+  const pct = Math.round((tracked / checked) * 100);
+  const status = pct === 0 ? 'CRITICAL' : pct < 50 ? 'WARN' : 'OK';
+  return finish(status, tracked, checked, `${pct}% tracked (${tracked}/${checked})`);
+}
+
+function rank(s) { return { OK: 0, WARN: 1, CRITICAL: 2, UNKNOWN: -1 }[s] ?? -1; }
+
+async function finish(status, tracked, checked, detail) {
+  let prev = {};
+  try { prev = JSON.parse(fs.readFileSync(STATE, 'utf8')); } catch (e) {}
+  const now = { status, tracked, checked, detail, ts: new Date().toISOString() };
+  try { fs.mkdirSync(path.dirname(STATE), { recursive: true }); fs.writeFileSync(STATE, JSON.stringify(now, null, 2)); } catch (e) {}
+
+  // alert ONLY on a worsening transition (e.g. OK->WARN, WARN->CRITICAL)
+  const worsened = rank(status) > rank(prev.status || 'OK');
+  console.log(`[tracked-coverage] ${status} — ${detail}${worsened ? '  (WORSENED, alerting)' : ''}`);
+  if (worsened && (status === 'WARN' || status === 'CRITICAL')) {
+    const title = `IDC affiliate tracking ${status}: ${detail}`;
+    const note = `interiordesignershowroom.com /go redirects are ${detail}. `
+      + `Untracked links earn $0 commission. Fix = approve CJ advertiser programs (Wayfair, Herman Miller, etc.) `
+      + `in members.cj.com, then re-run scripts/ingest-cj-catalog.js. Prev: ${prev.status || 'n/a'}.`;
+    await postCncp(title, note);
+  }
+  process.exit(0);
+}
+
+function postCncp(title, note) {
+  return new Promise((resolve) => {
+    const data = JSON.stringify({ project: 'interiordesignershowroom', title, note, source: 'tracked-coverage-canary' });
+    const req = require('http').request(CNCP, {
+      method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) }, timeout: 5000,
+    }, (res) => { res.on('data', () => {}); res.on('end', resolve); });
+    req.on('error', resolve); req.on('timeout', () => { req.destroy(); resolve(); });
+    req.write(data); req.end();
+  });
+}
+
+main().catch((e) => { console.error(e); process.exit(1); });

← 64a5449 feat: affiliate cart — collect pieces, checkout opens each p  ·  back to Interiordesignershowroom  ·  feat: auto guide generator — gen-guide.js builds a room (sha 213ca8b →