feat(GRO-1794): add booking funnel analytics events
CI / Lint & Typecheck (pull_request) Failing after 15s
CI / Test (pull_request) Failing after 18s
CI / Build & Push Docker Image (pull_request) Has been skipped

- New analytics utility (src/lib/analytics.ts) with ANALYTICS_EVENTS constants
  and fireAnalyticsEvent() – thin wrapper over window.dispatchEvent, no-op safe
  Built for Plausible/GTM integration later.

- Public booking wizard (Book.tsx): fires step-transition events at each step
  (service → time → contact → submit) plus booking_confirmed on the dedicated
  confirmation page.

- Portal BookingFlow (Appointments.tsx): fires equivalent events for the
  portal booking flow. booking_confirmed fires via useEffect when the inline
  success state is shown.

- BookingErrorPage: fires booking_error on mount (no PII in payload).

Events include step name and flow type (public/portal) but contain no PII:
no names, emails, phone numbers, or pet names in any payload.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Flea Flicker
2026-05-26 12:38:58 +00:00
parent 344a32e3e4
commit 2e99ed520f
6 changed files with 157 additions and 2 deletions
+16 -2
View File
@@ -1,5 +1,6 @@
import React, { useState, useEffect } from 'react';
import { Calendar, Clock, Plus, ChevronRight, ChevronDown, Loader2 } from 'lucide-react';
import { ANALYTICS_EVENTS, fireAnalyticsEvent } from '../../lib/analytics';
export interface Appointment {
id: string;
@@ -720,6 +721,11 @@ function BookingFlow({ onClose, sessionId }: BookingFlowProps) {
const [notes, setNotes] = useState('');
const [recurring, setRecurring] = useState('');
const [confirmed, setConfirmed] = useState(false);
useEffect(() => {
if (confirmed) {
fireAnalyticsEvent(ANALYTICS_EVENTS.BOOKING_CONFIRMED, { step: "confirmed", flow: "portal" });
}
}, [confirmed]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [submitting, setSubmitting] = useState(false);
@@ -801,6 +807,7 @@ function BookingFlow({ onClose, sessionId }: BookingFlowProps) {
if (response.ok) {
setConfirmed(true);
fireAnalyticsEvent(ANALYTICS_EVENTS.BOOKING_STEP_SUBMIT, { step: "submit", flow: "portal" });
setTimeout(() => {
window.location.reload();
}, 1500);
@@ -876,6 +883,7 @@ function BookingFlow({ onClose, sessionId }: BookingFlowProps) {
onClick={() => {
setSelectedPet(pet);
setStep(2);
fireAnalyticsEvent(ANALYTICS_EVENTS.BOOKING_STEP_SERVICE, { step: "service", flow: "portal" });
}}
className={`w-full flex items-center gap-3 p-3 rounded-xl border text-left transition-colors ${
selectedPet?.id === pet.id
@@ -1034,7 +1042,10 @@ function BookingFlow({ onClose, sessionId }: BookingFlowProps) {
Back
</button>
<button
onClick={() => setStep(4)}
onClick={() => {
setStep(4);
fireAnalyticsEvent(ANALYTICS_EVENTS.BOOKING_STEP_CONTACT, { step: "groomer", flow: "portal" });
}}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium"
>
Next
@@ -1093,7 +1104,10 @@ function BookingFlow({ onClose, sessionId }: BookingFlowProps) {
Back
</button>
<button
onClick={() => setStep(5)}
onClick={() => {
setStep(5);
fireAnalyticsEvent(ANALYTICS_EVENTS.BOOKING_STEP_TIME, { step: "time", flow: "portal" });
}}
disabled={!selectedDate || !selectedTime}
className="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium disabled:opacity-50"
>