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.
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.
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.
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.
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.
| Value | Means | Set by |
|---|---|---|
| validated | The flow ran to completion and validation evaluated all three sources. | validateEvents |
| section-aborted | The 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 |
| Value | Means | Typical cause |
|---|---|---|
| tracking-broken | Flow 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.
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.
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.
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.
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/.
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.
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.
Two structural guarantees behind the diagram, both new in v1.0:
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.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.
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.
gallery_next and gallery_previous — 0 / 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_prev — missing entirely from OpenSearch. Exactly the silent-drop class v1.0 reconciliation will surface as flow-skipped placeholders once deployed.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.
| Site | Runs | Pass % | trk | site | bot | timeout | Top miss |
|---|---|---|---|---|---|---|---|
| abc | 126 | 27.8% | — | — | — | — | articlePart_next (21) |
| ahaonline | 683 | 47.3% | — | — | — | — | gallery_next (123) |
| auto | 214 | 23.4% | — | — | — | — | page_ready (42) |
| autorevue | 63 | 22.2% | — | — | — | — | articlePart_next (21) |
| blesk | 654 | 48.6% | — | — | — | — | gallery_next (117) |
| dama | 63 | 22.2% | — | — | — | — | articlePart_next (21) |
| e15 | 297 | 64.6% | — | — | — | — | page_ready (37) |
| maminka | 21 | 0.0% | — | — | — | — | page_ready (21) |
| mojezdravi | 84 | 41.7% | — | — | — | — | gallery_next (21) |
| reflex | 205 | 31.2% | — | — | — | — | page_ready (53) |
| zeny | 63 | 44.4% | — | — | — | — | gallery_next (14) |
| zive | 354 | 43.8% | — | — | — | — | page_ready (51) |
| Total | 2 827 | 43.4% | — | — | — | — |
@date in last 7d, aggregate by mediaName.keyword, sub-aggregate by failureKind.keyword and passed.
Overall pass rate: (this week %) vs (last week %). New failure clusters: (list). Regressions: (list).
data/event-mapping.json or data/event-name-aliases.json — note who decided and why)| Site | Runs | Pass % | trk | site | bot | timeout | Δ wow |
|---|---|---|---|---|---|---|---|
| abc | 288 | 27.8% | — | — | — | — | ±0pp |
| ahaonline | 1 570 | 47.5% | — | — | — | — | ±0pp |
| auto | 482 | 21.4% | — | — | — | — | +2.9pp |
| autorevue | 144 | 22.2% | — | — | — | — | ±0pp |
| blesk | 1 524 | 47.8% | — | — | — | — | +1.7pp |
| dama | 144 | 22.9% | — | — | — | — | -1.6pp |
| e15 | 673 | 64.2% | — | — | — | — | +3.8pp |
| maminka | 48 | 0.0% | — | — | — | — | ±0pp |
| mojezdravi | 192 | 41.1% | — | — | — | — | ±0pp |
| reflex | 470 | 33.6% | — | — | — | — | -5.4pp |
| zeny | 144 | 44.4% | — | — | — | — | ±0pp |
| zive | 793 | 46.9% | — | — | — | — | -6.4pp |
| Total | 6 472 | 43.7% | — | — | — | — |
| Flow | Runs | Pass | Fail | Pass % | Dominant failureKind |
|---|---|---|---|---|---|
| page_ready | 2 586 | 2 023 | 563 | 78.2% | — |
| page_next | 0 | 0 | 0 | — | not measured · silent absence — v1.0 reconciliation will surface as flow-skipped |
| page_prev | 0 | 0 | 0 | — | not measured · silent absence — v1.0 reconciliation will surface as flow-skipped |
| galleryOpen | 1 030 | 759 | 271 | 73.7% | — |
| gallery_next | 937 | 0 | 937 | 0.0% | never matching · likely capture/mapping issue |
| gallery_previous | 937 | 0 | 937 | 0.0% | never matching · likely capture/mapping issue |
| player | 399 | 33 | 366 | 8.3% | below threshold — investigate |
| articlePart_next | 281 | 13 | 268 | 4.6% | below threshold — investigate |
| articlePart_previous | 302 | 0 | 302 | 0.0% | never matching · likely capture/mapping issue |
@date in last 30d, aggregate by eventType.keyword, sub-aggregate passed + top-hit failureKind.keyword.
.planning/milestones/v1.0-ROADMAP.md.player canonical corrected from literal player to player_start (matches what every site actually fires; substring matcher was hiding the divergence).(failures that recurred across multiple weeks this month — escalate to a fix, not a re-triage)
(domains added / removed / flows extended; tag every change to the config commit)
bi-measurement.ts:232 closed (Phase 7 audit follow-up).outcome/failureKind keywords..planning/milestones/v1.0-REQUIREMENTS.md v2 section)Replace the placeholder hrefs with the real URLs the first time you publish this page — left blank deliberately so a stale link can't masquerade as live.
window.dataLayer array — the tagging team's canonical event stream. We capture it via page.evaluate.collect endpoint and reading the en (event name) query parameter or POST body.et parameter.page_ready, gallery_next, …). Each maps to a tuple of expected event names — one per tracker — in data/event-mapping.json.data/event-name-aliases.json. Currently one entry: page_prev.dataLayer = ["page_prev"].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.flow-skipped placeholder. This is what stops silent drops.