Traffic sources: referring domain on join-link visits and joins (per-member table), click sources per campaign (surface or outside host)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-10 10:12:13 -05:00
parent 6e187c4f41
commit ecdee89be1
6 changed files with 74 additions and 22 deletions
+6 -2
View File
@@ -500,11 +500,12 @@ const server = http.createServer(async (req, res) => {
const cookies = parseCookies(req);
const angle = String(u.searchParams.get('v') || '').toLowerCase();
const ang = JOIN_ANGLES[angle] || null;
if (req.method === 'GET') coach.recordView(tok, ang ? angle : ''); // link stats per angle
if (req.method === 'GET') coach.recordView(tok, ang ? angle : '', req.headers.referer); // link stats per angle + source
const cookieTail = `; Path=/; SameSite=Lax; Max-Age=${180 * 24 * 3600}${IS_PROD ? '; Secure' : ''}`;
const set = [];
if (!cookies['iap.sponsor']) set.push('iap.sponsor=' + tok + cookieTail);
if (ang) set.push('iap.angle=' + angle + cookieTail);
if (!cookies['iap.ref']) set.push('iap.ref=' + encodeURIComponent(coach.refHost(req.headers.referer)) + cookieTail); // first-touch source
return serveJoinPage(res, tok, ang ? angle : '', ang, set);
}
if (p === '/unsubscribe' && req.method === 'GET') {
@@ -676,7 +677,8 @@ const server = http.createServer(async (req, res) => {
emailCodes.delete(e);
const ref = parseCookies(req)['iap.sponsor'] || '';
const via = parseCookies(req)['iap.angle'] || '';
const r = await accounts.ensure(e, ref, via); // first touch wins; existing accounts unchanged
const joinedRef = decodeURIComponent(parseCookies(req)['iap.ref'] || '') || null;
const r = await accounts.ensure(e, ref, via, joinedRef); // first touch wins; existing accounts unchanged
if (r.error) return json(res, 400, r);
// the lead is in the door: queue the getting-started sequence (opt-in box is pre-checked on both forms)
if (r.created && (b.followups || b.newsletter)) drip.enqueue(e, ref, via).catch(() => {});
@@ -1542,6 +1544,7 @@ const server = http.createServer(async (req, res) => {
if (m && req.method === 'GET') {
const target = await ads.click(m[1]);
if (!target) { res.writeHead(404, baseHeaders()); return res.end(); }
coach.recordClick(m[1], req.headers.referer); // where the click happened
res.writeHead(302, baseHeaders({ Location: target }));
return res.end();
}
@@ -1550,6 +1553,7 @@ const server = http.createServer(async (req, res) => {
if (!s || !s.email) return json(res, 401, { error: 'Sign in first.' });
const memberId = await auth.refreshMemberId(s);
const out = { campaigns: await ads.listCampaigns(s.email), rates: ads.rates(), bannerSizes: ads.bannerSizes() };
out.clickSources = await coach.clickSources(out.campaigns.map(c => c.id));
const pool = await ads.pooledCredits((await myMemberIds(s)).ids);
out.purchasedCredits = pool.total;
out.largestPosition = pool.best.avail; // a single campaign budget has to fit one position