marketing-events.ts45 lines · main
| 1 | import { Hono } from 'hono'; |
| 2 | |
| 3 | import { ipKey, rateLimit } from '../middleware/rate-limit.js'; |
| 4 | import { hashIp } from '../services/audit.js'; |
| 5 | import { trackMarketingEvent } from '../services/marketing-events.js'; |
| 6 | import type { AppEnv } from '../types/app-env.js'; |
| 7 | |
| 8 | /** |
| 9 | * Public beacon for marketing funnel tracking. Receives `migrate_view` |
| 10 | * events from the /migrate and /migrate/<source> marketing pages. |
| 11 | * |
| 12 | * Rate-limited per IP to keep the endpoint from being a write-amp |
| 13 | * vector against the meta-DB. Lead-submit events are NOT fired through |
| 14 | * this surface — they fire server-side from the POST /v1/migration-leads |
| 15 | * handler so we don't trust a public POST to claim a conversion. |
| 16 | */ |
| 17 | export const marketingEventsPublicRouter = new Hono<AppEnv>(); |
| 18 | |
| 19 | marketingEventsPublicRouter.post( |
| 20 | '/v1/marketing-events', |
| 21 | rateLimit({ |
| 22 | scope: 'marketing-events', |
| 23 | limit: 30, |
| 24 | windowMs: 60_000, |
| 25 | key: ipKey, |
| 26 | }), |
| 27 | async (c) => { |
| 28 | const body = (await c.req.json().catch(() => null)) as Record<string, unknown> | null; |
| 29 | if (!body) return c.json({ ok: true }); |
| 30 | const eventType = typeof body.eventType === 'string' ? body.eventType : ''; |
| 31 | const source = typeof body.source === 'string' ? body.source : ''; |
| 32 | // We only accept page-view events through the public surface; |
| 33 | // conversions can only be claimed server-side from the lead route. |
| 34 | if (eventType !== 'migrate_view') return c.json({ ok: true }); |
| 35 | const fwd = c.req.raw.headers.get('cf-connecting-ip') ?? c.req.raw.headers.get('x-forwarded-for'); |
| 36 | const ip = fwd ? fwd.split(',')[0]!.trim() : null; |
| 37 | await trackMarketingEvent({ |
| 38 | eventType, |
| 39 | source, |
| 40 | ipHash: hashIp(ip), |
| 41 | userAgent: c.req.header('user-agent') ?? null, |
| 42 | }); |
| 43 | return c.json({ ok: true }); |
| 44 | }, |
| 45 | ); |