← back to Re Flyer Aggregator

scripts/broker-ad-signals.mjs

113 lines

#!/usr/bin/env node
// TK-10708  Broker intel crawl: for every CA commercial broker, mine their page code for
// (a) ANY ad-platform tracking pixel (Google/Bing/Meta/LinkedIn/TikTok/Twitter/Pinterest/
// Reddit/Snap/Amazon/Criteo/Taboola/Outbrain/AdRoll/DoubleClick) and (b) flyer / OM / PDF
// links on their site. Slow, polite, READ-ONLY public pages. Outputs JSON + CSV.
//
// Usage: node scripts/broker-ad-signals.mjs [--n 300] [--delay 2500] [--state CA]

import { execFileSync } from 'node:child_process';
import { writeFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';

const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const args = process.argv.slice(2);
const getArg = (f, d) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : d; };
const N = parseInt(getArg('--n', '400'), 10);
const DELAY = parseInt(getArg('--delay', '2500'), 10);
const STATE = getArg('--state', 'CA');
const JSONF = join(ROOT, 'public', 'advertising', 'broker-ad-signals.json');
const CSVF = join(ROOT, 'public', 'advertising', 'broker-ad-signals.csv');
const UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0 Safari/537.36';
const sleep = ms => new Promise(r => setTimeout(r, ms));

// ANY ad platform — signature lives in the page code (Steve). Bing included + native/programmatic.
const AD = {
  google_ads: /googleadservices\.com|AW-\d{6,}|google_conversion_id|gtag\(\s*['"]config['"]\s*,\s*['"]AW-|googleads\.g\.doubleclick/i,
  bing_ads: /bat\.bing\.com|\buetq\b|uet_bing|\/bat\.js/i,
  meta: /connect\.facebook\.net\/[^"']*fbevents\.js|fbq\(\s*['"]init['"]/i,
  linkedin: /snap\.licdn\.com\/li\.lms-analytics|_linkedin_partner_id/i,
  tiktok: /analytics\.tiktok\.com|ttq\.load/i,
  twitter: /static\.ads-twitter\.com\/uwt\.js|twq\(\s*['"]init['"]/i,
  pinterest: /s\.pinimg\.com\/ct\.js|pintrk\(\s*['"]load['"]/i,
  reddit: /redditstatic\.com\/ads\/pixel\.js|rdt\(\s*['"]init['"]/i,
  snap: /sc-static\.net\/scevent|snaptr\(\s*['"]init['"]/i,
  amazon: /amazon-adsystem\.com|amzn\.to\/ads/i,
  criteo: /criteo\.(com|net)|Criteo_/i,
  taboola: /taboola\.com\/libtrc|_tfa\b/i,
  outbrain: /outbrain\.com\/outbrain\.js|obApi\(/i,
  adroll: /adroll\.com|__adroll/i,
  doubleclick: /doubleclick\.net|dv360|fls\.doubleclick/i,
};
const google_analytics = /gtag\(\s*['"]config['"]\s*,\s*['"]G-|GTM-[A-Z0-9]+|google-analytics\.com\/analytics\.js|googletagmanager\.com\/gtag/i;
const statcounter = /statcounter\.com|sc_project|StatCounter/i;
const PLATS = Object.keys(AD);

// flyer / OM / PDF links in the page code
function flyers(html, base) {
  const out = new Set();
  const re = /href\s*=\s*["']([^"']+)["']/gi; let m;
  while ((m = re.exec(html))) {
    const href = m[1]; const low = href.toLowerCase();
    const ctx = html.slice(Math.max(0, m.index - 80), m.index + 120).toLowerCase();
    const isPdf = /\.pdf(\?|#|$)/.test(low);
    const looksFlyer = /flyer|brochure|offering[-_ ]?mem|\bom\b|marketing[-_ ]?package|e-?brochure|property[-_ ]?package|listing[-_ ]?flyer|tour[-_ ]?book|deal[-_ ]?sheet|for[-_ ]?sale/.test(low + ' ' + ctx);
    if (isPdf || (looksFlyer && /\.pdf|\/download|\/flyer|\/brochure|\/om/.test(low))) {
      try { out.add(new URL(href, base).href); } catch {}
    }
  }
  return [...out].slice(0, 8);
}

const stateClause = STATE === 'ALL' ? '' : `AND hq_state='${STATE}'`;
const rows = execFileSync('psql', ['usre', '-t', '-A', '-F', '\t', '-c',
  `SELECT coalesce(name,''), website FROM firm WHERE asset_class='commercial' AND website IS NOT NULL AND website<>'' ${stateClause} ORDER BY agent_count DESC NULLS LAST LIMIT ${N * 3}`],
  { encoding: 'utf8' }).trim().split('\n').filter(Boolean).map(l => l.split('\t'));
const domOf = u => { try { return new URL(u.startsWith('http') ? u : 'https://' + u).hostname.replace(/^www\./, ''); } catch { return null; } };
const seen = new Set(), targets = [];
for (const [name, web] of rows) { const d = domOf(web); if (d && !seen.has(d)) { seen.add(d); targets.push({ name, domain: d }); } if (targets.length >= N) break; }

console.log(`Broker intel crawl: ${targets.length} ${STATE} commercial brokers, ${DELAY}ms apart (slow).`);
const results = [];
for (const t of targets) {
  const rec = { firm: t.name, domain: t.domain };
  try {
    const res = await fetch('https://' + t.domain + '/', { headers: { 'User-Agent': UA }, redirect: 'follow', signal: AbortSignal.timeout(15000) });
    const html = await res.text();
    // GTM injects pixels client-side that never appear in raw HTML — fetch the container(s) and scan them too.
    const gtmIds = [...new Set((html.match(/GTM-[A-Z0-9]+/g) || []))].slice(0, 3);
    let gtmBlob = '';
    for (const id of gtmIds) {
      try {
        const g = await fetch(`https://www.googletagmanager.com/gtm.js?id=${id}`, { headers: { 'User-Agent': UA }, signal: AbortSignal.timeout(10000) });
        if (g.ok) gtmBlob += '\n' + await g.text();
      } catch {}
    }
    rec.gtm_ids = gtmIds;
    const scan = html + gtmBlob;
    for (const p of PLATS) { rec[p] = AD[p].test(scan); if (rec[p] && !AD[p].test(html)) (rec.via_gtm = rec.via_gtm || []).push(p); }
    rec.paid_count = PLATS.filter(p => rec[p]).length;
    rec.google_analytics = google_analytics.test(html);
    rec.statcounter = statcounter.test(html);
    rec.flyer_urls = flyers(html, 'https://' + t.domain + '/');
    rec.flyer_count = rec.flyer_urls.length;
    const on = PLATS.filter(p => rec[p]);
    console.log(`  ${rec.paid_count ? '💰' : '  '}${rec.flyer_count ? '📄' : '  '} ${t.domain.padEnd(32)} ${on.join(',') || '-'}${rec.flyer_count ? '  ('+rec.flyer_count+' flyers)' : ''}`);
  } catch (e) { rec.error = e.message.slice(0, 40); console.log(`  ⚠  ${t.domain.padEnd(32)} ${e.message.slice(0, 30)}`); }
  rec.scanned_at = new Date().toISOString();
  results.push(rec);
  writeFileSync(JSONF, JSON.stringify(results, null, 1));   // resumable
  await sleep(DELAY);
}

// CSV (easy list)
const cols = ['firm', 'domain', ...PLATS, 'paid_count', 'google_analytics', 'statcounter', 'flyer_count', 'flyer_urls'];
const esc = v => { v = v == null ? '' : Array.isArray(v) ? v.join(' ; ') : String(v); return /[",\n]/.test(v) ? '"' + v.replace(/"/g, '""') + '"' : v; };
const csv = [cols.join(','), ...results.map(r => cols.map(c => esc(c === 'google_analytics' || c === 'statcounter' || PLATS.includes(c) ? (r[c] ? 'yes' : '') : r[c])).join(','))].join('\n');
writeFileSync(CSVF, csv);

const adv = results.filter(r => r.paid_count > 0);
console.log(`\nDone. ${results.filter(r => !r.error).length} scanned · ${adv.length} run ANY paid ads · ${results.filter(r => r.google_analytics).length} GA · ${results.filter(r => r.flyer_count > 0).length} have flyers/PDFs on site.`);
console.log(`CSV -> ${CSVF.replace(ROOT + '/', '')} · JSON -> ${JSONF.replace(ROOT + '/', '')}`);