WHA Docs
Measurement

Event Tracking & Analytics

How the analytics system records on-site events, enriches them with context and forwards them to FreshPaint, GA4 and other destinations, consent-aware and first-party by default.

The site has its own lightweight, first-party event layer provided by the analytics system (the yax-analytics mu-plugin). It records what visitors do on the site, enriches every event with context about where it happened and forwards events to any external analytics destinations that are present and consented.

This is not a replacement for FreshPaint, GA4 or Google Ads. It's a thin, consent-aware dispatch layer that (a) gives us a first-party record that survives even when a visitor declines cookies and (b) lets the theme emit clean, named events that external tools can use as conversions, instead of relying on brittle CSS selectors or exact-URL matching.

How it works

Events are fired declaratively (data-track attributes), automatically (link-intent and form detection) or programmatically (window.wha.track()). Each event is enriched with context and dispatched to every detected adapter.

Adapters

The yax-analytics plugin ships five adapters and auto-detects each at runtime; an adapter only sends when its destination is present (and, for third-party adapters, consent is granted). WHA explicitly disables two of them (see below), so three are live:

AdapterDetectionConsent requiredRole
Local (WP REST API)Always activeNo (first-party)On-site record + sanity check
FreshPaintwindow.freshpaint existsYesMarketing / conversions
Plausiblewindow.plausible exists, consent-gatedPrivacy-friendly, cookielessOperator lens — see below

Third-party adapters only fire when the visitor has granted analytics consent via window.yaxConsent.hasConsent('analytics'). If the cookie-consent plugin is not active, consent is assumed. The local adapter is first-party and always records, so we retain on-site visibility regardless of consent state.

GA4 and Meta are downstream of FreshPaint, not direct adapters. FreshPaint is the tag layer: the site sends named events to FreshPaint, which forwards them to GA4 and the Meta Pixel. The plugin also ships GTM/GA4 (dataLayer) and Umami adapters, but WHA disables both through the yax_analytics_disabled_adapters filter in the theme's functions.php.

Why dataLayer is disabled — double-count prevention. Because FreshPaint already forwards to GA4, a live dataLayer adapter would send the same named events to GA4 a second time and double the numbers. Auto-detection means it would switch on the moment anything put window.dataLayer on the page (a directly-installed GA4 gtag.js, a Google Ads tag, GTM, or another plugin). Disabling it by name guarantees FreshPaint stays the single path to GA4 regardless. If GA4/Ads is ever intentionally added outside FreshPaint, re-enable dataLayer and route conversions through one path only.

Two classes of destination. FreshPaint is the marketing stack — where conversions are defined and the content/marketing team looks (it forwards to GA4 and Meta). Plausible is the operator lens: a cookieless analytics instance on private infrastructure that the developer uses to verify events actually fire and to sanity-check traffic, independent of the marketing tools. It's the analytics counterpart to error monitoring — see Monitoring.

Auto-tracked events

These fire automatically, with no per-element annotation. Every event also carries the page context below.

EventTriggerProperties
page_viewEvery frontend page loadtitle
cta_clickCTA buttons (btn.php) and any link matching a known intenttype, label
outbound_clickClick on a link to an external hosturl, host, label
file_downloadClick on a link to a document fileurl, ext, label
form_submitAny <form> submissionform, action
schedule_an_appointment_startAppointment wizard opensprovider_id, location_id
schedule_an_appointment_stepWizard advances a stepstep
schedule_an_appointment_completeRedirect to MyHealthtype_id, subtype_id
schedule_an_appointment_abandonWizard closed before completingstep
language_changeLanguage selector usedfrom, to, page

For un-annotated links, analytics.js infers a cta_click type from the href. It only covers universal intents so the shared plugin stays generic across sites:

hreftype
tel:phone_call
mailto:email
sms:sms
geo: or a map provider (Google, Apple, Waze, Bing, MapQuest)directions

Site-specific intents (scheduling, the patient portal, find-a-provider and so on) aren't guessed here. The theme stamps those explicitly with data-track (see btn.php).

Links that match no intent fall through to outbound_click (external host) or file_download (document extension).

Explicit data-track elements always take priority, so a button stamped by btn.php never double-counts with link auto-detection. To exclude a specific link from auto-tracking, add data-track-ignore.

Event context

Every event is enriched so you know where it happened. Precedence is explicit props → element context → page context (most specific wins).

Page context (server-side)

Injected by class-enqueue.php and merged into every event. Public, non-PII identifiers only.

FieldExampleWhen
page_typesingle, archive, taxonomy, home, blog, search, 404, otheralways
post_typewha_providerssingular + post-type archives
post_id1234singular
titleDr. Jane Smithsingular
templatetemplate-locationssingular with a page template
taxonomy / term / term_idwha_topics / pregnancytaxonomy archives
langen_USalways

Element context (client-side)

Captured from the clicked element and its ancestors.

FieldSource
labelthe element's aria-label or visible text (≤80 chars)
context / context_idnearest ancestor with data-track-context / data-track-context-id

Any component can scope the interactions inside it with a context wrapper, for example a provider card:

<article data-track-context="provider" data-track-context-id="1234">

  <a href="tel:+15035551234">Call this office</a>
</article>

A phone click inside that card fires:

{
  "type": "phone_call",
  "label": "Call this office",
  "context": "provider",
  "context_id": "1234",
  "post_type": "wha_providers",
  "page_type": "single",
  "lang": "en_US"
}

Adding tracking

There are three places to add tracking, most robust first: in the theme (code, below), in wp-admin (a per-menu-item toggle), or in FreshPaint (URL and click rules). Prefer the highest one available: named events in code carry page and element context and survive redesigns.

Declarative click tracking

Add data-track to any element. Additional data-track-* attributes become event properties.

<a href="/providers/" data-track="cta_click" data-track-type="find_provider">
  Find a Provider
</a>

Fires cta_click with { type: "find_provider" } (plus context).

Impression tracking

Add data-track-impression to fire once when an element scrolls 50% into view. This fires section_viewed (or whatever event name you supply).

<section data-track-impression="section_viewed" data-track-section="hero">…</section>

Impression tracking is supported but not currently used in the wha2025 theme. Add data-track-impression to a section to start recording it.

Scope context to a container

Wrap a card/region with data-track-context (and optionally data-track-context-id) so every interaction inside it is attributed.

<article data-track-context="location" data-track-context-id="42">…</article>

Programmatic events

window.wha.track('event_name', { key: 'value' });

Available after analytics.js loads; dispatches to all detected adapters.

Adding tracking without code

Not everything needs a deploy. Two no-code paths feed the same pipeline.

In wp-admin (menu items)

Nav menu items can fire an event on click with no code. In Appearance → Menus, expand a menu item and set:

  • Track Clicks — toggle on to send an analytics event when the item is clicked.
  • Event Label — optional. Becomes the event type (e.g. schedule_appointment, bill_pay); defaults to the menu item slug.

This fires a nav_click event with your label as type, dispatched to every detected adapter like any other event.

In FreshPaint

FreshPaint can define an event from its own UI, on a URL or click/href rule, with no deploy. Use it for a page or intent the theme doesn't already emit a named event for. Where a named event exists, prefer it: it carries context and won't break on a redesign, whereas selector- and URL-based rules can. To turn any event into a GA4 key event or Google Ads conversion, see Conversions.

Relationship to FreshPaint and conversions

Because window.wha.track() forwards named events to FreshPaint (when consented), conversions can be defined on stable, named events rather than fragile CSS selectors or exact-URL pageview matches.

Why this matters for conversion accuracy:

  • Exact full-URL matches ($current_url Equals …) silently exclude paid traffic carrying ?gclid= / UTM params. Match on path or on a named event instead.
  • Click conversions tied to old CSS classes break on every redesign. The data-track / cta_click hooks live in code we control and survive design changes.
  • The first-party Analytics Log is not consent-gated, so it can sanity-check true on-site activity against FreshPaint's consented subset.

Conversions themselves are defined in FreshPaint (and downstream in GA4 / Google Ads), not in this layer. The site's job is to emit clean, named events; which of those count as a conversion is configured in FreshPaint.

What the events do and don't measure

A few boundaries worth knowing before reading these as outcomes:

  • schedule_an_appointment_complete is a booking handoff, not a confirmed appointment. It fires when the wizard hands the visitor to MyHealth (it opens the MyHealth URL). The actual appointment is booked on MyHealth, a separate system on another domain. Treat it as "reached the booking handoff," a strong intent signal, not a booked visit.
  • No cross-domain tracking into MyHealth. The session ends at the handoff; what happens on MyHealth (completed, abandoned, rescheduled) is invisible to this layer and to FreshPaint/GA4. Measuring true booked appointments needs data from MyHealth itself.
  • Phone and directions are click intent, not outcomes. phone_call / directions record that someone clicked, not that a call connected or a visit happened.

Consent is enforced at the adapter layer: third-party adapters only send when window.yaxConsent.hasConsent('analytics') is true (opt-in by default via the cookie-consent plugin; assumed if that plugin is absent). The first-party local log always records.

This is custom upstream gating, not Google Consent Mode; there are no gtag('consent', …) signals. Consent is applied by allowing or blocking whole adapters before they fire, rather than by passing consent state into Google's tags.

Viewing the data

SurfaceWhat it shows
Tools → Analytics LogSearchable rolling event log (event, properties, URL, timestamp) + total page-view stats. Capped at 5,000 rows.
Dashboard → Analytics (Last 7 Days)Aggregate event counts with sparklines and a recent activity feed (manage_options).

Page views are reported as total, not unique: there's no visitor deduplication.

Storage

Two custom tables, created automatically:

  • {prefix}_yax_analytics_counts: compact aggregate daily counts per event.
  • {prefix}_yax_analytics_log: rolling event log with properties (JSON), URL, user ID, timestamp. Capped at 5,000 rows (oldest pruned).

The first-party log stores the path only (window.location.pathname), not the full URL — query strings like ?gclid= / UTM params are not recorded here. That's intentional (it keeps the log clean and aligns with matching conversions on path or named event, per above), but it means campaign parameters won't appear in the Analytics Log.

Reference

  • Plugin source: mu-plugins/yax-analytics/ (assets/js/analytics.js, includes/class-enqueue.php, includes/class-rest.php, includes/class-db.php)
  • Button auto-detection: wha2025/components/ui/btn.php
  • See also: Plugin System, Editing Basics

On this page