Add analytics tracking and admin dashboard#139
Conversation
- Create new file `app/admin/analytics/page.tsx` for analytics page with data fetching logic - Update `app/api/track/route.ts` to handle tracking events from the frontend - Modify `app/layout.tsx` and `app/page.tsx` to integrate Google Analytics and scroll tracking - Update various components (`AboutSection.tsx`, `ContactCTA.tsx`, etc.) to emit tracking events - Add new `ScrollTracker.tsx` component for scroll depth tracking - Enhance `lib/ab-test.ts` with A/B testing functionality - Create `lib/tracking.ts` for centralized tracking event handling - Add SQL migration script to create `page_events` table in Supabase
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
|
||
| useEffect(() => { | ||
| if (initialized.current) return; | ||
| initialized.current = true; |
There was a problem hiding this comment.
Ref guard breaks scroll tracking in strict mode
Medium Severity
The initialized.current guard combined with a thorough cleanup function breaks the ScrollTracker in React strict mode. During the strict mode remount cycle, the cleanup correctly removes all listeners and disconnects the observer, but initialized.current remains true (refs persist across remounts). The second effect invocation then immediately returns, leaving no observers or listeners attached. All scroll tracking and section visibility tracking silently stops working in development.
Additional Locations (1)
|
|
||
| const scrollToContact = () => { | ||
| contactRef.current?.scrollIntoView({ behavior: 'smooth' }); | ||
| }; |
There was a problem hiding this comment.
Unused contactRef after removing scroll function
Low Severity
contactRef is defined and attached to a wrapper div but is never read. The scrollToContact function that previously consumed it was removed in this commit, making the ref and its wrapping div dead code. The useRef import also becomes unnecessary since nothing else uses it.
Additional Locations (1)
supabase/migrations/20260216000000_create_page_events_table.sql
Outdated
Show resolved
Hide resolved
.cursor/debug.log
Outdated
| {"location":"app/blog/tag/[tag]/page.tsx:TagPage:lookup","message":"Tag lookup result","data":{"tagParam":"investor%20pitch","found":true,"postCount":1,"willNotFound":false},"timestamp":1771279521544,"hypothesisId":"H2,H5"} | ||
| {"location":"app/blog/tag/[tag]/page.tsx:TagPage:entry","message":"TagPage request","data":{"tagParam":"investor%20pitch","decodedTag":"investor pitch","hasTagData":true,"totalKeys":401,"sampleKeys":["pitch-reflections","death-care-ar","prototyping","investor-strategy","revenant-hollow"]},"timestamp":1771279521609,"hypothesisId":"H1,H3,H4"} | ||
| {"location":"app/blog/tag/[tag]/page.tsx:TagPage:lookup","message":"Tag lookup result","data":{"tagParam":"investor%20pitch","found":true,"postCount":1,"willNotFound":false},"timestamp":1771279521609,"hypothesisId":"H2,H5"} | ||
| {"location":"app/blog/tag/[tag]/page.tsx:generateStaticParams","message":"Static params generated","data":{"paramCount":401,"sampleParams":["pitch-reflections","death-care-ar","prototyping","investor-strategy","revenant-hollow"]},"timestamp":1771279521683,"hypothesisId":"H3,H4"} |
There was a problem hiding this comment.
Debug log file accidentally committed to repository
Medium Severity
The .cursor/debug.log file containing 252 lines of debug session data (timestamps, tag parameters, hypothesis IDs) is being committed to the repository. The .cursor directory is not in .gitignore, so this debug output will be checked into source control and deployed.
app/admin/analytics/page.tsx
Outdated
| .select('session_id') | ||
| .gte('created_at', sinceISO); | ||
|
|
||
| const uniqueSessions = new Set(uniqueSessionsData?.map((r) => r.session_id)).size; |
There was a problem hiding this comment.
Analytics queries silently capped at 1000 rows
Medium Severity
Supabase returns a maximum of 1000 rows by default. The uniqueSessionsData, sectionWithSessions, depthData, and variantData queries all fetch rows without .range() or .limit(), then aggregate client-side. Once traffic exceeds ~1000 events in the selected period, these queries will silently truncate, causing uniqueSessions, section visibility, scroll depth, and A/B test metrics to all report incorrect values.
Additional Locations (2)
- Remove .cursor/debug.log file containing outdated debug information - Update .gitignore to exclude log files - Enhance app/admin/analytics/page.tsx for better data visualization - Add SEO improvements in app/page.tsx - Modify components/ScrollTracker.tsx to optimize performance - Refactor lib/rate-limit.ts and lib/turnstile.ts to improve security practices - Create new page events table with supabase/migrations/20260216000000_create_page_events_table.sql - Drop anonymous insert policy from page events table using supabase/migrations/20260217000000_drop_page_events_anon_insert_policy.sql
app/blog/page.tsx
Outdated
|
|
||
| // Filter by tag when ?tag= is present | ||
| if (tagFilter) { | ||
| const decodedTag = decodeURIComponent(tagFilter); |
There was a problem hiding this comment.
Redundant decodeURIComponent on already-decoded search params
Medium Severity
Next.js App Router searchParams values are already URL-decoded. Calling decodeURIComponent(tagFilter) again is redundant and will throw a URIError if the decoded value contains a % followed by non-hex characters (e.g., a tag like "100% growth" or a manually crafted URL). This crashes the page at three call sites: in generateMetadata, in the tag filter logic, and in the heading render.
Additional Locations (2)
…r directly in all titles and descriptions. Tag filter logic (lines 78–85) – Replaced decodeURIComponent(tagFilter) with tagFilter in the filter, and added a comment noting that searchParams are already decoded. Heading render (line 147) – Switched from decodeURIComponent(tagFilter) to tagFilter in the page heading.
| .range(from, to); | ||
| return { data: r.data, error: r.error }; | ||
| }); | ||
| const uniqueSessions = new Set(uniqueSessionsData.map((r) => r.session_id)).size; |
There was a problem hiding this comment.
Unfiltered query fetches all events for unique sessions
Medium Severity
The uniqueSessionsData query fetches every row from page_events without filtering by event_name. Each session generates many events (page_view, section_visible ×6, scroll_depth ×4, cta_click, etc.), so this fetches roughly 10–15× more rows than needed. Combined with the sequential 1000-row pagination in fetchAllRows, the 90-day admin view could require hundreds of sequential Supabase API calls. Adding .eq('event_name', 'page_view') would drastically reduce data transfer since every session has exactly one page_view event.
components/HeroSection.tsx
Outdated
| useEffect(() => { | ||
| const result = getVariant('hero_headline_v1'); | ||
| setVariant(result.variant); | ||
| }, []); |
There was a problem hiding this comment.
A/B variant flash for half of users
Medium Severity
The variant state is initialized to 'A' and only updated to the actual assigned variant inside a useEffect, which runs after the first paint. Users assigned to variant B will see variant A's headline briefly before it switches, causing a visible content flash. This undermines A/B test validity because B-variant users are exposed to both variants, and the layout shift hurts Core Web Vitals (CLS).
…6 + scroll_depth ×4 + others) to ~1 per page visit Cuts API calls – for a 90‑day range, far fewer pages of 1000 rows Keeps the metric the same – unique session count still comes from distinct session_id values, and every session has at least one page_view
Client component that calls getVariant('hero_headline_v1') directly in render (no useState/useEffect)
Renders the correct A or B headline from the start
Designed to be loaded with dynamic(..., { ssr: false }) so getVariant() only runs on the client, where it can access localStorage
Updated components/HeroSection.tsx
Dynamically imports HeroHeadline with ssr: false
Adds a loading placeholder (skeleton with animate-pulse) sized to the headline/subline to reduce CLS while the chunk loads
Removes variant state, useEffect, and getVariant usage
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
| const handleOpenForm = () => { | ||
| trackEvent('cta_click', { cta: 'contact_start' }); | ||
| setShowForm(true); | ||
| }; |
There was a problem hiding this comment.
Contact form never fires lead_submit tracking event
Medium Severity
The ContactCTA/ContactForm flow fires cta_click when the form opens but never fires a lead_submit event on successful submission. Only the HeroSection hero email form fires lead_submit. The analytics dashboard relies on lead_submit events for "Lead Submissions" count, "Lead Conv. Rate", and A/B test conversion rates — so all leads captured through the full contact form are invisible to analytics.
Additional Locations (1)
Co-authored-by: Jay Long <jay@cyberworldbuilders.com>


app/admin/analytics/page.tsxfor analytics page with data fetching logicapp/api/track/route.tsto handle tracking events from the frontendapp/layout.tsxandapp/page.tsxto integrate Google Analytics and scroll trackingAboutSection.tsx,ContactCTA.tsx, etc.) to emit tracking eventsScrollTracker.tsxcomponent for scroll depth trackinglib/ab-test.tswith A/B testing functionalitylib/tracking.tsfor centralized tracking event handlingpage_eventstable in SupabaseNote
Medium Risk
Adds a new anonymous tracking endpoint with rate limiting and introduces Turnstile verification in lead submission, which can affect traffic ingestion and form conversion if misconfigured. Also changes blog tag URLs/routing, impacting SEO and existing inbound links.
Overview
Adds first-party analytics collection and reporting. Introduces
POST /api/trackto accept a bounded set of event types, validate payloads, and persist to Supabasepage_events, with optional Upstash Redis rate limiting; adds a new/admin/analyticspage to aggregate sessions, section visibility, scroll depth, CTA clicks, leads, and A/B variant performance.Updates the marketing funnel and blog tagging. Lead submissions now validate Cloudflare Turnstile tokens (when configured) and the homepage emits new tracking events via
ScrollTracker, CTA clicks, and a new hero-headline A/B test (lib/ab-test.ts+HeroHeadline). Blog tag pages are removed in favor of?tag=filtering (updated links, sitemap/test/docs), and various homepage sections/copy are refreshed (IDs added for tracking, newProofSection).Written by Cursor Bugbot for commit 1d67bb9. This will update automatically on new commits. Configure here.