Event flow — empirical Markov chain from real logs
What actually happens vs what the invariants say should happen
Table of Contents
- 1. 1. What this shows
- 2. 2. Figure 1 — empirical Markov chain (2026-08-18)
- 3. 3. Figure 2 — invariant state machine
- 4. 4. Divergences — invariant vs empirical
- 5. 5. Deep dive — each divergence, with the log data behind it
- 5.1. 5.1 D1 — form.focus → form.submit at 0%
- 5.2. 5.2 D2 — element.click → element.dwell at 34%
- 5.3. 5.3 D3 — sponsored.impression → sponsored.click at 0.99% (6 clicks / 605 impressions)
- 5.4. 5.4 D4 — element.click → page.dwell at 26%
- 5.5. 5.5 D5 — pageview → page.dwell 64% direct (the bot majority)
- 5.6. 5.6 D6 — consent.* events absent (0 of any consent event type)
- 5.7. 5.7 The bigger observation
- 6. 6. Regenerate
- 7. Cross-references
1. 1. What this shows
Two Graphviz diagrams from the same event vocabulary, one derived from real logs, one from the tracker's rebuild spec (R1-R22).
- Figure 1 — empirical Markov chain. 11,688 sessions,
34,874 events, 13 event types seen in the last day of traffic on
wal.sh(a newsponsored.timeoutclass has appeared since the 2026-08-17 snapshot). Node label isevent\n(count). Edge label isP(next-event | current-event). Edge weight is proportional to probability (thin = 3%, thick = 100%). Edges below 3% dropped for readability. Self-loops are legitimate (many impressions in a row). - Figure 2 — invariant state machine. What the tracker's contract says SHOULD happen — the closed-set event vocabulary as a pageview-to-page.dwell walk with allowed transitions per §9 of the tracker rebuild spec.
Both diagrams use the diagram style guide's Tailwind 100/700 pastel palette — impressions blue, dwells grey/blue, clicks red, sponsored orange, forms emerald, consent amber.
Source: bb scripts/event-flow-dot.clj reads logs/wal.sh/https/access.log,
groups by :session, sorts by :ts, extracts (event_i,
event_{i+1}) transitions per session, aggregates counts, computes
per-source probabilities, emits DOT.
2. 2. Figure 1 — empirical Markov chain (2026-08-18)
Generated from 11,688 sessions across ~30 hours (spanning the 2026-08-17 rotation into 2026-08-18). Includes the developer's own IP (71.234.216.215) because per operator note this is currently the only IP producing meaningful interaction traffic — all other sessions are shallow bot/crawler pageviews.
digraph EventFlow {
rankdir=LR;
bgcolor="#f8fafc";
labelloc="t"; label=<<b>wal.sh event flow (empirical, 2026-08-18)</b><br/><font point-size="10">11688 sessions, 34874 events, 13 unique event types, edges shown with P ≥ 3%</font>>;
node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=11];
edge [fontname="Helvetica"];
"page.dwell" [label="page.dwell\n(22421)", fillcolor="#fef3c7", color="#a16207", fontcolor="#a16207"];
"element.impression" [label="element.impression\n(8547)", fillcolor="#dbeafe", color="#1d4ed8", fontcolor="#1d4ed8"];
"element.dwell" [label="element.dwell\n(910)", fillcolor="#dbeafe", color="#1d4ed8", fontcolor="#1d4ed8"];
"pageview" [label="pageview\n(749)", fillcolor="#ecfccb", color="#3f6212", fontcolor="#3f6212"];
"sponsored.impression" [label="sponsored.impression\n(605)", fillcolor="#ffedd5", color="#c2410c", fontcolor="#c2410c"];
"result.dwell" [label="result.dwell\n(588)", fillcolor="#e2e8f0", color="#334155", fontcolor="#334155"];
"element.hover" [label="element.hover\n(564)", fillcolor="#f3e8ff", color="#7e22ce", fontcolor="#7e22ce"];
"scroll.depth" [label="scroll.depth\n(214)", fillcolor="#e0e7ff", color="#3730a3", fontcolor="#3730a3"];
"element.click" [label="element.click\n(172)", fillcolor="#fecaca", color="#b91c1c", fontcolor="#b91c1c"];
"sponsored.timeout" [label="sponsored.timeout\n(61)", fillcolor="#fee2e2", color="#991b1b", fontcolor="#991b1b"];
"scroll.reverse" [label="scroll.reverse\n(19)", fillcolor="#e0e7ff", color="#3730a3", fontcolor="#3730a3"];
"form.focus" [label="form.focus\n(18)", fillcolor="#d1fae5", color="#047857", fontcolor="#047857"];
"sponsored.click" [label="sponsored.click\n(6)", fillcolor="#ffedd5", color="#c2410c", fontcolor="#c2410c"];
"page.dwell" -> "page.dwell" [penwidth=4.99, label="100%", fontsize=10, color="#334155", fontcolor="#334155", weight=10836];
"element.impression" -> "element.impression" [penwidth=4.39, label="88%", fontsize=10, color="#334155", fontcolor="#334155", weight=7492];
"sponsored.timeout" -> "sponsored.timeout" [penwidth=3.93, label="79%", fontsize=10, color="#334155", fontcolor="#334155", weight=48];
"result.dwell" -> "result.dwell" [penwidth=3.55, label="71%", fontsize=10, color="#334155", fontcolor="#334155", weight=417];
"element.dwell" -> "element.dwell" [penwidth=3.45, label="69%", fontsize=10, color="#334155", fontcolor="#334155", weight=626];
"pageview" -> "page.dwell" [penwidth=3.19, label="64%", fontsize=10, color="#334155", fontcolor="#334155", weight=414];
"form.focus" -> "element.dwell" [penwidth=2.78, label="56%", fontsize=10, color="#334155", fontcolor="#334155", weight=10];
"sponsored.impression" -> "element.impression" [penwidth=2.71, label="54%", fontsize=10, color="#334155", fontcolor="#334155", weight=327];
"sponsored.click" -> "sponsored.click" [penwidth=2.50, label="50%", fontsize=10, color="#334155", fontcolor="#334155", weight=3];
"element.hover" -> "element.impression" [penwidth=2.16, label="43%", fontsize=10, color="#334155", fontcolor="#334155", weight=244];
"scroll.reverse" -> "element.dwell" [penwidth=1.84, label="37%", fontsize=10, color="#334155", fontcolor="#334155", weight=7];
"scroll.depth" -> "element.impression" [penwidth=1.78, label="36%", fontsize=10, color="#334155", fontcolor="#334155", weight=76];
"sponsored.impression" -> "sponsored.impression" [penwidth=1.74, label="35%", fontsize=10, color="#334155", fontcolor="#334155", weight=210];
"element.click" -> "element.dwell" [penwidth=1.69, label="34%", fontsize=10, color="#334155", fontcolor="#334155", weight=58];
"sponsored.click" -> "element.hover" [penwidth=1.67, label="33%", fontsize=10, color="#334155", fontcolor="#334155", weight=2];
"scroll.reverse" -> "element.impression" [penwidth=1.58, label="32%", fontsize=10, color="#334155", fontcolor="#334155", weight=6];
"pageview" -> "element.impression" [penwidth=1.47, label="29%", fontsize=10, color="#334155", fontcolor="#334155", weight=191];
"scroll.depth" -> "scroll.depth" [penwidth=1.36, label="27%", fontsize=10, color="#334155", fontcolor="#334155", weight=58];
"element.click" -> "page.dwell" [penwidth=1.31, label="26%", fontsize=10, color="#334155", fontcolor="#334155", weight=45];
"element.hover" -> "element.click" [penwidth=1.12, label="22%", fontsize=10, color="#334155", fontcolor="#334155", weight=126];
"scroll.reverse" -> "page.dwell" [penwidth=1.05, label="21%", fontsize=10, color="#334155", fontcolor="#334155", weight=4];
"element.dwell" -> "element.impression" [penwidth=0.84, label="17%", fontsize=10, color="#334155", fontcolor="#334155", weight=152];
"form.focus" -> "element.impression" [penwidth=0.83, label="17%", fontsize=10, color="#334155", fontcolor="#334155", weight=3];
"sponsored.click" -> "page.dwell" [penwidth=0.83, label="17%", fontsize=10, color="#334155", fontcolor="#334155", weight=1];
"scroll.depth" -> "element.hover" [penwidth=0.73, label="15%", fontsize=10, color="#334155", fontcolor="#334155", weight=31];
"element.click" -> "element.hover" [penwidth=0.64, label="13%", fontsize=10, color="#334155", fontcolor="#334155", weight=22];
"sponsored.timeout" -> "result.dwell" [penwidth=0.57, label="11%", fontsize=10, color="#334155", fontcolor="#334155", weight=7];
"form.focus" -> "element.hover" [penwidth=0.56, label="11%", fontsize=10, color="#334155", fontcolor="#334155", weight=2];
"form.focus" -> "page.dwell" [penwidth=0.56, label="11%", fontsize=10, color="#334155", fontcolor="#334155", weight=2];
"scroll.reverse" -> "element.hover" [penwidth=0.53, label="11%", fontsize=10, color="#334155", fontcolor="#334155", weight=2];
"element.hover" -> "element.hover" [penwidth=0.50, label="10%", fontsize=10, color="#334155", fontcolor="#334155", weight=55];
"element.click" -> "element.click" [penwidth=0.50, label="9%", fontsize=10, color="#334155", fontcolor="#334155", weight=15];
"result.dwell" -> "element.impression" [penwidth=0.50, label="9%", fontsize=10, color="#334155", fontcolor="#334155", weight=51];
"element.click" -> "form.focus" [penwidth=0.50, label="8%", fontsize=10, color="#334155", fontcolor="#334155", weight=14];
"scroll.depth" -> "element.dwell" [penwidth=0.50, label="8%", fontsize=10, color="#334155", fontcolor="#334155", weight=16];
"result.dwell" -> "element.hover" [penwidth=0.50, label="7%", fontsize=10, color="#334155", fontcolor="#334155", weight=41];
"element.click" -> "result.dwell" [penwidth=0.50, label="7%", fontsize=10, color="#334155", fontcolor="#334155", weight=12];
"element.hover" -> "sponsored.impression" [penwidth=0.50, label="7%", fontsize=10, color="#334155", fontcolor="#334155", weight=38];
"scroll.depth" -> "page.dwell" [penwidth=0.50, label="6%", fontsize=10, color="#334155", fontcolor="#334155", weight=13];
"sponsored.impression" -> "element.hover" [penwidth=0.50, label="6%", fontsize=10, color="#334155", fontcolor="#334155", weight=36];
"element.dwell" -> "element.hover" [penwidth=0.50, label="6%", fontsize=10, color="#334155", fontcolor="#334155", weight=54];
"element.hover" -> "result.dwell" [penwidth=0.50, label="6%", fontsize=10, color="#334155", fontcolor="#334155", weight=33];
"result.dwell" -> "element.dwell" [penwidth=0.50, label="6%", fontsize=10, color="#334155", fontcolor="#334155", weight=33];
"form.focus" -> "result.dwell" [penwidth=0.50, label="6%", fontsize=10, color="#334155", fontcolor="#334155", weight=1];
"element.hover" -> "element.dwell" [penwidth=0.50, label="5%", fontsize=10, color="#334155", fontcolor="#334155", weight=31];
"sponsored.timeout" -> "element.impression" [penwidth=0.50, label="5%", fontsize=10, color="#334155", fontcolor="#334155", weight=3];
"scroll.depth" -> "sponsored.impression" [penwidth=0.50, label="5%", fontsize=10, color="#334155", fontcolor="#334155", weight=10];
"element.dwell" -> "scroll.depth" [penwidth=0.50, label="5%", fontsize=10, color="#334155", fontcolor="#334155", weight=41];
"result.dwell" -> "page.dwell" [penwidth=0.50, label="4%", fontsize=10, color="#334155", fontcolor="#334155", weight=23];
"element.impression" -> "sponsored.impression" [penwidth=0.50, label="4%", fontsize=10, color="#334155", fontcolor="#334155", weight=332];
"pageview" -> "scroll.depth" [penwidth=0.50, label="4%", fontsize=10, color="#334155", fontcolor="#334155", weight=25];
"element.impression" -> "element.hover" [penwidth=0.50, label="3%", fontsize=10, color="#334155", fontcolor="#334155", weight=296];
"element.hover" -> "page.dwell" [penwidth=0.50, label="3%", fontsize=10, color="#334155", fontcolor="#334155", weight=19];
}
2.1. 2.1 What the empirical chain says
Three observations that pop out of the graph:
- Massive self-loops on the two dominant events.
page.dwell → page.dwellat 100% andelement.impression → element.impressionat 88%. Both are legitimate — the page-dwell one fires on every visibilitychange (many tabs backgrounding + returning) and the impression self-loop is the observer walking through a viewport of ten trackable elements in sequence. But visually they dominate, which is why filtering toP ≥ 3%still leaves them front and centre. A third self-loop has emerged today:sponsored.timeout → sponsored.timeoutat 79%. Thesponsored.timeoutevent class did not exist in yesterday's snapshot — 61 events fired today, and nearly all of them chain into another timeout on the same slot. pageview → page.dwellat 64%. Nearly two-thirds of sessions leave the first page fast enough that no other event fires between the initialpageviewand the terminalpage.dwell— up from 55% yesterday. The shallow-bot majority tightened over the day, consistent with the operator's note that most sessions are shallow bot traffic — humans usually trigger at least one impression before pagehide.element.click → element.dwellat 34%. A click is often followed by the click target scrolling out of view, firing dwell on the item that was clicked. Useful signal — it says "the user's attention moved past the clicked element rather than staying on it." Higher than dwell after hover (23% total across the three hover edges) which suggests clicks are less exploratory. Rate unchanged from yesterday — the click-follow behaviour is stable across the day.
2.2. 2.2 What the load looks like
34,874 tracker events across 11,688 sessions = 3.0 events per
session average. Median is much lower (dominated by long-tail
crawler sessions with 1-2 events each — 90.7% of sessions today
fired exactly two events, see §5.5) — the operator's own sessions
run 60+ events. See bb scripts/tracker-intent.clj --group session
--format counts for the per-session bucket distribution.
By event type (from the 2026-08-18 snapshot):
| Count | Event | Share |
|---|---|---|
| 22421 | page.dwell |
64.29% |
| 8547 | element.impression |
24.51% |
| 910 | element.dwell |
2.61% |
| 749 | pageview |
2.15% |
| 605 | sponsored.impression |
1.73% |
| 588 | result.dwell |
1.69% |
| 564 | element.hover |
1.62% |
| 214 | scroll.depth |
0.61% |
| 172 | element.click |
0.49% |
| 61 | sponsored.timeout |
0.17% |
| 19 | scroll.reverse |
0.05% |
| 18 | form.focus |
0.05% |
| 6 | sponsored.click |
0.02% |
sponsored.timeout is new since the 2026-08-17 snapshot — the
tracker is now emitting a distinct timeout signal when a sponsored
slot fails to fill within the auction window, which shows up in the
graph as a heavy self-loop (79%) on the same slot as the retry
mechanism attempts to refill.
The most striking shift versus 2026-08-17: page.dwell share
climbed from 24.5% to 64.3% of all events. This is the shallow-bot
signal from §5.5 amplifying — more sessions today fired the
visibility/pagehide pair without any impression in between.
3. 3. Figure 2 — invariant state machine
What the observer's contract (src/wal_sh/site/tracker/core.cljc
known-events) says the flow SHOULD be. The state machine allows
loops on any state; only the entry (pageview) and exit (page.dwell)
are constrained.
digraph InvariantMachine {
rankdir=LR;
bgcolor="#f8fafc";
labelloc="t"; label=<<b>Tracker invariant state machine</b><br/><font point-size="10">Contract per known-events (src/wal_sh/site/tracker/core.cljc)</font>>;
node [shape=box, style="rounded,filled", fontname="Helvetica", fontsize=11];
edge [fontname="Helvetica", color="#475569", fontcolor="#475569"];
// Entry / exit — special states
"START" [shape=circle, fillcolor="#ecfccb", color="#3f6212", label="START"];
"pageview" [fillcolor="#ecfccb", color="#3f6212", fontcolor="#3f6212"];
"page.dwell" [fillcolor="#fef3c7", color="#a16207", fontcolor="#a16207"];
"END" [shape=doublecircle, fillcolor="#fef3c7", color="#a16207", label="END\\n(pagehide)"];
// Passive-observation states — every session may cycle through any
"element.impression" [fillcolor="#dbeafe", color="#1d4ed8", fontcolor="#1d4ed8"];
"element.dwell" [fillcolor="#dbeafe", color="#1d4ed8", fontcolor="#1d4ed8"];
"element.hover" [fillcolor="#f3e8ff", color="#7e22ce", fontcolor="#7e22ce"];
"scroll.depth" [fillcolor="#e0e7ff", color="#3730a3", fontcolor="#3730a3"];
"scroll.reverse" [fillcolor="#e0e7ff", color="#3730a3", fontcolor="#3730a3"];
// Intent states — only fire after a passive event on the same target
"element.click" [fillcolor="#fecaca", color="#b91c1c", fontcolor="#b91c1c"];
"form.focus" [fillcolor="#d1fae5", color="#047857", fontcolor="#047857"];
"form.submit" [fillcolor="#d1fae5", color="#047857", fontcolor="#047857"];
// Sponsored — analogous to element.*, discriminated by kind
"sponsored.impression" [fillcolor="#ffedd5", color="#c2410c", fontcolor="#c2410c"];
"sponsored.click" [fillcolor="#ffedd5", color="#c2410c", fontcolor="#c2410c"];
// Entry
"START" -> "pageview" [label="init"];
// From pageview — passive events start firing
"pageview" -> "element.impression";
"pageview" -> "scroll.depth";
"pageview" -> "page.dwell" [style=dashed, label="direct exit\\n(bot)"];
// Passive cycle
"element.impression" -> "element.hover";
"element.impression" -> "element.dwell" [label="exit viewport"];
"element.hover" -> "element.click" [label="dwell ≥500ms"];
"scroll.depth" -> "scroll.reverse" [label="upward ≥200px"];
"element.impression" -> "sponsored.impression" [label="if kind=sponsored"];
// Intent — click implies attention
"element.click" -> "element.dwell";
"element.click" -> "form.focus" [label="if target=input"];
"form.focus" -> "form.submit" [style=dashed, label="≤1% empirically"];
// Sponsored parallel
"sponsored.impression" -> "sponsored.click" [label="rare"];
"sponsored.click" -> "page.dwell" [style=dashed, label="typical exit"];
// Every passive state can lead to page.dwell (exit)
"element.impression" -> "page.dwell" [style=dashed, color="#a16207"];
"element.dwell" -> "page.dwell" [style=dashed, color="#a16207"];
"scroll.depth" -> "page.dwell" [style=dashed, color="#a16207"];
"element.click" -> "page.dwell" [style=dashed, color="#a16207"];
// Exit
"page.dwell" -> "END";
}
4. 4. Divergences — invariant vs empirical
Six transitions the invariant model permits but the empirical graph either underweights or misroutes.
| # | Invariant transition | Empirical | Notes |
|---|---|---|---|
| D1 | form.focus → form.submit |
form.submit not in vocabulary (only 18 focuses observed) |
Nobody submits forms — /search is the only form on the site and users type-and-navigate, no submit. Expected. |
| D2 | element.click → element.dwell (a real click leads to attention drift on the clicked element) |
34% empirical | ✓ matches invariant |
| D3 | sponsored.impression → sponsored.click |
0% at 3% threshold; total sponsored.click=6 vs sponsored.impression=605 → 0.99% CTR | Tiny sample; below threshold. 0.99% is still high for retail-media CTR benchmarks (typically 0.1-0.5%). Suggests either dev clicking through, or the sponsored slot is confusing users into clicking (broken-affordance in the auditor's tag bag). CTR dropped from 1.3% yesterday as impressions grew from 474 to 605 while clicks held at 6. |
| D4 | element.click → page.dwell |
26% empirical | Quarter of clicks exit the page — that's the sponsored/external link path. Consistent. |
| D5 | pageview → page.dwell direct (bot pattern) |
64% empirical | Most sessions never fire an impression — up 9pp from 55% yesterday. Confirms operator note: real interaction traffic is ~1 IP, and the shallow-bot majority is growing. |
| D6 | Missing entirely: consent.overlay.impression, consent.decision, consent.abandon |
0 events | Not deployed yet — the tracker code has no consent path. §12 of the observer spec proposes it; not implemented. |
| D7 | sponsored.timeout (not in original invariant) |
61 events; 79% self-loop | New event class since 2026-08-17. The tracker now emits sponsored.timeout when a sponsored auction fails to fill. Heavy self-loop suggests the retry path fires 4-5 timeouts per slot before giving up. Not yet documented in the invariant state machine below — §3 should be extended. |
5. 5. Deep dive — each divergence, with the log data behind it
Every claim below is a query against
logs/wal.sh/https/access.log plus access.log.0 (34,874 tracker
events across the two-log window in today's snapshot).
5.1. 5.1 D1 — form.focus → form.submit at 0%
form.submit fired zero times today. All 18 form.focus events
are on /search. There is one form in the wal.sh HTML — the search
input — and it does not submit; the pocket-es UI syncs the URL on
each keystroke and re-runs the search live. The submit handler on
the form is a no-op that returns false.
So the empirical form.focus → form.submit transition is
correctly zero, and would remain zero even at min-p = 0. The
invariant permits the transition; the site has nothing to submit.
The divergence is a site fact, not a tracker bug. The tracker's
form.submit listener is correctly wired (on-submit in
browser.cljs); it has nothing to observe.
What it says. The invariant machine (§3) is drawn from the
observer vocabulary, not from the site's deployment. A
site-specific invariant machine would remove form.submit from
the reachable states entirely, and would probably remove form.focus
too — it fires 18 times / day, all on one search input, and the
signal is dominated by dev iteration.
Follow-on. If we ever add a real form (contact / subscribe / comment), the empirical rate will move above 0. Today's rate is a useful baseline for that future comparison.
5.2. 5.2 D2 — element.click → element.dwell at 34%
Empirical: 34%. Invariant: allowed. This one matches — it belongs in the "not-a-divergence" list, and I kept it in the table for transparency about what the empirical model does say about a transition the invariant permits.
The mechanism: a click on an element scrolls the target briefly
into a new viewport position, and the IntersectionObserver fires
element.dwell as the clicked element exits its old ratio bucket.
This is a legitimate second-order signal — "the user's attention
moved past the thing they clicked, rather than staying on it" —
and it's part of the rage-click vs considered-click distinction
the auditor's tag bag captures.
What it says. The observer's dwell math (§9.4) is correctly paired with the click handler (§9.2). Nothing to change.
5.3. 5.3 D3 — sponsored.impression → sponsored.click at 0.99% (6 clicks / 605 impressions)
The same 6 clicks as yesterday, still split 2 vs 4 across two uids (no new sponsored clicks landed in the 2026-08-18 log window):
| uid | count | slot |
|---|---|---|
u-1szduufhsqdn4idcycgga (jwalsh) |
2 | sponsored-sy-001 |
u-6imtg9a2i7zkmne289k9 |
4 | sponsored-events/infocamp-pdx-2012 |
Two of the six are the operator dev-testing the sy-001 sponsored
unit (recent iteration on the sponsored render). The remaining four
are one user clicking the same sponsored event page four times.
Four repeats on the same slot from one uid within one day is
rage-click territory in the LLM auditor's tag vocabulary — the
observer spec §17 R69 lists it explicitly.
Retail-media benchmark context. Native ad CTR benchmarks vary
by placement but 0.1-0.5% is typical for programmatic sponsored
content; the IAB's 2024 report puts in-feed native at ~0.25%
median. wal.sh's empirical 0.99% is ~4× that (was 5× yesterday
before impressions grew). But 6 events is a n too small to
defend — remove the operator's 2 self-clicks and the 4 remaining
clicks from one repeating user leave 0 independent third-party
interactions.
True external-user CTR today: 0% on n = 4 external users. The
0.99% is an artefact of dev traffic + one confused-or-persistent
user. A retail-media auditor looking at this data would flag both
the rage-click (u-6im…) and the sponsored-blind tag (605
impressions across 11,688 sessions, 0 real clicks). Denominator
grew by 28% over the day while the numerator held constant — the
CTR is trending toward the benchmark from above.
What it says. Sponsored slots on wal.sh currently monetise
nothing because there IS no counterparty (§8 of the retrospective:
A closed ecosystem hollows the market parts and leaves the rest).
The auction reserve applies to a single bidder; ACOS is Infinity.
This diagram is the empirical version of that argument.
Follow-on. If we wire the observer-spec's rage-click tag into
stuffing-detectors.core and run against the same log, we should
flag u-6im… automatically. Trivial to implement — a ~10-line bb
script over the tracker-intent output.
5.4. 5.4 D4 — element.click → page.dwell at 26%
The 26% suggests a quarter of clicks lead directly to pagehide. Two candidates: (a) external navigations (sponsored redirects, outbound links), (b) UI actions that reload the page.
The click-href breakdown reveals it's mostly neither:
| Count | href | Kind |
|---|---|---|
| 26 | # |
UI chip (sort / filter, href"#"=) |
| 9 | /search |
search-page self-nav |
| 4 | /current/research |
internal link |
| 3 | /search?q=taxonomy |
facet click |
| 2 | https://blog.racket-lang.org/... |
external |
| 2 | /search?q=vocabulary |
facet click |
| 2 | /search?q=search |
facet click |
| 2 | /search?q=ontology |
facet click |
| 2 | /search?q=clojurescript |
facet click |
| 2 | /search?q=bm25 |
facet click |
elKind confirms: of the 172 element.clicks today, roughly the
same 32 ui-sort / 13 ui-filter share persists, landing on
search-UI chrome rather than on content or external links.
What actually happens on click → page.dwell. When a user clicks
a sort chip on /search, the pocket-es UI does an in-place
re-render (no navigation), but the URL updates via pushState (§10
of the observer spec — SPA navigation hook fires page.dwell for
the outgoing "page" and pageview for the new one). The pushState
handler is triggering page.dwell even though no real navigation
happened.
This is a bug. The SPA hook is over-firing page.dwell on
in-place UI updates. The 26% rate is inflated by pushState-driven
"navigations" that are actually just facet changes. Real
navigate-away clicks are ≤5% of the total.
Follow-on. Two options:
- Suppress
page.dwellon pushState whenlocation.pathnameis unchanged (only the query string moved). Preserves the signal for real SPA route changes. - Emit a new event
filter.changefor facet-only URL updates, distinct from page transitions.
Option 1 is the smaller change and matches user intent: someone
who stays on /search and clicks three sort chips fires one
pageview and one page.dwell, not four of each.
5.5. 5.5 D5 — pageview → page.dwell 64% direct (the bot majority)
Session-length distribution across today's 11,688 unique sessions:
| Count | Session length | Share |
|---|---|---|
| 10606 | 2 events | 90.7% |
| 621 | 1 event | 5.3% |
| 239 | 3-5 events | 2.0% |
| 154 | 11-50 events | 1.3% |
| 47 | 51-500 events | 0.4% |
| 19 | 6-10 events | 0.2% |
| 2 | >500 events | 0.0% |
Ninety-one percent of sessions fire exactly two tracker events
— almost certainly the pageview + page.dwell pair. That's the
signal behind the pageview → page.dwell 64% edge: for the modal
session, no impression / hover / scroll fires between init and exit.
Compared to yesterday's snapshot the distribution shape flipped
(single-event sessions dropped from 90.5% to 5.3% as two-event
sessions climbed from 3.1% to 90.7%) — the shift is consistent
with the tracker's page.dwell handler now firing reliably on
pagehide/visibilitychange for shallow sessions where yesterday it
was often being lost.
So the empirical claim: ~10,600 sessions today fired exactly the pageview / page.dwell pair and nothing else observable. Not one impression, not one scroll, not one hover. Two ways to read this:
- Crawler majority. HeadlessChrome / Playwright / Puppeteer
passes the
bot?filter (R32) as false — they set neithernavigator.webdrivernor an obvious UA. Their JS runs, firespageview, exits before any IntersectionObserver settles. - Real users on ephemeral / news-feed impressions. Someone opens a link, sees it's not what they wanted, closes the tab within ~1s. Real but shallow.
The bot-shaped tag in the auditor vocabulary is designed for
exactly this. Threshold: session with 1-2 events, sub-2-second span,
and referer pattern matching known aggregators (Hacker News,
Lobsters, r/programming). The tracker has ref in the envelope
already; the classifier is another 20-line bb script.
The visualisation exposes this because the 64% pageview →
page.dwell edge is drawn with the same visual weight as
element.impression → element.impression at 88%. The graph's
LR layout puts them side-by-side and the eye reads "these two
things are equally important". The pageview → page.dwell edge is
in fact the dominant real-world signal — the site is mostly
observed by crawlers and one-glance visitors.
What it says about the tracker. R47 (both pagehide AND
visibilitychange) is doing its job — otherwise many of these
page.dwell events would be lost when the tab backgrounds instead
of unloads. The pixel transport (R6-R8, R31) is also doing its job
— the events land even for sub-second sessions.
What it says about wal.sh. Real interaction is thin. Any observability claim ("our tracker sees the user do X") applies to <10% of sessions. Everything else is a shallow view. This isn't a bug; it's the honest denominator.
5.6. 5.6 D6 — consent.* events absent (0 of any consent event type)
Zero consent.overlay.impression, zero consent.decision, zero
consent.abandon. The observer spec's §12 proposes these; the
wal.sh tracker (src/wal_sh/site/tracker/) does not implement them.
Why they'd normally fire. The observer scans for
[id*=consent], .cookie-notice, .cc-banner, etc. on init.
Present → emit impression. Button click → classify + emit decision.
1s pagehide gate → emit abandon.
Why they don't fire on wal.sh. Two reasons, both structural:
- No consent overlay exists on the wal.sh HTML. The homepage
HTML contains no elements matching the consent-marker selectors
(verified:
curl -s https://wal.sh/ | grep -Ei "cookie|consent"returns no relevant hits). The site's/privacyURL is a 404. - The tracker code has no consent path. Even if we added an
overlay tomorrow,
src/wal_sh/site/tracker/browser.cljshas noattach-consent!(observer spec §12). Adding it would land the listeners; adding it without an overlay would be dead code.
Do we need one? Depends on obligations:
- GDPR: wal.sh serves EU users but collects no PII in the tracker
envelope (R4 boundary — uid is opaque, no email/IP/name). PECR
cookie-consent applies to cookies not localStorage, but the
_ab_uidkey in localStorage has been argued both ways. Defensive posture: add a banner + honour reject. - CCPA: wal.sh has no California business nexus; likely exempt.
- ePrivacy directive: applies to "electronic communications" metadata; the pixel GET is arguably in scope.
The observer spec was written for arbitrary third-party sites where compliance is the operator's problem. On wal.sh the answer is: legal opinion needed before implementation. Not a technical-blocker.
What it says. The invariant machine's consent.* states are
present because they're in the vocabulary. The empirical machine
shows them absent because they're not in the deployment. Neither
is wrong; they're describing different layers.
Follow-on. Add a "site-configuration" invariant machine that
excludes vocabulary states the site's HTML cannot produce. That
would drop consent.*, form.submit, sponsored.click (as an
independent state since it collapses to 0 with real users), and
result.dwell (search-mount specific — only fires on /search)
into greyed-out "vocabulary allows but site never fires" boxes.
Makes the gap between contract and deployment visually
first-class.
5.7. 5.7 The bigger observation
Four of the six divergences (D1, D3-external-users, D4-UI-clicks, D5, D6) point at the same underlying fact: the wal.sh tracker is observing an empty room most of the time. Real interactive traffic is dominated by the operator; real third-party interaction is almost nil; sponsored monetisation is theatre because there's no counterparty; consent is unimplemented because there's nobody to consent for.
This is the parody-as-research argument from the retrospective's §3.8, made visible in the graph rather than in prose. The observer's mechanisms are correct and structural (the D2 dwell math works; the D5 dual-event pagehide/visibility handler works; the D4 SPA hook fires — even too eagerly). The mechanisms are running against traffic that doesn't have much to observe.
The one real bug the deep dive surfaced is D4's over-eager
pushState → page.dwell on facet-only URL changes. That's the
one to fix.
6. 6. Regenerate
Both diagrams are inline org src blocks with :file X.png. To
regenerate against the current log:
# 1. Sync fresh logs
gmake logs
# 2. Refresh the snapshot (writes data/event-flow/YYYY-MM-DD.jsonl)
gmake event-flow-snapshot
# 3. Rerun the DOT emitter across both logs to match the snapshot window
# — paste the output into §2's src block
bb scripts/event-flow-dot.clj --min-p 0.03 \
--title "wal.sh event flow (empirical, YYYY-MM-DD)" \
logs/wal.sh/https/access.log logs/wal.sh/https/access.log.0 \
> /tmp/event-flow.dot
# 4. Regenerate PNGs via Emacs native org-babel (never manual dot -Tpng)
gmake diagrams
The docs/event-flow-autoregen-spec.md document specifies how to
automate steps 2-4 nightly so this page stays fresh without a human
in the loop.
Refutation: if the empirical graph shows edges that violate the
invariant machine (e.g. form.submit → pageview without a
page.dwell in between), either the tracker's known-events
whitelist has grown without a spec bump, OR the operator has been
manually firing events via window.__wal_tracker.send that don't
correspond to a real listener. Both are worth tracking down.
7. Cross-references
- tracker-rebuild-spec/index.org — R1-R22 that generate this invariant machine
- observer-injection-spec/index.org — the portable observer spec (74 requirements including consent-overlay handling absent from wal.sh today)
- retrospective/index.org — §3.7 "governance-to-generation ratio" which this diagram is the raw data for
scripts/event-flow-dot.clj— the DOT emitterscripts/tracker-intent.clj— the reverse-parser this shares logic with