WhatAbility — Analytics, Attribution & Lead Tracking
Master Implementation Document
| Site | whatability.com.au |
| Scope | Web analytics, ad attribution, lead tracking, loop closure |
| Version | 1.3 — §19 Compliance, Privacy & Consent added (APP gap analysis, intake form, cookie disclosure) |
| Date | 22 May 2026 |
| Supersedes | whatability-closing-the-loop, whatability-analytics-tracking, whatability-data-assessment, whatability-datalayer, data-layer-briefing, WhatAbility-GA4-Audit-Checklist, whatability-datalayer-audit |
| Audience | Marketing, operations, web developer, leadership |
| Purpose | Single source of truth for analytics, attribution and lead tracking — readable as both technical documentation and operational playbook. |
0. How to Use This Document
This document merges four earlier working documents into one. Read it in the order it is written — sections build on each other. Different readers need different parts:
- Leadership / non-technical — Sections 1, 2, 13 (loop closure), 18, 19.
- Marketing team — Sections 7–12, 15, 17.
- Web developer — Sections 4–6, 13, 14, 16.
- Whoever owns rollout — Sections 17 and 18 are the action list.
Diagrams are written in Mermaid and render in GitHub, Obsidian, VS Code and most Markdown viewers.
1. Executive Summary
WhatAbility runs paid campaigns (Google Ads, Meta) across five states — Sydney, Perth, Melbourne, Brisbane, Gold Coast — promoting NDIS services. The business needs to answer two questions it currently cannot:
- Which location and service combinations generate leads — and from which channel and campaign.
- Which leads become confirmed clients — closing the loop from ad spend to actual revenue.
The fix has three parts:
- Install the tracking foundation — GTM, GA4, Meta Pixel, Google Ads conversions.
- Add a structured data layer — so every page and every conversion carries service, location and campaign context.
- Close the loop — connect confirmed-client outcomes (in Supportmate / the intake process) back to the marketing data.
Parts 1 and 2 are achievable in roughly 4–6 weeks. Part 3 is staged over the following months. Even before the loop is closed, the data layer transforms visibility: from "we got 43 enquiries" to "43 enquiries — 18 Community Access Sydney from Google Ads, 11 Perth Camps from Meta, 7 SIL Melbourne from organic."
2. Business Process & Lead Flow
2.1 The lead journey — five steps
A person who becomes a WhatAbility client passes through five steps. Tracking covers steps 1–3 automatically once implemented. Steps 4–5 happen inside the intake process and require deliberate connection (loop closure — Section 13).
| Step | What happens | Tracked by | Status |
|---|---|---|---|
| 1. Ad click | Person clicks a Google or Meta ad | UTM parameters → GA4 | ✅ Automatic |
| 2. Browse | Lands on a service/location page, scrolls, navigates | Data layer + GTM | ✅ Automatic |
| 3. Enquiry | Submits the Onboardify form, or taps the phone number | Conversion events → GA4 + Google Ads + Meta | ✅ Automatic |
| 4. Qualification | Team calls back, assesses NDIS eligibility and fit | Internal process only | ❌ Gap |
| 5. Confirmed client | Person is onboarded into Supportmate | Requires loop closure | ⚡ Needs connection |
2.2 Lead entry points
All known ways a lead can enter the business. Today, none carry channel or page attribution.
| Entry point | Where on site | Tool | Attribution today |
|---|---|---|---|
| Main enquiry form | All service & location pages, /pages/get-support/ | Onboardify (multi-step, embedded) | None |
| Phone call | Every page — 1300 358 714 | 1300 virtual number (provider unknown) | Call volume only |
| Event / camp signup | /events/ | No booking tool — phone/email | Fully manual |
| Support worker application | /pages/support-worker/, /about/jobs/ | Onboardify | None |
| SIL direct booking | /services/supported-independent-living/ | Google Calendar booking link | None |
| Welcome pack download | /resources/participant-welcome-pack/ | Direct PDF link (ungated) | N/A — not a lead capture |
2.3 Diagram — lead flow across systems
Google / Meta / Direct"] --> B["Website
whatability.com.au"] B --> C{"Lead capture
method"} C -->|Form| D["Onboardify
enquiry / application"] C -->|Phone| E["1300 358 714
call"] C -->|SIL| F["Google Calendar
booking"] C -->|Event| G["Phone / email
manual"] D --> H["Team qualifies
& follows up"] E --> H F --> H G --> H H --> I["Confirmed client
Supportmate"] classDef tracked fill:#e6f7ed,stroke:#16a34a,color:#14532d classDef gap fill:#fdecec,stroke:#dc2626,color:#7f1d1d classDef partial fill:#fff6e6,stroke:#b45309,color:#7c2d12 class A,B tracked class D,E,F,G partial class H,I gap
Green = trackable once tracking is live. Amber = data exists but is siloed or incomplete. Red = no visibility today; requires loop closure.
3. Tool Stack Overview
3.1 Stack map
| Tool | Role | Status | Marketing value |
|---|---|---|---|
| Google Tag Manager | Tag deployment container | Active — GTM-MCXFB7T, injected via the Head & Footer Code plugin (clean-up — Appendix B) | Deploys all other tags without developer involvement |
| GA4 | Web analytics | Active — property 250532328, configured via MonsterInsights (clean-up — Appendix A) | Page traffic, sessions by source, conversions, location/service segmentation |
| Google Ads | Paid search/display | Active (campaigns running) | Conversion-based Smart Bidding once tracking is connected |
| Microsoft Clarity | Heatmaps / session recording | Active | Behavioural insight on forms and pages — must be in consent scope (Appendix C) |
| Meta Pixel | Meta ad attribution + audiences | Not installed | Conversion attribution for Meta spend, remarketing audiences |
| Onboardify | Lead capture / onboarding forms | Active | Lead volume; no source attribution today |
| Supportmate | Participant / client management (CRM equivalent) | Active | Client conversion milestone; no marketing source data today |
| HIVO | Photo / digital asset management | Active | Not relevant to lead generation |
| 1300 number provider | Phone line | Active | Call volume only; no source tracking |
| Google Calendar | SIL booking link | Active | Booking count; no attribution |
| WordPress | Website platform (Elementor / Elementor Pro builder) | Active | Hosts GTM (via Head & Footer Code plugin) and the data layer (§5.7) — see Appendix C for the full plugin audit |
3.2 Diagram — data flow between platforms
WordPress"] --> GTM["Google Tag Manager"] GTM --> GA4["GA4 Property"] GTM --> META["Meta Pixel"] GTM --> GADS_RMK["Google Ads
remarketing tag"] GA4 -->|conversion import| GADS["Google Ads
conversions"] GA4 <-->|linked| GSC["Google Search Console"] GA4 -->|audiences| GADS GA4 -->|audiences| META ONB["Onboardify"] -.->|loop closure| SM["Supportmate"] SM -.->|confirmed client signal| GA4 SM -.->|offline conversion CSV| GADS SM -.->|offline conversion CSV| META classDef live fill:#e6f7ed,stroke:#16a34a,color:#14532d classDef todo fill:#fff6e6,stroke:#b45309,color:#7c2d12 class W,GTM,GA4 live class META,GADS,GADS_RMK,ONB,SM,GSC todo
Solid lines = core tracking pipeline. Dotted lines = loop-closure connections (later phases, Section 13).
4. Tracking Architecture
4.1 Principles
- GTM is the single installation. It is placed on the site once. Every other tag — GA4, Meta, Google Ads — deploys through GTM. The developer is needed once.
- The data layer is the source of truth for page context. Every page writes its service, location and type into a structured data layer object. GTM reads it; GA4, Google Ads and Meta all receive it.
- Conversions carry context. A conversion event is never just "a form was submitted" — it carries which service, which location, which campaign, and a dollar value.
- Accuracy over volume. A conversion that fires twice, or with a missing value, is worse than one that doesn't fire — it corrupts Smart Bidding. Validate before going live (Section 16).
4.2 Diagram — GTM / data layer event flow
Layer 1 — page context
page_type, service_name, location_name"] end subgraph Action["User action"] A1["dataLayer.push
Layer 2 — conversion event
e.g. enquiry_submitted"] end P1 --> GTM["GTM container"] A1 --> GTM GTM --> V["GTM reads Data Layer Variables
dlv - page_type, dlv - service_name, etc."] V --> T1["GA4 Configuration tag
(All Pages)"] V --> T2["GA4 Event: page_context
(All Pages)"] V --> T3["GA4 Event: conversion
(on trigger)"] V --> T4["Meta Pixel event
(on trigger)"] T1 --> GA4["GA4"] T2 --> GA4 T3 --> GA4 T4 --> META["Meta"]
5. Data Layer Specifications
The data layer is a structured JavaScript object (window.dataLayer) that every page populates. It has three layers.
5.0 Current state of the data layer
A data-layer audit (21 May 2026, 5 key pages) found the data layer already initialised on every page — but extremely thin. Only three variables are pushed today:
| Variable present today | Captures |
|---|---|
event | Event name (e.g. a form-submission trigger) |
formId | ID of the submitted form |
response | Form submission response |
These exist solely to support form-submission tracking. There is no page, user, or business context — every page looks identical in GA4 reporting. Standard GTM events (page views, button clicks, scroll, video, downloads, errors) fire, but no custom events are defined and outbound-link tracking is off. The schema in §5.1–§5.4 is the target state that replaces this thin setup; see Appendix A for the full current-state audit.
5.1 Layer 1 — Page Context
Pushed on every page, before the GTM snippet. Values come from the URL or CMS template variables. Persists for the whole page session.
<script>
window.dataLayer = window.dataLayer || [];
dataLayer.push({
// Page classification
'page_type': 'service', // see value table
'content_group': 'service', // GA4 content grouping
// Service context (null unless a /services/* page)
'service_name': 'community-access', // slug
'service_label': 'Community Access', // human-readable
// Location context (null unless a /locations/* page)
'location_name': null, // slug
'location_state': null,
// Debug / template tracing
'page_template': 'service'
});
</script>
page_type values
page_type | Example URL | service_name | location_name |
|---|---|---|---|
homepage | / | null | null |
service | /services/community-access/ | service slug | null |
location | /locations/perth/ | null | location slug |
about | /about/, /about/our-team/, /about/our-story/ | null | null |
careers | /about/jobs/, /pages/support-worker/, /pages/support-coordinators/ | null | null |
enquiry | /pages/get-support/ | null | null |
event | /event/{slug}/, /events/, /pages/event-waitlist/ | null | null |
blog | /blog/, /blog/{slug}/ | null | null |
resource | /resources/participant-photo-albums/ | null | null |
partner | /partners/optus/ | null | null |
info | /pages/feedback-and-complaints/, /privacy-policy/, /terms-of-use/ | null | null |
landing | /autism-support-with-what-ability/ (paid-ad pages) | null | null |
thankyou | /thank-you-need-support/ | null | null |
other | /venue/, /organiser/, test/template pages | null | null |
service-location pages. Services (/services/{slug}/) and locations (/locations/{slug}/) are separate page trees — no /services/{service}/{location}/ URL exists. On-page service_name and location_name never co-occur. - No /contact/ page. Enquiries route through /pages/get-support/ (page_type: enquiry). - The Events Calendar dominates. 303 of 341 pages (89%) are /event/, /events/, /venue/, /organiser/. event is by far the largest page_type; /venue/ and /organiser/ are taxonomy pages mapped to other. The five service slugs are community-access, ndis-camps, ndis-respite, supported-independent-living, behaviour-support-plans. The five location slugs are sydney, perth, melbourne, brisbane, gold-coast.5.2 Layer 2 — Conversion Events
Pushed when a conversion occurs. GTM automatically reads Layer 1 context, so only push the event-specific properties. Full event specifications are in Section 7.
<script>
dataLayer.push({
'event': 'enquiry_submitted',
'form_type': 'get-support', // get-support | contact | other
'service_interest': 'community-access', // from form field OR page context
'location_interest': 'sydney',
'conversion_value': 250,
'currency': 'AUD',
'enquiry_id': 'ENQ-00123' // optional — if Onboardify exposes it
});
</script>
service_interest fallback — if the Onboardify form does not expose the chosen service, fall back to service_name from Layer 1 (the page the form was opened on). Accurate for single-service pages; will be null on the homepage or the general enquiry page (/pages/get-support/), which is acceptable.5.3 Layer 3 — User Context
Optional enrichment for returning-user segmentation and first-touch attribution.
<script>
dataLayer.push({
'user_type': 'returning', // new | returning
'first_touch_source': 'google',
'first_touch_medium': 'cpc',
'first_touch_campaign': 'community-access_sydney'
});
</script>
5.4 Per-page implementation
The rules below are the URL → page-context mapping. Under Method A (recommended) the GTM JS - page_type function (§5.6) implements them; under Method B the §5.7 snippet does. Verified against a full-site crawl (22 May 2026).
| URL pattern | page_type | service_name | location_name |
|---|---|---|---|
/ | homepage | null | null |
/services/, /services/{service}/ | service | from slug | null |
/locations/, /locations/{location}/ | location | null | from slug |
/about/jobs/, /pages/support-worker/, /pages/support-coordinators/ | careers | null | null |
/about/* (all other) | about | null | null |
/pages/get-support/ | enquiry | null | null |
/event/, /events/, /pages/event-waitlist/ | event | null | null |
/blog/* | blog | null | null |
/resources/* | resource | null | null |
/partners/* | partner | null | null |
/pages/* (all other), /privacy-policy/, /terms-of-use/, /incident-reporting/ | info | null | null |
| paid-ad landing pages (explicit slug list — §5.6) | landing | null | null |
/thank-you* | thankyou | null | null |
/venue/, /organiser/, test/template pages, anything else | other | null | null |
5.5 Implementation methods — overview
Layer 1 page context can be populated two ways. Both are documented in full — Method A in §5.6, Method B in §5.7 — so the implementer can choose based on access and the site's plugin setup (Appendix C).
| Method | How it works | Feeds GTM via | Pros | Cons |
|---|---|---|---|---|
| A — GTM JavaScript variables | GTM Custom JavaScript variables parse service/location from the URL path | {{JS - ...}} / {{LT - ...}} variables | No WordPress change at all; lives entirely inside GTM (version-controlled, one-click rollback) | Page context stays inside GTM — cannot reach server-side systems; service_label needs a Lookup Table variable (§5.6); URL-dependent |
| B — Real data layer push | A script pushes a real dataLayer object before GTM loads | {{dlv - ...}} variables (§6.2) | Real data layer; supplies every field; the §5.1 schema works as written | Requires one WordPress change |
Recommendation: Method A. It needs no change to WordPress — all logic lives inside GTM, which is version-controlled and reversible (one-click rollback to a prior container version). It avoids editing the live Head & Footer Code HEAD field, which carries the GTM loader and has no version history; a fat-finger error there takes down all tracking. The service_label field that a real data layer would provide is supplied by a GTM Lookup Table variable instead (§5.6). Method B remains documented as the alternative — choose it only if page context must reach a server-side system (e.g. BigQuery Stage 4, §13.8), which a GTM-only variable cannot do. The §6.2 dlv variables and §5.1 schema describe the Method B path; under Method A, tags reference {{JS - ...}} / {{LT - ...}} instead. Pick one — do not run both.
5.6 Method A — GTM JavaScript variables (full implementation)
Populates page context entirely inside GTM, with no change to WordPress.
Step 1 — create three Custom JavaScript Variables. GTM → Variables → New → Variable type: Custom JavaScript. Name them JS - page_type, JS - service_name, JS - location_name; paste the functions below (one per variable).
Step 2 — create the LT - service_label Lookup Table variable. GTM → Variables → New → Variable type: Lookup Table. Input variable = {{JS - service_name}}. Map each service slug to its label — community-access → Community Access, ndis-camps → NDIS Camps, ndis-respite → NDIS Respite, supported-independent-living → Supported Independent Living, behaviour-support-plans → Behaviour Support Plans. Default value (not set). This supplies the service_label field that a real data layer (Method B) would otherwise provide.
Step 3 — create JS - conversion_value — the variable in §6.4.
Step 4 — wire them into tags. Every GA4 tag references these as {{JS - page_type}}, {{LT - service_label}} etc., in place of the {{dlv - ...}} variables listed in §6.2. The GA4 page_context event tag (§6.3, tag #2) carries them to GA4.
Step 5 — validate in GTM Preview (§16.1) — visit one page of each type and confirm each variable resolves.
The three variable functions:
// GTM Custom JavaScript Variable — "JS - page_type"
// URL rules verified against a full-site crawl, 22 May 2026 (341 pages).
function() {
var parts = window.location.pathname.split('/').filter(Boolean);
if (parts.length === 0) return 'homepage';
var seg0 = parts[0];
if (seg0 === 'services') return 'service';
if (seg0 === 'locations') return 'location';
if (seg0 === 'about') return parts[1] === 'jobs' ? 'careers' : 'about';
if (seg0 === 'pages') {
var slug = parts[1];
if (slug === 'get-support') return 'enquiry';
if (slug === 'event-waitlist') return 'event';
if (slug === 'support-worker' || slug === 'support-coordinators') return 'careers';
return 'info';
}
if (seg0 === 'event' || seg0 === 'events') return 'event';
if (seg0 === 'venue' || seg0 === 'organiser') return 'other';
if (seg0 === 'blog') return 'blog';
if (seg0 === 'resources') return 'resource';
if (seg0 === 'partners') return 'partner';
if (seg0.indexOf('thank-you') === 0) return 'thankyou';
if (seg0 === 'terms-of-use' || seg0 === 'privacy-policy' || seg0 === 'incident-reporting') return 'info';
// Paid-ad landing pages — unlinked from site nav, reached only via ad clicks.
// MAINTAIN THIS LIST: add a slug here whenever marketing launches a new landing page.
var landingPages = [
'explore-our-ndis-services', 'our-services-in-brisbane',
'autism-support-with-what-ability', 'downs-syndrome-support-with',
'how-to-use-funding', 'how-to-epilepsy-training'
];
if (landingPages.indexOf(seg0) !== -1) return 'landing';
return 'other';
}
// GTM Custom JavaScript Variable — "JS - service_name"
function() {
var parts = window.location.pathname.split('/').filter(Boolean);
if (parts[0] === 'services' && parts.length >= 2) return parts[1];
return null;
}
// GTM Custom JavaScript Variable — "JS - location_name"
function() {
var parts = window.location.pathname.split('/').filter(Boolean);
if (parts[0] === 'locations' && parts.length >= 2) return parts[1];
return null;
}
5.7 Method B — real data layer push (full implementation)
Method B pushes a real dataLayer object on every page before the GTM container loads. GTM then reads it via the {{dlv - ...}} variables in §6.2. The §5.1 schema and §6.2 dlv variables are written around this path; Method A (§5.6) is the recommended alternative — see §5.5.
The WordPress environment audit (Appendix C) found: GTM is injected by the Head & Footer Code plugin (active); WPCode Lite is installed but inactive. That gives two routes — pick one.
Route B1 — Head & Footer Code plugin (recommended)
The site already injects GTM through the Head & Footer Code plugin. Add the data layer in the same place — no new plugin, no PHP, no activation step.
- WordPress Admin → Tools → Head & Footer Code.
- In the HEAD Code field, paste the script below above the existing GTM snippet (same field, ordered first).
- Save.
<!-- WhatAbility — Data Layer (Layer 1: Page Context) -->
<!-- Tools > Head & Footer Code > HEAD Code — paste ABOVE the GTM snippet -->
<script>
// URL rules verified against a full-site crawl, 22 May 2026 (341 pages).
(function () {
var parts = window.location.pathname.split('/').filter(Boolean);
var serviceLabels = {
'community-access': 'Community Access',
'ndis-camps': 'NDIS Camps',
'ndis-respite': 'NDIS Respite',
'supported-independent-living': 'Supported Independent Living',
'behaviour-support-plans': 'Behaviour Support Plans'
};
var states = {
'sydney': 'NSW', 'perth': 'WA', 'melbourne': 'VIC',
'brisbane': 'QLD', 'gold-coast': 'QLD'
};
// Paid-ad landing pages — unlinked from nav. MAINTAIN when a new one launches.
var landingPages = [
'explore-our-ndis-services', 'our-services-in-brisbane',
'autism-support-with-what-ability', 'downs-syndrome-support-with',
'how-to-use-funding', 'how-to-epilepsy-training'
];
var pageType = 'other';
var serviceName = null, locationName = null;
var seg0 = parts[0];
if (parts.length === 0) {
pageType = 'homepage';
} else if (seg0 === 'services') {
pageType = 'service';
if (parts.length >= 2) serviceName = parts[1];
} else if (seg0 === 'locations') {
pageType = 'location';
if (parts.length >= 2) locationName = parts[1];
} else if (seg0 === 'about') {
pageType = (parts[1] === 'jobs') ? 'careers' : 'about';
} else if (seg0 === 'pages') {
if (parts[1] === 'get-support') pageType = 'enquiry';
else if (parts[1] === 'event-waitlist') pageType = 'event';
else if (parts[1] === 'support-worker' || parts[1] === 'support-coordinators') pageType = 'careers';
else pageType = 'info';
} else if (seg0 === 'event' || seg0 === 'events') {
pageType = 'event';
} else if (seg0 === 'venue' || seg0 === 'organiser') {
pageType = 'other';
} else if (seg0 === 'blog') {
pageType = 'blog';
} else if (seg0 === 'resources') {
pageType = 'resource';
} else if (seg0 === 'partners') {
pageType = 'partner';
} else if (seg0.indexOf('thank-you') === 0) {
pageType = 'thankyou';
} else if (seg0 === 'terms-of-use' || seg0 === 'privacy-policy' || seg0 === 'incident-reporting') {
pageType = 'info';
} else if (landingPages.indexOf(seg0) !== -1) {
pageType = 'landing';
}
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
page_type: pageType,
content_group: pageType,
service_name: serviceName,
service_label: (serviceName && serviceLabels[serviceName]) ? serviceLabels[serviceName] : null,
location_name: locationName,
location_state: (locationName && states[locationName]) ? states[locationName] : null,
page_template: pageType
});
})();
</script>
Route B2 — WPCode PHP snippet (alternative)
A server-side equivalent. Requires activating WPCode Lite first (it is installed but inactive — Appendix C). Use this route only if a server-side push is specifically preferred.
Step 1 — activate WPCode. WordPress Admin → Plugins → activate "WPCode Lite".
Step 2 — add the snippet. WPCode → Add Snippet → Add Your Custom Code → PHP Snippet. Paste the code below. Set Location = Site Wide Header, Priority = 1 (must run before the GTM container). Save and Activate.
<?php
/**
* WhatAbility — Data Layer (Layer 1: Page Context)
* WPCode snippet — Location: Site Wide Header, Priority: 1
* Outputs dataLayer.push() before the GTM container loads.
*/
add_action( 'wp_head', function () {
// URL rules verified against a full-site crawl, 22 May 2026 (341 pages).
$path = trim( (string) parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ), '/' );
$parts = ( $path === '' ) ? array() : explode( '/', $path );
// service slug => human-readable label
$services = array(
'community-access' => 'Community Access',
'ndis-camps' => 'NDIS Camps',
'ndis-respite' => 'NDIS Respite',
'supported-independent-living' => 'Supported Independent Living',
'behaviour-support-plans' => 'Behaviour Support Plans',
);
// location slug => state
$locations = array(
'sydney' => 'NSW', 'perth' => 'WA', 'melbourne' => 'VIC',
'brisbane' => 'QLD', 'gold-coast' => 'QLD',
);
// paid-ad landing pages — unlinked from nav. MAINTAIN when a new one launches.
$landing_pages = array(
'explore-our-ndis-services', 'our-services-in-brisbane',
'autism-support-with-what-ability', 'downs-syndrome-support-with',
'how-to-use-funding', 'how-to-epilepsy-training',
);
$page_type = 'other';
$service_name = null;
$location_name = null;
$seg0 = isset( $parts[0] ) ? $parts[0] : '';
$seg1 = isset( $parts[1] ) ? $parts[1] : '';
if ( empty( $parts ) ) {
$page_type = 'homepage';
} elseif ( $seg0 === 'services' ) {
$page_type = 'service';
if ( $seg1 !== '' ) { $service_name = $seg1; }
} elseif ( $seg0 === 'locations' ) {
$page_type = 'location';
if ( $seg1 !== '' ) { $location_name = $seg1; }
} elseif ( $seg0 === 'about' ) {
$page_type = ( $seg1 === 'jobs' ) ? 'careers' : 'about';
} elseif ( $seg0 === 'pages' ) {
if ( $seg1 === 'get-support' ) {
$page_type = 'enquiry';
} elseif ( $seg1 === 'event-waitlist' ) {
$page_type = 'event';
} elseif ( $seg1 === 'support-worker' || $seg1 === 'support-coordinators' ) {
$page_type = 'careers';
} else {
$page_type = 'info';
}
} elseif ( $seg0 === 'event' || $seg0 === 'events' ) {
$page_type = 'event';
} elseif ( $seg0 === 'venue' || $seg0 === 'organiser' ) {
$page_type = 'other';
} elseif ( $seg0 === 'blog' ) {
$page_type = 'blog';
} elseif ( $seg0 === 'resources' ) {
$page_type = 'resource';
} elseif ( $seg0 === 'partners' ) {
$page_type = 'partner';
} elseif ( strpos( $seg0, 'thank-you' ) === 0 ) {
$page_type = 'thankyou';
} elseif ( in_array( $seg0, array( 'terms-of-use', 'privacy-policy', 'incident-reporting' ), true ) ) {
$page_type = 'info';
} elseif ( in_array( $seg0, $landing_pages, true ) ) {
$page_type = 'landing';
}
$data = array(
'page_type' => $page_type,
'content_group' => $page_type,
'service_name' => $service_name,
'service_label' => ( $service_name && isset( $services[ $service_name ] ) )
? $services[ $service_name ] : null,
'location_name' => $location_name,
'location_state' => ( $location_name && isset( $locations[ $location_name ] ) )
? $locations[ $location_name ] : null,
'page_template' => $page_type,
);
echo "\n<script>window.dataLayer=window.dataLayer||[];"
. "dataLayer.push(" . wp_json_encode( $data ) . ");</script>\n";
}, 1 );
Step 3 — verify ordering. WPCode Priority 1 on wp_head runs early. View the page source (Ctrl+U) and confirm the dataLayer.push <script> appears above the GTM container snippet. If GTM still loads first, the Head & Footer Code plugin (Route B1) gives precise control of ordering and is the safer choice.
Validation (both routes)
After either route, validate in GTM Preview (§16.1): visit one page of each type and confirm every {{dlv - ...}} variable resolves to the expected value. View the page source and confirm the dataLayer.push runs before the GTM container.
landingPages / $landing_pages) or it falls back to page_type: other and loses page-type reporting. See Risk R4.6. GTM Implementation
6.1 GTM is already installed — verify it
GTM container GTM-MCXFB7T is already live on the site, injected via the Head & Footer Code plugin — the HEAD and BODY snippets are both correctly in place (Appendix C). Do not reinstall it. Verify before building tags:
- GTM → Preview → enter the site URL → confirm the debug connection establishes.
- Confirm the container fires on every page type (homepage, service, location, blog).
- Confirm there is only one GTM container on the site — a second container would double-count.
All new work (variables, tags, triggers) goes into the existing GTM-MCXFB7T container. Before adding anything, work through the container remediation in Appendix B — the container carries dead Universal Analytics tags, no consent mode, and ~28 redundant per-state tags that must be cleaned up first.
6.2 Data Layer Variables in GTM
Create these Data Layer Variables (dlv) once. Every tag and trigger can then reference them.
| GTM Variable | Data Layer key | Default |
|---|---|---|
dlv - page_type | page_type | unknown |
dlv - content_group | content_group | (not set) |
dlv - service_name | service_name | (not set) |
dlv - service_label | service_label | (not set) |
dlv - location_name | location_name | (not set) |
dlv - location_state | location_state | (not set) |
dlv - service_interest | service_interest | (not set) |
dlv - location_interest | location_interest | (not set) |
dlv - conversion_value | conversion_value | 0 |
dlv - event_name | event_name | (not set) |
dlv - cta_location | cta_location | (not set) |
6.3 GTM tag reference
| # | Tag | Type | Trigger |
|---|---|---|---|
| 1 | GA4 — Configuration | GA4 Config | All Pages |
| 2 | GA4 — Page Context | GA4 Event (page_context) | All Pages |
| 3 | GA4 — Phone Call Click | GA4 Event | Click URL contains tel:1300 |
| 4 | GA4 — Enquiry Submitted | GA4 Event | Onboardify completion (see Section 13) |
| 5 | GA4 — Event Signup | GA4 Event | Form submission on /events/ pages |
| 6 | GA4 — Support Worker Applied | GA4 Event | Form submission on /careers/ pages |
| 7 | GA4 — CTA Clicked | GA4 Event | Click Classes contains btn/cta/button |
| 8 | GA4 — Scroll Depth | GA4 Event | Scroll depth 50% / 75% / 90% |
| 9 | Meta Pixel — Base | Custom HTML | All Pages |
| 10 | Meta Pixel — Lead | Custom HTML | Same as tag 4 |
| 11 | Meta Pixel — Contact | Custom HTML | Same as tag 3 |
| 12 | Meta Pixel — CompleteRegistration | Custom HTML | Same as tag 5 |
| 13 | Google Ads — Remarketing | Google Ads Remarketing | All Pages |
6.4 Conversion value variable
GTM calculates the dollar value automatically from the service — no per-page updates needed.
// GTM Custom JavaScript Variable — "JS - conversion_value"
function() {
var service = {{JS - service_name}};
var values = {
'supported-independent-living': 400,
'ndis-camps': 300,
'ndis-respite': 280,
'community-access': 250,
'behaviour-support-plans': 200
};
return values[service] || 200;
}
6.5 Diagram — conversion tracking pathways
completed"] E2["tel: link
clicked"] E3["Event/camp form
submitted"] E4["Careers form
submitted"] end E1 --> GTM["GTM"] E2 --> GTM E3 --> GTM E4 --> GTM GTM --> GA4E["GA4 events:
enquiry_submitted
phone_call_clicked
event_signup
support_worker_applied"] GTM --> METAE["Meta events:
Lead / Contact /
CompleteRegistration"] GA4E -->|import: 3 of 4| GADS["Google Ads
conversions"] GA4E -.->|support_worker_applied
NOT imported| GADS
support_worker_applied is deliberately not imported into Google Ads — mixing recruitment conversions with client-acquisition conversions corrupts Smart Bidding.
7. Conversion Events
Four conversion events. Mark each as a conversion in GA4 (Admin → Events → "Mark as conversion"), then import the first three into Google Ads.
| GA4 event | Fires when | Meta event | Google Ads | Value (AUD) |
|---|---|---|---|---|
enquiry_submitted | Get Support / contact form completed via Onboardify | Lead | Import | $200–$400 by service |
phone_call_clicked | Click on tel:1300358714 | Contact | Import | $150 |
event_signup | Camp / event waitlist or registration submitted | CompleteRegistration | Import | $150 |
support_worker_applied | Support worker / careers application submitted | Lead | Do not import | No value |
7.1 Conversion values
Values drive Google Ads value-based bidding (tROAS). They are estimates — refine as real data accumulates.
| Conversion | Value | Rationale |
|---|---|---|
enquiry_submitted — SIL | $400 | Highest lifetime-value service |
enquiry_submitted — NDIS Camps | $300 | High-ticket, recurring |
enquiry_submitted — NDIS Respite | $280 | High-ticket, recurring |
enquiry_submitted — Community Access | $250 | Highest-volume entry service — anchor for tROAS target |
enquiry_submitted — Behaviour Support | $200 | Specialist, lower volume |
phone_call_clicked | $150 | High intent, unqualified |
event_signup | $150 | Entry to relationship; often converts to ongoing support |
support_worker_applied | — | Recruitment, not revenue — tracked separately |
7.2 Event payloads
<!-- phone_call_clicked — GTM fires automatically on tel: click -->
<script>
dataLayer.push({ 'event': 'phone_call_clicked', 'conversion_value': 150, 'currency': 'AUD' });
</script>
<!-- event_signup -->
<script>
dataLayer.push({
'event': 'event_signup',
'event_name': 'Summer Camp Sydney 2026',
'event_type': 'camp', // camp | workshop | community
'event_location': 'sydney',
'event_dates': '2026-01-15',
'conversion_value': 150, 'currency': 'AUD'
});
</script>
<!-- support_worker_applied — no value, kept separate from client conversions -->
<script>
dataLayer.push({
'event': 'support_worker_applied',
'application_type': 'support-worker', // support-worker | coordinator | other
'location_interest': 'sydney'
});
</script>
8. Custom Events (Engagement / Micro-conversions)
Non-conversion events that explain behaviour. Implemented via GTM — most need no developer.
| Event | Trigger | Key properties |
|---|---|---|
cta_clicked | Click on a primary CTA button | button_text, cta_location (hero/nav/footer/inline/sticky-bar), cta_destination |
service_page_viewed | Page view on /services/* | service_name, location_name |
location_page_viewed | Page view on /locations/* | location_name |
blog_post_viewed | Page view on /blog/* | post_title, post_category |
scroll_depth | User scrolls 50% / 75% / 90% | percent_scrolled, page_type |
outbound_link_clicked | Click on external link (Onboardify, Supportmate, social) | link_url, link_text |
GA4 automatic events (no setup — confirm Enhanced Measurement is on): page_view, scroll (90%), click (outbound), session_start, first_visit.
<!-- cta_clicked -->
<script>
dataLayer.push({
'event': 'cta_clicked',
'button_text': 'Get Support', // {{Click Text}}
'cta_location': 'hero', // hero | nav | footer | inline | sticky-bar
'cta_destination': 'enquiry-form' // enquiry-form | phone | email | external
});
</script>
9. Custom Dimensions
Create in GA4 Admin → Custom definitions. These unlock segmentation ("which location converts best", "which service page drives enquiries").
| Dimension | Scope | Parameter | Example values |
|---|---|---|---|
| Page Type | Event | page_type | homepage, service, location, about, careers, enquiry, event, blog, resource, partner, info, landing, thankyou, other |
| Service Name | Event | service_name | community-access, ndis-camps, ndis-respite, supported-independent-living, behaviour-support-plans |
| Location Name | Event | location_name | sydney, perth, melbourne, brisbane, gold-coast |
| Service Interest | Event | service_interest | (same as Service Name — what the lead chose) |
| Location Interest | Event | location_interest | (same as Location Name — what the lead chose) |
| CTA Location | Event | cta_location | hero, nav, footer, inline, sticky-bar |
| First Touch Source | User | first_touch_source | google, facebook, newsletter, (direct) |
| First Touch Medium | User | first_touch_medium | cpc, paid-social, email, organic |
| First Touch Campaign | User | first_touch_campaign | community-access_sydney, etc. |
service_name vs service_interest — service_name is the page the user was on; use it for content-performance analysis. service_interest is what the lead chose in the form (or page context at submission); use it for lead-quality analysis.10. Meta Ads Tracking
10.1 Meta Pixel events
| Meta event | Fires when | Maps to GA4 event |
|---|---|---|
PageView | Every page load | page_view |
Lead | Enquiry form submitted | enquiry_submitted |
Contact | Phone number clicked | phone_call_clicked |
CompleteRegistration | Event / camp signup submitted | event_signup |
Lead | Support worker application | support_worker_applied |
ViewContent | Service or location page viewed | service_page_viewed |
10.2 Conversions API (CAPI)
The browser-only pixel loses roughly 30% of conversions to iOS 14+ and ad-blockers. Enable the Conversions API in Meta Events Manager (partner integration or server-side) for accurate attribution. This requires developer involvement and is a Phase 2+ task.
<!-- Meta Pixel Lead — Custom HTML tag, fires with enquiry_submitted -->
<script>
fbq('track', 'Lead', {
content_name: 'NDIS Enquiry',
content_category: 'Get Support'
});
</script>
11. Google Ads Tracking
11.1 Conversion import
Link the GA4 property to Google Ads (GA4 Admin → Google Ads Links), then import conversions (Google Ads → Tools → Conversions → Import → Google Analytics 4). Use GA4-linked conversions, not native Google Ads tags — avoids duplicate counting and gives unified attribution.
| Conversion action | Source | Counting | Value |
|---|---|---|---|
| Enquiry Submitted | Import — enquiry_submitted | One per conversion | Per-service (Section 7.1) |
| Phone Call Clicked | Import — phone_call_clicked | One per conversion | $150 |
| Event Signup | Import — event_signup | One per conversion | $150 |
11.2 Attribution settings
| Setting | Recommended | Why |
|---|---|---|
| Attribution model | Data-driven | Google ML across all touchpoints |
| Conversion window — click | 30 days | NDIS has a long consideration cycle; the 7-day default misses conversions |
| Conversion window — view | 1 day | View-through is rarely the primary driver here |
| Counting | One per conversion | Prevents double-counting repeat submissions |
| Included in "Conversions" | enquiry_submitted, phone_call_clicked, event_signup only | Keep support_worker_applied out — it corrupts client-acquisition bidding |
11.3 Smart Bidding rollout
Smart Bidding quality depends entirely on the conversion signal it is fed. Roll out in stages.
| Phase | Timeframe | Bidding strategy | Notes |
|---|---|---|---|
| Learning | Weeks 1–6 | Maximise Conversions (enquiry signal only) | Expect higher CPC and volatility while it learns |
| Transition | Month 2–3 | Maximise Conversions + start uploading confirmed-client data | Add confirmed client as a separate conversion action; keep enquiry as secondary |
| tROAS | Month 4+ (30+ confirmed clients/month) | Target ROAS using confirmed-client value | Start target at 150–200%, tighten as volume grows |
12. UTM Naming Conventions
Enforce lowercase + underscores/hyphens. Maintain a shared UTM spreadsheet so every campaign link is documented.
| Channel | utm_source | utm_medium | utm_campaign pattern | Example |
|---|---|---|---|---|
| Google Search Ads | google | cpc | {service}_{location} | community-access_sydney |
| Google Display Ads | google | display | {service}_{audience} | respite_parents |
| Facebook Ads | facebook | paid-social | {service}_{location} | ndis-camps_perth |
| Instagram Ads | instagram | paid-social | {service}_{location} | community-access_melbourne |
| Email newsletter | newsletter | email | {month}_{topic} | may_camps-open |
| Organic Facebook | facebook | social | {post-topic} | camp-highlights |
| Partner / referral | {partner-name} | referral | {placement} | footer-link |
utm_content — use to split-test creatives within a campaign: video_testimonial, static_image_family, carousel_activities.
{service} and {location} tokens must match the data layer slugs exactly (Section 5.1) so campaign data joins cleanly to on-site behaviour.13. CRM / Onboardify Integration & Loop Closure
This is the hardest and highest-value part. "Closing the loop" means being able to say: "The Perth Camps campaign spent $3,200, produced 12 enquiries, 8 of which became confirmed clients — $400 per client."
13.1 Onboardify form tracking — three scenarios
Enquiry forms run through Onboardify (a third-party tool). How to track the conversion depends on how Onboardify is embedded. First action: submit a test enquiry on the live site and watch the URL bar.
| Scenario | How to detect | Tracking approach |
|---|---|---|
| A — Thank-you redirect | After submit, user lands on a whatability.com.au/thank-you/ page | GTM Page View trigger on the thank-you URL fires enquiry_submitted. No developer needed. |
| B — Embedded iframe | Form is on the page; URL does not change on submit | GTM cannot see inside iframes. Ask Onboardify support if they fire a postMessage on completion; if yes, a small JavaScript snippet listens for it and pushes enquiry_submitted. Add it via the Head & Footer Code plugin (already active) or by activating WPCode Lite. Developer needed. |
| C — Stays on Onboardify domain | User completes the form on onboardify.au and never returns | Requires an Onboardify webhook → server-side GA4 Measurement Protocol event. Developer needed. |
id="onboardify_iframe") loading portal.onboardify.au/client_form/.../2/112 — a Scenario B/C hybrid: the form markup lives on the Onboardify domain inside an iframe, so GTM cannot read its submit event. Conversion tracking (enquiry_submitted) therefore still needs either an Onboardify completion postMessage (Scenario B) or a webhook (Scenario C). GCLID passthrough into this iframe is already built and working — see §13.8.// Scenario B — add via Head & Footer Code plugin (HEAD or FOOTER field)
// Confirm the exact message format with Onboardify support first.
window.addEventListener('message', function(event) {
if (event.origin.indexOf('onboardify') === -1) return;
if (event.data && event.data.type === 'form_complete') {
window.dataLayer = window.dataLayer || [];
dataLayer.push({
'event': 'enquiry_submitted',
'form_type': 'get-support',
'service_interest': event.data.service || null,
'location_interest': event.data.location || null
});
}
});
13.2 Diagram — CRM & tracking integrations
captures gclid / fbclid /
GA client ID / page context"] --> ONB["Onboardify
enquiry form"] ONB -->|"hidden fields carry
IDs + UTM"| ONBREC["Onboardify
lead record"] ONBREC -->|"intake — native /
Zapier / manual?"| SM["Supportmate
participant record"] SM -->|"confirmed milestone
+ lead source"| RPT["Reporting
cost per client"] SM -.->|"Option B: webhook"| GA4["GA4"] SM -.->|"Option C: monthly CSV"| ADS["Google Ads + Meta
offline conversions"] classDef gap fill:#fdecec,stroke:#dc2626,color:#7f1d1d class ONBREC,SM gap
Red nodes are where attribution currently breaks — see open questions Q1–Q9 (Section 18).
13.3 The three loop-closure mechanisms
These connect a confirmed client back to the original ad click. Listed simplest to most powerful — they stack; start with one, upgrade later.
| Mechanism | Common ID | Precision | Effort | What it gives you |
|---|---|---|---|---|
| 1 — Enquiry ID | Onboardify reference (ENQ-00123) | Campaign-level (manual cross-reference) | Lowest — needs Onboardify to expose the ref | Enquiry-to-client rate by service & state |
| 2 — GCLID / FBCLID | Google/Meta click ID | Per-campaign, per-keyword | Medium — capture click ID, store in CRM, monthly CSV upload | True per-ad attribution; Smart Bidding learns which clicks become clients |
| 3 — GA4 Client ID | GA4 cookie ID | Full session/campaign history | Highest — server-to-server API call from CRM to GA4 | Confirmed-client events appear in GA4 automatically, in real time |
13.4 The three implementation options
| Option | Mechanism | Developer | Ongoing effort | Data quality |
|---|---|---|---|---|
| A — Internal "Client Confirmed" page | Team submits an internal form (/internal/client-confirmed/) per confirmed client; fires a GA4 event | 1–2 days | Manual, per client | Depends on team discipline — good enough to start |
| B — Onboardify/CRM webhook → GA4 | Lead status change to "confirmed" auto-fires a signal to GA4 | 3–5 days | None — automatic | High — as accurate as CRM status updates |
| C — Offline conversion import | Monthly CSV of confirmed clients matched by GCLID/FBCLID, uploaded to Google Ads + Meta | ~1 week (click-ID capture + CRM field) | ~30 min/month | Highest — tells ad platforms directly who converted |
Minimum viable version (before any option is built): the team keeps a simple spreadsheet — one row per confirmed client, columns for service and state. At month-end, compare against GA4 enquiry counts. No per-campaign precision, but it answers the most important question: which services and states produce actual clients.
13.5 Diagram — attribution flow (loop closed)
toward confirmed clients, not just enquiries
13.6 First-touch attribution
GA4 natively stores only the last session's UTMs. To answer "which campaigns introduce leads", capture first-touch UTMs in a cookie:
// On page load — store first UTM in a 90-day cookie, push to data layer every visit
const params = new URLSearchParams(window.location.search);
const ftSource = params.get('utm_source');
if (ftSource && !getCookie('ft_source')) {
setCookie('ft_source', ftSource, 90);
setCookie('ft_medium', params.get('utm_medium'), 90);
setCookie('ft_campaign', params.get('utm_campaign'), 90);
}
window.dataLayer = window.dataLayer || [];
dataLayer.push({
'first_touch_source': getCookie('ft_source') || '(direct)',
'first_touch_medium': getCookie('ft_medium') || '(none)',
'first_touch_campaign': getCookie('ft_campaign') || '(not set)'
});
Register the three first_touch_* fields as User-scoped custom dimensions (Section 9).
13.7 Supportmate
For the loop to close, Supportmate must hold two things it likely does not today:
- A confirmed-client milestone with a date — service agreement signed, or first booking confirmed. The exact definition must be agreed (see Q6).
- A lead-source field — channel/campaign carried in from Onboardify at intake.
13.8 GCLID attribution pipeline — live test status (22 May 2026)
A GCLID implementation test on the live "Get Support" page (onboardify-gclid-tracking-test.md, two runs — direct landing, and homepage → "Get Support" nav journey) verified the GCLID attribution pipeline already on the site. This is the mechanism-2 path (§13.3) that feeds Option C (§13.4) — and the front half is already built and working.
Pipeline stages
| Stage | What it does | Status |
|---|---|---|
| 1.1 — Capture & persist | GCLID read from the landing URL, written to a 1st-party cookie; survives cross-page navigation | ✅ Working — cookies gclid and gclid_cookie both set; both survive a homepage → form journey |
| 1.2 — Iframe injection | GCLID appended to the Onboardify iframe src | ✅ Working — ?gclid=... present on the portal.onboardify.au iframe src in both runs |
| 3 — Hidden field capture | GCLID lands in a hidden <input id="gclid"> inside the Onboardify form and persists across all 3 steps | ✅ Working — value correct at pre-submit |
| 4 — BigQuery validation | Submitted GCLID validated / processed in BigQuery | ⬜ Not yet tested — needs a real or staging submission |
| 5 — Measurement Protocol | Validated leads pushed back to GA4 via the Measurement Protocol | ⬜ Not yet tested |
Verdict: GCLID capture → cookie persistence → iframe injection → hidden-field handoff works end-to-end across both direct and cross-page journeys. Three gaps:
| Gap | Severity | Detail & action |
|---|---|---|
| Cookie naming diverges from spec | Low | The pipeline spec called for one tracking_params cookie (JSON of gclid + UTMs). The live site uses two cookies — gclid and gclid_cookie — each holding only the gclid value. Functional, but any downstream code (BigQuery SQL, other GTM tags) referencing tracking_params finds nothing. Resolve by aligning the spec/downstream references to the live cookie names, or renaming the cookies. Ties to Appendix B-9. |
| UTM parameters not passed | Medium | utm_source and utm_medium were present in the landing URL but were not stored in any cookie and not appended to the iframe src. Channel attribution is lost at the form layer. If Stage 4 BigQuery logic filters or qualifies leads by UTM, valid leads will fail on null values. Fix: extend the GTM injection tag to read utm_source/utm_medium from the URL, append them to the iframe src, and persist them to the tracking cookie. |
| No custom GTM events | Low | Only standard lifecycle events (gtm.js, gtm.dom, gtm.load) fire — no gclid_iframe_injected or similar. Add a dataLayer.push({ event: 'gclid_iframe_injected', gclid: gclidValue }) when the injection script runs, for observability in GTM Preview / GA4 DebugView. |
Next: test Stage 4 (BigQuery) and Stage 5 (Measurement Protocol) after a real or staging submission; fix UTM passthrough before any UTM-based lead qualification goes live.
14. Call Tracking / 1300 Number Handling
The site uses a single national number — 1300 358 714 — on every page. Two distinct tracking needs:
14.1 On-site phone-click tracking (Phase 1 — easy)
GTM fires phone_call_clicked when a visitor clicks the tel: link. This captures the click with full page context (service, location, page type) — but not whether the call connected or who called. No developer needed.
14.2 True call attribution (later — needs a decision)
A single 1300 number cannot tell a Perth call from a Brisbane call, or a Google call from a Meta call. To attribute calls properly, Dynamic Number Insertion (DNI) is required: a tracking tool swaps the displayed number based on the visitor's source.
- DNI providers (AU): Delacon, Invoca; CallRail is a common reference but check AU availability.
- This is a budget + procurement decision — see Q5.
14.3 Diagram — call tracking & 1300 routing
tel:1300358714"] --> C2["GTM fires
phone_call_clicked"] C2 --> C3["GA4 event +
Meta Contact event"] C3 --> C4["Captured: page, service,
location, click time"] end subgraph Later["Phase 5 — DNI call attribution"] D1["Visitor arrives
with UTM source"] --> D2["DNI tool swaps
displayed number"] D2 --> D3["Visitor calls
source-specific number"] D3 --> D4["Call connected +
attributed to channel/location"] end
15. Reporting Requirements
15.1 Questions answerable once tracking is live
| Question | How |
|---|---|
| Which state generates the most enquiries? | GA4 report segmented by location_name |
| Which service drives the most enquiries? | GA4 by service_interest on enquiry_submitted |
| Which campaign drove which enquiries? | Session campaign × service_interest |
| Phone vs form — by state? | phone_call_clicked vs enquiry_submitted by location_name |
| Which service pages engage vs bounce? | scroll_depth by service_name + location_name |
| Which CTA placement converts? | cta_location on cta_clicked |
| Which specific camp drove signups? | event_name on event_signup |
15.2 Questions answerable only after loop closure
| Question | Needs |
|---|---|
| Of all enquiries, how many became clients? | Confirmed-client signal (Section 13) |
| Cost per confirmed client by campaign? | Loop closure + ad spend join |
| Which states have the best lead quality? | Confirmed-client signal by location |
| Which service has the highest confirmation rate? | Confirmed-client signal by service |
15.3 Core GA4 explorations to build
- Service × Location matrix — Free Form. Rows
service_interest, columnslocation_interest, metricenquiry_submittedcount. (Uses the form-side_interest dimensions, not page context — the site has no combined service-location pages, so on-pageservice_name/location_namenever co-occur. See §5.1.)* - Primary conversion funnel — Funnel Exploration: Arrival → Service Interest (
page_type= service/service-location) → Intent Signal (cta_clickedorscroll_depth≥ 75%) → Conversion. - First-touch campaign report — rows
first_touch_campaign, metricenquiry_submitted. - Phone vs form by service — dimensions
service_name×event_name, filtered to the two conversion events.
15.4 Remarketing audiences
Build in GA4; they populate Google Ads and Meta automatically once linked.
| Audience | GA4 condition | Membership | Use |
|---|---|---|---|
| All Site Visitors | page_view occurred | 30 days | Display / brand awareness |
| Service Page Visitors | page_type in (service, service-location) | 14 days | Search RLSA, Meta retargeting |
| SIL Researchers | service_name = supported-independent-living | 30 days | RLSA bid +30%, Meta Lead Ads |
| NDIS Camps Researchers | service_name = ndis-camps | 21 days | Camps campaign RLSA / carousel |
| Sydney Visitors | location_name = sydney | 14 days | Sydney geo-retargeting |
| Converters — Suppress | enquiry_submitted occurred | 90 days | Exclude from acquisition campaigns |
| Job Seekers — Suppress | page_type = careers | 30 days | Exclude from client-acquisition campaigns |
16. QA / Testing Checklist
Test everything in GTM Preview and GA4 DebugView before publishing.
16.1 Layer 1 — page context
- ☐
dataLayerpush fires before the GTM snippet (Variables tab shows values on load) - ☐
service_namecorrect on/services/community-access/→community-access - ☐
service_name+location_nameboth set on/services/ndis-camps/perth/ - ☐
service_name= null/not-set on the homepage - ☐
page_typecorrect on one page of each template type
16.2 Layer 2 — conversion events
- ☐
enquiry_submittedfires exactly once per submission (not twice) - ☐
conversion_valuepopulates (not 0 / undefined) onenquiry_submitted - ☐
service_interestcarries the correct value (test from a known service page) - ☐
phone_call_clickedfires ontel:click withpage_type+service_namepopulated - ☐ Meta
Leadevent fires alongsideenquiry_submitted(verify with Meta Pixel Helper)
16.3 Platform wiring
- ☐ GA4 receiving data in Realtime
- ☐ All 4 conversion events marked as conversions in GA4
- ☐ GA4 linked to Google Ads; 3 conversions imported (
support_worker_appliedexcluded) - ☐
support_worker_appliedconfirmed not in Google Ads primary conversions - ☐ Custom dimensions registered in GA4 (
page_type,service_name,location_name, etc.)
conversion_value is validated in GA4 DebugView. A value of 0/undefined makes Smart Bidding treat all conversions as equal and wastes budget.17. Implementation Priorities & Rollout Order
Merged from the three source documents into one master sequence.
Foundation
Week 1"] --> P1["Phase 1
Context + Core Conversions
Weeks 2-3"] P1 --> P2["Phase 2
Enrichment
Weeks 4-6"] P2 --> P3["Phase 3
Loop Closure I
Month 2"] P3 --> P4["Phase 4
Loop Closure II
Month 3+"] P2 --> P5["Phase 5
CMS Migration + DNI
When dev available"]
Phase 0 — Foundation (Week 1)
Depends on: nothing. Everything else depends on this.
- Clean up the existing GA4 property — work through Appendix A (rogue data stream, PII capture, disabled Enhanced Measurement). This is Risk R1.
- Clean up the existing GTM container — work through Appendix B remediation (dead UA tags, no consent mode, per-state tag sprawl, GCLID mess). Verify
GTM-MCXFB7Tfires correctly (§6.1); do not reinstall. Review the WordPress plugin environment in Appendix C. - Create the GA4 Configuration tag → All Pages; verify in Realtime.
- Add the Meta Pixel base code in GTM; verify with Pixel Helper.
- Agree the UTM convention; create the shared UTM spreadsheet (Section 12).
Phase 1 — Page context & core conversions (Weeks 2–3)
Depends on: Phase 0.
- Implement page context Layer 1 via Method A — §5.6 GTM JavaScript variables (no WordPress change; GTM-versioned and reversible). Method B (§5.7) is the alternative if page context must reach a server-side system later.
- Create GA4 custom dimensions (
page_type,service_name,location_name, …). - Phone-call click tracking (
phone_call_clicked). - Resolve Onboardify conversion capture — the form is a confirmed embedded iframe (§13.1); confirm Scenario B (
postMessage) vs C (webhook) with Onboardify, then implementenquiry_submitted. - Event signup + support worker application tracking.
- Mark all 4 conversions in GA4; link GA4 → Google Ads; import 3 conversions.
- Add Meta conversion events (Lead, Contact, CompleteRegistration).
Phase 2 — Enrichment (Weeks 4–6)
Depends on: Phase 1.
- CTA click tracking, scroll depth, outbound link tracking.
- Per-service conversion values (
JS - conversion_value). - Build remarketing audiences and core GA4 explorations.
- Meta Conversions API (CAPI).
Phase 3 — Loop closure I (Month 2)
Depends on: Phase 1; answers to Q1–Q9.
- Define the confirmed-client milestone (Q6).
- Implement Option A (internal client-confirmed page) or a Supportmate lead-source field — whichever fits the process.
- Begin the minimum-viable spreadsheet: confirmed clients by service + state.
Phase 4 — Loop closure II (Month 3+)
Depends on: Phase 3; 2–3 months of enquiry data.
- Carry GCLID/FBCLID from Onboardify into Supportmate. (GCLID capture into the Onboardify form is already live — confirmed 22 May 2026, §13.8; the remaining work is the Onboardify→Supportmate handoff. FBCLID was not tested — verify separately.)
- Option C — monthly offline conversion import to Google Ads + Meta.
- Transition Google Ads to tROAS once 30+ confirmed clients/month (Section 11.3).
Phase 5 — First-touch attribution & call DNI (when time allows)
Depends on: Phase 2.
- Implement first-touch attribution (Section 13.6).
- Decide on and implement phone DNI for true call attribution (Q8).
(Page context Layer 1 is implemented via Method A in Phase 1 — no migration step needed.)
18. Open Questions, Assumptions & Risks
18.1 Risks
| ID | Risk | Impact | Action |
|---|---|---|---|
| R1 | The four planning documents assume a greenfield build, but the site already has a live GA4 property (250532328, configured via MonsterInsights) and GTM container (GTM-MCXFB7T, injected via the Head & Footer Code plugin) — carrying a rogue laletours.com data stream, PII (email_address) capture, disabled Enhanced Measurement, and a thin 3-variable data layer. | High — building "from scratch" on top of a messy live setup creates duplicate tracking and data conflicts | Do not create a new property. Clean the existing setup first — Appendices A, B, C. This is Phase 0. |
| R2 | Onboardify form is an embedded iframe on the onboardify.au domain (confirmed 22 May 2026) — GTM cannot see its submit event | Medium — enquiry_submitted needs an Onboardify completion postMessage or a webhook | Confirm with Onboardify whether they emit a completion postMessage (Scenario B) or support a webhook (Scenario C). §13.1, §13.8 |
| R3 | Conversion values are estimates | Medium — wrong values mislead tROAS bidding | Treat as provisional; refine once 2–3 months of client data exists |
| R4 | Page context is derived from the URL path. A full-site crawl (22 May 2026, 341 pages — whatability_page_inventory.csv) verified the §5.6/§5.7 parsers against every live page: 100% classified, zero misses. Residual risk: paid-ad landing pages are unlinked from site nav and matched by a hardcoded slug list in the parser; a new landing page not added to that list falls back to page_type: other and loses page-type reporting. | Low | When marketing launches a new paid landing page, add its slug to the landingPages / $landing_pages list (§5.6 / §5.7). Make this a step in the campaign-launch checklist. |
| R5 | No consent mode. GTM container has zero consent gating (Appendix B-1) and Microsoft Clarity session recording is active (Appendix C) — recordings can capture PII with no consent gate | High — compliance exposure under AU Privacy Act | Implement Consent Mode v2; bring Clarity into the consent scope |
| R6 | Source documents claimed "no heatmapping/session recording installed" — the WordPress audit found Microsoft Clarity active | Low — but the planning docs were wrong on current state | Treat Clarity as live; include it in consent and privacy-policy scope |
| R7 | Intake form (Onboardify Get Support) collects sensitive disability-related information without a visible privacy acknowledgement, collection notice, explicit sensitive-information consent, guardian/carer authority confirmation, or marketing opt-in | High — Privacy Act 1988 (Cth) compliance failure; disability data is sensitive information requiring explicit consent under APPs 1, 3, 5 and 6 | Add mandatory consent checkboxes and collection notice to the form before next intake campaign. See §19. |
18.2 Open questions — must be answered by the business
Onboardify
- Q1. What fields does the Onboardify form collect — does it ask service and location?
- Q2. Does Onboardify capture the referring page URL automatically?
- Q3. Does Onboardify support hidden fields (to carry UTM / click IDs)?
- Q4. Does Onboardify support webhooks or an API? Does it redirect to a thank-you URL?
Supportmate
- Q5. How does Onboardify connect to Supportmate — native, Zapier, manual, CSV?
- Q6. Does Supportmate record a confirmed-client milestone date (agreement signed / first booking)? What is the agreed definition of "confirmed client"?
- Q7. Can a custom "Lead Source" field be added to Supportmate participant records?
Phone / 1300
- Q8. Who is the 1300 provider and what call reporting do they offer? Is there budget for DNI?
Lead process
- Q9. Average time from enquiry to first booking? (Sets the ad attribution window.) Rough enquiry-to-client conversion rate and main drop-off reasons?
18.3 Assumptions
- The site is WordPress with Elementor/Elementor Pro (confirmed — Appendix C) — Method B (§5.7) and the in-page snippets assume this.
- URLs follow the structured pattern
/services/{service}/{location}/— both Method A's JS variables (§5.6) and the Method B snippet (§5.7) depend on this. The GCLID test (22 May 2026) already found this broken — the live "Get Support" page is at/pages/get-support/. See Risk R4. - Five services and five locations as listed in Sections 5 and 9; new ones inherit tracking automatically once Method B is live.
- Google Ads campaigns are already running — meaning spend is currently happening with no conversion data, making Phases 0–1 urgent.
19. Compliance, Privacy & Consent
privacy_app_assessment.md. The organisation's current privacy policy and terms are at Web_Policy.txt.19.1 Overview
What Ability collects sensitive disability-related personal information as part of its core service delivery. This triggers heightened privacy obligations under the Privacy Act 1988 (Cth) — specifically the requirement for explicit consent and collection notice before or at the time of collection (APPs 3 and 5).
The analytics and intake-form setup currently presents three distinct compliance gaps:
- Intake form — missing consent and collection notice (APP 1, 3, 5, 6)
- Analytics tools — not disclosed in the Privacy Policy (APP 1, 5)
- No cookie consent banner — no mechanism to obtain consent before tracking begins (APP 3, Consent Mode v2)
19.2 Privacy Policy vs. Live Implementation Gap
The current Privacy Policy (last updated 29 Jan 2026) discloses three third-party processors:
| Disclosed | Purpose |
|---|---|
| Onboardify | Onboarding and enquiry management |
| Supportmate | Participant portal and service management |
| HIVO | Digital asset / photo management |
Not disclosed:
| Tool | Type | Data collected | Status |
|---|---|---|---|
Google Analytics 4 (250532328) | Analytics | Pages visited, device, general location, session behaviour | Active — not in Privacy Policy |
| Google Tag Manager / Google Ads | Advertising | GCLID, conversion events | Active — not in Privacy Policy |
| Microsoft Clarity | Session recording / heatmapping | Full session replay, click maps, potentially PII-containing recordings | Active — not in Privacy Policy |
| Meta Pixel | Advertising | Event data, pixel IDs | Planned — not in Privacy Policy |
Relevant APPs: APP 1 (Open and Transparent Management), APP 5 (Notification of Collection).
Action: Update Privacy Policy to name all analytics and advertising tools. Implement a cookie consent banner (CMP) gating these tools before they fire — see §19.4 and Risk R5.
19.3 Intake Form Consent Gaps
The Onboardify Get Support form was observed to collect participant name, date of birth, email and enquiry details (intake form assessment, privacy_app_assessment.md). Because What Ability is a disability support provider, the free-text enquiry details routinely contain disability, health or behaviour-support information — which is sensitive information under s 6(1) of the Privacy Act 1988 (Cth) and requires explicit, informed consent before collection.

Screen recording of the Get Support form at the time of assessment (22 May 2026). No privacy acknowledgement, collection notice, or consent checkboxes are visible.
The form currently lacks:
| Required element | Status | Relevant APP / Law |
|---|---|---|
| Privacy Policy acknowledgement | Missing | APP 1 |
| Collection notice (why collected, how used, who it is shared with) | Missing | APP 5 |
| Explicit sensitive-information consent | Missing | APP 3 |
| Guardian / carer authority confirmation | Missing | APP 3 |
| Separate marketing opt-in (optional, unticked by default) | Missing | APP 6, Spam Act 2003 |
| Media / photo consent (if applicable at enquiry stage) | Missing | APP 6 |
Consequence: Collecting sensitive disability information without explicit consent is a direct exposure under APP 3. This is Risk R7.
19.4 Cookie & Analytics Disclosure
The Privacy Policy states cookies may collect pages visited, time on site, device type, and general location, and directs users to disable cookies via browser settings. This describes GA4 behaviour but does not name the tool, does not disclose session recording (Clarity), and does not provide a consent mechanism — browser settings are not a valid opt-out under APP 3.
Action: Implement a CMP (cookie consent banner) that gates GA4, Clarity, and Meta Pixel behind prior user consent. Deploy Consent Mode v2 in GTM so tags respect consent state before firing. Risk R5.
19.5 Sensitive Information & Data Layer Security
As an NDIS provider, What Ability handles disability, health, and behaviour support data across all collection points. The higher consent and handling standard under the Privacy Act applies.
Current posture:
- The existing live GA4 / GTM setup currently captures an
email_addressparameter — i.e. PII is being collected by analytics today (Risk R1, Appendix B). This must be removed in Phase 0. - The planned data layer spec prohibits pushing PII (§5.3 SECURITY NOTE). This prohibition must be enforced after any data layer expansion — it is a forward control, not a description of the current state.
- Microsoft Clarity session recording is active and may capture form input containing sensitive information if a participant types disability details into a visible field before submitting.
Action: Restrict Clarity from recording the /pages/get-support/ enquiry page (or mask all form inputs site-wide in Clarity settings) before Clarity is disclosed and consented to.
19.6 Historical Lead Database Risk
Inactive enquiries and historical leads in Onboardify and Supportmate may not have a valid marketing consent record. Reusing this data for campaigns without confirmed consent is an exposure under APP 6 and the Spam Act 2003.
Recommended actions:
- Audit the existing database and identify contacts with no confirmed marketing consent record.
- Run a re-permission campaign for contacts where consent status is uncertain.
- Suppress non-responsive contacts from campaign lists after a reasonable re-permission window.
- Establish a consent register recording: name, consent type, date/time, source/method, wording version, opt-in/out status.
19.7 Recommended Actions — Priority Order
| Priority | Action | Owner | Relevant APP / Law |
|---|---|---|---|
| Immediate | Add consent checkboxes and collection notice to Onboardify Get Support form | Web / Operations | APP 1, 3, 5 |
| Immediate | Update Privacy Policy to disclose GA4, GTM, Clarity and (when live) Meta Pixel | Marketing / Legal | APP 1, 5 |
| Phase 0 | Implement cookie consent banner (CMP) + Consent Mode v2 in GTM | Developer | APP 3, R5 |
| Phase 0 | Restrict Clarity session recording from /pages/get-support/ | Developer | APP 3 |
| Phase 1 | Establish formal consent register in Onboardify / Supportmate | Operations | APP 3, APP 11 |
| Phase 2 | Audit historical lead database; run re-permission campaign | Marketing | APP 6, Spam Act 2003 |
| Phase 2 | Define and enforce data retention and destruction policy for inactive leads | Operations | APP 11 |
Appendix A — Existing GA4 / GTM Setup: Audit & Clean-up
WhatAbility-GA4-Audit-Checklist and data-layer-briefing audits. It documents what is already live on the site and what must be fixed before the rollout in Section 17. Phase 0, step 1 = work through this appendix.A.1 What exists today
| Item | Detail |
|---|---|
| GA4 property | 250532328 (Account 173068783) |
| GA4 measurement ID | G-GDJSG62EF9 (confirmed in the GTM container export — Appendix B) |
| GA4 configured via | MonsterInsights WordPress plugin |
| GTM container | GTM-MCXFB7T — injected via the Head & Footer Code plugin (Appendix C) |
| Data layer | Initialised on all pages but thin — only event, formId, response (see §5.0) |
| Standard events firing | Page views, form submissions, button clicks, scroll, video, downloads, errors |
| Not configured | Outbound-link click tracking; any custom events; page/user/business context |
cta_clicked / support_cta_click in §8).A.2 GA4 clean-up — remediation list
Grouped by priority. Complete the Critical and Warning items before Phase 1.
🔴 Critical
| Item | Action |
|---|---|
Rogue laletours.com data stream | A second stream (MonsterInsights - laletours.com, Stream ID 8176470249, MID G-JVH1196BE4) from an unrelated travel site is in this property. Delete it: Admin → Data Streams → select → Delete. |
PII capture — email_address dimension | MonsterInsights captures email_address at event scope — likely breaches Google ToS and the Australian Privacy Act. Review triggering events; remove or hash. |
| Data redaction disabled | Enable Email redaction and URL query-parameter redaction: Admin → Data Streams → What Ability → Redact data. |
🟠 Warning
| Item | Action |
|---|---|
| Enhanced Measurement OFF | Only Page Views active — scrolls, outbound clicks, site search, video, file downloads not captured. Audit MonsterInsights overlap, then re-enable. |
| Internal Traffic filter stuck in "Testing" | Staff visits inflate counts. Admin → Data Filters → Internal Traffic → set to Active (confirm IP ranges first). |
| Broken key events | ajax_form_submit, purchase, work_with_us all show "No stream data". Fix or remove as key events. |
| Google Tag "Needs attention" — 2 issues | Admin → Data Streams → Configure tag settings → resolve. |
| "Unassigned" channel spike (+1,605%) | Traffic arriving without UTMs / unmatched source-medium. Audit campaign URLs; enforce the UTM convention (§12). |
| Consent Mode v2 not implemented | All four consent signals inactive. Integrate the cookie banner / CMP with the GA tag (also Risk R5). |
Historical laletours.com contamination | Check whether the rogue stream previously sent data into this property; assess affected reports. |
🟡 Info / housekeeping
| Item | Action |
|---|---|
| Currency set to USD | Change to AUD: Admin → Property Details → Currency. |
| Business Size / Objectives blank | Complete in Property Details. |
| 17 custom dimensions, many legacy | Review gclid, wp_user_id, legacy event_category / event_label, redundant page_path, undocumented action; add descriptions to all MonsterInsights dimensions. |
gclid_capture event (~4.6k/week) | Manual Google Ads attribution workaround — assess whether still needed now Google Ads is linked. |
| 72 custom reports, many test/diagnostic | Audit and archive ("test", "QLD check July", "gclid test", etc.). |
| Unnamed audiences / collections | Name or delete "Untitled segment", "Untitled collection", "What Ability Custom Reports"; consolidate duplicate Search Console collection. |
| Personalised Advertising enabled | Confirm appropriate for an NDIS/disability org under policy and regulation. |
| No annotations ever created | Start annotating significant tracking changes. |
✅ Already correct — no action
Data retention at 14 months (max); Search Console linked; attribution model is Data-driven.
A.3 Reconciliation with this document
| Source planning docs assumed | Existing setup reality | Resolution (applied in this document) |
|---|---|---|
| "Create a GA4 property" | Property 250532328 already exists | Keep it; clean per A.2. Do not create a new one. |
| "Install GTM" | GTM-MCXFB7T already installed | Verify the install only (§6.1); build new tags/variables into the existing container. |
| Data layer 3-layer schema (§5) | Thin 3-variable data layer exists | The §5 schema replaces the thin one — additive, not a rebuild. |
| Custom events to "implement" (§8) | Generic GTM events already fire | Define structured custom events; retire/ignore the generic ones where they overlap. |
Appendix B — GTM Container Audit & Remediation
GTM-MCXFB7T_workspace40.json (workspace 40, exported 21 May 2026). The container holds 39 tags, 34 triggers, 25 variables, 3 folders. GA4 measurement ID confirmed as G-GDJSG62EF9. Every finding below has a solution. Complete the Critical and High items as part of Phase 0 (Section 17) before building new tracking.B.1 Critical
B-1 — No consent mode. All 39 tags have consentStatus: NOT_SET. Solution: Implement GTM Consent Mode v2. Install a CMP (Complianz or Cookiebot for WordPress). Add a Consent Initialisation trigger. Set each tag's consent requirements: GA4 tags require analytics_storage; Google Ads tags require ad_storage + ad_user_data + ad_personalization. Until the CMP is live, set GA4/Ads tags to "require additional consent" so they hold by default.
B-2 — Dead Universal Analytics tags. Tags #13, #14 (ua type) send to UA-173068783-1 — a property Google shut down on 1 July 2024. Solution: Delete tags #13 and #14, the GA UA-173068783-1 settings variable, and triggers #10/#11 if unused elsewhere. No replacement needed — GA4 form events (#158, #160, #182) already cover these actions.
B-3 — PII to Google Ads with no consent gate. awec variables ("User data - Work with Us", "User data - Need support") capture name, email, phone, address and feed Enhanced Conversions on tags #16/#21. Solution: Keep Enhanced Conversions (it is legitimate and valuable) but (a) gate tags #16/#21 behind ad_user_data + ad_storage consent (B-1), (b) confirm the awec variable hashes fields before send — GTM's user-provided-data variable hashes automatically; verify it is configured, not raw, (c) document the practice in the privacy policy.
B.2 High — structural
B-4 — Per-state tag sprawl (~28 tags doing the work of ~4). "Need Support" and "Work with Us" each have 6 per-state tags; plus 8 per-state "Click" tags. Solution: Consolidate. Create one need_support GA4 tag and one work_with_us GA4 tag, each passing a state event parameter. Derive the state from one variable — a RegEx Table or Lookup Table variable that reads the thank-you URL slug. Replace the 8 state "Click" tags with one state_click tag + state parameter. Result: ~28 tags → ~4. Every future state then needs zero new tags.
B-5 — Conversion double-counting. Trigger #157 (Page URL CONTAINS thank-you-work-with-us) also matches thank-you-work-with-us-nsw etc., so both work_with_us and work_with_us_nsw fire on the same page. Same bug on Need Support (#159). Solution: Fixed automatically by B-4 (one tag, one fire). If B-4 is deferred: anchor the generic triggers with RegEx thank-you-work-with-us/?$ so they do not match state suffixes, or add the state pages as a blocking trigger.
B-6 — Duplicate Conversion Linker. Tags #15 and #115 (_import_2) are identical. Solution: Delete #115. Keep one Conversion Linker on All Pages.
B-7 — Broken ajax_form_submit path. Trigger #181 → tag #182 depends on the site pushing an ajax_form_submit dataLayer event; the GA4 audit confirmed this event receives no data. Solution: Resolve the Onboardify tracking scenario (Section 13.1 — A/B/C). If Onboardify can emit a completion event, wire #182 to it; otherwise delete trigger #181 + tag #182 and implement enquiry_submitted per Section 13.1.
B-8 — Dead CF7 need-support path + duplicate listeners. Nothing pushes CF7needsupportSubmitted, so trigger #11 / tag #13 never fire. Tags #9 and #116 are duplicate CF7 listeners pushing different event names. Solution: Keep #116 ("CF7 - All Page") as the single CF7 listener. Delete tag #9 and triggers #10/#11. Route conversions off the cf7submission event, distinguishing forms by {{dlv- formId}}.
B.3 Medium
B-9 — Two competing GCLID cookie mechanisms. #178 writes cookie gclid (90d) + localStorage; #186 writes gclid_cookie (30d). #185 reads gclid_cookie; #187 reads the URL only. Solution: Keep #186 as the single mechanism; extend its cookie to 90 days to match the Google Ads click window. Delete #178 (also has an uncustomised '.yourdomain.com' placeholder). Fix #187 to read the gclid_cookie cookie as a fallback when the URL has no gclid. Confirmed live (22 May 2026): a GCLID test observed both cookies — gclid and gclid_cookie — set and carrying the correct value, and both surviving a homepage → form navigation. The duplication is live and real; this consolidation still applies. See §13.8.
B-10 — gclid_capture fires on every page. Tag #183 sends a GA4 event on All Pages (~4,600/week of noise). Solution: Add a trigger condition so it fires only when {{URL - GCLID}} is set (not undefined/empty). It should fire on GCLID arrival, not every pageview.
B-11 — Fragile state Click triggers. Triggers #149–152, #165–172 match Click Text EQUALS "QLD" etc.; NSW uses CONTAINS while others use EQUALS. Solution: Resolved by B-4 consolidation. Source the state from a stable data-state="QLD" attribute on the link (ask the developer to add it) and trigger on Click Element — not on visible text.
B-12 — Inconsistent thank-you URL matching. Mix of Page URL vs Page Path; #20 matches the odd slug thank-you-participant; #111 matches the malformed slug https-whatability-com-au-thank-you-work-with-us-vic. Solution: Standardise every trigger to Page Path with an anchored RegEx. In WordPress, rename the malformed VIC thank-you page to a clean slug /thank-you-work-with-us-vic/ and align #20 to the standard pattern.
B-13 — Inconsistent event-name casing. need_support_SA vs need_support_nsw; work_with_us_SA vs work_with_us_wa — GA4 treats these as different events. Solution: Lowercase all state values. Moot once B-4 lands (state becomes a parameter) — just ensure the state parameter values are consistently lowercase.
B-14 — #185/#187 fire on DOM Ready. Onboardify iframe/forms may load asynchronously after DOM Ready, so the tag can run before the element exists. Solution: Switch the trigger to Element Visibility on the Onboardify iframe/link selector (fires when it actually appears), or to Window Loaded. Combine with the B-9 fix so #187 reads the cookie.
B.4 Low / housekeeping
| Finding | Solution |
|---|---|
| Folder named "a" | Rename; organise all tags into meaningful folders (e.g. Config, Conversions, GCLID, Engagement). |
| No conversion values on GA4 event tags | Add value + currency parameters per the Section 7.1 service value table. Required before tROAS bidding. |
send_page_view: true on GA4 Config | Acceptable — leave as is. |
B.5 What is missing entirely
| Gap | Solution |
|---|---|
| No Meta Pixel in the container | Add the Meta Pixel base tag + conversion event tags per Sections 6.3 and 10. |
| No data-layer page context | No page_type / service_name / location_name variables exist. Implement Layer 1 per Section 5 — Method A GTM JavaScript variables (§5.6, recommended) or Method B (§5.7). |
B.6 Recommended remediation sequence
- Clean-out (do first, low risk): delete dead/duplicate items — B-2 (UA tags), B-6 (duplicate Conversion Linker), B-8 (dead CF7 path), B-7 (dead
ajax_form_submitif Onboardify cannot emit it). - Consent (compliance gate): B-1 + B-3 — implement Consent Mode v2 and gate all GA4/Ads tags. Do this before any new tags go live.
- Consolidate: B-4 — collapse per-state sprawl into parameterised tags. This also closes B-5, B-11, B-13.
- GCLID cleanup: B-9, B-10, B-14 — single cookie mechanism, conditional firing, robust triggers.
- Standardise: B-12 (thank-you URLs), B.4 housekeeping (folders, conversion values).
- Build new: B.5 — Meta Pixel, data-layer page context, then proceed with Section 17 Phases 1+.
Appendix C — WordPress Environment Audit
whatability-datalayer-audit (21 May 2026): WordPress plugin inventory and how GTM / the data layer are actually injected. Determines the Method B install route (§5.7).C.1 Plugin inventory
The site runs 32 plugins (24 active, 8 inactive). Those relevant to tracking and code injection:
Active
| Plugin | Version | Relevance |
|---|---|---|
| Head & Footer Code | 1.5.6 | Injects the GTM container (HEAD + BODY snippets) — and the chosen home for the data layer (§5.7 Route B1) |
| SEOPress | 9.6 | Has its own HEAD/BODY/FOOTER custom-tracking fields — not used for GTM; avoid splitting tracking across plugins |
| Microsoft Clarity | 0.10.23 | Active heatmap / session recording — must be in consent scope (Risk R5) |
| Meta Tag Manager | 3.3 | Meta tag management |
| Advanced Custom Fields | 6.8.0 | Field management — usable for per-page data layer values if needed (R4) |
| Elementor / Elementor Pro | 3.35.8 / 3.35.1 | Page builder |
Inactive
| Plugin | Version | Note |
|---|---|---|
| WPCode Lite | 2.3.5 | Installed but NOT activated — no snippets configured. Method B Route B2 requires activating it first. |
| Query Monitor | 3.20.4 | Developer tool — inactive |
| Wordfence Security | 8.1.4 | Inactive |
| WP Accessibility Helper | 0.6.6 | Inactive |
C.2 GTM installation — confirmed
GTM GTM-MCXFB7T is fully and correctly installed via the Head & Footer Code plugin:
- HEAD snippet — present in the plugin's HEAD Code field (standard GTM loader +
dataLayerreference). - BODY snippet — present in the BODY Code field (
<noscript><iframe>fallback). - The BODY field also holds an unrelated custom
<script>(aDOMContentLoadedbreadcrumb-truncation script) — existing custom JS, harmless, noted for awareness.
This corrects the earlier assumption that GTM was installed via MonsterInsights. MonsterInsights configures GA4; the GTM container lives in Head & Footer Code.
C.3 Data layer install path — decision
Relevant only if Method B (§5.7) is chosen. Method A — the recommended approach (§5.5) — lives entirely in GTM and needs no WordPress install path.
| Option | Verdict |
|---|---|
| Head & Footer Code plugin | Recommended (§5.7 Route B1). Already active, already manages both GTM snippets — keeps all tracking code in one place. Paste the data layer script above the GTM snippet in the HEAD field. |
| WPCode Lite | Possible (§5.7 Route B2) but requires activating an otherwise-unused plugin — splits tracking management across two tools. Use only if a server-side PHP push is specifically wanted. |
| SEOPress custom tracking | Not recommended — would fragment code across a third plugin. |
C.4 Findings carried into the document
| Finding | Where addressed |
|---|---|
| GTM injected via Head & Footer Code (not MonsterInsights) | §3.1, §6.1, Appendix A.1 corrected |
| WPCode Lite inactive | §5.7 Route B2 requires activation; §13.1 Scenario B note |
| Head & Footer Code is the recommended data layer home | §5.7 Route B1 |
| Microsoft Clarity active (session recording) | §3.1 stack; Risks R5, R6 — consent scope |
| Data layer not explicitly pushed today (GTM only inits an empty one) | §5.0; resolved by §5.7 |
End of master document.