-
Notifications
You must be signed in to change notification settings - Fork 0
Architecture
Centralized observability for early-stage teams. Captures errors, performance signals, and user feedback from any instrumented web app, and correlates them with deployment events from CI.
WatchTower exists to answer one question well: "Which deployment started the fire?"
Three signal sources flow into one place:
- Runtime errors and unhandled rejections from a monitored web app.
- Core Web Vitals (LCP, INP, CLS) and page-load timing.
- User-supplied feedback (rating widget + short text).
These are correlated with deploy events from GitHub Actions so the team can spot when a regression landed.
This document describes the architecture chosen to deliver a working MVP in three weeks under the CSE 110 project constraints — vanilla JS only on the client, Cloudflare or GitHub Pages for any server-side, no third-party frameworks, and process discipline (ADRs, CI, PR review on batches >300 LoC).
flowchart TD
A["Client browser (target app)<br/>watchtower.js — errors, vitals, feedback"]
B["GitHub Actions (CI/CD)<br/>posts deploy events"]
C["Cloudflare Worker — /ingest<br/>validate · enrich · insert"]
D[("Cloudflare D1 (SQLite)<br/>events · deploys · projects · users")]
E["Cloudflare Worker — /api/*<br/>auth · errors · perf · feedback · deploys"]
F["Dashboard SPA (Cloudflare Pages)<br/>vanilla JS + Chart.js"]
A -->|"POST /ingest (sendBeacon · fetch)"| C
B -->|"POST /ingest"| C
C -->|INSERT| D
D -->|SELECT| E
E -->|JSON| F
The system is a five-layer vertical pipeline. Two distinct sources (the client SDK and GitHub Actions) both feed a single ingestion endpoint. Events are persisted in a relational store and read back through a separate API for the dashboard.
A single, dependency-free JavaScript file (~5 KB minified) included by any app the team wants to monitor.
- Hooks
window.onerrorandunhandledrejectionto capture runtime errors with stack traces. - Uses
PerformanceObserverto capture Core Web Vitals (LCP, INP, CLS) and basic navigation timing. - Renders an opt-in feedback widget (star rating + short text) that the host app can show on demand.
- Batches events in memory and flushes via
navigator.sendBeaconon page unload, falling back tofetchwithkeepalive: truefor in-session sends. - Authenticates with a public project API key embedded at script load time.
A single Worker route that accepts JSON payloads from both the client SDK and CI. Three steps, fail-fast, stateless:
- Validate — checks the project API key, payload schema, and event-type enum.
-
Enrich — adds server-side timestamp, parses User-Agent, derives country from the
cf-ipcountryheader. - Insert — writes to D1 in a single prepared statement.
Returns 204 on success so clients don't waste bandwidth on response bodies.
D1 is Cloudflare's managed SQLite. The free tier comfortably covers a class project. Five tables:
| Table | Purpose |
|---|---|
projects |
One row per monitored app. Holds the project API key. |
events |
Polymorphic table for errors, performance samples, and feedback. A type column discriminates; payload-specific fields live in a JSON column. |
deploys |
Deployment events from CI. Commit SHA, environment, timestamp. |
users |
Dashboard accounts (email, hashed password). |
sessions |
Issued auth tokens. |
Collapsing errors/perf/feedback into one events table is a deliberate scope reduction. It removes triplicate CRUD code at the cost of slightly larger rows.
A second route group, on the same Worker deployment, that serves the dashboard.
-
POST /api/login— issues a signed session cookie. -
GET /api/events?type=error&since=...— filtered event listing with pagination. -
GET /api/summary— pre-aggregated counts for the overview screen. -
GET /api/deploys— recent deploy events for correlation.
All authenticated endpoints check a signed session cookie. No OAuth, no external auth provider.
A static single-page app:
- Vanilla JS, no framework, hash-based routing (
#/overview,#/errors,#/perf,#/feedback,#/deploys). - Chart.js loaded from cdnjs for graphs (line charts for performance trends, bar charts for error frequency).
- Plain CSS, no preprocessor.
- Deployed to Cloudflare Pages on every push to
main.
- On every PR: ESLint, unit tests, e2e tests (Playwright headless), build check.
-
On merge to
main: deploy Workers viawrangler deploy, deploy dashboard via Cloudflare Pages. -
Post-deploy:
curl/ingestwith a deploy event so WatchTower observes its own deploys (free dogfooding for demos).
A typical flow on the error path:
- A user clicks something in a monitored app and the JS throws.
-
watchtower.js's error handler captures the stack trace, batches it, and within ~5 s firesnavigator.sendBeacon('/ingest', payload). - The Ingest Worker validates the API key, enriches the payload with country and timestamp, and
INSERTs into theeventstable. - A team member opens the dashboard. The SPA calls
GET /api/events?type=error&since=24h. - The Reporting Worker queries D1 and returns JSON.
- The dashboard renders the error in a sortable table. Clicking it opens a detail view that joins the error timestamp to the most recent
deploysrow, surfacing the likely culprit commit.
Constraint match. The course rules permit only vanilla JS/HTML/CSS on the client and Cloudflare or GitHub Pages on the server. Cloudflare Workers + D1 + Pages is the simplest combination that satisfies all three with one vendor and one CLI (wrangler).
Three-week realism. Every layer is chosen so one person can stand it up in a day:
- D1 needs no DB admin or local server —
wrangler d1 createand you're done. - Workers deploy in seconds and have generous free-tier limits.
- The dashboard has no build step (vanilla JS + CDN).
- Auth is a signed cookie, not OAuth.
Process over product. The architecture is deliberately small to leave time for the Agile process the project rubric weights heavily — sprint planning, retros, ADRs, CI checks, and code review on PRs over 300 LoC. A repeatable process producing a smaller working product scores higher than a feature-rich one done opaquely.
- No multi-region replication. D1 has a single primary; cross-region reads are eventually consistent. Acceptable for a class demo.
- No queueing on ingest. A spike in events can drop writes if D1 throttles. WatchTower is operational-facing software, so we explicitly accept this and document it rather than building a queue.
-
Polymorphic
eventstable trades schema clarity for development speed. Splittable later if event volume grows. - No PII redaction in errors. Stack traces may include user input. Scope adds only a coarse "scrub query strings" toggle.
Each major decision above is captured as a separate MADR file under docs/adr/:
0001-cloudflare-over-node-php.md0002-d1-over-kv-or-external-postgres.md0003-vanilla-js-dashboard.md0004-single-events-table.md0005-signed-cookie-auth.md
The following are deliberately deferred and noted here so future maintainers don't mistake their absence for an oversight:
- Source-map upload and stack-trace symbolication.
- Alerting (Slack, email, PagerDuty).
- Long-term retention beyond 30 days.
- Multi-tenant isolation beyond the project API key.
- Native mobile SDKs.