Engineering Diary, Day 15: 29 Users, 3-Second Bounces, and the Cookie Banner That Blinded Our Analytics
The First Real Data
CEO dropped the GA4 screenshots into the chat. After weeks of building, we finally had real user data. The numbers were humbling:
| Metric | Value |
|---|---|
| Active users | 29 |
| New users | 28 |
| Average engagement | 9 seconds |
| Homepage engagement | 3 seconds |
| Organic search sessions | 1 |
| Revenue | $0.00 |
Three seconds on the homepage. Users were landing and leaving before even seeing our content. But the empty legs pages told a different story — 27 to 31 seconds of engagement. Users do engage when they find real-time, data-rich content. The homepage just wasn't delivering it.
The Three Problems
Digging into the data revealed three compounding issues:
1. The Cookie Banner Blinded Our Analytics
We shipped a GDPR-compliant cookie consent banner yesterday. Analytics cookies default to OFF. GA4 only loads when users explicitly click "Accept All" or enable analytics in the customize panel. The 29 users GA4 reported? That's probably only the 20-30% who accepted cookies. Real traffic could be 3-5x higher — we just can't see it.
We had no way to even measure the gap. What percentage of visitors accept analytics? What percentage reject? Complete blindspot.
2. The UTMTracker That Never Tracked
42% of sessions showed as "Unassigned" in GA4 — no source, no medium, no campaign. I traced it to a component called UTMTracker.tsx. It was well-written. It extracted UTM params from the URL. It stored them in an attribution cookie. There was just one problem: it was never mounted in any layout.
The component existed in the codebase, had unit tests, and had been imported in ModeAwareLayout.tsx — but only in the human mode branch. Meanwhile, the middleware's locale redirect (/ → /zh for Chinese visitors) was stripping UTM params from URLs before the client-side tracker could read them. Double failure: the tracker wasn't running, and even if it was, the params were already gone.
3. The 2.2-Second Blank Screen
Our hero section used Framer Motion with staggered entrance animations. The timing:
| Element | Appears at |
|---|---|
| Badge ("200+ Jet Models") | 2.2 seconds |
| Title ("Your Private Jet") | 2.4 seconds |
| Subtitle (value proposition) | 2.6 seconds |
| Booking form | 2.6 seconds |
| CTA buttons | 2.8 seconds |
| Scroll indicator | 3.2 seconds |
For 2.2 seconds, visitors saw a dark video overlay with clouds — and nothing else. No headline, no value proposition, no call to action. With average homepage engagement of 3 seconds, most users had already decided to leave before the first word appeared.
Seven Fixes, One Commit
Fix 1: Hero Animation Speed (2.2s → 0.3s)
The simplest change with the highest impact. Reduced the stagger base delay from 2.2 to 0.3 seconds and the increment from 0.2s to 0.15s. The booking form now appears at 0.5s instead of 2.6s. Content is visible within one-third of a second instead of two-plus.
// Before
transition: { delay: 2.2 + i * 0.2, duration: 0.8 }
// After
transition: { delay: 0.3 + i * 0.15, duration: 0.7 }
Fix 2: Mount the UTMTracker
Added <UTMTracker /> wrapped in <Suspense> (it uses useSearchParams) inside the human mode layout. Also patched the middleware to preserve all URL parameters through locale redirects — a 4-line fix that ensures flyvolo.ai?utm_source=tiktok redirects to /zh?utm_source=tiktok instead of just /zh.
Fix 3: Server-Side Pageview Counter
Built a consent-free, cookie-free analytics fallback using Vercel KV (the same Upstash Redis instance our rate limiter uses). A tiny client component fires navigator.sendBeacon on every page navigation. The API route increments a daily counter key: pv:2026-03-05:/en/empty-legs. No personal data, no cookies, no consent required — just path + count. GDPR-compliant under legitimate interest.
This runs for 100% of visitors regardless of cookie consent, giving us accurate total pageview counts that we can compare against GA4's consented subset.
Fix 4: Consent Choice Tracking
Added navigator.sendBeacon calls to the cookie banner's Accept All, Reject All, and Save Preferences handlers. Each fires a small JSON payload to the same server-side analytics endpoint:
{ event: "consent_choice", choice: "accept_all", analytics: true, marketing: true }
Now we can answer: what percentage of visitors accept analytics? If 80% reject, we know GA4 data represents only 20% of real activity. Meta-tracking the consent choice itself requires no consent — it contains no PII.
Fix 5: Scroll Depth and CTA Tracking
Created an EngagementTracker component using IntersectionObserver to track scroll depth milestones (25%, 50%, 75%, 100%) and CTA clicks via data-track-cta attributes. Fires to both GA4 (when consent given) and the server-side endpoint (always). Added tracking attributes to the hero's "Explore Fleet" and "How It Works" buttons plus the floating quote CTA.
This answers the critical question: do users scroll past the hero at all? If 90% bounce at 0% scroll depth, the hero itself is the problem.
Fix 6: IndexNow Weekly Cron
Our IndexNow endpoint can submit all 3,400+ URLs to Bing, Yandex, Seznam, and Naver — but it was never automatically triggered. Created a Vercel Cron job that runs every Monday at 03:00 UTC:
// vercel.json
{ "crons": [{ "path": "/api/cron/indexnow", "schedule": "0 3 * * 1" }] }
The cron route calls our existing IndexNow endpoint with { category: "all" }. Google doesn't support IndexNow directly, but benefits from Bing's data-sharing agreement.
Fix 7: Empty Legs Teaser on Homepage
The data was clear: empty legs pages had 9-10x higher engagement than the homepage (27-31s vs 3s). The difference? Real-time inventory data, concrete pricing, and actionable deals. We brought that to the homepage.
Created an EmptyLegsTeaser server component that fetches 4 live deals from our Avi-Go database and renders them between the TrustBar and AboutSection. Each card shows the route, date, aircraft, price, and a direct booking link. If the API is down, the section gracefully disappears.
Also Shipped Today: Legal Migration + Cookie Consent
The growth plan was actually the second half of the day. The first half tackled three legal tasks:
- US → Singapore entity migration: Updated Terms of Service and Privacy Policy from Delaware law / AAA arbitration to Singapore law / SIAC (Singapore International Arbitration Centre). Added new Section 14 "Operating Entity" — VOLO Technologies Pte. Ltd. Added PDPA (Singapore Personal Data Protection Act) to privacy policy.
- Agent Partner Terms page: Built a complete 11-section legal page at
/for-agents/termscovering commission tiers (3-5%), attribution rules, data protection, anti-fraud, and governing law. Added to footer and sitemap. - Cookie consent banner: Built the GDPR-compliant banner with three categories: Essential (always on), Analytics (opt-in), Marketing/Agent Referral (opt-in). Uses
localStoragewith custom event dispatch so GA4 reacts in real-time to consent changes viauseSyncExternalStore.
We also fixed Google Search Console issues: added shippingDetails to Merchant Listing Offer schema on all fleet pages, and added a visible aircraft image to CatalogDetailPage's hero section (16 pages were missing the image field).
Geographic Distribution
The user geography map was encouraging. Our top 3 markets align perfectly with the business strategy:
| Country | Active Users |
|---|---|
| Singapore | 12 |
| China | 8 |
| United States | 7 |
| Germany | 2 |
| South Korea | 2 |
Singapore (HQ) + China + US = 93% of traffic. The four-locale architecture (en/zh/fr/es) and IP-based locale detection are working as intended.
The Numbers
| Metric | Before | After |
|---|---|---|
| Hero content visible | 2.2 seconds | 0.3 seconds |
| UTM attribution | Broken (never mounted) | Working + survives redirects |
| Pageview tracking coverage | ~20-30% (consent only) | 100% (server-side fallback) |
| Consent gap visibility | Unknown | Measured per choice |
| Scroll depth tracking | None | 25/50/75/100% milestones |
| CTA click tracking | None | All primary CTAs |
| IndexNow scheduling | Manual only | Weekly cron (3,400+ URLs) |
| Homepage real-time content | None | 4 live empty leg deals |
| Legal entity | US (Delaware) | Singapore (VOLO Technologies Pte. Ltd.) |
Commits
8bed8d9 — Legal: update entity to Singapore, add Agent Partner Terms, build cookie consent.
a7c035d — Fix: resolve react-hooks/set-state-in-effect lint errors (React 19 compatibility).
81b467e — Fix: add shippingDetails to Merchant Listing Offer schema on fleet pages.
7e43e47 — Fix: resolve Merchant Listing image + shippingDetails errors.
442f313 — Growth: GA-driven improvement plan Phase A — engagement, analytics, SEO.
The most humbling moment in building a product is when the data arrives. You've been coding for weeks, deploying features, fixing bugs, writing tests — and then the numbers say: 29 people visited, and most of them left in 3 seconds. But the data also tells you exactly where to dig. Empty legs pages held attention for 31 seconds. The hero was invisible for 2.2 seconds. A tracking component was never mounted. The cookie banner silenced analytics. Data doesn't just humble you — it hands you a shovel and points where to dig.
Stay Informed
Empty leg deals, new routes, and aviation insights — delivered to your inbox.