← back to Interiordesignershowroom
scripts/tracked-coverage-canary.js
92 lines
#!/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); });