Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).
bookmarks survive the move and search engines are told where the pages went.

### Fixed
- **A bad analytics-ledger path can no longer take the whole site down.**
Pointing `TRAFFIC_ANALYTICS_FILE` at a persistent-disk path before the disk
existed crashed every worker at boot, so the deploy never went live and the
old build kept serving. The tracker now creates the directory when it can,
and when the path is truly unwritable it disables itself with a clear log
line — the site serves either way, and a test pins both behaviours.
- **Search engines were being told this site is a copy of a site that does not
exist.** The page template still carried the URL it was built with
(`dash-mui-scheduler.onrender.com`) rather than the address the docs actually
Expand Down
25 changes: 23 additions & 2 deletions lib/analytics_tracker.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,28 @@ class AnalyticsTracker:

def __init__(self, data_file=None):
self.data_file = Path(data_file or os.getenv("TRAFFIC_ANALYTICS_FILE") or "visitor_analytics.json")
self._ensure_file_exists()
# An unusable ledger path must NEVER crash the boot. This module is
# imported at the top of run.py, so an exception here kills every
# worker before a port is bound and the platform loops the deploy
# forever — which is exactly what happened when TRAFFIC_ANALYTICS_FILE
# pointed at /var/data before the disk was attached. Analytics is an
# accessory; the site serving is the product.
self._disabled = False
try:
self._ensure_file_exists()
except Exception as exc:
self._disabled = True
print(f"[analytics] ledger {self.data_file} is not writable "
f"({exc!r}) — visitor tracking DISABLED. Fix "
"TRAFFIC_ANALYTICS_FILE (attach the disk it points at, or "
"unset it) to re-enable; nothing else is affected.")

def _ensure_file_exists(self):
"""Create analytics file if it doesn't exist."""
"""Create the analytics file (and its directory) if missing."""
if not self.data_file.exists():
# A persistent-disk path like /var/data/… may not exist yet on
# first boot — create the directory rather than failing on it.
self.data_file.parent.mkdir(parents=True, exist_ok=True)
self.data_file.write_text(json.dumps({
"visits": [],
"stats": {
Expand Down Expand Up @@ -193,6 +210,10 @@ def track_visit(self, path, user_agent, ip_address=None, auth_name=None,
"""Track a visitor. auth_name (the verified Clerk display name, when the
caller resolved one) stamps the hit as authenticated. country is the
edge-supplied CF-IPCountry code, when the request carried one."""
# Ledger unusable at boot (see __init__) → tracking is off, not broken.
if self._disabled:
return

# The network's internal-traffic contract: hub health sweeps, CI smoke
# batteries and satellite-to-satellite calls identify themselves with
# INTERNAL_UA_TOKEN in the User-Agent. Dropped at WRITE time — before
Expand Down
51 changes: 51 additions & 0 deletions tests/test_analytics_boot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""The analytics ledger must never be able to kill the boot.

lib/analytics_tracker is imported at the top of run.py and constructs its
module-level tracker at import time — so an exception there crashes every
worker before a port is bound, and the platform loops the deploy forever
while the old build keeps serving. That is exactly what happened in
production on 2026-08-02: TRAFFIC_ANALYTICS_FILE pointed at
/var/data/visitor_analytics.json before the persistent disk was attached,
and `_ensure_file_exists()` raised FileNotFoundError in `__init__`.

Analytics is an accessory. These tests pin the two required behaviours:
a missing parent directory is created, and a genuinely unwritable path
disables tracking instead of raising.
"""

from __future__ import annotations

import json

from lib.analytics_tracker import AnalyticsTracker

UA = "Mozilla/5.0 test-browser"


def test_a_missing_parent_directory_is_created(tmp_path):
"""The /var/data case once the disk IS there but empty: first boot must
create the directory chain rather than failing on it."""
ledger = tmp_path / "var" / "data" / "visitor_analytics.json"
tracker = AnalyticsTracker(data_file=str(ledger))

assert not tracker._disabled
assert ledger.exists(), "the ledger file was not seeded"

tracker.track_visit("/quickstart", UA, "203.0.113.9")
visits = json.loads(ledger.read_text())["visits"]
assert len(visits) == 1 and visits[0]["path"] == "/quickstart"


def test_an_unwritable_ledger_path_disables_tracking_not_the_boot(tmp_path):
"""The production crash, pinned: a path that cannot be created (here, a
directory component that is actually a FILE) must yield a tracker that
constructs fine and no-ops — never an exception at import time."""
blocker = tmp_path / "not-a-directory"
blocker.write_text("occupied")
ledger = blocker / "visitor_analytics.json"

tracker = AnalyticsTracker(data_file=str(ledger)) # must not raise

assert tracker._disabled
tracker.track_visit("/quickstart", UA, "203.0.113.9") # must not raise
assert not ledger.exists()
Loading