// visitor-recommendation-ctr — graph-backed CTR replacement for the // log-scan aggregation that ships with Task 336. Operates on Recommendation // nodes (materialised from [property-recommended] log lines via backfill) // joined to :Click events. Output schema matches the log-scan tool's so // the cutover (separate follow-up task) is a one-line swap. import { getSession } from "../lib/neo4j.js"; export interface RecommendationCtrParams { accountId: string; /** Days back from now. Defaults to 7. */ sinceDays?: number; /** Optional listingSlug filter. */ listingSlug?: string; } export interface CtrRow { listingSlug: string; recommended: number; clicked: number; ctr: number; } export interface CtrSummary { windowDays: number; totalRecommended: number; totalClicked: number; ctr: number; perListing: CtrRow[]; } export async function visitorRecommendationCtr(p: RecommendationCtrParams): Promise { const since = p.sinceDays ?? 7; const session = getSession(); try { const result = await session.run( `MATCH (rec:Recommendation {accountId: $accountId}) WHERE rec.recommendedAt >= datetime() - duration({days: $since}) AND ($slug IS NULL OR rec.listingSlug = $slug) OPTIONAL MATCH (rec)-[:FOR_SESSION]->(s:Session)-[:HAS_EVENT]->(cl:Click {label: 'card'}) WITH rec.listingSlug AS slug, count(DISTINCT rec) AS recommended, count(DISTINCT cl) AS clicked RETURN slug AS listingSlug, recommended, clicked, CASE WHEN recommended > 0 THEN toFloat(clicked) / recommended ELSE 0.0 END AS ctr ORDER BY recommended DESC`, { accountId: p.accountId, since, slug: p.listingSlug ?? null }, ); const perListing: CtrRow[] = result.records.map((r) => ({ listingSlug: r.get("listingSlug") ?? "", recommended: Number(r.get("recommended") ?? 0), clicked: Number(r.get("clicked") ?? 0), ctr: Number(r.get("ctr") ?? 0), })); const totalRecommended = perListing.reduce((a, b) => a + b.recommended, 0); const totalClicked = perListing.reduce((a, b) => a + b.clicked, 0); return { windowDays: since, totalRecommended, totalClicked, ctr: totalRecommended > 0 ? totalClicked / totalRecommended : 0, perListing, }; } finally { await session.close(); } }