track-page-view.tsx38 lines · main
1'use client';
2
3import { useEffect, useRef } from 'react';
4
5interface Props {
6 apiOrigin: string;
7 source: string;
8}
9
10/**
11 * Fire-and-forget pageview beacon. Posts once per mount to
12 * /v1/marketing-events on the api origin. The api is rate-limited
13 * 30/min per IP so a misbehaving client can't write-amplify the
14 * meta-DB. We deliberately don't surface failures — analytics gaps
15 * are preferable to UX impact.
16 */
17export function TrackPageView({ apiOrigin, source }: Props) {
18 // Strict Mode + Next dev mounts components twice; the ref guards
19 // against the duplicate beacon. The api ignores duplicates within
20 // the rate window anyway.
21 const fired = useRef(false);
22
23 useEffect(() => {
24 if (fired.current) return;
25 fired.current = true;
26 if (!apiOrigin) return;
27 void fetch(`${apiOrigin}/v1/marketing-events`, {
28 method: 'POST',
29 keepalive: true,
30 headers: { 'content-type': 'application/json' },
31 body: JSON.stringify({ eventType: 'migrate_view', source }),
32 }).catch(() => {
33 // analytics failure is a no-op
34 });
35 }, [apiOrigin, source]);
36
37 return null;
38}