Skip to content

Comments

Add analytics tracking and admin dashboard#139

Merged
jaylong255 merged 8 commits intomainfrom
enhancement/seo
Feb 17, 2026
Merged

Add analytics tracking and admin dashboard#139
jaylong255 merged 8 commits intomainfrom
enhancement/seo

Conversation

@jaylong255
Copy link
Member

@jaylong255 jaylong255 commented Feb 16, 2026

  • 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

Note

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/track to accept a bounded set of event types, validate payloads, and persist to Supabase page_events, with optional Upstash Redis rate limiting; adds a new /admin/analytics page 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, new ProofSection).

Written by Cursor Bugbot for commit 1d67bb9. This will update automatically on new commits. Configure here.

- 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
@vercel
Copy link
Contributor

vercel bot commented Feb 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
legend Ready Ready Preview, Comment Feb 17, 2026 2:21pm

Request Review


useEffect(() => {
if (initialized.current) return;
initialized.current = true;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in Cursor Fix in Web


const scrollToContact = () => {
contactRef.current?.scrollIntoView({ behavior: 'smooth' });
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in Cursor Fix in Web

{"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"}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

.select('session_id')
.gte('created_at', sinceISO);

const uniqueSessions = new Set(uniqueSessionsData?.map((r) => r.session_id)).size;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in Cursor Fix in Web

- 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

// Filter by tag when ?tag= is present
if (tagFilter) {
const decodedTag = decodeURIComponent(tagFilter);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in Cursor Fix in Web

…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;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

useEffect(() => {
const result = getVariant('hero_headline_v1');
setVariant(result.variant);
}, []);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Fix in Cursor Fix in Web

…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
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Fix in Cursor Fix in Web

Co-authored-by: Jay Long <jay@cyberworldbuilders.com>
@jaylong255 jaylong255 merged commit 166aa3b into main Feb 17, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants