WP Manifestindependent plugin directory
manifest / analytics / wp-clean-datalayer

WP Clean Data Layer

WordPress plugin that standardizes GA4/GTM data layer output across WooCommerce, Gravity Forms, and Contact Form 7. Single schema enforced via a central EventPayload class. Async form and cart events handled via vanilla JS listeners.

by Medium & Message · github.com/d-voorhees/wp-clean-datalayer · website

1stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/d-voorhees/wp-clean-datalayer/archive/refs/heads/main.zip

Readme

WP Clean Data Layer

Most WordPress GTM implementations break on timing. PHP-side hooks fire during AJAX requests that never trigger a full page load, so the data layer push happens where GTM cannot see it. The result is missed form conversions, phantom ecommerce events, and purchase data that disappears between the checkout page and the thank-you page. This plugin solves the timing problem for each integration individually, pushing a narrow set of GA4-aligned events with explicit handling for the async flows that break naive implementations.

For the full technical walkthrough of the AJAX timing problems and the integration-specific solutions, see the companion post: https://dvoorhees.com/2026/03/17/the-ajax-timing-problem-that-breaks-wordpress-gtm-tracking/

What it does

  • Injects a GTM container and initializes window.dataLayer before GTM loads, so no race condition exists between container initialization and event pushes.
  • Tracks Contact Form 7 submissions via the wpcf7mailsent DOM event, with a cookie-based fallback for non-AJAX submissions. The PHP wpcf7_mail_sent hook is retained only for that fallback path.
  • Tracks Gravity Forms submissions via gform_confirmation_loaded, firing after the confirmation markup is injected so the user has seen the success state before tracking fires. Entry metadata is injected into the AJAX confirmation response via the gform_confirmation filter.
  • Tracks WooCommerce ecommerce events aligned with the GA4 spec: view_item, view_item_list, add_to_cart, remove_from_cart, view_cart, begin_checkout, add_payment_info, add_shipping_info, purchase. Add-to-cart uses WooCommerce's added_to_cart jQuery event with a fetch observer fallback for themes that bypass jQuery.
  • Handles redirect boundaries (purchase, login, signup) by queuing event payloads server-side and injecting them into the destination page HTML before GTM processes the container.
  • Tracks authentication events (login, sign_up, logout) and internal search with term and result count.
  • Provides a settings page to paste a GTM container ID and toggle event groups on or off.
  • Exposes wp_clean_datalayer_event and wp_clean_datalayer_build_payload filters for modifying any event payload before it reaches the data layer.

The plugin requires no build step, no Composer, no npm, and no vendor directory. Copy the plugin folder into wp-content/plugins/ and activate.

Repository structure

wp-clean-datalayer/
├── wp-clean-datalayer.php     Bootstrap and plugin header
├── uninstall.php              Removes plugin options on delete
├── src/
│   ├── Integrations/          Auth, ContactForm7, GravityForms, Search, WooCommerce
│   ├── Support/               EventPayload value object
│   ├── DataLayer.php          Central push manager
│   ├── Plugin.php             Lifecycle and hook registration
│   └── Settings.php           Admin settings page
├── assets/js/
│   ├── datalayer-core.js      Data layer init and cookie fallback reader
│   ├── cf7-listener.js        Contact Form 7 DOM event listener
│   ├── gforms-listener.js     Gravity Forms AJAX confirmation listener
│   └── woocommerce-listener.js  Add-to-cart and cart fragment listeners
├── tests/                     PHPUnit tests for payload validation and data layer output
├── docs/
│   ├── event-reference.md     Full payload field documentation for every event
│   ├── extending-events.md    Filter usage examples
│   └── debugging-with-gtm-preview.md
├── readme.txt                 WordPress.org format readme
└── LICENSE                    GPL-2.0-or-later

Total source: roughly 1,400 lines of PHP and 260 lines of JavaScript across 14 source files.

Installation

  1. Clone or download this repository.
  2. Copy the wp-clean-datalayer folder to wp-content/plugins/.
  3. Activate the plugin in WordPress admin.
  4. Go to Settings, then Clean Data Layer.
  5. Paste your GTM container ID (format: GTM-XXXXXXX).
  6. Enable the event groups you need.
  7. In GTM, create triggers matching the event names in docs/event-reference.md.

Event reference

Event Trigger
form_submit CF7 AJAX success, Gravity Forms AJAX confirmation, WordPress comment posted
view_item Single product page load
view_item_list Shop, category, or tag archive
add_to_cart WooCommerce AJAX or standard add to cart
remove_from_cart Cart item removed via AJAX
view_cart Cart page load
begin_checkout Checkout page load
add_shipping_info Shipping method selected at checkout
add_payment_info Payment method selected at checkout
purchase Order thank-you page
login Successful login (next page load)
sign_up New user registration (next page load)
logout User logout
search Search results page

Full payload schemas with sample JSON for every event: docs/event-reference.md

Extending events

Every event passes through the wp_clean_datalayer_event filter before it reaches the browser. For lower-level control before the event key is set, use wp_clean_datalayer_build_payload. Examples and patterns: docs/extending-events.md

Debugging

Open GTM Preview mode, submit a form or complete a test purchase, and check the Data Layer tab in the Summary timeline. If an event is missing, check the browser console for wpCleanDataLayerPush and confirm the relevant event group is enabled under Settings. Full checklist: docs/debugging-with-gtm-preview.md

Tests

composer install
composer test

PHPUnit covers data layer output structure and EventPayload validation. Tests run against mock WordPress hooks and DOM state, not live plugin instances. This approach keeps the test suite fast and credential-free while verifying that payload schemas match the GA4 spec.

Design decisions and tradeoffs

One listener strategy per integration. CF7, Gravity Forms, and WooCommerce each have different async lifecycles. A generic "intercept all AJAX" approach misses the confirmation-state timing that matters for accurate conversion tracking. The cost is maintaining a separate listener file per integration.

Schema enforcement via EventPayload. Every push goes through a value object that validates required fields before anything reaches the browser. This catches misconfigured payloads at the PHP layer rather than silently pushing malformed events that GA4 accepts without error.

Cookie fallback for non-AJAX form submissions. CF7 can be configured to skip AJAX. When it does, the PHP hook stores the payload in a short-lived cookie that datalayer-core.js reads on the next page load. The tradeoff is a one-pageload delay in tracking for that configuration, but the alternative is losing the conversion entirely.

No build step. The JavaScript files are plain, unminified, and deploy as-is. This keeps the plugin installable by copying a folder, with no toolchain to maintain and no compiled output to debug through.

What this does not do

This plugin does not manage consent. It pushes events to the data layer unconditionally; consent gating belongs in the GTM container configuration using Consent Mode or equivalent. It does not handle server-side GTM tagging or Meta Conversions API, which operate at a different layer. It does not support WooCommerce Blocks checkout, which uses a different JavaScript architecture than the classic checkout flow.

Compatibility

Requirement Version
WordPress 6.0+
PHP 8.0+
WooCommerce (ecommerce events) 8.0+
Contact Form 7 (optional) Latest stable
Gravity Forms (optional) Latest stable

Tested with block and classic themes. WooCommerce AJAX cart listeners require jQuery when the theme uses WooCommerce's default cart fragments.

Why this exists

Built by Medium & Message as a reference implementation for GA4 and GTM instrumentation on WordPress. The patterns here come from solving the same timing problems across client implementations. The plugin is the generalized, public version of that work.

License

GPL-2.0-or-later. See LICENSE.

Read the full README on GitHub →