Files
avbeat-com/src/lib/ad-tracking.ts
Claude (Phase B) c0584092eb ad analytics: impressions + clicks + admin stats + /r/[slug] redirect
Tables (Phase B+):
  bb.ad_impressions   — every render = a row. is_bot + viewport flags
                        for filtering. No unique constraints — gross
                        counts per Ry an spec.
  bb.ad_clicks        — every /r/[slug] hit = a row.
  bb.ad_campaign_clients — schema only; future client reporting.

FK target is bb.banner_creatives (the renderer table from 5/15 banner
refresh). bb.ad_campaigns is the AdOps billing schema and unrelated.

Routes:
  GET  /r/[slug]                   — log click, 302 to bb.banner_creatives.click_url
  POST /api/track/impression       — record impression (slug | campaign_id),
                                     used by client beacon

AdImage rewrite:
  - link href is /r/{slug} target=_blank rel=sponsored noopener noreferrer
  - sendBeacon on mount fires impression (viewport=false)
  - additional sendBeacon on IntersectionObserver ≥0.5 (viewport=true)
  - removed direct supabase.from('banner_analytics').insert path

Ad type carries slug now; FALLBACK list + DB mapper both populate it.

/admin/banners:
  per-row stats — 24h, 7d, lifetime impressions/clicks/CTR
  link to /admin/banners/[id]/analytics

/admin/banners/[id]/analytics (new):
  recharts line charts for impressions + clicks (30d), top page paths,
  top referrer domains, bot/human toggle.

Tower Products orphan image file removed; banner was not in any active
table or in code (already off rotation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 01:45:33 +00:00

72 lines
2.1 KiB
TypeScript

import { createAdminClient } from "@/lib/supabase/admin";
const BOT_REGEX = /bot|crawl|spider|googlebot|bingbot|slurp|baidu|yandex|duckduck|facebook|pinterest|whatsapp|twitter|skype|linkedin|preview|fetch|curl|wget|monitor|uptime/i;
export function isBot(userAgent: string | null | undefined): boolean {
if (!userAgent) return true;
return BOT_REGEX.test(userAgent);
}
export function clientIp(headers: Headers): string | null {
const xff = headers.get("x-forwarded-for") || "";
const first = xff.split(",")[0]?.trim();
if (first) return first;
return headers.get("x-real-ip") || null;
}
export function sessionIdFromCookies(cookieHeader: string | null): string | null {
if (!cookieHeader) return null;
const m = cookieHeader.match(/bb_sid=([^;]+)/);
return m ? decodeURIComponent(m[1]) : null;
}
export interface ImpressionInput {
campaignId: string;
pageUrl?: string | null;
pagePath?: string | null;
referrer?: string | null;
userAgent?: string | null;
ipAddress?: string | null;
isBot?: boolean;
viewport?: boolean;
sessionId?: string | null;
}
export interface ClickInput {
campaignId: string;
pageUrl?: string | null;
referrer?: string | null;
userAgent?: string | null;
ipAddress?: string | null;
isBot?: boolean;
sessionId?: string | null;
}
export async function recordImpression(i: ImpressionInput): Promise<void> {
const admin = createAdminClient();
await admin.from("ad_impressions").insert({
campaign_id: i.campaignId,
page_url: i.pageUrl ?? null,
page_path: i.pagePath ?? null,
referrer: i.referrer ?? null,
user_agent: i.userAgent ?? null,
ip_address: i.ipAddress ?? null,
is_bot: !!i.isBot,
viewport: !!i.viewport,
session_id: i.sessionId ?? null,
});
}
export async function recordClick(c: ClickInput): Promise<void> {
const admin = createAdminClient();
await admin.from("ad_clicks").insert({
campaign_id: c.campaignId,
page_url: c.pageUrl ?? null,
referrer: c.referrer ?? null,
user_agent: c.userAgent ?? null,
ip_address: c.ipAddress ?? null,
is_bot: !!c.isBot,
session_id: c.sessionId ?? null,
});
}