A minimal FastAPI notes app with 4 deliberately seeded issues, sitting behind an nginx reverse proxy that injects an OpenReplay tracker snippet. Use this to validate an observability setup before rebuilding it into the real app.
observability_poc/
├── app/
│ ├── main.py # FastAPI backend (CRUD + 1 seeded slow endpoint)
│ ├── static/
│ │ ├── index.html # UI incl. 1 seeded dead-click button
│ │ ├── app.js # incl. 1 seeded broken API call, 1 seeded JS error
│ │ └── style.css
├── tests/
│ └── test_api.py # pytest, core CRUD only (written before app code)
├── proxy/
│ └── nginx.conf # reverse proxy, injects tracker snippet
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
└── README.md
docker compose up --buildThis starts two containers:
app— FastAPI backend on port 8000, not published to the hostproxy— nginx on port 8080, the only entry point
Access the app through the proxy, not directly: http://localhost:8080
python3 -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
pytest tests/ -vAll 10 tests cover core CRUD (create/list/get/delete notes) and pass against the real implementation — none were weakened to pass.
Lives in proxy/nginx.conf. The proxy uses sub_filter
to rewrite the HTML response from the app, inserting the tracker
immediately before </head> on every page load.
The project's snippet was given in npm/ESM form
(import { tracker } from '@openreplay/tracker'), and this app is plain
static HTML/JS with no bundler, so it's loaded as a native ES module via
a jsdelivr +esm CDN re-export of the package instead:
sub_filter '</head>' '<script type="module">
import { tracker } from "https://cdn.jsdelivr.net/npm/@openreplay/tracker@latest/+esm";
tracker.configure({
projectKey: "Xg0cg4l4qtBpkAcQGwCM",
ingestPoint: "https://analyticspoc.ap.openreplay.cloud/ingest",
});
tracker.start();
</script>
</head>';
sub_filter_once on;To change the project key/ingest point, edit those values directly in
proxy/nginx.conf and restart the proxy container
(docker compose restart proxy) — no app rebuild needed.
| # | Type | Where | What happens |
|---|---|---|---|
| a | broken-api-call | app/static/app.js, "Export Notes" button |
Calls GET /api/notes/export-csv, which doesn't exist on the backend → 404 |
| b | unhandled-js-error | app/static/app.js, "View Note Stats" button |
Sets .innerHTML on a DOM element (#note-stats-panel) that's never rendered → uncaught TypeError |
| c | dead-click | app/static/index.html, "Settings" button |
Rendered as a normal button, no event listener ever attached to it |
| d | slow-response | app/main.py, GET /api/notes/{id}/archive |
time.sleep(6) before responding — triggered by the "Archive" button on any note |
To exercise (c) and (b), add a note first via the form, then click "View Note Stats" / "Archive" / "Export Notes" / "Settings" from the UI at http://localhost:8080.