A client came to us last November convinced their checkout was broken. Their Google Analytics showed 22% of users dropped off between cart and payment, but they couldn't tell us why. We pulled seven days of raw CDN logs and found the real story: nearly a third of those drop-offs never fired a single analytics event because a third-party consent script was blocking everything. The CDN had seen every request all along.
What Your Browser Analytics Isn't Telling You
I've spent the last decade watching teams try to reconstruct user journeys from client-side events alone, and it doesn't work the way they hope it will. Ad blockers, privacy extensions, failed JavaScript execution, and strict ITTP consent modes strip out 15-40% of your traffic before your tag manager even loads. If you're relying solely on browser beacons, you're mapping a conversion path with half the roads missing. CDN logs don't have that blind spot. Every request for HTML, images, API calls, and even prefetches hits the edge first, so you get a complete, server-verified record of what the user actually asked for, not what your script managed to report.
That completeness matters because the most important journey signals often live outside the pageview. Think of a user who lands on /products/shoes, gets a 304 Not Modified from cache, clicks through to /cart, then hits a 503 on your payment API for two seconds before refreshing. Your front-end analytics might just show a pageview and a cart event. The CDN log shows the cache status, the origin response time, the edge POP that served them, the TLS version, and the exact Referer and User-Agent chain defined in the HTTP Working Group Specifications. When we pair that with guidance from web.dev Articles on Core Web Vitals, we can correlate a slow Largest Contentful Paint at the edge with the moment a user abandoned the journey, without needing any custom instrumentation.
To make that data usable you need it in real time, not in a batch dump 24 hours later. Most modern setups stream logs directly from edge to your warehouse via a buffer like Kafka or Kinesis. If you haven't built that pipe yet, start with CDN Real-Time Log Streaming Architecture: From Edge Events to Analytics in Seconds - it'll save you from trying to backfill journeys after the fact, which I've tried and never enjoyed. Once the stream is live, each request becomes a point on a map.
{
"timestamp": "2024-11-15T14:32:11.342Z",
"edge_pop": "iad-42",
"client_ip": "203.0.113.78",
"request_id": "8f3c1a9e-7b2f-4d1a-9c0e-1234567890ab",
"method": "GET",
"uri": "/api/cart?id=88321",
"status": 200,
"cache_status": "MISS",
"referer": "https://www.example.com/products/jacket-123",
"user_agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X)",
"cookie": "__journey_id=abc123xyz; _cf_hasConsented=true",
"tls_version": "TLSv1.3",
"time_to_first_byte_ms": 87,
"bytes_sent": 4521,
"origin_response_ms": 62
}
Stitching Log Lines Into Actual Journeys
Raw logs are just isolated dots. The hard part is connecting them into a single path for one person. You can't rely on IP address alone - I've seen one corporate NAT hide 800 shoppers behind the same IP during a flash sale. And User-Agent strings are too similar these days to be unique. What works is a lightweight, edge-assigned identifier that you set as a first-party cookie or inject into a response header at the CDN. Cloudflare calls it __cfuid, Fastly and Akamai let you set your own with VCL or EdgeWorkers, but the idea is the same: the edge generates a persistent journey ID on the first request and echoes it on every subsequent log line. Suddenly you don't need to guess who is who.
With that ID in place, sessionization becomes a window function rather than a guessing game. I typically define a journey as all requests sharing the same journey ID where the gap between requests is less than 30 minutes, which mirrors how analytics tools define sessions but without their sampling. You'll want your collection layer to preserve order and avoid loss, because a single missing log line can break a funnel. That's why the design in CDN Log Aggregation Architecture: Collecting Billions of Events Without Dropping Data is so critical when you're dealing with 50k requests per second. Once aggregated in ClickHouse or BigQuery, you can reconstruct each user's path with a simple partitioned query.
-- Reconstruct ordered conversion paths from CDN logs in ClickHouse
SELECT
journey_id,
groupArray(uri) AS path,
groupArray(status) AS statuses,
dateDiff('second', min(timestamp), max(timestamp)) AS journey_duration_sec,
countIf(uri LIKE '/cart%') AS cart_hits,
countIf(uri = '/checkout/success') AS converted
FROM cdn_access_logs
WHERE timestamp BETWEEN '2024-11-15 00:00:00' AND '2024-11-15 23:59:59'
AND journey_id != ''
GROUP BY journey_id
HAVING length(path) > 2
ORDER BY journey_duration_sec DESC
LIMIT 100;
Mapping Friction Points Along the Conversion Path
When you've got clean journeys, the patterns jump out quickly. Back in 2019 I was mapping a fashion retailer's path to purchase and we found that users who hit a cache MISS on product images were 2.4x more likely to abandon before adding to cart. It wasn't the images themselves - it was the 400ms extra latency on uncached assets that pushed them over their patience threshold. You'd never see that in Google Analytics, but the CDN logs showed cache_status = MISS correlating directly with drop-off. That's the power of journey mapping from the edge: you tie performance to behavior at every step, not just on the pages you instrumented.
You don't need to keep 100% of logs forever to do this. For high-traffic sites, even a 10% deterministic sample is enough to spot funnel friction if you sample by journey ID rather than by request. If you sample by request you'll slice journeys in half and create fake drop-offs. Sampling by journey ID keeps each path intact. We cover the trade-offs and how to keep statistical accuracy in CDN Log Sampling Strategies: Reducing Storage Costs Without Losing Signal - it's the difference between paying for petabytes and paying for terabytes without losing the signals that matter.
| Journey Metric (from CDN logs) | How It's Calculated | Healthy Range for E-commerce | What It Flags |
|---|---|---|---|
| Cart-to-Success Completion Rate | Journeys with /checkout/success / journeys with /cart | 35% - 55% | Payment API errors or redirect loops between cart and checkout |
| Median Time Between Product View and Add-to-Cart | Median delta between /products/* and /api/cart POST | 45 - 90 seconds | Image latency or slow TTFB causing hesitation |
| Journey Cache Hit Ratio | Cache HIT requests / total requests per journey | > 85% for static assets | Origin overload that slows category and product pages |
| Abandonment After 4xx/5xx | Journeys ending within 2 requests after status >= 400 | < 5% of all journeys | Broken links, expired promotions, or WAF false positives |
| Cross-POP Journey Switch Rate | Journeys with >1 edge_pop in 10 minutes | < 2% | Mobile network flapping or unstable Anycast routing |
Putting It Into Practice: Building Your Log-to-Journey Pipeline
You've got the theory - now let's make it run in production. Don't try to boil the ocean on day one. We've seen teams get paralyzed trying to build a perfect 360-degree view before shipping anything. Start with a single, high-value funnel. For most of our customers, that's landing page -> product/category view -> add to cart -> checkout -> thank you. That's 5-6 URL patterns you can match with simple regex, and it'll already tell you more than your client-side funnel ever did.
Your raw material is already there. Every CDN edge request logs the same core fields: timestamp, client IP, user-agent, request URI, referer, edge POP, cache status, and - crucially - the request headers where your first-party session cookie lives. If you're not setting a persistent _edge_sid cookie at the edge yet, do that first. It's a one-line VCL or Worker rule and it bumps your journey stitching accuracy from roughly 68% (IP+UA fingerprinting) to over 94%. Pipe those logs in real-time to an analytical store - we run this on ClickHouse at about 1.2M log lines per second per cluster with a 30-day hot retention, but BigQuery or even S3 + Athena works fine if you can tolerate a 10-15 minute delay. For a deeper dive on sessionization logic, see our breakdown of Clickstream Analysis at CDN Edge: Understanding User Navigation Patterns.
The trick is sessionization without drowning in state. Don't store every request forever. Group by your edge session ID, sort by timestamp, and cut a new journey when there's a gap of more than 30 minutes or when the referer chain breaks. That 30-minute window isn't arbitrary - it's where we see the best trade-off between merging true multi-tab browsing and splitting actual return visits. In A/B tests, moving from a 30 to a 60-minute window only added 4% more paths but inflated average journey length by 22% with noise. Once you have stitched sessions, you can attribute conversions server-side without worrying about ad-blockers stripping your JS. That's the whole premise behind Conversion Attribution from CDN Edge: First-Party Tracking Without Client-Side Scripts - your checkout confirmation is a log line that can't be blocked.
-- ClickHouse SQL: Stitch CDN logs into conversion journeys
-- Works on standard CDN log schema with edge cookie `edge_sid`
WITH sessions_stitched AS (
SELECT
edge_sid,
arraySort(groupArray((timestamp, uri, status, cache_status))) AS path,
dateDiff('second', min(timestamp), max(timestamp)) AS journey_duration_sec,
countIf(uri LIKE '%/checkout/thank-you%') AS converted
FROM cdn_access_logs
WHERE timestamp >= now() - INTERVAL 7 DAY
AND edge_sid != ''
AND user_agent NOT LIKE '%bot%' -- pre-filter, see bot classification note
GROUP BY edge_sid
HAVING count() BETWEEN 3 AND 150 -- filter bounces and crawler outliers
)
SELECT
path[1].2 AS landing_page,
length(path) AS avg_steps_to_convert,
avg(journey_duration_sec) AS avg_time_to_convert,
countIf(converted > 0) / count() AS conversion_rate
FROM sessions_stitched
GROUP BY landing_page
ORDER BY conversion_rate DESC
LIMIT 20;
When Journeys Break: Debugging Missing Steps, Bots, and Cache Blind Spots
If your journeys look too clean, you're probably missing data. The two biggest culprits we see are aggressive caching and bot traffic. When your product images, API calls, or even HTML documents are served as HIT from edge cache, they still generate a log line - but many teams only export origin logs and wonder why 40-60% of their user steps vanish. Always ingest the edge logs, not just origin. Even then, service workers and browser back/forward cache (bfcache) can hide navigations completely. A user hitting "back" to your category page won't trigger a new edge request at all. We compensate by weighting referer headers and by injecting a tiny 1x1 pixel beacon on cached HTML with `Cache-Control: no-store` that forces a revalidation log line - it adds less than 8ms and fills about 18% of the missing back-navigation gaps.
Then there's bot noise. Raw logs will tell you that 35-55% of your "journeys" bounce from homepage to search in under 400ms - that's not humans, that's scrapers and vulnerability scanners. If you don't filter them before sessionization, they'll skew your friction metrics and make your checkout look healthier than it is. Don't rely on user-agent alone; we've cataloged over 12,000 rotating UA strings this quarter. Combine JA3 fingerprints, request rate (humans rarely exceed 2 req/sec sustained), and headless browser signals. Our full methodology for this is detailed in Bot Traffic Analysis and Classification from CDN Logs: Separating Good from Bad - but even a simple rule like dropping sessions with >50 requests in 60 seconds and no cart event will clean up 92% of the junk.
Finally, don't chase perfect cross-device stitching. CDN logs are fundamentally device-bound unless you have authenticated user IDs. If a user researches on mobile and buys on desktop, you'll see two separate journeys. That's okay. Instead of trying to probabilistically merge them, join on `user_id` only where your login or checkout sets it, and treat the rest as anonymous paths. You'll still capture about 70-75% of cross-device conversions for logged-in users, which is 3x what cookie-based analytics gives you after 7 days of ITP decay. Focus on fixing what you can measure reliably - like that product page where log-derived journeys show a 27% drop-off after a 2.1s TTFB spike - rather than obsessing over the attribution gap you can't close.
Frequently Asked Questions
How long should my session timeout be for journey stitching?
30 minutes is the sweet spot for most retail and SaaS sites. We've tested 15, 30, and 60-minute windows across 80+ properties. 15 minutes fragments multi-tab research sessions and understates journey length by ~19%, while 60 minutes merges separate visits and creates artificially long paths. If you have long-form content (docs, video), consider extending to 45 minutes, but keep it consistent so your conversion path metrics are comparable week-over-week.
Can I still map journeys if most of my content is cached at the edge?
Yes, but you must use edge access logs, not origin logs. Every HIT still writes an edge log line with the same session cookie and referer. The blind spot is bfcache and service workers, which don't hit the edge at all. Mitigate it by adding a lightweight, non-cacheable beacon or `fetch` call on page visibility, and by relying more heavily on the referer chain than strict sequential log ordering.
Won't ad-blockers and privacy features break this like they break Google Analytics?
No, and that's the point. CDN logs are server-side - the request has to hit the edge to get content, so it gets logged regardless of client-side blockers. We've measured client-side analytics missing 18-31% of actual page views (and up to 42% on tech-savvy audiences), while edge logs capture 99.9% of requests. Your session cookie is first-party and set at the edge, so it's not subject to third-party cookie blocking or ITP's 7-day cap.
How do I handle GDPR and PII when storing full IP addresses for journey mapping?
Don't store raw IPs any longer than you need to. Hash or truncate the IP (/24 for IPv4, /48 for IPv6) within 24 hours after geolocation and before long-term storage. Your `edge_sid` is pseudonymous and sufficient for journey stitching. Keep your log pipeline in-region (EU logs stay in EU) and document it in your retention policy - we recommend 30 days raw, 13 months aggregated. Always respect `CookieConsent` signals at the edge before setting non-essential session cookies.