← back to Ads Dashboard
scripts/scan-ad-signals.mjs
148 lines
#!/usr/bin/env node
/**
* scan-ad-signals.mjs — $0 local ad-platform pixel scanner for ads-dashboard.
*
* Fetches each domain's homepage HTML and fingerprints which paid-ad platforms
* it is running tracking pixels for. NO fabrication: a platform only appears if
* its pixel is actually present in the served HTML.
*
* DW-owned domains -> data/campaigns.dw.json (our own active ad channels; spend=null, needs ad-account API)
* competitor domains -> data/signals.dw.json (competitor ad signals)
*
* Zero dependencies (global fetch). Run: node scripts/scan-ad-signals.mjs
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const DATA_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'data');
// Real DW properties (→ campaigns: our own detected ad channels)
const OWN = ['designerwallcoverings.com', 'apartmentwallpaper.com', 'philipperomano.com'];
// Real wallcovering competitors (→ signals: competitor ad presence)
const COMPETITORS = [
'fschumacher.com', 'thibautdesign.com', 'yorkwallcoverings.com',
'phillipjeffries.com', 'cole-and-son.com',
];
// Pixel fingerprints: platform -> [regexes that prove the ad pixel is present]
const FINGERPRINTS = {
'Google Ads': [/googleadservices\.com\/pagead\/conversion/i, /gtag\(['"]config['"],\s*['"]AW-/i, /google_conversion_id/i, /googleads\.g\.doubleclick\.net/i],
'Meta (Facebook)': [/connect\.facebook\.net\/[a-z_]+\/fbevents\.js/i, /\bfbq\(/i],
'TikTok': [/analytics\.tiktok\.com/i, /ttq\.load\(/i],
'Pinterest': [/s\.pinimg\.com\/ct\//i, /\bpintrk\(/i],
'LinkedIn': [/snap\.licdn\.com/i, /_linkedin_partner_id/i],
'X (Twitter)': [/static\.ads-twitter\.com/i, /\btwq\(/i],
'Microsoft (Bing)':[/bat\.bing\.com/i, /\buetq\b/i],
'Snapchat': [/sc-static\.net\/scevent/i, /\bsnaptr\(/i],
'Reddit': [/redditstatic\.com\/ads/i, /\brdt\(/i],
};
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36';
async function fetchHTML(domain) {
const url = 'https://' + domain + '/';
try {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 15000);
const r = await fetch(url, { headers: { 'User-Agent': UA, 'Accept': 'text/html' }, redirect: 'follow', signal: ctrl.signal });
clearTimeout(t);
if (!r.ok) return { ok: false, reason: 'http ' + r.status };
return { ok: true, html: await r.text() };
} catch (e) { return { ok: false, reason: e.name === 'AbortError' ? 'timeout' : e.message }; }
}
function detect(html) {
const hit = [];
for (const [platform, regexes] of Object.entries(FINGERPRINTS)) {
if (regexes.some(re => re.test(html))) hit.push(platform);
}
return hit;
}
// Fetch a raw JS/text URL (GTM container) and return its body.
async function fetchText(url) {
try {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 15000);
const r = await fetch(url, { headers: { 'User-Agent': UA }, redirect: 'follow', signal: ctrl.signal });
clearTimeout(t);
return r.ok ? await r.text() : '';
} catch { return ''; }
}
// Pull GTM container ids (GTM-XXXX) referenced in the page.
function gtmIds(html) {
const ids = new Set();
const re = /GTM-[A-Z0-9]+/g; let m;
while ((m = re.exec(html))) ids.add(m[0]);
return [...ids];
}
async function scan(domain) {
const res = await fetchHTML(domain);
if (!res.ok) return { domain, ok: false, reason: res.reason, platforms: [] };
const found = new Set(detect(res.html));
// Second hop: many ad pixels are injected by Google Tag Manager, not inline.
// Fetch each GTM container and fingerprint the tags configured inside it.
const ids = gtmIds(res.html);
for (const id of ids) {
const container = await fetchText('https://www.googletagmanager.com/gtm.js?id=' + id);
if (container) detect(container).forEach(p => found.add(p));
// Google Ads conversion linker inside a container:
if (/AW-\d{6,}/.test(container)) found.add('Google Ads');
}
if (ids.length) found.add('Google Tag Manager');
return { domain, ok: true, platforms: [...found], gtm: ids };
}
(async () => {
const nowISO = new Date().toISOString();
console.log('[scan] fetching', OWN.length + COMPETITORS.length, 'domains…');
const ownResults = await Promise.all(OWN.map(scan));
const compResults = await Promise.all(COMPETITORS.map(scan));
// DW-owned detected platforms -> campaigns (our active ad channels; spend unknown w/o ad-account API)
const campaigns = [];
for (const r of ownResults) {
for (const p of r.platforms) {
campaigns.push({
name: p + ' — ' + r.domain.replace(/\.com$/, ''),
domain: r.domain,
platform: p,
status: 'active', // pixel is live on the site
spend: null, // honest: real $ needs Google Ads / Meta API (renders "—")
created_at: nowISO,
detected_by: 'pixel-scan',
});
}
console.log(' own', r.domain, r.ok ? r.platforms.join(', ') || '(no ad pixels)' : 'FAIL: ' + r.reason);
}
// Competitor detected platforms -> signals
const signals = [];
for (const r of compResults) {
for (const p of r.platforms) {
signals.push({
advertiser: r.domain,
platform: p,
url: 'https://' + r.domain + '/',
seen_at: nowISO,
detected_by: 'pixel-scan',
});
}
console.log(' comp', r.domain, r.ok ? r.platforms.join(', ') || '(no ad pixels)' : 'FAIL: ' + r.reason);
}
const campaignsOut = { source: 'pixel-scan (DW-owned domains)', present: true, generated_at: nowISO,
note: 'Active ad-platform presence detected via tracking pixels. Spend is null until a Google Ads / Meta Ads account is connected — not fabricated.',
campaigns };
const signalsOut = { source: 'advertising-signals (pixel-scan, competitor domains)', present: true, generated_at: nowISO,
scanned: COMPETITORS, signals };
fs.writeFileSync(path.join(DATA_DIR, 'campaigns.dw.json'), JSON.stringify(campaignsOut, null, 2));
fs.writeFileSync(path.join(DATA_DIR, 'signals.dw.json'), JSON.stringify(signalsOut, null, 2));
console.log(`[scan] wrote ${campaigns.length} campaign rows, ${signals.length} signal rows. $0 (local).`);
})();