diff --git a/.claude/skills/wizwar-pulse/SKILL.md b/.claude/skills/wizwar-pulse/SKILL.md index 6cefdf6..3257cce 100644 --- a/.claude/skills/wizwar-pulse/SKILL.md +++ b/.claude/skills/wizwar-pulse/SKILL.md @@ -40,7 +40,15 @@ quiet, say so in one line before the details. serviceStarts, ledgerMB). Report day-over-day direction, not raw rows. Its Sentry cron monitor is slug `wizwar-rollup` (a missed check-in there means the rollup itself is broken). Re-roll a day by - hand: `wizwar-rollup.sh YYYY-MM-DD`. + hand: `wizwar-rollup.sh YYYY-MM-DD`. `campaigns` counts visitors + (one per address per day) whose link carried `?ref=` — the tags + on Eric's posts (reddit, bgg, …) — or a Facebook fbclid; referrers + alone can't attribute Reddit or BGG, which send none. + **Right now** (on request, or when the announcement is fresh): live + sockets `ss -tn state established '( sport = :8787 )' | tail -n +2 | wc -l`, + rooms touched in the last hour `find /var/lib/wizwar/rooms -name '*.jsonl' -mmin -60 | wc -l`, + and today so far `wizwar-rollup.sh $(date -u +%F)` (a partial row, + replaced again at 00:10; the vitals in it are the moment's). 5. **The table itself**: - Room count and growth: `ls /var/lib/wizwar/rooms/*.jsonl | wc -l` - New human names this week: room ledgers' join lines minus bots diff --git a/deploy/wizwar-rollup.sh b/deploy/wizwar-rollup.sh index 95461ef..16e8226 100755 --- a/deploy/wizwar-rollup.sh +++ b/deploy/wizwar-rollup.sh @@ -3,7 +3,9 @@ # yesterday's traffic (from Caddy's access log), the table's growth (from # the room ledgers, stats.json and feedback.jsonl), and the box's vitals. # Counts only: no addresses are kept. The pulse reads it for trends the -# self-rotating access log cannot keep. +# self-rotating access log cannot keep. Referrers are what browsers send +# (Reddit and BGG send none); campaigns are the ?ref= tags on links +# Eric posts, one count per address per day, plus Facebook's fbclid. # Cron: 10 0 * * * (UTC), see /etc/cron.d/wizwar-rollup (deploy.sh installs both). # Sentry Crons check-in: /root/.wizwar-sentry-cron-rollup holds the URL # (absent = no check-ins). @@ -25,6 +27,9 @@ BOT = ("bot", "crawl", "spider", "facebookexternalhit", "slack", "discord", "twi # --- traffic: every access log file touched since the day began --- req = 0; human = 0; bot = 0; hips = set(); bips = set() paths = collections.Counter(); status = collections.Counter(); refs = collections.Counter(); ws = 0 +# Where a visit came from, when the link itself says so: ?ref= on a +# link Eric posted, a Facebook click's fbclid, or a utm_source. +campaigns = collections.Counter(); seen_campaign = set() for f in sorted(glob.glob("/var/lib/caddy/access*.log*")): if os.path.getmtime(f) < t0: continue opener = gzip.open if f.endswith(".gz") else open @@ -41,8 +46,13 @@ for f in sorted(glob.glob("/var/lib/caddy/access*.log*")): req += 1 if isbot: bot += 1; bips.add(ip) else: human += 1; hips.add(ip) - uri = q.get("uri", "").split("?")[0] + full = q.get("uri", ""); uri = full.split("?")[0] if uri == "/ws": ws += 1 + if not isbot and "?" in full and ip not in seen_campaign: + qs = dict(kv.split("=", 1) if "=" in kv else (kv, "") for kv in full.split("?", 1)[1].split("&")) + tag = qs.get("ref") or qs.get("utm_source") or ("facebook" if "fbclid" in qs else None) + if tag and re.fullmatch(r"[a-z0-9_-]{1,24}", tag): + campaigns[tag] += 1; seen_campaign.add(ip) key = ("/clips" if uri.startswith("/clips") else "/watch" if uri.startswith("/watch") else "(assets)" if uri.startswith("/assets") or re.search(r"\.(png|jpg|svg|ico|css|js|webmanifest|mp4)$", uri) else uri if uri in ("/", "/ws", "/robots.txt") else "(other)") @@ -99,6 +109,7 @@ row = { "day": day, "requests": req, "human": human, "bot": bot, "humanAddresses": len(hips), "botAddresses": len(bips), "socketConnects": ws, "paths": dict(paths.most_common()), "status": dict(status), "referrers": dict(refs.most_common(10)), + "campaigns": dict(campaigns.most_common(10)), "newRooms": new_rooms, "humanSeats": sum(seats.values()), "humanNames": sorted(seats), "gamesFinishedTotal": stats.get("gamesFinished"), "gamesCreatedTotal": stats.get("gamesCreated"), "reports": reports, "replies": replies,