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:
| Adapter | Detection | Consent required | Role |
|---|---|---|---|
| Local (WP REST API) | Always active | No (first-party) | On-site record + sanity check |
| FreshPaint | window.freshpaint exists | Yes | Marketing / conversions |
| Plausible | window.plausible exists, consent-gated | Privacy-friendly, cookieless | Operator 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.
| Event | Trigger | Properties |
|---|---|---|
page_view | Every frontend page load | title |
cta_click | CTA buttons (btn.php) and any link matching a known intent | type, label |
outbound_click | Click on a link to an external host | url, host, label |
file_download | Click on a link to a document file | url, ext, label |
form_submit | Any <form> submission | form, action |
schedule_an_appointment_start | Appointment wizard opens | provider_id, location_id |
schedule_an_appointment_step | Wizard advances a step | step |
schedule_an_appointment_complete | Redirect to MyHealth | type_id, subtype_id |
schedule_an_appointment_abandon | Wizard closed before completing | step |
language_change | Language selector used | from, to, page |
Link-intent auto-detection
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:
| href | type |
|---|---|
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.
| Field | Example | When |
|---|---|---|
page_type | single, archive, taxonomy, home, blog, search, 404, other | always |
post_type | wha_providers | singular + post-type archives |
post_id | 1234 | singular |
title | Dr. Jane Smith | singular |
template | template-locations | singular with a page template |
taxonomy / term / term_id | wha_topics / pregnancy | taxonomy archives |
lang | en_US | always |
Element context (client-side)
Captured from the clicked element and its ancestors.
| Field | Source |
|---|---|
label | the element's aria-label or visible text (≤80 chars) |
context / context_id | nearest 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_clickhooks live in code we control and survive design changes. - The first-party
Analytics Logis 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_completeis 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/directionsrecord that someone clicked, not that a call connected or a visit happened.
Consent
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
| Surface | What it shows |
|---|---|
| Tools → Analytics Log | Searchable 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