Problem

Once the site was live, there was no way to answer basic questions about how it was being used. Is anyone visiting? Which pages are people looking at? Is the LinkedIn profile link driving any traffic? Where are visitors coming from geographically?

The standard answer is Google Analytics or a similar third-party tool. That answer comes with a JavaScript snippet on every page, a cookie consent banner, and visitor data handed to a third party. For a site that needs none of that complexity, it is the wrong trade.

Intended Solution

CloudFront already knows everything about every request it serves — date, time, URL, status code, referrer, and with minimal configuration the visitor’s country. Those logs land in S3. Athena can query S3 directly without moving the data anywhere.

The intended result: a fully AWS-native analytics pipeline with no client-side JavaScript, no cookies, no third-party services, and near-zero incremental cost.

What Was Built

The S3 log bucket was configured correctly. The rest was blocked before it could be used.

Key finding — Object Ownership: CloudFront standard logging uses ACL-based log delivery. The destination S3 bucket must use BucketOwnerPreferred object ownership. BucketOwnerEnforced disables ACLs and causes CloudFront to reject the logging configuration.

Where It Stopped

CloudFront’s Free pricing plan blocks all logging features. Attempting to enable standard logging returned:

Distributions with the Free pricing plan can't have the following features: Standard logging

The Free plan ($0/month) bundles DDoS protection, managed WAF rules, CDN, and edge compute — but no logging. Logging requires the Pro plan at $15/month.

The distribution has an auto-created WAF WebACL that was investigated as an alternative: WAF inspects every request and can log to S3. That path hit the same restriction — WAF logging also requires Pro.

Intended Architecture

%%{init: {'theme': 'base', 'themeVariables': {'fontSize': '18px'}}}%%
flowchart LR
    U("Browser\nVisitor"):::browser
    CF("CloudFront\ntacedata.ca"):::aws
    S3LOG("S3\ntacedata-cloudfront-logs\ncloudfront-logs/tacedata/"):::aws
    ATH("Athena\ntacedata_analytics\ncloudfront_logs"):::aws
    Q("Saved Queries\ntrends · pages · geo · referrer"):::aws

    U -->|"HTTPS request"| CF
    CF -->|"access log + viewer country"| S3LOG
    S3LOG -->|"partitioned by date"| ATH
    ATH -->|"SQL on-demand"| Q

    classDef aws     fill:#FF9900,stroke:#c97a00,color:#000,rx:8,ry:8
    classDef browser fill:#475569,stroke:#334155,color:#fff,rx:8,ry:8

Privacy Design

The intended setup would have collected only the minimum fields needed:

GoalFields
Traffic trendsdate, time
Geographic distributionviewer country (ISO 3166 alpha-2)
Page visit trackingcs-uri-stem, sc-status
Traffic sourcecs-referer

Excluded: IP addresses, user agent strings, query strings, bytes transferred, cache status.

Status

Closed 2026-05-07. The design is sound — if the distribution is upgraded to the Pro plan, the implementation picks up from Step 2 (enable CloudFront logging) with the architecture unchanged.

Build Series