v1.0 shipped · 20 May 2026 · 67/67 tests passing

The reference behind accurate event measurement.

A single page for the BI team: what we monitor across the 12 CNC sites, the canonical DataLayer / GA4 / Gemius name for every flow, how to read a result row, and who to escalate to when something breaks.

Sites
12
Flows
9
Trackers
3
Surfaces
4
About

What the tool does, and why we trust it.

A short answer to "what is this thing?" — the kind of thing you paste into a new starter's onboarding.

The Event Measurement tool drives scripted browser flows (Playwright + Chromium) across the 12 CNC news sites and verifies that the analytics events we expect actually fire — in all three tracking systems we care about: DataLayer, GA4, and Gemius.

Each run captures every event each tracker emits, validates them against the canonical mapping in data/event-mapping.json, and writes a structured result row per flow. Rows fan out to OpenSearch (history), Grafana (dashboards), Slack (alerts), and Confluence (snapshots).

When something is missing, the BI team sees it — accurately, with no silent drops and no false "pass." This page is the human-readable reference behind that promise.

Coverage

Twelve sites, five flow families.

Each ✓ is a configured URL in data/config-per-domain/<key>-config.json that the runner exercises every run. Two sites omit one flow on purpose.

Key Site Host Homepage Pagination Gallery Article parts Player
abc ABC www.abicko.cz
ahaonline Aha! www.ahaonline.cz ·
auto Auto www.auto.cz
autorevue Auto Revue www.autorevue.cz
blesk Blesk www.blesk.cz
dama Dáma www.dama.cz
e15 E15 www.e15.cz
maminka Maminka www.maminka.cz
mojezdravi Moje zdravíwww.mojezdravi.cz ·
reflex Reflex www.reflex.cz
zeny Zeny www.zeny.cz
zive Zive www.zive.cz

Coverage is enforced before every run by npm run config:check: it validates that every URL in a section belongs to that section's registrable domain, so copy-paste bugs (e.g. an autorevue section pointing at auto.cz) fail fast with a clear message.

Event mapping

The canonical name per flow, per tracker.

This is the contract the tool validates against. Source of truth: data/event-mapping.json (BI-editable), mirrored automatically to docs/event-mapping.md via npm run docs:event-mapping.

If a row shows alias, the site actually emits a different name and we've explicitly accepted that alternative in data/event-name-aliases.json. Add aliases only after a deliberate decision — surfaced by npm run dry-run:matching — never as a safety net that hides real tracking bugs.

Flow key DataLayer GA4 (en) Gemius (et) Note
page_ready page_ready page_view view Fires when a page finishes loading.
page_next page_next page_next view Pagination: forward to the next page in a list.
page_prev page_previous page_previous view Pagination: back. DL alias accepts the literal page_prev.
galleryOpen gallery_open gallery_open view Gallery viewer opened.
gallery_next gallery_next gallery_next view Gallery: next slide.
gallery_previous gallery_previous gallery_previous view Gallery: previous slide.
player player_start player_start stream Player started playback. Corrected 2026-05-20 from literal player.
articlePart_next articlePart_next articlePart_next view Multi-part article: next part loaded.
articlePart_previous articlePart_previous articlePart_previous view Multi-part article: previous part.

Matching is exact and case-sensitive: gallery_next never matches gallery_next_v2. The substring matcher that used to allow that was removed in v1.0.

Reading a result

What outcome and failureKind mean.

Every result row carries two semantic fields beyond passed. outcome tells you what happened to the row; failureKind tells you which team owns the fix.

outcome — what happened to the row

ValueMeansSet by
validated The flow ran to completion and validation evaluated all three sources.validateEvents
section-abortedThe flow threw mid-run (timeout, navigation error, robot intercept). One row per aborted section; the run continues with the next.per-section try/catch
flow-skipped The flow was planned (config has the URL) but never produced a result — end-of-run reconciliation synthesizes a placeholder.reconcileMissingFlows

failureKind — who owns the fix

ValueMeansTypical cause
tracking-brokenFlow ran, but an expected event did not fire in one or more sources.DataLayer / GA4 / Gemius tagging incorrect or missing on the site.
site-broken Page failed to load or the network refused the request.net::ERR_*, DNS failure, server 5xx during navigation.
robot-broken The runner hit a UI obstacle or selector miss — our problem, not the site's.Consent modal intercepts a click, selector moved, guard return.
timeout A timed operation exceeded its budget.Playwright TimeoutError, navigation timeout.

In Grafana, filter by outcome.keyword and failureKind.keyword (template variables). In Slack, both fields are annotated inline on every reported row.

Escalation

Who you tell, and what you send them.

Sort failures by failureKind first. That tells you which team owns it. Then attach the row's mediaName, eventType, url, captured event list, and (for aborts) the truncated statusMessage.

tracking-broken

Tracking / Web Analytics

Page loaded, the robot did its job, but the expected event didn't fire in one of the three sources. Almost always a tagging issue on the site. Send: flow key, expected name per source, captured-events list.

site-broken

Web team / Editorial ops

The page itself failed — network error, server error, missing route. The tool can't measure tracking if the page never loaded. Send: mediaName, url, error excerpt.

robot-broken

Tool maintainer (us)

The runner couldn't navigate or interact — usually a selector that moved, a consent modal we didn't dismiss, or a guard return. Send: flow, section name, error excerpt; we patch src/robots/.

timeout

Triage first

Timeouts straddle teams. Same flow times out on many sites → robot's wait. One site only → probably site slowness. Compare across the failure cluster before escalating.

How it works

One run, four delivery surfaces.

A Playwright runner drives one browser context per domain. For each configured flow it attaches a request listener (GA4 + Gemius), navigates the page, runs the user actions, then validates the captured set against the canonical mapping.

Playwright Chromium (chrome) per-domain context drives the flow DataLayer page.evaluate window.dataLayer scrape GA4 context.on('request') collect endpoint → en Gemius context.on('request') gemius host → et validate exact + alias match stamps outcome stamps failureKind OpenSearch history index Grafana dashboard Slack alert digest Confluence snapshot pages
FIG · one run · three captures · four delivery surfaces

Two structural guarantees behind the diagram, both new in v1.0:

  • No silent drops. If a flow aborts mid-run it emits a section-aborted row; if a planned flow never runs at all, end-of-run reconciliation synthesizes a flow-skipped row. Grafana and Slack render both distinctly from event misses.
  • No spurious matches. Validation is exact (not substring). Real name divergences are handled by an explicit per-flow alias map. Behavior changes were vetted via a dry-run diff against a captured fixture before the default flipped.
Status — edit this block

Three time horizons, hand-maintained.

Three callouts: today's snapshot, this week's trend, this month's bigger picture. Pull totals from Grafana, escalations from the active Slack thread, and canonical numbers from npm run dry-run:matching if you re-capture a fresh fixture.

Today · last 24 hours

snapshot 20 May 2026 · refresh daily

Overall pass rate (last 7d)

43.4% across 2 827 result rows over the 12 sites. Best site: e15 at 64.6%. Worst: maminka at 0.0% (21/21 fail) — investigate first.

Flows in trouble

  • gallery_next and gallery_previous0 / 937 pass each across all sites over the last 30 days. Systemic, not site-specific — likely capture timing or DOM-event divergence rather than per-site tagging.
  • articlePart_previous — 0 / 302 pass over 30 days. Same pattern.
  • player — 8.3% pass (33 / 399). Did the player_start canonical rename land in prod yet? Pre-deployment docs still expect literal player.
  • page_next and page_prevmissing entirely from OpenSearch. Exactly the silent-drop class v1.0 reconciliation will surface as flow-skipped placeholders once deployed.

What ran today

Most recent doc indexed 2026-05-20 07:55 UTC (ahaonline / player, failed). config:check exit 0 against current configs; dry-run:matching against blesk fixture: 24 / 24 unchanged.

This week · 14 May – 20 May 2026

data from OpenSearch · refresh weekly

Per-site results — 7-day window

Site Runs Pass % trk site bot timeout Top miss
abc12627.8%articlePart_next (21)
ahaonline68347.3%gallery_next (123)
auto21423.4%page_ready (42)
autorevue6322.2%articlePart_next (21)
blesk65448.6%gallery_next (117)
dama6322.2%articlePart_next (21)
e1529764.6%page_ready (37)
maminka210.0%page_ready (21)
mojezdravi8441.7%gallery_next (21)
reflex20531.2%page_ready (53)
zeny6344.4%gallery_next (14)
zive35443.8%page_ready (51)
Total2 82743.4%
OpenSearch · detail index · range @date in last 7d, aggregate by mediaName.keyword, sub-aggregate by failureKind.keyword and passed.

Trend vs last week

Overall pass rate: (this week %)  vs  (last week %). New failure clusters: (list). Regressions: (list).

Open escalations

  • (carry-over from the daily list plus anything that's outlived 48h)

Aliases / mapping decisions this week

  • (any deliberate change to data/event-mapping.json or data/event-name-aliases.json — note who decided and why)

This month · May 2026

data from OpenSearch · refresh monthly

Per-site results — 30-day window

Site Runs Pass % trk site bot timeout Δ wow
abc28827.8%±0pp
ahaonline1 57047.5%±0pp
auto48221.4%+2.9pp
autorevue14422.2%±0pp
blesk1 52447.8%+1.7pp
dama14422.9%-1.6pp
e1567364.2%+3.8pp
maminka480.0%±0pp
mojezdravi19241.1%±0pp
reflex47033.6%-5.4pp
zeny14444.4%±0pp
zive79346.9%-6.4pp
Total6 47243.7%

Per-flow breakdown — 30-day window

Flow Runs Pass Fail Pass % Dominant failureKind
page_ready2 5862 02356378.2%
page_next000not measured · silent absence — v1.0 reconciliation will surface as flow-skipped
page_prev000not measured · silent absence — v1.0 reconciliation will surface as flow-skipped
galleryOpen1 03075927173.7%
gallery_next93709370.0%never matching · likely capture/mapping issue
gallery_previous93709370.0%never matching · likely capture/mapping issue
player399333668.3%below threshold — investigate
articlePart_next281132684.6%below threshold — investigate
articlePart_previous30203020.0%never matching · likely capture/mapping issue
OpenSearch · detail index · range @date in last 30d, aggregate by eventType.keyword, sub-aggregate passed + top-hit failureKind.keyword.

Shipped this month

  • v1.0 Correctness Milestone (20 May) — 7 phases, 18 requirements, both audits closed end-to-end. See .planning/milestones/v1.0-ROADMAP.md.

Mapping & alias changes

  • 20 May — player canonical corrected from literal player to player_start (matches what every site actually fires; substring matcher was hiding the divergence).

Persistent failure clusters

(failures that recurred across multiple weeks this month — escalate to a fix, not a re-triage)

  • (none yet)

Coverage changes

(domains added / removed / flows extended; tag every change to the config commit)

  • (none this month)

Tool debt closed

  • Dormant section-aborted gap in bi-measurement.ts:232 closed (Phase 7 audit follow-up).
  • OpenSearch detail-index mapping now explicitly declares outcome/failureKind keywords.

Next month — watching

  • (carryovers and v1.1 candidates from .planning/milestones/v1.0-REQUIREMENTS.md v2 section)
Glossary

Terms you'll see in result rows.

DataLayer
The in-page window.dataLayer array — the tagging team's canonical event stream. We capture it via page.evaluate.
GA4
Google Analytics 4. Captured passively by intercepting requests to the collect endpoint and reading the en (event name) query parameter or POST body.
Gemius
Audience-measurement provider. Captured via request interception on the Gemius host; the event-type lives in the et parameter.
flow key
One of nine logical flow names (page_ready, gallery_next, …). Each maps to a tuple of expected event names — one per tracker — in data/event-mapping.json.
outcome
What happened to the row: validated · section-aborted · flow-skipped.
failureKind
Who owns the fix: tracking-broken · site-broken · robot-broken · timeout. Absent on passing rows.
alias map
An explicit, BI-editable list of accepted alternative event names per flow, per source. Lives in data/event-name-aliases.json. Currently one entry: page_prev.dataLayer = ["page_prev"].
dry-run
npm run dry-run:matching. Replays the matcher against a captured eventStorage fixture, reports every flow whose verdict would change between old (substring) and new (exact + alias) logic. Used to vet alias decisions before cutover.
reconciliation
End-of-run pass that compares the set of planned flows (derived from the per-domain config) against the rows actually emitted. Anything planned but missing gets a flow-skipped placeholder. This is what stops silent drops.