From 7208095b062f095f5a0c1a4f607d891f9466896f Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 5 Sep 2026 23:43:57 -0400 Subject: [PATCH] feat(web): count the two moments on the contact form that matter There is not a single custom event in this codebase, which is why every argument about the site's copy has been an argument about taste. Two events, a visitor beginning the contact form and a visitor sending it, turn the hero rewrite that follows this into a before-and-after instead of an opinion. `contact-started` fires once per mounted form, on the first real edit rather than on focus: someone tabbing through on the way elsewhere is not a lead. `contact-submitted` fires on success. Both carry only which form it was, inquiry or support. `track()` does no consent checking of its own on purpose. UmamiAnalytics already declines to load the script under Global Privacy Control and the script honours Do Not Track, so `window.umami` is simply absent for a visitor who opted out and every call is a no-op. A second gate here could disagree with the first. It also never throws: an analytics call is not worth losing a form submission over. /privacy now says what we count, in the same commit that starts counting it. --- apps/web/src/app/privacy/page.tsx | 7 + .../src/components/contact/contact-form.tsx | 139 ++++++++++++------ apps/web/src/lib/analytics.ts | 35 +++++ 3 files changed, 139 insertions(+), 42 deletions(-) create mode 100644 apps/web/src/lib/analytics.ts diff --git a/apps/web/src/app/privacy/page.tsx b/apps/web/src/app/privacy/page.tsx index a132563..901e45c 100644 --- a/apps/web/src/app/privacy/page.tsx +++ b/apps/web/src/app/privacy/page.tsx @@ -84,6 +84,13 @@ export default function PrivacyPage() { collected from your visit. We do not use Google Analytics, advertising pixels, or similar third-party tracking tools on the Site.

+

+ Alongside page views, we count two things you do on purpose: that someone began filling in + the contact form, and that someone sent it. These are counters, not contents. They record + which of the two forms it was and nothing you typed, and like everything else here they are + not collected at all if Do Not Track or Global Privacy Control is on. We use them to tell + whether a change to the site helped or hurt, rather than guessing. +

Server logs

diff --git a/apps/web/src/components/contact/contact-form.tsx b/apps/web/src/components/contact/contact-form.tsx index 243a3cf..ce29bbe 100644 --- a/apps/web/src/components/contact/contact-form.tsx +++ b/apps/web/src/components/contact/contact-form.tsx @@ -1,10 +1,11 @@ -'use client'; -import { useActionState } from 'react'; -import { useFormStatus } from 'react-dom'; -import { submitContactForm } from '@/actions/submit-contact'; -import { TurnstileWidget } from './turnstile-widget'; -import { useState, useEffect, useRef } from 'react'; -import { Button } from '@repo/ui/components/button'; +"use client"; +import { useActionState } from "react"; +import { useFormStatus } from "react-dom"; +import { submitContactForm } from "@/actions/submit-contact"; +import { TurnstileWidget } from "./turnstile-widget"; +import { useState, useEffect, useRef } from "react"; +import { Button } from "@repo/ui/components/button"; +import { EVENTS, track } from "@/lib/analytics"; function SubmitButton({ label = "Send Message" }: { label?: string }) { const { pending } = useFormStatus(); @@ -21,50 +22,68 @@ function SubmitButton({ label = "Send Message" }: { label?: string }) { } interface ContactFormProps { - source?: 'inquiry' | 'support'; + source?: "inquiry" | "support"; } -export function ContactForm({ source = 'inquiry' }: ContactFormProps) { - const [state, formAction] = useActionState(submitContactForm, { success: false, message: '' }); - const [token, setToken] = useState(''); +export function ContactForm({ source = "inquiry" }: ContactFormProps) { + const [state, formAction] = useActionState(submitContactForm, { + success: false, + message: "", + }); + const [token, setToken] = useState(""); const formRef = useRef(null); + // Fires once per mounted form, on the first real edit. A focus would count a visitor who + // tabbed through on the way somewhere else; typing is intent. + const startedRef = useRef(false); - const isSupport = source === 'support'; + const noteStart = () => { + if (startedRef.current) return; + startedRef.current = true; + track(EVENTS.contactStarted, { source }); + }; + + const isSupport = source === "support"; // Reset form on successful submission useEffect(() => { if (state.success && formRef.current) { formRef.current.reset(); - setToken(''); + setToken(""); + startedRef.current = false; + track(EVENTS.contactSubmitted, { source }); } - }, [state.success]); + }, [state.success, source]); return (

-
-
+
+
-
-

- {isSupport ? 'Send Feedback' : 'Project Inquiry'} -

-

- {isSupport - ? 'Found a bug? Have a feature request? We read every message and typically respond within 24 hours.' - : 'Tell us about your technical challenges. From Medusa v2 migrations to real-time custom engines, we\'re ready to architect your solution.'} -

-
+
+

+ {isSupport ? "Send Feedback" : "Project Inquiry"} +

+

+ {isSupport + ? "Found a bug? Have a feature request? We read every message and typically respond within 24 hours." + : "Tell us about your technical challenges. From Medusa v2 migrations to real-time custom engines, we're ready to architect your solution."} +

+
-
-
-