From deaa1810d79ec17b87025d9bc83e939770873342 Mon Sep 17 00:00:00 2001 From: Mannu Date: Sat, 30 May 2026 00:40:05 +0530 Subject: [PATCH] feat: add Umami self-hosted analytics with custom event tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Root layout: load Umami script (afterInteractive) — covers all pages including SPA navigation auto-tracking - Marketing layout: remove Plausible script (Umami now covers marketing pages too) - src/lib/analytics.ts: type-safe track() wrapper + typed helpers for each event; window.umami declared globally; safe no-op on SSR/ad-block - Custom events wired: log-created { logType } — LogModal on successful save garment-added — wardrobe/add after save memory-added — memories after upload pipeline completes growth-logged — growth page after measurement saved pwa-installed — InstallPrompt when Android prompt accepted Co-Authored-By: Claude Sonnet 4.6 --- src/app/(app)/growth/page.tsx | 2 + src/app/(app)/memories/page.tsx | 2 + src/app/(app)/wardrobe/add/page.tsx | 2 + src/app/(marketing)/layout.tsx | 10 +---- src/app/layout.tsx | 7 ++++ src/components/InstallPrompt.tsx | 2 + src/components/LogModal.tsx | 2 + src/lib/analytics.ts | 63 +++++++++++++++++++++++++++++ 8 files changed, 81 insertions(+), 9 deletions(-) create mode 100644 src/lib/analytics.ts diff --git a/src/app/(app)/growth/page.tsx b/src/app/(app)/growth/page.tsx index e8b9d5a..b8b9cf3 100644 --- a/src/app/(app)/growth/page.tsx +++ b/src/app/(app)/growth/page.tsx @@ -7,6 +7,7 @@ import { Button, Card, Input, ConfirmDialog } from "@/components/ui"; import { WHO_BOY_WEIGHT, WHO_GIRL_WEIGHT, getAgeInMonthsFromBirth, getPercentile } from "@/lib/growth-standards"; import { formatAge } from "@/lib/formatting"; import { fmtDate } from "@/lib/date-ist"; +import { trackGrowthLogged } from "@/lib/analytics"; import type { GrowthRecord, Goal } from "@/types"; import { Chart as ChartJS, @@ -116,6 +117,7 @@ export default function GrowthPage() { setSaveError(err.error || "Failed to save"); return; } + trackGrowthLogged(); resetForm(); fetchGrowthData(); } catch (e) { diff --git a/src/app/(app)/memories/page.tsx b/src/app/(app)/memories/page.tsx index d24eac6..2e053ed 100644 --- a/src/app/(app)/memories/page.tsx +++ b/src/app/(app)/memories/page.tsx @@ -6,6 +6,7 @@ import { useFamily } from "@/app/FamilyProvider"; import { Button, ConfirmDialog, Modal } from "@/components/ui"; import { StorageMeter, StorageQuotaBanner } from "@/components/StorageMeter"; import { formatBytes } from "@/lib/format-bytes"; +import { trackMemoryAdded } from "@/lib/analytics"; /** Some Android cameras return file.type = "" — detect from extension as fallback. */ function resolveContentType(file: File): string { @@ -213,6 +214,7 @@ export default function MemoriesPage() { return; } + trackMemoryAdded(); // Optimistic grid update const proxyUrl = `/api/img?key=${encodeURIComponent(key)}`; setMemories(prev => [{ diff --git a/src/app/(app)/wardrobe/add/page.tsx b/src/app/(app)/wardrobe/add/page.tsx index 2edefa1..4914b01 100644 --- a/src/app/(app)/wardrobe/add/page.tsx +++ b/src/app/(app)/wardrobe/add/page.tsx @@ -5,6 +5,7 @@ import { useRouter } from "next/navigation"; import Image from "next/image"; import { useFamily } from "@/app/FamilyProvider"; import { Button } from "@/components/ui"; +import { trackGarmentAdded } from "@/lib/analytics"; import { GARMENT_CATEGORIES, GARMENT_SIZE_ORDER } from "@/db/schema/wardrobe"; type Step = "capture" | "form"; @@ -224,6 +225,7 @@ export default function AddGarmentPage() { }), }); if (!res.ok) throw new Error((await res.json()).error); + trackGarmentAdded(); handleAddAnother(); } catch (err) { setError(`Save failed: ${err}`); diff --git a/src/app/(marketing)/layout.tsx b/src/app/(marketing)/layout.tsx index 16f8928..3a04c86 100644 --- a/src/app/(marketing)/layout.tsx +++ b/src/app/(marketing)/layout.tsx @@ -1,7 +1,6 @@ import type { Metadata } from "next"; import Link from "next/link"; import { MarketingNav } from "@/components/marketing/MarketingNav"; -import Script from "next/script"; export const metadata: Metadata = { title: { @@ -32,15 +31,8 @@ export default function MarketingLayout({ }) { return ( <> - {/* Privacy-respecting analytics — no cookies, no tracking */} -