A visitor id, a session id and a page view id; a collector on our own domain; Firehose into S3; Athena with saved queries. How we built analytics that ad blockers do not drop, nobody else holds, and that costs less than a coffee at our traffic, and the three bugs we found on the way.
Hosted analytics is the right answer for many sites. We built our own for three reasons that apply to a lot of B2B companies: a meaningful share of a technical audience blocks third-party trackers, we wanted the raw events next to our own data rather than in a dashboard we rent, and at our traffic a serverless pipeline costs almost nothing. It is the same design we deploy for clients, so it also had to be something we would trust there.
The page view id is the one most setups skip and the one that answers the most useful question — which page, and which button on it, a lead came from — as a join rather than a guess from timestamps. Campaign tags (utm_* and ad click ids) are read once on arrival and stamped on every event in the visit.
Book a free 30-minute call. You'll leave knowing what we'd do first and roughly what it costs, whether or not you hire us.
A Lambda function behind CloudFront at /v1/events on the site's own domain. Same origin means no CORS and nothing for third-party blocklists to match. It checks the write key before reading the body, validates each event on its own so one bad event never costs the batch, stamps the project and the time it received the event from its own clock — never from the payload — and sends good events to Firehose and bad ones to a quarantine prefix in S3 where they can be inspected.
Firehose buffers for five minutes and writes gzipped, newline-delimited JSON into S3 under a date prefix. A Glue table with partition projection makes each day queryable the moment it lands, with no crawler. Athena runs saved queries: visits and visitors by day, landing pages by source, clicks toward booking, visits by channel (search, AI assistants, social, paid, email, direct) and experiment results with a confidence range. Duplicates — Firehose can deliver a batch twice — are removed in the queries by event id.
Anything tracked before the analytics code finished loading was silently lost — including the first page of every visit's content events. Calls now queue until the provider is ready, with page views flushed first so the events after them carry the right page view id.
Nothing that does not run JavaScript could read the site. The pages were a single-page app; crawlers and AI assistants received an empty shell. Every page is now prerendered at build time.
Dates written as 2026-10-24 displayed as October 23 for every visitor west of London, because a bare date parses as midnight UTC. Small, embarrassing, and invisible from the developer's own time zone.
None of these showed up in tests. All three showed up within minutes of driving the real site in a browser and reading what the collector received — which is the habit we would recommend to anyone building tracking: look at the events, not the dashboard.
Once a month: what changed in tech that month and what it means for you, from cloud and AI to security and the tools you build with. Every item links to its source. No sales pitch, unsubscribe in one click.
Book a free 30-minute call about it. Bring your setup and your bill; you'll leave knowing what we'd do first and roughly what it costs, whether or not you hire us.
Or keep reading
Scaling an Event Pipeline on AWS: From Firehose and Athena to Iceberg and S3 Tables
The same analytics design works from a thousand events a day to billions: if you change the right piece at the right time. The four stages, what breaks at each, and when Iceberg, S3 Tables, Parquet and a warehouse start earning their cost.
Read
Ad-Hoc Questions on DynamoDB vs Postgres: 16 Analytics Queries at 100,000, 1 Million and 5 Million Orders
Every question across customers is a full table scan in DynamoDB: 217,000 read units at 5 million orders, whether it returns 10 rows or all of them. Postgres answered the same 16 questions in 0.08 to 2.7 seconds without a single index added for them, and an index built in 2.7 seconds took one from 148 ms to 44 ms. Measured in AWS.
Read
As of October 26, 2026. Provided as is, for general information, and accurate to the best of our knowledge on the date shown. Not legal, compliance, tax, financial or other professional advice. Prices, limits and vendor services change; check current vendor documentation before relying on anything here. Terms of use.