GMT+8 SG --:-- 0000 X 0000 Y /
← 全部文章← All writing

VOLO engineering diary

Engineering Diary, Day 15: 29 Users, 3-Second Bounces, and the Cookie Banner That Blinded Our Analytics

Originally published on flyvolo.ai as part of the VOLO Engineering Diary (2026-03-05), documenting the build of the VOLO private-aviation platform between February and May 2026. When VOLO changed direction in September 2026 the 25 entries were moved here as an archive. The text is unchanged; the products, pages and links it mentions no longer exist. Original byline: VOLO CTO.

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:

MetricValue
Active users29
New users28
Average engagement9 seconds
Homepage engagement3 seconds
Organic search sessions1
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:

ElementAppears at
Badge ("200+ Jet Models")2.2 seconds
Title ("Your Private Jet")2.4 seconds
Subtitle (value proposition)2.6 seconds
Booking form2.6 seconds
CTA buttons2.8 seconds
Scroll indicator3.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/terms covering 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 localStorage with custom event dispatch so GA4 reacts in real-time to consent changes via useSyncExternalStore.

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:

CountryActive Users
Singapore12
China8
United States7
Germany2
South Korea2

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

MetricBeforeAfter
Hero content visible2.2 seconds0.3 seconds
UTM attributionBroken (never mounted)Working + survives redirects
Pageview tracking coverage~20-30% (consent only)100% (server-side fallback)
Consent gap visibilityUnknownMeasured per choice
Scroll depth trackingNone25/50/75/100% milestones
CTA click trackingNoneAll primary CTAs
IndexNow schedulingManual onlyWeekly cron (3,400+ URLs)
Homepage real-time contentNone4 live empty leg deals
Legal entityUS (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.