Deploying a visitor beacon with salted IP hashing to production
I shipped the phase-3 visitor beacon to production. The beacon is a GET to /api/me with a URL-encoded path and referrer host passed as query parameters, never read from the Referer header.
Pre-deploy cleanup
I cleared 16 dev verification rows from beacon_log so the first production day would start clean. beacon_breaker_log was already empty. I committed the phase-3 work as 5c98be3, which included exactly 4 paths with the experimental classifier deliberately left out.
Promotion and restart
I pushed dev, fetched on prod, ran git merge --ff-only origin/dev, pushed master, and restarted with supervisorctl restart epsteinscan. The pid changed from 3420719 to 3452712 and 4 workers booted clean.
Before merging, app-dev's remote-tracking ref for master was stale at one commit while prod sat at another. It looked like divergence that would break the ff-only merge. A plain git fetch resolved it with no actual divergence.
Production verification
I made a real request through the public Cloudflare hostname rather than against the origin so the row would carry genuine CF-Connecting-IP and CF-IPCountry headers. I chose a Chrome/145 user agent for the test after reading _FLEET_UA. The pool is Chrome 118-120 and Firefox 120-121 exact-string matches, so 145 passes the write-time filter. Picking a user agent blind could have produced a silent no-row and looked like a broken deploy.
The test row landed with a 32-character salted ip_hash, path=/document/622268, ref_host=www.google.com, ua_sig=Chrome/145|Win, country=US, and authenticated=0. The request carried Referer: https://www.google.com/search?q=secret+terms+here. Zero rows matched %secret% or contained ?. Only the referrer host was stored.
The salted hash does not join the legacy unsalted tables. beacon_log.ip_hash JOIN search_log.ip_hash returned 0 rows. JOIN doc_view_log.ip_hash also returned 0.
I deleted the test row by primary key rather than by timestamp range because organic production rows were already interleaving with it.
Harness artifact
Timing the analytics path via ./venv/bin/python -c ... printed [beacon] DISABLED: IP_HASH_SALT is not set. That standalone process does not inherit supervisor's environment, so the beacon failed closed exactly as designed. The live gunicorn workers were unaffected. The test row landed and 0 matches for beacon|disabled|traceback appeared in the current run's error log. This is a harness artifact, not a production fault, and it will reappear on any future ad-hoc python -c against web_search.
What shipped
web_search.py gained 351 lines: batched deque, background flusher, write-time fleet filter, per-worker hourly circuit breaker, salted per-day IP hashing, and the panel data path. templates/base.html added client-side beacon fetch appending ?p= path and &r= referrer host. templates/admin_analytics.html added the "Visitor Signals (beacon)" card. scripts/beacon_schema.py is the new DDL for beacon_log and beacon_breaker_log. scripts/visitor_classify.py stayed uncommitted on purpose and still carries its failure banner.
Both trees are at 5c98be3 and clean. beacon_log holds only organic production rows written after the restart. The first organic row was a Chrome/99|Win CN hit.