A small Dockerized lab for understanding how browser tracking, event collection, and analytics dashboards work end to end.
Most analytics tools hide the interesting parts. You add a script tag, open a dashboard, and the system feels like magic.
This project makes those parts visible.
You run a demo website, click through a small e-commerce funnel, send real tracking events, store them in Postgres, and inspect them in Metabase. It is meant to be cloned, run, broken, queried, and extended.
Built for developers, data engineers, growth engineers, and anyone curious about what happens after a user clicks a button.
| Service | URL | What It Does |
|---|---|---|
| Demo Site | http://localhost:8080 | HTML pages that fire tracking events |
| FastAPI API | http://localhost:8000/docs | Receives, validates, stores events |
| PostgreSQL | port 5432 | Stores all tracking data |
| Metabase | http://localhost:3000 | Analytics & dashboard UI |
| Layer | Technology |
|---|---|
| Backend | Python 3.12, FastAPI, SQLAlchemy 2, Uvicorn |
| Database | PostgreSQL 16 |
| Frontend | Vanilla HTML, CSS, JavaScript |
| Analytics | Metabase |
| Infra | Docker Compose |
Small HTML pages that simulate a simple funnel:
index.html— Home pageproduct.html— Product detail pagesignup.html— User registration pagecheckout.html— Checkout & purchase pageevents.html— Live feed of recent tracking eventsreports.html— Links to Metabase dashboards
window.myPixel is a lightweight tracking client. It:
- Fires
page_viewautomatically on every page load - Sends custom events with
window.myPixel.track(eventName, payload) - Stores
anonymous_user_idandsession_idinlocalStorage - Captures UTM parameters from the URL
- Adds browser metadata like referrer, language, screen size, and user agent
- Supports both JSON POST and image pixel GET tracking
The backend receives and stores events:
GET /health— liveness checkPOST /track— JSON event trackingGET /pixel— image pixel tracking, returns a transparent 1x1 GIFGET /events?limit=100— recent events for the live feed
Events are stored in one table: tracking_events.
It includes:
event_id(UUID, primary key)event_name,event_time,page_url,referrersession_id,anonymous_user_iduser_agent,screen_width,screen_heightip_addressutm_source,utm_medium,utm_campaign,utm_term,utm_contentpayload_json(JSONB — free-form data per event)source_type(js_pixelorimage_pixel)
- Docker Desktop running on your machine
git clone <repo-url>
cd pixel-tracking-lab
cp .env.example .env
docker compose up --buildWait about 30 seconds, then open:
| What | URL |
|---|---|
| Demo Site | http://localhost:8080 |
| Live Events Feed | http://localhost:8080/events.html |
| API Swagger Docs | http://localhost:8000/docs |
| Metabase Dashboard | http://localhost:3000 |
docker compose downdocker compose down -v- Open http://localhost:8080
- Navigate: Home → Product → Signup → Checkout
- Click the action buttons on each page
- Open http://localhost:8080/events.html
You should see automatic page_view events and custom button-click events.
Open the site with UTM parameters:
http://localhost:8080?utm_source=newsletter&utm_medium=email&utm_campaign=spring_launch
Fire a few events, then check:
curl "http://localhost:8000/events?limit=5"The events should include utm_source, utm_medium, and utm_campaign.
Click the button that uses the image pixel transport on the Home page.
You should see an event with source_type = image_pixel.
# Health check
curl http://localhost:8000/health
# Recent events via API
curl "http://localhost:8000/events?limit=20"
# Direct database query
docker compose exec postgres psql -U pixel -d pixel_lab \
-c "SELECT event_name, source_type, event_time FROM tracking_events ORDER BY id DESC LIMIT 20;"- Open http://localhost:3000
- Connect to PostgreSQL with:
- Host:
postgres - Port:
5432 - Database:
pixel_lab - User:
pixel - Password:
pixel
- Host:
- Browse the
tracking_eventstable - Build charts: event counts by name, funnel by page, events by UTM campaign
There are two common ways to send tracking events.
window.myPixel.track("add_to_cart", { product_id: "SKU-42", price: 49.99 });
// → POST /track with JSON body
// → Stored as source_type = js_pixel<img src="http://localhost:8000/pixel?event=add_to_cart&product_id=SKU-42" width="1" height="1" />
// → GET /pixel → returns transparent 1×1 GIF
// → Stored as source_type = image_pixelThe image pixel trick is old, but still useful to understand. Because it is just an image request, it can work in places where JavaScript does not, such as many email clients.
Use this when you want to publish a new version of the lab.
- Make your changes.
- Run the stack locally:
docker compose up --build- Check the main flow:
- Open http://localhost:8080
- Click through the demo pages
- Confirm events show up at http://localhost:8080/events.html
- Confirm Metabase opens at http://localhost:3000
- Make sure local secrets are not staged:
git status --short --ignored.env, .env.local, .DS_Store, and cache files should stay ignored.
- Commit and push:
git add .
git commit -m "Describe the change"
git push origin main- Optional: create a Git tag for a named release:
git tag v0.1.0
git push origin v0.1.0pixel-tracking-lab/
├── docker-compose.yml
├── .env.example
├── README.md
│
├── backend/
│ ├── Dockerfile
│ ├── requirements.txt
│ └── app/
│ ├── main.py ← FastAPI app, CORS, startup
│ ├── config.py ← Settings via pydantic-settings
│ ├── db.py ← Engine, session, retry logic
│ ├── models.py ← TrackingEvent ORM model
│ ├── schemas.py ← Pydantic request/response schemas
│ ├── routes/
│ │ ├── health.py
│ │ ├── track.py ← POST /track
│ │ ├── pixel.py ← GET /pixel
│ │ └── events.py ← GET /events
│ ├── services/
│ │ └── event_service.py
│ └── utils/
│ └── transparent_pixel.py
│
├── demo-site/
│ ├── Dockerfile
│ ├── nginx.conf
│ ├── index.html
│ ├── product.html
│ ├── signup.html
│ ├── checkout.html
│ ├── events.html
│ ├── reports.html
│ ├── js/
│ │ ├── pixel.js ← window.myPixel tracking library
│ │ └── app.js ← Page-specific event bindings
│ └── css/
│ └── styles.css
│
└── sql/
└── init.sql ← Schema + indexes
Built to make analytics infrastructure easier to see and reason about.