Skip to content

feat(sponsors): track impressions and clicks for every sponsored slot - #644

Merged
hikariming merged 1 commit into
mainfrom
feat/sponsor-slot-metrics
Nov 5, 2026
Merged

hikariming merged 1 commit into
mainfrom
feat/sponsor-slot-metrics

Conversation

@hikariming

Copy link
Copy Markdown
Collaborator

Why

Nothing recorded sponsor or hiring clicks until now. /api/sponsors/:id/visit only 302s, and GA carries only growth-funnel events. Sponsor reports need impressions, clicks, and CTR by day, month, and sponsor. Counting starts at deploy; there is no history to backfill.

What

  • migrations/0034_ad_daily_stats.sql: a rollup table with one row per Beijing day × ad_kind × ad_id × placement, holding impressions and clicks. It also adds sponsorships.advertiser_id, a stable key filled for all 7 existing rows. Sponsor names get retitled (Vibe42, three times), and ModaLeap's sponsor cell, job cards, and careers link should all roll up under modaleap.
  • POST /api/sponsors/pulse: the beacon endpoint.
    • Each upsert is INSERT … SELECT from sponsorships/jobs/job_companies, so unknown ids write nothing.
    • Crawler UAs are dropped (204, no write), and Sec-Fetch-Site: cross-site gets 403.
    • Bodies are capped at 8 KB / 40 events.
  • SlotMetrics: mounted once in the Next locale layout and the Astro Shell.
    • An impression is ≥50% visible for 1s, once per slot per path. Every click counts.
    • Slots opt in with data-track-slot, so server-rendered hiring cards ship no extra JS.
    • DOM attributes and URLs avoid "ad" wording because of generic ad-block filters (same reason as the visit hop).
  • Tagged surfaces: top strip, homepage sponsor row, MiniCPM model credit, footer, pill, /sponsor holders, homepage hiring cards, /jobs cards and careers links, /jobs/<id> apply and company links.
  • docs/operations/sponsor-metrics.md: field reference and ready-made day / month / sponsor / placement queries.

⚠️ Deploy note

Every wrangler env points GHFIND_D1 at the same ghfind database, and both deploy workflows run d1 migrations apply ghfind --remote. The migration therefore lands on the shared production D1 at deploy. It is additive only: one new table, one nullable column, seven UPDATEs.

Verification

  • tsc (root, release, apps/api), astro check, frontend-stack:check, and ESLint all pass. On top of main, vitest passes 237 files / 2506 tests.
  • Ran all migrations in SQLite to confirm:
    • The upsert increments correctly.
    • Unknown ids insert nothing.
    • advertiser resolves across kinds.
    • Every SQL query in the ops doc runs.
  • Real-app check with pnpm dev:local on a local D1 (guest, no login needed). Playwright with a desktop UA loaded /zh and /zh/jobs, scrolled, and clicked a sponsor cell and a job card. Rows landed with the right Beijing day, kinds, and advertisers: impressions only for slots that were on screen, one click on each of the two clicked slots. A headless UA was dropped, as intended.
  • The first browser run caught a 403. The Origin vs request.url comparison fails when the request is proxied (dev, and the router → api hop). It is fixed to use Sec-Fetch-Site only, and a regression test is added.

Frontend checklist

  • Deps: none added
  • Components: no new UI primitives; only data attributes on existing links
  • Icons / motion: unchanged
  • Cards: no layout change
  • Light/dark: no visual change (no class or style edits), so no screenshots
  • Mobile 390px: no visual change

Known limits

Counts are operational, not billing-grade: a scripted client can still inflate them. Visitors who block the beacon are missing from both impressions and clicks, so CTR stays consistent. README-embedded share cards (SVG/PNG images) can't report clicks.

🤖 Generated with Claude Code

Nothing recorded sponsor or hiring clicks before: /api/sponsors/:id/visit
only redirected and GA carried growth-funnel events. This adds a daily
rollup so sponsor reports can be cut by day, month, and advertiser.

- migrations/0034: ad_daily_stats (Beijing day x kind x id x placement,
  impressions/clicks) plus a stable sponsorships.advertiser_id, so a
  sponsor's strip, row, and job ads roll up under one key across renames.
- POST /api/sponsors/pulse: same-site beacon sink. Upserts resolve the
  advertiser from sponsorships/jobs/job_companies, so unknown ids write
  nothing; crawler UAs and cross-site requests are dropped.
- SlotMetrics: one tracker per shell (Next layout and Astro Shell). An
  impression is >=50% visible for 1s, once per slot per path; every click
  counts. Slots opt in with a data-track-slot attribute, so server-rendered
  hiring cards ship no extra JS.
- Tagged: top strip, homepage sponsor row, model credit, footer, pill,
  /sponsor holders, homepage hiring cards, /jobs cards and careers links,
  /jobs/<id> apply and company links.
- docs/operations/sponsor-metrics.md: field reference and report queries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@hikariming
hikariming merged commit a9ce22e into main Nov 5, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant