From fb7d63ad06c6064c1ccca057919b9ec050e293b4 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Sat, 2 May 2026 10:07:05 -0700 Subject: [PATCH] feat(website): add PostHog GTM analytics --- apps/website/.env.example | 5 + apps/website/instrumentation-client.ts | 16 + apps/website/package.json | 2 + apps/website/src/app/api/leads/route.ts | 5 + apps/website/src/app/api/newsletter/route.ts | 5 + .../src/app/api/whitepaper-signup/route.ts | 5 + .../src/components/docs/CopyPromptButton.tsx | 6 + .../src/components/docs/DocsSearch.tsx | 21 +- .../src/components/docs/mdx/CodeBlock.tsx | 6 + .../src/components/landing/WhitePaperGate.tsx | 16 + .../components/landing/WhitePaperSection.tsx | 28 ++ .../landing/angular/AngularWhitePaperGate.tsx | 27 ++ .../ChatLandingWhitePaperGate.tsx | 27 ++ .../landing/render/RenderWhitePaperGate.tsx | 27 ++ .../src/components/pricing/LeadForm.tsx | 26 +- .../components/shared/AnnouncementToast.tsx | 39 +- apps/website/src/components/shared/Footer.tsx | 65 +++ apps/website/src/components/shared/Nav.tsx | 57 ++- apps/website/src/lib/analytics/client.ts | 38 ++ apps/website/src/lib/analytics/events.ts | 55 +++ .../src/lib/analytics/properties.spec.ts | 48 ++ apps/website/src/lib/analytics/properties.ts | 59 +++ apps/website/src/lib/analytics/server.ts | 111 +++++ .../plans/2026-05-02-posthog-gtm-analytics.md | 416 ++++++++++++++++++ package-lock.json | 374 +++++++++++++++- 25 files changed, 1461 insertions(+), 23 deletions(-) create mode 100644 apps/website/instrumentation-client.ts create mode 100644 apps/website/src/lib/analytics/client.ts create mode 100644 apps/website/src/lib/analytics/events.ts create mode 100644 apps/website/src/lib/analytics/properties.spec.ts create mode 100644 apps/website/src/lib/analytics/properties.ts create mode 100644 apps/website/src/lib/analytics/server.ts create mode 100644 docs/superpowers/plans/2026-05-02-posthog-gtm-analytics.md diff --git a/apps/website/.env.example b/apps/website/.env.example index 4449b83c5..55153d89e 100644 --- a/apps/website/.env.example +++ b/apps/website/.env.example @@ -1,6 +1,11 @@ # LangGraph Platform URL for the Angular Elements live demo NEXT_PUBLIC_LANGGRAPH_URL=http://localhost:2024 +# PostHog analytics (https://posthog.com) +NEXT_PUBLIC_POSTHOG_TOKEN= +NEXT_PUBLIC_POSTHOG_HOST=https://us.i.posthog.com +NEXT_PUBLIC_POSTHOG_CAPTURE_LOCAL=false + # Anthropic API key for docs generation scripts ANTHROPIC_API_KEY=your-anthropic-api-key-here diff --git a/apps/website/instrumentation-client.ts b/apps/website/instrumentation-client.ts new file mode 100644 index 000000000..2f08ee5ee --- /dev/null +++ b/apps/website/instrumentation-client.ts @@ -0,0 +1,16 @@ +import posthog from 'posthog-js'; +import { + normalizePostHogHost, + shouldCaptureAnalytics, +} from './src/lib/analytics/properties'; + +const token = process.env.NEXT_PUBLIC_POSTHOG_TOKEN; +const captureLocal = process.env.NEXT_PUBLIC_POSTHOG_CAPTURE_LOCAL === 'true'; +const browserHost = typeof window === 'undefined' ? undefined : window.location.host; + +if (shouldCaptureAnalytics({ token, captureLocal, host: browserHost })) { + posthog.init(token!, { + api_host: normalizePostHogHost(process.env.NEXT_PUBLIC_POSTHOG_HOST), + defaults: '2026-01-30', + }); +} diff --git a/apps/website/package.json b/apps/website/package.json index 4ceab553c..4cab1de69 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -7,6 +7,8 @@ "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "next": "~16.1.6", + "posthog-js": "^1.372.6", + "posthog-node": "^5.20.0", "react": "^19.0.0", "react-dom": "^19.0.0", "remark-gfm": "^4.0.1", diff --git a/apps/website/src/app/api/leads/route.ts b/apps/website/src/app/api/leads/route.ts index 769f704d1..740df5576 100644 --- a/apps/website/src/app/api/leads/route.ts +++ b/apps/website/src/app/api/leads/route.ts @@ -4,6 +4,8 @@ import path from 'path'; import { sendEmail, FROM, NOTIFY_TO, addToAudience } from '../../../../lib/resend'; import { loopsUpsertContact, loopsSendEvent } from '../../../../lib/loops'; import { leadNotificationHtml } from '../../../../emails/lead-notification'; +import { captureLeadConversion } from '../../../lib/analytics/server'; +import { getSourcePage } from '../../../lib/analytics/properties'; const LEADS_FILE = path.join(process.cwd(), 'data', 'leads.ndjson'); @@ -22,6 +24,7 @@ export async function POST(req: NextRequest) { } const ts = new Date().toISOString(); + const sourcePage = getSourcePage(req.headers.get('referer')); // NDJSON backup (always writes, even if Resend fails) try { @@ -57,5 +60,7 @@ export async function POST(req: NextRequest) { console.error('[resend] lead notification failed:', err); } + await captureLeadConversion({ email, company, sourcePage }); + return NextResponse.json({ ok: true }); } diff --git a/apps/website/src/app/api/newsletter/route.ts b/apps/website/src/app/api/newsletter/route.ts index bffda780e..1c095a488 100644 --- a/apps/website/src/app/api/newsletter/route.ts +++ b/apps/website/src/app/api/newsletter/route.ts @@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from 'next/server'; import { sendEmail, FROM, addToAudience } from '../../../../lib/resend'; import { loopsUpsertContact, loopsSendEvent } from '../../../../lib/loops'; import { newsletterWelcomeHtml } from '../../../../emails/newsletter-welcome'; +import { captureNewsletterConversion } from '../../../lib/analytics/server'; +import { getSourcePage } from '../../../lib/analytics/properties'; export async function POST(req: NextRequest) { let body: { email?: string }; @@ -12,6 +14,7 @@ export async function POST(req: NextRequest) { } const email = (body.email || '').trim().slice(0, 320); + const sourcePage = getSourcePage(req.headers.get('referer')); if (!email || !email.includes('@')) { return NextResponse.json({ error: 'Valid email required' }, { status: 400 }); @@ -40,5 +43,7 @@ export async function POST(req: NextRequest) { console.error('[resend] newsletter signup failed:', err); } + await captureNewsletterConversion({ email, sourcePage }); + return NextResponse.json({ ok: true }); } diff --git a/apps/website/src/app/api/whitepaper-signup/route.ts b/apps/website/src/app/api/whitepaper-signup/route.ts index 15472d585..276d2a6fb 100644 --- a/apps/website/src/app/api/whitepaper-signup/route.ts +++ b/apps/website/src/app/api/whitepaper-signup/route.ts @@ -8,6 +8,8 @@ import { whitepaperDownloadHtml } from '../../../../emails/whitepaper-download'; import { angularDownloadHtml } from '../../../../emails/angular-download'; import { renderDownloadHtml } from '../../../../emails/render-download'; import { chatDownloadHtml } from '../../../../emails/chat-download'; +import { captureWhitepaperConversion } from '../../../lib/analytics/server'; +import { getSourcePage } from '../../../lib/analytics/properties'; const SIGNUPS_FILE = path.join(process.cwd(), 'data', 'whitepaper-signups.ndjson'); @@ -38,6 +40,7 @@ export async function POST(req: NextRequest) { const name = (body.name || '').trim().slice(0, 200); const email = (body.email || '').trim().slice(0, 320); const paper = (VALID_PAPERS.includes(body.paper as PaperId) ? body.paper : 'overview') as PaperId; + const sourcePage = getSourcePage(req.headers.get('referer')); if (!email || !email.includes('@')) { return NextResponse.json({ error: 'Valid email required' }, { status: 400 }); @@ -71,5 +74,7 @@ export async function POST(req: NextRequest) { console.error('[whitepaper-signup] email pipeline failed:', err); } + await captureWhitepaperConversion({ email, paper, sourcePage }); + return NextResponse.json({ ok: true }); } diff --git a/apps/website/src/components/docs/CopyPromptButton.tsx b/apps/website/src/components/docs/CopyPromptButton.tsx index b0d241bb2..17505e68d 100644 --- a/apps/website/src/components/docs/CopyPromptButton.tsx +++ b/apps/website/src/components/docs/CopyPromptButton.tsx @@ -1,6 +1,8 @@ 'use client'; import { useState } from 'react'; import { tokens } from '@ngaf/design-tokens'; +import { analyticsEvents } from '../../lib/analytics/events'; +import { track } from '../../lib/analytics/client'; interface Props { prompt: string; @@ -14,6 +16,10 @@ export function CopyPromptButton({ prompt, variant = 'docs', label }: Props) { const handleClick = async () => { try { await navigator.clipboard.writeText(prompt); + track(analyticsEvents.docsCopyPromptClick, { + surface: variant === 'hero' ? 'home' : 'docs', + cta_id: label ?? 'copy_prompt', + }); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { diff --git a/apps/website/src/components/docs/DocsSearch.tsx b/apps/website/src/components/docs/DocsSearch.tsx index a51246a22..fb219e114 100644 --- a/apps/website/src/components/docs/DocsSearch.tsx +++ b/apps/website/src/components/docs/DocsSearch.tsx @@ -3,6 +3,8 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { useRouter } from 'next/navigation'; import { docsConfig, type LibraryId } from '../../lib/docs-config'; import { tokens } from '@ngaf/design-tokens'; +import { analyticsEvents } from '../../lib/analytics/events'; +import { track } from '../../lib/analytics/client'; interface SearchablePage { title: string; @@ -60,6 +62,13 @@ export function DocsSearch({ library }: { library?: LibraryId }) { }, [open]); const navigate = (page: SearchablePage) => { + track(analyticsEvents.docsSearchResultClick, { + surface: 'docs', + destination_url: `/docs/${page.library}/${page.section}/${page.slug}`, + library: page.library === 'agent' || page.library === 'render' || page.library === 'chat' ? page.library : 'unknown', + query_length: query.length, + result_count: results.length, + }); router.push(`/docs/${page.library}/${page.section}/${page.slug}`); setOpen(false); }; @@ -95,7 +104,17 @@ export function DocsSearch({ library }: { library?: LibraryId }) { { setQuery(e.target.value); setSelected(0); }} + onChange={(e) => { + const nextQuery = e.target.value; + setQuery(nextQuery); + setSelected(0); + if (nextQuery.length === 1) { + track(analyticsEvents.docsSearchSubmit, { + surface: 'docs', + library: library === 'agent' || library === 'render' || library === 'chat' ? library : 'unknown', + }); + } + }} onKeyDown={handleInputKeyDown} placeholder="Search documentation..." style={{ diff --git a/apps/website/src/components/docs/mdx/CodeBlock.tsx b/apps/website/src/components/docs/mdx/CodeBlock.tsx index 702468cb9..885e9034c 100644 --- a/apps/website/src/components/docs/mdx/CodeBlock.tsx +++ b/apps/website/src/components/docs/mdx/CodeBlock.tsx @@ -1,5 +1,7 @@ 'use client'; import { useRef, useState } from 'react'; +import { analyticsEvents } from '../../../lib/analytics/events'; +import { track } from '../../../lib/analytics/client'; function CopyIcon() { return ( @@ -25,6 +27,10 @@ export function Pre({ children, ...props }: React.HTMLAttributes const copy = async () => { const text = ref.current?.textContent ?? ''; await navigator.clipboard.writeText(text); + track(analyticsEvents.docsCopyCodeClick, { + surface: 'docs', + cta_id: 'copy_code', + }); setCopied(true); setTimeout(() => setCopied(false), 2000); }; diff --git a/apps/website/src/components/landing/WhitePaperGate.tsx b/apps/website/src/components/landing/WhitePaperGate.tsx index 9f94b1208..181dfab8c 100644 --- a/apps/website/src/components/landing/WhitePaperGate.tsx +++ b/apps/website/src/components/landing/WhitePaperGate.tsx @@ -3,6 +3,8 @@ import { useState } from 'react'; import { motion } from 'framer-motion'; import { tokens } from '../../../lib/design-tokens'; +import { analyticsEvents } from '../../lib/analytics/events'; +import { track } from '../../lib/analytics/client'; type FormState = 'idle' | 'submitting' | 'done' | 'error'; @@ -71,6 +73,11 @@ export function WhitePaperGate() { ? `Role: ${role}${message ? '\n\n' + message : ''}` : message; + track(analyticsEvents.marketingLeadFormSubmit, { + surface: 'home', + source_section: 'whitepaper-gate', + }); + try { const res = await fetch('/api/leads', { method: 'POST', @@ -89,8 +96,17 @@ export function WhitePaperGate() { throw new Error(data.error ?? 'Server error'); } + track(analyticsEvents.marketingLeadFormSuccess, { + surface: 'home', + source_section: 'whitepaper-gate', + }); setFormState('done'); } catch { + track(analyticsEvents.marketingLeadFormFail, { + surface: 'home', + source_section: 'whitepaper-gate', + error_reason: 'api_error', + }); setFormState('error'); } }; diff --git a/apps/website/src/components/landing/WhitePaperSection.tsx b/apps/website/src/components/landing/WhitePaperSection.tsx index d778aadc0..9e293bb4a 100644 --- a/apps/website/src/components/landing/WhitePaperSection.tsx +++ b/apps/website/src/components/landing/WhitePaperSection.tsx @@ -2,6 +2,8 @@ import { useState } from 'react'; import { motion } from 'framer-motion'; import { tokens } from '../../../lib/design-tokens'; +import { analyticsEvents } from '../../lib/analytics/events'; +import { track, trackWhitepaperDownloadClick } from '../../lib/analytics/client'; type FormState = 'idle' | 'submitting' | 'done' | 'error'; @@ -14,6 +16,11 @@ export function WhitePaperSection() { e.preventDefault(); if (!email) return; setFormState('submitting'); + track(analyticsEvents.marketingWhitepaperSignupSubmit, { + surface: 'home', + source_section: 'whitepaper-section', + paper: 'overview', + }); try { const res = await fetch('/api/whitepaper-signup', { method: 'POST', @@ -21,8 +28,19 @@ export function WhitePaperSection() { body: JSON.stringify({ name, email }), }); if (!res.ok) throw new Error('Server error'); + track(analyticsEvents.marketingWhitepaperSignupSuccess, { + surface: 'home', + source_section: 'whitepaper-section', + paper: 'overview', + }); setFormState('done'); } catch { + track(analyticsEvents.marketingWhitepaperSignupFail, { + surface: 'home', + source_section: 'whitepaper-section', + paper: 'overview', + error_reason: 'api_error', + }); setFormState('error'); } }; @@ -99,6 +117,11 @@ export function WhitePaperSection() { trackWhitepaperDownloadClick('overview', { + surface: 'home', + source_section: 'whitepaper-section', + cta_id: 'whitepaper_section_direct_download', + })} style={{ display: 'inline-block', marginTop: 12, @@ -162,6 +185,11 @@ export function WhitePaperSection() { trackWhitepaperDownloadClick('overview', { + surface: 'home', + source_section: 'whitepaper-section', + cta_id: 'whitepaper_section_direct_download', + })} style={{ display: 'inline-block', marginTop: 12, diff --git a/apps/website/src/components/landing/angular/AngularWhitePaperGate.tsx b/apps/website/src/components/landing/angular/AngularWhitePaperGate.tsx index 2fef219a7..dae850e54 100644 --- a/apps/website/src/components/landing/angular/AngularWhitePaperGate.tsx +++ b/apps/website/src/components/landing/angular/AngularWhitePaperGate.tsx @@ -3,6 +3,8 @@ import { useState } from 'react'; import { motion } from 'framer-motion'; import { tokens } from '@ngaf/design-tokens'; +import { analyticsEvents } from '../../../lib/analytics/events'; +import { track, trackWhitepaperDownloadClick } from '../../../lib/analytics/client'; type FormState = 'idle' | 'submitting' | 'done' | 'error'; @@ -15,6 +17,12 @@ export function AngularWhitePaperGate() { e.preventDefault(); if (!email) return; setFormState('submitting'); + track(analyticsEvents.marketingWhitepaperSignupSubmit, { + surface: 'library_landing', + source_section: 'angular-whitepaper-gate', + library: 'agent', + paper: 'angular', + }); try { const res = await fetch('/api/whitepaper-signup', { method: 'POST', @@ -22,8 +30,21 @@ export function AngularWhitePaperGate() { body: JSON.stringify({ name, email, paper: 'angular' }), }); if (!res.ok) throw new Error('Server error'); + track(analyticsEvents.marketingWhitepaperSignupSuccess, { + surface: 'library_landing', + source_section: 'angular-whitepaper-gate', + library: 'agent', + paper: 'angular', + }); setFormState('done'); } catch { + track(analyticsEvents.marketingWhitepaperSignupFail, { + surface: 'library_landing', + source_section: 'angular-whitepaper-gate', + library: 'agent', + paper: 'angular', + error_reason: 'api_error', + }); setFormState('error'); } }; @@ -85,6 +106,12 @@ export function AngularWhitePaperGate() { Part of the Cacheplane Angular Agent Framework.

trackWhitepaperDownloadClick('angular', { + surface: 'library_landing', + source_section: 'angular-whitepaper-gate', + library: 'agent', + cta_id: 'angular_whitepaper_download', + })} style={{ display: 'inline-flex', alignItems: 'center', gap: 8, background: tokens.colors.accent, color: '#fff', diff --git a/apps/website/src/components/landing/chat-landing/ChatLandingWhitePaperGate.tsx b/apps/website/src/components/landing/chat-landing/ChatLandingWhitePaperGate.tsx index af51fe113..4a40f5771 100644 --- a/apps/website/src/components/landing/chat-landing/ChatLandingWhitePaperGate.tsx +++ b/apps/website/src/components/landing/chat-landing/ChatLandingWhitePaperGate.tsx @@ -3,6 +3,8 @@ import { useState } from 'react'; import { motion } from 'framer-motion'; import { tokens } from '@ngaf/design-tokens'; +import { analyticsEvents } from '../../../lib/analytics/events'; +import { track, trackWhitepaperDownloadClick } from '../../../lib/analytics/client'; type FormState = 'idle' | 'submitting' | 'done' | 'error'; @@ -15,6 +17,12 @@ export function ChatLandingWhitePaperGate() { e.preventDefault(); if (!email) return; setFormState('submitting'); + track(analyticsEvents.marketingWhitepaperSignupSubmit, { + surface: 'library_landing', + source_section: 'chat-whitepaper-gate', + library: 'chat', + paper: 'chat', + }); try { const res = await fetch('/api/whitepaper-signup', { method: 'POST', @@ -22,8 +30,21 @@ export function ChatLandingWhitePaperGate() { body: JSON.stringify({ name, email, paper: 'chat' }), }); if (!res.ok) throw new Error('Server error'); + track(analyticsEvents.marketingWhitepaperSignupSuccess, { + surface: 'library_landing', + source_section: 'chat-whitepaper-gate', + library: 'chat', + paper: 'chat', + }); setFormState('done'); } catch { + track(analyticsEvents.marketingWhitepaperSignupFail, { + surface: 'library_landing', + source_section: 'chat-whitepaper-gate', + library: 'chat', + paper: 'chat', + error_reason: 'api_error', + }); setFormState('error'); } }; @@ -85,6 +106,12 @@ export function ChatLandingWhitePaperGate() { Part of the Cacheplane Angular Agent Framework.

trackWhitepaperDownloadClick('chat', { + surface: 'library_landing', + source_section: 'chat-whitepaper-gate', + library: 'chat', + cta_id: 'chat_whitepaper_download', + })} style={{ display: 'inline-flex', alignItems: 'center', gap: 8, background: tokens.colors.chatPurple, color: '#fff', diff --git a/apps/website/src/components/landing/render/RenderWhitePaperGate.tsx b/apps/website/src/components/landing/render/RenderWhitePaperGate.tsx index c1490d7cb..87951fffa 100644 --- a/apps/website/src/components/landing/render/RenderWhitePaperGate.tsx +++ b/apps/website/src/components/landing/render/RenderWhitePaperGate.tsx @@ -3,6 +3,8 @@ import { useState } from 'react'; import { motion } from 'framer-motion'; import { tokens } from '@ngaf/design-tokens'; +import { analyticsEvents } from '../../../lib/analytics/events'; +import { track, trackWhitepaperDownloadClick } from '../../../lib/analytics/client'; type FormState = 'idle' | 'submitting' | 'done' | 'error'; @@ -15,6 +17,12 @@ export function RenderWhitePaperGate() { e.preventDefault(); if (!email) return; setFormState('submitting'); + track(analyticsEvents.marketingWhitepaperSignupSubmit, { + surface: 'library_landing', + source_section: 'render-whitepaper-gate', + library: 'render', + paper: 'render', + }); try { const res = await fetch('/api/whitepaper-signup', { method: 'POST', @@ -22,8 +30,21 @@ export function RenderWhitePaperGate() { body: JSON.stringify({ name, email, paper: 'render' }), }); if (!res.ok) throw new Error('Server error'); + track(analyticsEvents.marketingWhitepaperSignupSuccess, { + surface: 'library_landing', + source_section: 'render-whitepaper-gate', + library: 'render', + paper: 'render', + }); setFormState('done'); } catch { + track(analyticsEvents.marketingWhitepaperSignupFail, { + surface: 'library_landing', + source_section: 'render-whitepaper-gate', + library: 'render', + paper: 'render', + error_reason: 'api_error', + }); setFormState('error'); } }; @@ -85,6 +106,12 @@ export function RenderWhitePaperGate() { Part of the Cacheplane Angular Agent Framework.

trackWhitepaperDownloadClick('render', { + surface: 'library_landing', + source_section: 'render-whitepaper-gate', + library: 'render', + cta_id: 'render_whitepaper_download', + })} style={{ display: 'inline-flex', alignItems: 'center', gap: 8, background: tokens.colors.renderGreen, color: '#fff', diff --git a/apps/website/src/components/pricing/LeadForm.tsx b/apps/website/src/components/pricing/LeadForm.tsx index 39b1ab70f..9bd0b3b42 100644 --- a/apps/website/src/components/pricing/LeadForm.tsx +++ b/apps/website/src/components/pricing/LeadForm.tsx @@ -1,6 +1,8 @@ 'use client'; import { useState } from 'react'; import { tokens } from '@ngaf/design-tokens'; +import { analyticsEvents } from '../../lib/analytics/events'; +import { track } from '../../lib/analytics/client'; export function LeadForm() { const [status, setStatus] = useState<'idle' | 'sending' | 'sent' | 'error'>('idle'); @@ -10,14 +12,36 @@ export function LeadForm() { setStatus('sending'); const form = e.currentTarget; const data = Object.fromEntries(new FormData(form)); + track(analyticsEvents.marketingLeadFormSubmit, { + surface: 'pricing', + source_section: 'lead-form', + }); try { const res = await fetch('/api/leads', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); - setStatus(res.ok ? 'sent' : 'error'); + if (res.ok) { + track(analyticsEvents.marketingLeadFormSuccess, { + surface: 'pricing', + source_section: 'lead-form', + }); + setStatus('sent'); + } else { + track(analyticsEvents.marketingLeadFormFail, { + surface: 'pricing', + source_section: 'lead-form', + error_reason: 'api_error', + }); + setStatus('error'); + } } catch { + track(analyticsEvents.marketingLeadFormFail, { + surface: 'pricing', + source_section: 'lead-form', + error_reason: 'network_error', + }); setStatus('error'); } }; diff --git a/apps/website/src/components/shared/AnnouncementToast.tsx b/apps/website/src/components/shared/AnnouncementToast.tsx index 6c9ea7cef..c14712cb1 100644 --- a/apps/website/src/components/shared/AnnouncementToast.tsx +++ b/apps/website/src/components/shared/AnnouncementToast.tsx @@ -2,6 +2,8 @@ import { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { tokens } from '@ngaf/design-tokens'; +import { analyticsEvents } from '../../lib/analytics/events'; +import { track, trackWhitepaperDownloadClick } from '../../lib/analytics/client'; /** * Bump this date to re-show the toast for all users. @@ -40,13 +42,30 @@ export function AnnouncementToast() { e.preventDefault(); if (!email) return; setSubmitting(true); + track(analyticsEvents.marketingWhitepaperSignupSubmit, { + surface: 'toast', + source_section: 'announcement-toast', + paper: 'overview', + }); try { await fetch('/api/whitepaper-signup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }), }); - } catch { /* best-effort */ } + track(analyticsEvents.marketingWhitepaperSignupSuccess, { + surface: 'toast', + source_section: 'announcement-toast', + paper: 'overview', + }); + } catch { + track(analyticsEvents.marketingWhitepaperSignupFail, { + surface: 'toast', + source_section: 'announcement-toast', + paper: 'overview', + error_reason: 'api_error', + }); + } setStep('sent'); setSubmitting(false); // Auto-dismiss after showing success @@ -148,7 +167,14 @@ export function AnnouncementToast() {