Files
instantadpay/traffic.js
T
martbost ef088601c3 Admin Traffic tab: referring domains, landing pages, angles, by day
traffic.js logs public page views by referring domain (30 s buffered; page_hits
table or traffic.json), coach exposes all join-page views, and
/api/admin/traffic merges page views, join-page views, signups, registrations
and $20+ buyers by first-touch source for 7/30/90/365-day ranges.
Also: line-tree chips no longer truncate own-position labels.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-12 07:51:01 -05:00

61 lines
3.0 KiB
JavaScript

// Page-hit log for the admin Traffic tab (Marty, 2026-09-12): which referring domains send
// visitors to the public pages, per day and per landing page. Counters are buffered in memory
// and flushed every 30 s: MySQL table page_hits (day, host, path, n) when the DB is on, else
// DATA_DIR/traffic.json. Obvious crawlers are skipped. Our own pages as referrer = 'direct'.
const fs = require('fs');
const path = require('path');
const db = require('./db');
let DATA_DIR = null, buf = {}, jsonStore = null, timer = null;
const BOT = /bot|crawl|spider|slurp|facebookexternalhit|preview|monitor|curl\/|wget|python-requests|headless/i;
function init(opts) {
DATA_DIR = opts.dataDir;
if (!timer) { timer = setInterval(() => flush().catch(() => {}), 30 * 1000); timer.unref(); }
}
function host(referer) {
try { const h = new URL(String(referer || '')).hostname.replace(/^www\./, '').toLowerCase(); return (!h || /instantadpay\.com$/.test(h)) ? 'direct' : h.slice(0, 80); } catch (e) { return 'direct'; }
}
function family(p) {
if (p === '/' || p === '') return 'home';
if (p.startsWith('/join/')) return 'join';
if (p.startsWith('/from/')) return p.slice(1, 40); // from/faucetwave, from/tieroneads
if (p.startsWith('/wall/')) return 'wall';
return p.replace(/^\//, '').slice(0, 40) || 'home'; // ledger, contract, shorts, plays ...
}
const day = ts => new Date(ts).toISOString().slice(0, 10);
function hit(p, referer, ua) {
if (BOT.test(String(ua || ''))) return;
const k = day(Date.now()) + '|' + host(referer) + '|' + family(p);
buf[k] = (buf[k] || 0) + 1;
}
function loadJson() {
if (jsonStore) return jsonStore;
try { jsonStore = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'traffic.json'), 'utf8')); } catch (e) { jsonStore = {}; }
return jsonStore;
}
async function flush() {
const pending = buf; buf = {};
const keys = Object.keys(pending); if (!keys.length) return;
if (db.enabled()) {
for (const k of keys) { const [d, h, f] = k.split('|'); await db.q('INSERT INTO page_hits (day,host,path,n) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE n=n+VALUES(n)', [d, h, f, pending[k]]); }
return;
}
const J = loadJson();
for (const k of keys) J[k] = (J[k] || 0) + pending[k];
// keep the JSON store bounded: drop days older than 400
const cutoff = day(Date.now() - 400 * 86400000);
for (const k of Object.keys(J)) if (k.slice(0, 10) < cutoff) delete J[k];
fs.writeFileSync(path.join(DATA_DIR, 'traffic.json'), JSON.stringify(J));
}
// rows since a day (inclusive): [{day, host, path, n}]
async function rows(sinceDay) {
await flush();
if (db.enabled()) {
const r = await db.q('SELECT day, host, path, n FROM page_hits WHERE day>=?', [sinceDay]);
return r.map(x => ({ day: x.day, host: x.host, path: x.path, n: Number(x.n) }));
}
const J = loadJson();
return Object.keys(J).filter(k => k.slice(0, 10) >= sinceDay).map(k => { const [d, h, p] = k.split('|'); return { day: d, host: h, path: p, n: J[k] }; });
}
module.exports = { init, hit, flush, rows, host, family };