Install
Upload it under Plugins → Add New Plugin → Upload Plugin, then activate.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/yuri-parfentcov/zen-ad-clicks/releases/download/v1.0.1/zen-ad-clicks.zipSee which paid ads bring people to your WordPress site — recorded on your own server, in PHP, while the page is generated. No JavaScript, no cookies, no third-party calls.
The idea
Every ad platform appends a click id to its click-through URL:
| Platform | Parameters |
|---|---|
| Google Ads | gclid, wbraid, gbraid, dclid |
| Microsoft Ads | msclkid |
| Meta | fbclid |
| TikTok | ttclid |
li_fat_id |
|
| X (Twitter) | twclid |
rdt_cid |
|
| Snapchat | sccid |
epik |
That parameter is already in the URL when your server starts building the page. Zen Ad Clicks reads it there — in PHP, on template_redirect — instead of waiting for a browser script that an ad blocker may never run.
Each landing is stored with its platform, click id, campaign parameters (utm_*), landing path and referring host, and reported in WP admin with a chart, a campaign breakdown, a filterable table and a CSV export.
Why server-side
- Unblockable. No script to block, no request to intercept.
- Works without JavaScript, and on landings that 404.
- Sets no cookie, reads no device storage. Nothing for a consent banner to gate under ePrivacy Art. 5(3).
- No outbound calls. The data stays in your database.
What it deliberately does not do
-
It does not filter traffic. Any request carrying a recognised click parameter is recorded. No user-agent heuristics — the report is meant to reconcile against the ad platforms' own click counts, and silently dropping landings would break that. Exclude things explicitly with
zen_ad_clicks_should_recordif you need to. -
It is not a full analytics package: it records ad landings, not sessions or conversions.
-
It stores no visitor identifier, so a click cannot be tied to a later purchase.
-
It sends nothing back to the ad platforms.
Privacy
No IP address, no user agent, no visitor identifier is stored — a row describes the ad, not the person. Retention defaults to 365 days and is enforced by a daily cron. The Settings screen carries a ready-made privacy-policy sentence and an option to store no raw click id at all.
Caching
Recording needs PHP to run, so a byte-identical repeat of a landing URL served from a page cache is not recorded. Real clicks carry unique click ids, so they are always a cache miss. To capture repeats too, exclude click-parameter URLs from the cache — the readme has a Varnish snippet.
Filters
// Veto recording (e.g. gate on your own server-readable consent signal).
add_filter( 'zen_ad_clicks_should_record', function ( $record, $landing ) {
return my_marketing_consent_granted();
}, 10, 2 );
// Modify or redact a row before it is written.
add_filter( 'zen_ad_clicks_row', function ( $row, $landing ) {
$row['click_id'] = '';
return $row;
}, 10, 2 );
Architecture
template_redirect (prio 5)
└─ Zen_Ad_Clicks_Collector::maybe_record()
├─ empty($_GET)? → return (cost on a normal page view)
├─ Detector::from_request() → param => click id, allowlisted + validated
├─ request-shape guards → GET only; no feed/robots/favicon/trackback/embed/preview
├─ filter: zen_ad_clicks_should_record
├─ Attribution::from_request() → path, referrer host, utm_*
└─ Detector::by_platform() → Store::record_click() one row per platform
Zen_Ad_Clicks_Store is the only class that runs SQL. One table, {prefix}zen_ad_clicks_clicks.
Requirements
WordPress 6.2+, PHP 7.4+.
License
GPL v2 or later.
Releases
1 release. Each count is every asset in that release; expand a row for the breakdown.