Personal Project

Parcours Analytics: A Web Analytics Platform

A production-style website analytics app built with Django, FastAPI, PostgreSQL, Docker, and AWS

Project Summary

Parcours Analytics is a self-hosted web analytics platform I designed and built to track visitor behavior across websites without relying on a third-party analytics SaaS.

The project combines a lightweight browser tracking script, a multi-service backend, an authenticated dashboard, and a background processing pipeline for turning raw visitor activity into useful analytics. It tracks things like visits, page views, referrers, devices, countries, scroll depth, bounce rate, and content performance.

I built Parcours as both a practical analytics tool and a production-style engineering project. It gave me a chance to design a real event ingestion pipeline, operate multiple containerized services, separate application and analytics workloads, and host the system in AWS.

Why I Wanted to Build This

I built Parcours Analytics because Google Analytics felt far more complicated than what I actually wanted from a website analytics tool. But basically I thought it would be fun.

For many small sites, blogs, and content projects, the important questions are straightforward:

  • How many people visited?
  • What pages did they read?

Google Analytics can answer those questions, but its just too complicated to use. It feels like using an enterprise marketing platform when all you need is clear data on your sites traffic.

I really liked how simpler analytics products like Clicky and Fathom are built. These influenced the direction of the project. One thing I especially appreciated was how fast their dashboards feel.

For Parcours, I wanted the same kind of experience: open the dashboard, get useful numbers immediately, and avoid making the user wait while the system figures out basic traffic data. Computers should do the work for us, not turn simple questions into a loading screen.

I wanted a project that was more substantial than a typical CRUD app and closer to a real production system.

The other reason I built it was technical. Web analytics has interesting engineering problems: collecting high-volume browser events, validating tracked properties, buffering ingestion, enriching raw events, assigning sessions, aggregating data, and presenting it back through a dashboard.

I tried hard not to over-engineer upfront, but I knew that breaking everything out as services now would pay dividends later.

How Parcours Is Put Together

Parcours Analytics is built as a small multi-service application. I split the system into separate components because analytics traffic has different workloads: collecting events should be fast and lightweight, processing events can happen asynchronously, and dashboard queries should be isolated from the ingestion path.

At a high level, the system looks like this:

  • Parcours JavaScript tracker runs in the tracked website
  • nginx
  • Metrics ingestion API
  • File-backed event buffer
  • Background worker
  • Metrics PostgreSQL database
  • Dashboard API
  • Django dashboard

The public entry point is nginx. It acts as the reverse proxy for the application and routes requests to the correct internal service. Normal dashboard traffic goes to Django, tracking events from the browser go to the metrics ingestion API, dashboard charts make requests to the dashboard API, and generated browser tracking scripts are served as static JavaScript files.

Django is responsible for the user-facing application. It handles user accounts, authentication, web property management, and the dashboard pages. When a user adds a new website, Django creates a unique property ID and generates a custom tracking script for that property. The user installs the Parcours tracking plug-in on their WordPress site which fetches the Javascript hosted by nginx on each WordPress page load.

Data Collection Flow

The data collection flow starts when a user adds a website inside the Parcours dashboard. Django creates a new WebProperty record for that site and assigns it a unique property_id. That property_id becomes the key that ties the tracked website, incoming browser events, stored metrics, and dashboard queries together.

When the property is created, Django also generates a custom JavaScript tracking file for it. The script is based on a template, with the property ID and metrics endpoint injected into the final file. nginx serves these generated scripts from a static route, so a tracked site can load a script like:

<script defer src="https://app.useparcours.com/livelongandprosper/<property_id>.js"></script>

I’m a huge Star Trek nerd so I had to sprinkle a little bit of Spock in that URL. 🖖🏻

Once installed on a website, the tracker sends events back to Parcours using the browser’s sendBeacon API. It records basic page and engagement events, including:

Each event includes information such as the property ID, visitor ID, page URL, page title, referrer, browser language, locale, duration on page, scroll depth, and timestamp. The visitor ID is generated in the browser from a lightweight fingerprint using values like user agent, screen size, language, timezone, hardware concurrency, and device memory.

For WordPress sites, the Parcours plugin adds extra page metadata before loading the tracking script. This includes the page type, author, categories, tags, and WordPress page flags like whether the page is a home page, single post, a page, or an archive. That allows Parcours to report not only on URLs, but also on WordPress-specific content structure and topic performance.

The incoming events are sent to the /metrics endpoint. nginx forwards those requests to the FastAPI metrics ingestion service and passes along useful request metadata, including the original IP address and user agent. The ingestion API validates the submitted property_id against the Django database before accepting the event, so random or invalid property IDs are rejected.

After validation, the ingestion service then sanitizes the payload. It cleans the referrer URL, converts the reported scroll depth to a valid percentage, captures the visitor IP address, and records the event timestamp in UTC. Instead of writing directly to PostgreSQL, it writes each accepted event as a JSON file into a shared visitor_data directory. Though the IP is stored in the DB, its only used for geo lookup to identify the visitor’s country.

That file-backed buffer is an intentional design choice. It keeps the public tracking endpoint fast and reduces the amount of synchronous work required during event collection. The ingestion API’s job is simply to validate, clean, and durably stage the event. The heavier work, such as user-agent parsing, GeoIP lookup, bot detection, database inserts, grouping events into visitor sessions, and building rollups, is handled later by the background worker.

Event Processing Pipeline

For example, if a visitor clicks a few pages, leaves for an hour and returns to visit more pages, that’s counted as 2 different sessions.

The worker then builds higher-level session records from those grouped events. Each session includes start and end time, duration, total events, pageview count, bounce status, entry page, exit page, referrer domain, country, browser, operating system, locale, and entry page type.

This gives the dashboard a clean session-level view without having to recompute sessions from raw events on every request.

The pipeline also extracts specialized reporting data. For example, scroll-depth events are stored separately so the dashboard can report how far visitors read on each page. The worker also syncs each web property’s timezone into the metrics database, which allows reporting queries to calculate day and hour boundaries in the site owner’s local timezone.

How the Dashboard Gets Its Data

Example call for visitor counts:

{
  "requested_range": "today",
  "requested_data": {
    "total_visitors": 25,
    "total_actions": 25,
    "avg_time_on_site_seconds": 29,
    "avg_bounce_rate": 75.65,
    "total_visitors_change": -11,
    "total_actions_change": -11,
    "avg_time_on_site_seconds_change": 61,
    "avg_bounce_rate_change": -8
  },
  "related_range": "yesterday",
  "related_data": {
    "total_visitors": 28,
    "total_actions": 28,
    "avg_time_on_site_seconds": 79,
    "avg_bounce_rate": 89.29
  },
  "errors": []
}

Database Design

The metrics database stores the output of the analytics pipeline.

Once an event is accepted by the ingestion API, the worker enriches it with details like browser, operating system, country, referrer, bot status, content metadata, duration, and scroll depth, then stores it for reporting.

AWS And Deployment

WordPress Integration

Security And Privacy Considerations

Technical Challenges

What I Learned

Technologies

  • AWS and Docker
  • Python, Django, FastAPI
  • PostgreSQL