=== All-in-one CAPI for Meta & Pinterest + GTM ===

Contributors: suhanduman
Tags: capi, conversion api, meta pixel, pinterest, GTM
Requires at least: 6.0
Tested up to: 6.9
Stable tag: 3.5.3
Requires PHP: 7.4
WC requires at least: 8.0
WC tested up to: 10.7
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Free server-side CAPI for Meta & Pinterest + GTM dataLayer. Cache-safe, theme-agnostic, no cloud server needed.

== Description ==

Server-side tracking for your WooCommerce store — without a GTM Server Container, without a premium plugin, without a monthly bill.

This plugin sends Meta (Facebook) and Pinterest Conversions API events from your WordPress server, and sets up a clean GTM dataLayer so your browser-side tags (Pixel, GA4, Pinterest Tag) work alongside it. Same event ID on both sides, so Meta merges them into one event instead of double-counting.

**What it does:**

* **Meta & Pinterest CAPI** — PageView, ViewContent, AddToCart, InitiateCheckout, Purchase, Search, AddToWishlist, Lead, and the full checkout funnel (AddShippingInfo, AddPaymentInfo). Sent server-side with hashed user data for high Event Match Quality.
* **GTM dataLayer** — Automatic script injection and a standards-compliant dataLayer for GA4, Meta Pixel, Pinterest Tag, or anything else you run through GTM.
* **Full WooCommerce coverage** — 14 event types, classic checkout and block checkout, HPOS compatible.
* **Cache-safe** — Works with LiteSpeed Cache, WP Rocket, Varnish, and Cloudflare full-page cache. Events carry a unique random ID per visit so Meta never deduplicates cached traffic into one event.
* **Batch delivery** — Events are queued and sent in bulk every 60 seconds. No synchronous HTTP on user requests, no latency added to page loads.
* **Built-in debug log** — See exactly which events were sent, to which platform, and whether the API accepted them. Filterable by event type, with success/failure stats.
* **REST + AJAX fallback** — Browser events go to a REST endpoint (no nonce, cache-proof) and fall back to admin-ajax.php if REST is blocked by a firewall.

**What you don't need:**

* No GTM Server Container (saves $30-150/month in cloud fees)
* No premium tier — every feature is in the free plugin
* No external tracking SaaS subscription

**Please check the installation page for the recommended GTM settings for deduplication.**


== Stop Losing 30% of Your Data to Cookie Banners ==

When a visitor clicks "Deny" on your cookie banner, most tracking plugins simply shut down. You lose the conversion data, your ads fly blind, and your ROAS artificially drops. Other "aggressive" plugins ignore the banner and send personal data (PII) to Meta anyway, risking massive GDPR fines.

**We do something smarter.**

When combined with Google Consent Mode v2, this plugin uses an advanced technique called **Server-Side Consent Gating with Event ID Deduplication**:

* **The Cookieless Ping** — When consent is denied, the browser sends a tiny, anonymous ping to Meta and Google (no cookies, no personal identity).
* **The Safe Server Event** — Our CAPI integration fires from your server, strictly stripping out all personal data (email, phone, name) to protect user privacy, but securely transmitting the essential order data (cart value, products, currency).
* **The Magic (Event ID)** — We attach the exact same `event_id` to both. Meta's machine learning connects the anonymous browser ping with your server's exact order value.

**The result.** Meta and Google's modeling algorithms can now accurately model your "lost" conversions. You recover up to 50% of your missing ROAS data, feed your ad optimization with real purchase values, and remain 100% GDPR compliant.

Most other free plugins fall into one of two camps: the **"timid" approach** (shuts CAPI off entirely when consent is denied — your ads go blind), or the **"reckless" approach** (sends hashed PII to Meta regardless of consent — direct GDPR violation). This plugin is the only free option we know of that does the harder, correct thing: keep the signal flowing without sending the data the visitor refused. Enable it from **Settings → Privacy & Consent (Server-side) → Strict server-side consent mode**.


== Our Philosophy ==

This plugin is free. Not "free with limits" — just free. Every feature works, no pro version behind a paywall.

I built it because setting up server-side tracking shouldn't require a cloud engineering degree or a monthly bill. If it helps your store, that's good enough.


== Frequently Asked Questions ==

= Does this plugin replace the Meta Pixel? =
No, it works alongside it. The plugin sends server-side (CAPI) events, while GTM handles the browser-side Pixel. Both use the same `event_id`, so Meta merges them automatically without counting anything twice.

= What is the difference between this and a GTM Server Container? =
A GTM Server Container runs on Google Cloud and costs money every month. This plugin does the same job directly from your WordPress server — no extra infrastructure, no extra bill.

= Does it work with page caching plugins (WP Rocket, LiteSpeed, etc.)? =
Yes. PageView and ViewCategory events fire from JavaScript, so they work even on fully cached pages. Cart, checkout, and purchase pages are not cached by default.

= What plugins are required? =
WooCommerce. That's it. If you use other GTM plugins (like Google Site Kit), disable their e-commerce features to avoid conflicts.

= Is there a pro version? =
No. Everything is included.

= My events aren't showing in Meta Events Manager. What's wrong? =
Check these in order:
1. Open your **Event Log** tab in the plugin settings. If events appear there with "Success (Meta)", the plugin is sending them. If Meta isn't receiving them, the problem is on Meta's end — usually Pixel ID or Access Token mismatch.
2. If the log is empty after visiting your store, you likely have a JS optimizer (LiteSpeed / WP Rocket / Autoptimize) deferring the plugin's tracking scripts. See next answer.
3. Check the admin notice on the plugin settings page — the plugin auto-detects your cache setup and shows exclude-list instructions.

= I use LiteSpeed Cache. How do I configure it? =
Go to **LiteSpeed Cache → Page Optimization → JS Settings → JS Defer Excludes** and add these script IDs (one per line):
`mcapi-pageview-init`
`mcapi-viewcontent-events`
`mcapi-viewcategory-events`
`mcapi-frontend-events`
Then purge all cache.

= I use WP Rocket. How do I configure it? =
Go to **WP Rocket → File Optimization → JavaScript → Excluded JavaScript Files** and add the same script IDs listed above for LiteSpeed. Then clear WP Rocket cache.

= I use Autoptimize. How do I configure it? =
Go to **Autoptimize → JS, CSS & HTML → Exclude scripts from Autoptimize** and add the same script IDs.

= What about Cloudflare Rocket Loader? =
The plugin adds `data-cfasync="false"` to its inline scripts, which Cloudflare Rocket Loader respects. No configuration needed.

= Does it work with a block-based theme (e.g. Twenty Twenty-Five)? =
Yes. The plugin's tracking works with the WooCommerce Products block used by FSE themes.

= GA4 / Meta browser tags fire successfully in GTM Preview but the plugin's Event Log is empty. What's wrong? =
GTM tags firing means your dataLayer pushes work — but your CMP's auto-blocker may be stopping the plugin's inline scripts from running, so no event ever reaches the server. Open browser DevTools → Elements, search for `mcapi-pageview-init`, and check the script tag's `type` attribute. If it reads `type="text/plain"` (or anything other than `text/javascript` / no type), your CMP has blocked it. v3.5.0+ adds CMP exemption attributes (`data-cookieconsent="ignore"`, `data-cookieyes="cookieyes-necessary"`, `data-cmplz-no-cookielaw="1"`) automatically, so updating to the latest version usually resolves this. For less common CMPs use the `mcapi_inline_script_attrs` filter — see the **CMP Auto-Blocking and the Plugin's Inline Scripts** section in the **Advanced Configuration** tab.

= GTM container template import fails with "Error deserializing enum type [EventType]. Unrecognized value [customEvent]". =
This was a schema-mismatch bug in v3.4.x and earlier — the trigger types were emitted in the Tag Manager API format (camelCase) instead of the container-import format (UPPER_SNAKE_CASE). Fixed in v3.5.0. Update the plugin, re-download the template from the settings page, and re-import. Existing manual GTM setups continue to work; only the JSON file import needed correction.

= I sell subscriptions and Meta is over-attributing revenue to old ad campaigns. =
WooCommerce Subscriptions auto-renewals are sent to Meta CAPI as fresh `Purchase` events by default, so Meta credits the original ad with the renewal value. v3.5.0+ adds a "WooCommerce Subscriptions Integration" section to the plugin settings with four behavior modes — most subscription stores pick "Skip" or "Subscribe / SubscriptionRenewal events" to keep their `Purchase` metric clean. See the **WooCommerce Subscriptions Integration** section in the **Advanced Configuration** tab for the full breakdown.

= I serve EU traffic — does the plugin respect cookie-banner consent for server-side CAPI? =
By default, no — the server-side CAPI calls fire from PHP and never see your CMP's browser-side `gtag('consent', ...)` signals. v3.5.0 adds an opt-in "Strict server-side consent mode" toggle in the plugin's **Privacy & Consent (Server-side)** settings section. When enabled, the plugin reads your CMP's cookie (Cookiebot, CookieYes, Complianz auto-detected; others via the `mcapi_marketing_consent_granted` filter) and, if the visitor explicitly denied marketing consent, strips the hashed PII (email, phone, name, billing address, fbp/fbc) from the CAPI payload. The event still ships with `event_id` and non-PII context so Meta's browser↔CAPI dedup and conversion modeling keep working — the data simply no longer carries personally identifying fields. Default OFF preserves backward compatibility; recommended ON for EU stores. See the **Strict server-side consent mode** section in the **Advanced Configuration** tab for the full mechanism.


== External Services ==

This plugin connects your website to external services to send event data.

* **Service Used:** Meta Conversion API
    * **Purpose:** To send user interaction and e-commerce event data from your server to Meta's servers for ad performance measurement, optimization, and audience building.
    * **Data Sent:** Event details (product ID, price) and user parameters (IP address, user agent, hashed email/name/phone, Facebook cookies) are sent when a user performs a key action.
* **Service Used:** Pinterest Conversions API
    * **Purpose:** Same as the Meta CAPI, providing reliable tracking for ad performance and audience building on Pinterest.
    * **Data Sent:** Event details and hashed user parameters are sent upon user action.
* **Service Used:** Google Tag Manager
    * **Purpose:** To load a JavaScript container from Google's servers that allows you to manage and deploy marketing and analytics tags.
    * **Data Sent:** The plugin provides your GTM Container ID to Google to fetch the correct script. GTM itself may collect data based on how you configure your tags.



== Installation ==

1.  Upload the plugin folder to the `/wp-content/plugins/` directory and activate it.
2.  Navigate to the **Meta CAPI & GTM** page from your main WordPress menu.
3.  Enter your **GTM Container ID** to inject the GTM script on your site.
4.  Enter your **Meta Pixel ID** and **API Access Token** for the server-side connection. Do the same for Pinterest if applicable.
5.  Go to the "Event Management" tab to select which CAPI events you want to track.
6.  Configure your GTM container using the instructions in the "Recommended GTM Setup" section below.

---

### Recommended GTM Setup

**Crucial First Steps:**

To prevent duplicate events and ensure data accuracy, you must configure two settings in your Meta account and GTM container.
1.  **Turn Off Meta's Automatic Event Tracking:**
    * In your Meta Business Suite, navigate to **Events Manager** and select your Pixel.
    * Go to the **Settings** tab.
    * Scroll to the **Event Setup** section and turn **Off** the toggle for **Track Events Automatically Without Code**. This plugin will handle all event sending.

2.  **Pause Automatically Created GTM Tags:**
    * In your GTM container, please pause or delete any automatically created tags that start with `FB_`. Since we will be creating our own tags manually, only they should be active.


---


### TEMPLATE SETUP

To save time and prevent errors, we have created a GTM container template. You can import this file to automatically create all the necessary variables, triggers, and tags.

**Step 1: Download the Template**

Go to the "Meta CAPI & GTM" settings page in your WordPress admin panel. In the Main Settings tab, you will see a highlighted box with a link to download the `gtm-template.json` template file. Download this file to your computer.


**Step 2: Import the Template into GTM**

1.  Go to your Google Tag Manager container.
2.  Navigate to the **Admin** section.
3.  Click on **Import Container**.
4.  Click **Choose container file** and select the `gtm-template.json` file you downloaded.
5.  Choose a **New** workspace and give it a descriptive name (e.g., "CAPI Import").
6.  **IMPORTANT:** Choose the **Merge** import option. Do NOT choose "Overwrite", as this could delete your existing tags.
7.  The preview screen will show you all the new tags, triggers, and variables that will be added. Click **Confirm**.

**Step 3: Configure Your IDs**

After the import is complete, update the two placeholder constants with your own tracking IDs. Both Meta and GA4 tags read from these variables, so you only edit them once.

1.  Go to the **Variables** section.
2.  Find and click on the **"CONST - Meta Pixel ID"** variable.
    * Replace `META_PIXEL_ID` with your actual Meta Pixel ID. Save. All seven Meta tags now use this value.
3.  Find and click on the **"CONST - GA4 Measurement ID"** variable.
    * Replace `GA4_MEASUREMENT_ID` with your actual GA4 Measurement ID (e.g., `G-XXXXXXXXXX`). Save. The Google Tag (Configuration) and all ten GA4 Event tags now use this value via `measurementIdOverride`.

> **Note on Pinterest:** This template ships with GA4 and Meta tags only. The Pinterest Tag is a Community Template (not a GTM built-in), and embedding it inside a container export can fail import on some GTM workspaces with permission or template-version errors. If you use Pinterest CAPI, follow the **MANUAL SETUP → Step 3: B) Pinterest Tags** section below to add the Pinterest tags yourself — it takes a couple of minutes and avoids the import edge cases.

**Step 4: Publish**

Once you have updated your IDs, click the **Submit** button in the top right corner, then **Publish** your container. Your GTM setup is now complete!

If you prefer to configure Google Tag Manager manually, or if you encounter any issues with the template import, the following guide provides step-by-step instructions to get everything configured.

For Consent Mode v2 setup, the strict server-side consent mode toggle, CMP auto-block compatibility, and the WooCommerce Subscriptions integration, see the **Advanced Configuration** tab.


### MANUAL SETUP


### Step 1: Create GTM Variables

Create the following **Data Layer Variables** (Variable Type: Data Layer Variable):
* **Variable Name:** `DLV - event_id`
    * **Data Layer Variable Name:** `event_id`
* **Variable Name:** `DLV - ecommerce`
    * **Data Layer Variable Name:** `ecommerce`
* **Variable Name:** `DLV - Hashed Email`
    * **Data Layer Variable Name:** `user_data.email`
* **Variable Name:** `DLV - ecommerce.currency`
    * **Data Layer Variable Name:** `ecommerce.currency`
* **Variable Name:** `DLV - ecommerce.value`
    * **Data Layer Variable Name:** `ecommerce.value`
* **Variable Name:** `DLV - ecommerce.items`
    * **Data Layer Variable Name:** `ecommerce.items`
* **Variable Name:** `DLV - ecommerce.transaction_id`
    * **Data Layer Variable Name:** `ecommerce.transaction_id`
* **Variable Name:** `DLV - ecommerce.item_list_name`
    * **Data Layer Variable Name:** `ecommerce.item_list_name`
* **Variable Name:** `DLV - ecommerce.shipping_method`
    * **Data Layer Variable Name:** `ecommerce.shipping_method`
* **Variable Name:** `DLV - ecommerce.payment_method`
    * **Data Layer Variable Name:** `ecommerce.payment_method`

Then create the following **Constant Variables** (Variable Type: Constant). One placeholder per platform — every Meta tag and every GA4 tag references these, so when you rotate IDs you only edit them once.

* **Variable Name:** `CONST - Meta Pixel ID`
    * **Value:** Your Meta Pixel ID (e.g. `1234567890123456`)
* **Variable Name:** `CONST - GA4 Measurement ID`
    * **Value:** Your GA4 Measurement ID (e.g. `G-XXXXXXXXXX`)

Finally, create one **Custom JavaScript Variable** for Pinterest CAPI (only needed if you actually use Pinterest — the new Meta Pixel template auto-converts the GA4 ecommerce schema for you, so a Meta-side CJS variable is no longer required).

* **Variable Name:** `CJS - Pinterest Contents`
    * **Custom JavaScript:**
    `function() {`
    `  var ecommerce = {{DLV - ecommerce}};`
    `  if (!ecommerce || !ecommerce.items) return undefined;`
    `  return ecommerce.items.map(function(item) {`
    `    return {`
    `      id: item.id || item.item_id,`
    `      quantity: item.quantity || 1,`
    `      item_price: item.price`
    `    };`
    `  });`
    `}`

### Step 2: Create GTM Triggers

Create the following triggers using the **Custom Event** type.
* **Trigger Name:** `CE - PageView Meta`
    * **Event name:** `page_view_meta`
* **Trigger Name:** `CE - View Item`
    * **Event name:** `view_item`
* **Trigger Name:** `CE - Add to Cart`
    * **Event name:** `add_to_cart`
* **Trigger Name:** `CE - Begin Checkout`
    * **Event name:** `begin_checkout`
* **Trigger Name:** `CE - Purchase`
    * **Event name:** `purchase`
* **Trigger Name:** `CE - View Item List`
    * **Event name:** `view_item_list`
* **Trigger Name:** `CE - View Cart`
    * **Event name:** `view_cart`
* **Trigger Name:** `CE - Select Item`
    * **Event name:** `select_item`
* **Trigger Name:** `CE - Add Shipping Info`
    * **Event name:** `add_shipping_info`
* **Trigger Name:** `CE - Add Payment Info`
    * **Event name:** `add_payment_info`

### Step 3: Create GTM Tags

#### A) Meta Tags (Meta Pixel)

**Prerequisite:** Install the **"Meta Pixel"** template by `facebook` from the GTM Community Template Gallery (the older "Facebook Pixel" template under `facebookarchive` is deprecated and will not import). Then create one tag per event below.

For every Meta tag, set these common fields the same way (using the variables from Step 1):

* **Pixel ID:** `{{CONST - Meta Pixel ID}}`
* **Event ID:** `{{DLV - event_id}}` — required for browser↔CAPI deduplication
* **Use GA4 Ecommerce data:** **enabled** (Meta's template auto-converts the plugin's `ecommerce.items[]` into Meta's `contents[]` shape — no Custom JavaScript variable needed any more)
* **Consent:** **enabled** — respects Consent Mode v2 categories the visitor approved
* **Send page view:** leave default (the template handles it via `eventName`)

Then per tag, only the **Event Name** and **Trigger** change:

* **Meta - PageView** → Event Name: standard `PageView`, Trigger: `CE - PageView Meta`
* **Meta - ViewContent** → Event Name: standard `ViewContent`, Trigger: `CE - View Item`
* **Meta - AddToCart** → Event Name: standard `AddToCart`, Trigger: `CE - Add to Cart`
* **Meta - InitiateCheckout** → Event Name: standard `InitiateCheckout`, Trigger: `CE - Begin Checkout`
* **Meta - Purchase** → Event Name: standard `Purchase`, Trigger: `CE - Purchase`
* **Meta - AddPaymentInfo** → Event Name: standard `AddPaymentInfo`, Trigger: `CE - Add Payment Info`
* **Meta - ViewCategory** → Event Name: **custom**, Custom Event Name: `ViewCategory`, Trigger: `CE - View Item List` (Meta does not have a standard ViewCategory event, so this fires as a custom event in Events Manager)


#### B) Pinterest Tags

**Prerequisite:** Install the **"Pinterest Tag"** template from the GTM Community Template Gallery. The Data Layer Variables and the `CJS - Pinterest Contents` Custom JavaScript Variable from Step 1 are reused here.

For each tag below, the standard Custom Parameter set is the same: `event_id` → `{{DLV - event_id}}`, `value` → `{{DLV - ecommerce.value}}`, `currency` → `{{DLV - ecommerce.currency}}`, `contents` → `{{CJS - Pinterest Contents}}`, `content_ids` → `{{CJS - Pinterest Contents}}` (Pinterest extracts the ids from the array).

Create the following tags:

* **Tag: Pinterest - PageView**
    * **Tag ID:** Your Pinterest Tag ID
    * **Hashed Email:** `{{DLV - Hashed Email}}`
    * **Event to Fire:** `page_visit`
    * **Custom Parameters:** Name `event_id`, Value `{{DLV - event_id}}`
    * **Trigger:** `CE - PageView Meta`

* **Tag: Pinterest - ViewContent**
    * **Tag ID:** Your Pinterest Tag ID
    * **Hashed Email:** `{{DLV - Hashed Email}}`
    * **Event to Fire:** `view_content`
    * **Custom Parameters:** Add `event_id`, `value`, `currency`, `contents`, and `content_ids` per the standard set above.
    * **Trigger:** `CE - View Item`

* **Tag: Pinterest - AddToCart**
    * **Tag ID:** Your Pinterest Tag ID
    * **Hashed Email:** `{{DLV - Hashed Email}}`
    * **Event to Fire:** `add_to_cart`
    * **Custom Parameters:** Add `event_id`, `value`, `currency`, `contents`, and `content_ids` per the standard set above.
    * **Trigger:** `CE - Add to Cart`

* **Tag: Pinterest - InitiateCheckout**
    * **Tag ID:** Your Pinterest Tag ID
    * **Hashed Email:** `{{DLV - Hashed Email}}`
    * **Event to Fire:** `initiate_checkout`
    * **Custom Parameters:** Add `event_id`, `value`, `currency`, `contents`, and `content_ids` per the standard set above.
    * **Trigger:** `CE - Begin Checkout`

* **Tag: Pinterest - Purchase**
    * **Tag ID:** Your Pinterest Tag ID
    * **Hashed Email:** `{{DLV - Hashed Email}}`
    * **Event to Fire:** `checkout`
    * **Custom Parameters:** Add `event_id`, `value`, `currency`, `contents`, and `content_ids` per the standard set above.
    * **Trigger:** `CE - Purchase`

* **Tag: Pinterest - ViewCategory**
    * **Tag ID:** Your Pinterest Tag ID
    * **Hashed Email:** `{{DLV - Hashed Email}}`
    * **Event to Fire:** `view_category`
    * **Custom Parameters:**
        * Name `event_id`, Value `{{DLV - event_id}}`
        * Name `content_name`, Value `{{DLV - ecommerce.item_list_name}}`
    * **Trigger:** `CE - View Item List`

#### C) Google Analytics 4 Tags

**Prerequisite:** Make sure you have your **Measurement ID** (starts with `G-`) from your Google Analytics 4 property. The Data Layer Variables required here were already created in Step 1.

First, create the main configuration tag that loads GA4 on all pages.
* **Tag: GA4 - Google Tag (Configuration)**
    * **Tag Type:** Google Analytics > Google Tag
    * **Tag ID:** Your GA4 Measurement ID (e.g., `G-XXXXXXXXXX`)
    * **Important:** Uncheck the "Send a page view event when this configuration loads" box. We will send it manually with the next tag.
    * **Trigger:** `All Pages`

Next, create the event tags that will send data from the `dataLayer` to Google Analytics.
* **Tag: GA4 - Event - PageView**
    * **Tag Type:** Google Analytics > GA4 Event
    * **Configuration Tag:** Select your `GA4 - Google Tag (Configuration)` tag.
    * **Event Name:** `page_view`
    * **Trigger:** `CE - PageView Meta`

* **Tag: GA4 - Event - ViewItem**
    * **Tag Type:** Google Analytics > GA4 Event
    * **Configuration Tag:** Select your `GA4 - Google Tag (Configuration)` tag.
    * **Event Name:** `view_item`
    * **Event Parameters:**
        * Parameter Name: `currency`, Value: `{{DLV - ecommerce.currency}}`
        * Parameter Name: `value`, Value: `{{DLV - ecommerce.value}}`
        * Parameter Name: `items`, Value: `{{DLV - ecommerce.items}}`
    * **Trigger:** `CE - View Item`

* **Tag: GA4 - Event - AddToCart**
    * **Tag Type:** Google Analytics > GA4 Event
    * **Configuration Tag:** Select your `GA4 - Google Tag (Configuration)` tag.
    * **Event Name:** `add_to_cart`
    * **Event Parameters:**
        * Parameter Name: `currency`, Value: `{{DLV - ecommerce.currency}}`
        * Parameter Name: `value`, Value: `{{DLV - ecommerce.value}}`
        * Parameter Name: `items`, Value: `{{DLV - ecommerce.items}}`
    * **Trigger:** `CE - Add to Cart`

* **Tag: GA4 - Event - BeginCheckout**
    * **Tag Type:** Google Analytics > GA4 Event
    * **Configuration Tag:** Select your `GA4 - Google Tag (Configuration)` tag.
    * **Event Name:** `begin_checkout`
    * **Event Parameters:**
        * Parameter Name: `currency`, Value: `{{DLV - ecommerce.currency}}`
        * Parameter Name: `value`, Value: `{{DLV - ecommerce.value}}`
        * Parameter Name: `items`, Value: `{{DLV - ecommerce.items}}`
    * **Trigger:** `CE - Begin Checkout`

* **Tag: GA4 - Event - Purchase**
    * **Tag Type:** Google Analytics > GA4 Event
    * **Configuration Tag:** Select your `GA4 - Google Tag (Configuration)` tag.
    * **Event Name:** `purchase`
    * **Event Parameters:**
        * Parameter Name: `transaction_id`, Value: `{{DLV - ecommerce.transaction_id}}`
        * Parameter Name: `currency`, Value: `{{DLV - ecommerce.currency}}`
        * Parameter Name: `value`, Value: `{{DLV - ecommerce.value}}`
        * Parameter Name: `items`, Value: `{{DLV - ecommerce.items}}`
    * **Trigger:** `CE - Purchase`

* **Tag: GA4 - Event - ViewItemList**
    * **Tag Type:** Google Analytics > GA4 Event
    * **Configuration Tag:** Select your `GA4 - Google Tag (Configuration)` tag.
    * **Event Name:** `view_item_list`
    * **Event Parameters:**
        * Parameter Name: `item_list_name`, Value: `{{DLV - ecommerce.item_list_name}}`
        * Parameter Name: `items`, Value: `{{DLV - ecommerce.items}}`
    * **Trigger:** `CE - View Item List`

* **Tag: GA4 - Event - ViewCart**
    * **Tag Type:** Google Analytics > GA4 Event
    * **Configuration Tag:** Select your `GA4 - Google Tag (Configuration)` tag.
    * **Event Name:** `view_cart`
    * **Event Parameters:**
        * Parameter Name: `currency`, Value: `{{DLV - ecommerce.currency}}`
        * Parameter Name: `value`, Value: `{{DLV - ecommerce.value}}`
        * Parameter Name: `items`, Value: `{{DLV - ecommerce.items}}`
    * **Trigger:** `CE - View Cart`

* **Tag: GA4 - Event - SelectItem**
    * **Tag Type:** Google Analytics > GA4 Event
    * **Configuration Tag:** Select your `GA4 - Google Tag (Configuration)` tag.
    * **Event Name:** `select_item`
    * **Event Parameters:**
        * Parameter Name: `items`, Value: `{{DLV - ecommerce.items}}`
    * **Trigger:** `CE - Select Item`

* **Tag: GA4 - Event - AddShippingInfo**
    * **Tag Type:** Google Analytics > GA4 Event
    * **Configuration Tag:** Select your `GA4 - Google Tag (Configuration)` tag.
    * **Event Name:** `add_shipping_info`
    * **Event Parameters:**
        * Parameter Name: `currency`, Value: `{{DLV - ecommerce.currency}}`
        * Parameter Name: `value`, Value: `{{DLV - ecommerce.value}}`
        * Parameter Name: `shipping_tier`, Value: `{{DLV - ecommerce.shipping_method}}`
        * Parameter Name: `items`, Value: `{{DLV - ecommerce.items}}`
    * **Trigger:** `CE - Add Shipping Info`

* **Tag: GA4 - Event - AddPaymentInfo**
    * **Tag Type:** Google Analytics > GA4 Event
    * **Configuration Tag:** Select your `GA4 - Google Tag (Configuration)` tag.
    * **Event Name:** `add_payment_info`
    * **Event Parameters:**
        * Parameter Name: `currency`, Value: `{{DLV - ecommerce.currency}}`
        * Parameter Name: `value`, Value: `{{DLV - ecommerce.value}}`
        * Parameter Name: `payment_type`, Value: `{{DLV - ecommerce.payment_method}}`
        * Parameter Name: `items`, Value: `{{DLV - ecommerce.items}}`
    * **Trigger:** `CE - Add Payment Info`

After creating all tags, submit and publish your GTM container.





== Advanced Configuration ==

Setup details for Consent Mode v2, the strict server-side consent mode (GDPR PII gating), CMP auto-block compatibility, and the WooCommerce Subscriptions integration. None of these are required for a basic CAPI setup — turn them on as your store needs them.


### Consent Mode v2 Setup (GDPR / EU Compliance)

If you serve EU visitors, GA4 and Meta browser tags will not fire when a visitor refuses or has not yet acted on the cookie banner. The GTM tags wait for a `gtag('consent', 'update', ...)` signal that grants the relevant categories. Without **Google Consent Mode v2** wired up, this typically costs a store **20–50% of its measured event volume** in GA4 and Meta Events Manager. The data does not become "untrackable" — Google models it back, but only if you tell GTM that consent management is in play.

**How Consent Mode v2 actually recovers the lost data**

When a visitor denies consent, GA4 and Google Ads tags do not stop firing — they switch to **cookieless pings**: small anonymous beacons that carry no client identifier (no `_ga`, no `_fbp`, no IP retention) but include enough conversion context (event name, value, currency, timestamp) for Google's machine learning to perform **conversion modeling**. Modeled conversions appear in your standard reports, mixed with directly-observed ones, with a "modeled" footnote. Google publicly reports that Consent Mode users recover, on average, 20–50% of the conversions they would otherwise have lost to consent denial. The Meta Pixel GTM template reads the same `ad_storage` / `ad_user_data` / `ad_personalization` signals that Consent Mode v2 sets — so a single CMP integration repairs both GA4 and Meta attribution at once. Server-side CAPI continues to operate independently.

**This plugin's job is to push events to the dataLayer regardless of consent state. The CMP's job is to tell GTM which categories the visitor allowed. GTM does the actual gating and signals back to the vendors.**

**Step 1: Enable Consent Mode v2 in your CMP**

Most popular CMP plugins have native Consent Mode v2 support. Find the toggle in your CMP plugin's settings:

* **Cookiebot** — Settings → "Enable Google Consent Mode" (v2 is default in current versions)
* **CookieYes** — Site Settings → "Google Consent Mode" → enable
* **Complianz** — Integrations → Google Consent Mode → enable
* **Iubenda** — Cookie Solution → Advanced settings → "Enable Consent Mode" → "v2 (advanced)"
* **Termly** — Settings → Google Consent Mode → enable
* **OneTrust** — Geolocation Rules → Consent Mode → enable + map categories

Once enabled, your CMP will automatically call `gtag('consent', 'default', {...denied})` before GTM loads, then `gtag('consent', 'update', {...granted})` after the visitor accepts.

**Step 2: (Optional) Enable the Consent Defaults tag in GTM**

If you imported the GTM container template, it includes a paused Custom HTML tag named **"Consent Defaults (Pre-CMP) — Disabled, see readme"**. Enable this tag **only** if your CMP does **not** call `gtag('consent', 'default', ...)` on its own (rare with modern CMPs):

1. In GTM, open the tag.
2. Click the pause icon to enable it.
3. Change its trigger from `CE - PageView Meta` to the built-in **"Consent Initialization - All Pages"** trigger (visible in the trigger picker dropdown — guarantees this fires before any other tag).
4. Submit and publish.

The tag sets all four consent types (`ad_storage`, `ad_user_data`, `ad_personalization`, `analytics_storage`) to `denied` with a 500 ms `wait_for_update` window. Your CMP's `gtag('consent', 'update', ...)` call then grants the categories the visitor approved.

**Step 3: Verify it is working**

* **GTM Preview Mode** — Open a page with the GTM Preview, click on `Consent Initialization` (top of timeline). Confirm `default` consent values appear for all four categories. After accepting in your CMP, click `Consent` events lower in the timeline; you should see `update` calls flipping the relevant categories to `granted`.
* **Browser DevTools** — Network tab, filter for `g/collect` (GA4) or `tr` (Meta Pixel). Each request should include a `gcs=` query parameter. `gcs=G100` means denied, `G111` means granted, `G101` means analytics-only. If you see `gcs=` you have Consent Mode active. If you do not, the CMP wiring is incomplete.
* **Tag Assistant** (tagassistant.google.com) — Connect to your site, fire any event, click on it, scroll to the "Consent" panel. Should show the current state and any updates within the session.


### Strict server-side consent mode (PII gating for CAPI)

Consent Mode v2 controls the **browser** tags. The plugin's **server-side** CAPI calls do not see `gtag('consent', ...)` signals — they fire from PHP, hash the visitor's email/phone/address, and POST directly to `graph.facebook.com`. By default this happens regardless of cookie-banner choice, which is fine for non-EU stores but a GDPR concern for European traffic.

The **Privacy & Consent (Server-side)** section in the plugin settings adds a "Strict server-side consent mode" checkbox (default OFF). When enabled, the plugin reads your CMP's cookie before queuing each event:

* **Cookiebot** — `CookieConsent` cookie, looks for `marketing:true|false`
* **CookieYes** — `cookieyes-consent` cookie, looks for `advertisement:yes|no`
* **Complianz** — `cmplz_marketing` cookie, value `allow` / `deny`
* **Other CMPs** — supply state via the `mcapi_marketing_consent_granted` filter (return `true` / `false` / `null`)

If marketing consent is **explicitly denied**, identifying PII fields (`em`, `ph`, `fn`, `ln`, `ct`, `st`, `zp`, `country`, `external_id`, `fbp`, `fbc`, Pinterest click ID) are stripped from the CAPI payload. The event still ships — IP, user-agent, `event_id`, `value`, `currency`, `content_ids`, `contents` are retained — so Meta still receives a deduplicated server signal it can fold into conversion modeling, but the data is no longer personally identifying. If the cookie state is unknown (no recognized CMP) or consent is granted, behavior is unchanged.

**How this complements Consent Mode v2.** When a visitor denies marketing consent, your browser-side GA4 / Meta Pixel switches to cookieless pings — small anonymous beacons that Google's ML uses to model the conversions you would have measured. That recovers some of the data, but it's modeled, not observed. With Strict server-side consent mode enabled, your **server-side CAPI continues to fire alongside the cookieless ping** — it ships the same `event_id` the browser ping carries, with `value` / `currency` / `contents` filled in from the order, just without the PII. Meta deduplicates the two events by `event_id` and now has a full server-side observed signal feeding the same conversion record the cookieless ping created. That is a cleaner input than what either browser-only or naïve "send everything" CAPI provides — and it is GDPR-defensible because no identifying user data is transmitted without consent.

The toggle is OFF by default so existing setups don't see a sudden drop in CAPI matching once they update; turn it on after you've configured Consent Mode v2 in your CMP.


### CMP Auto-Blocking and the Plugin's Inline Scripts

Some CMPs (especially CookieYes and Cookiebot when "auto-blocking" is enabled) scan every `<script>` tag on page load and convert any tag they suspect of tracking into `type="text/plain"` until consent is granted. The plugin's inline scripts (`mcapi-pageview-init`, `mcapi-viewcontent-events`, `mcapi-viewcategory-events`, `mcapi-inline-bootstrap`) only POST first-party events to your own `/wp-json/mcapi/v1/event` endpoint — they do not directly track the visitor — but a generic auto-blocker cannot tell the difference. If they get blocked, no events reach the queue, and the plugin's Event Log stays empty.

To prevent this, every plugin-rendered inline script carries CMP exemption attributes:

* `data-cookieconsent="ignore"` — Cookiebot's documented opt-out
* `data-cookieyes="cookieyes-necessary"` — categorizes the script as essential for CookieYes
* `data-cmplz-no-cookielaw="1"` — excludes from Complianz auto-blocking

These attributes are added automatically. If you use a CMP not listed above (OneTrust, Quantcast Choice, in-house CMPs), you can append more attributes via the `mcapi_inline_script_attrs` filter:

`add_filter( 'mcapi_inline_script_attrs', function( $attrs ) { return $attrs . ' data-your-cmp="ignore"'; } );`


### WooCommerce Subscriptions Integration

If your store sells **subscriptions** via WooCommerce Subscriptions, by default Meta CAPI receives a fresh `Purchase` event every time a subscription auto-renews. Meta then attributes the renewal revenue to the original ad campaign that brought the customer in, so your reported ROAS keeps climbing month after month from the same conversion. Most subscription advertisers want to keep their `Purchase` metric clean of recurring-revenue contamination so optimization signals stay honest.

The plugin auto-detects WooCommerce Subscriptions and adds a "WooCommerce Subscriptions Integration" section to the settings page with two controls:

**Subscription Renewal Behavior** (radio):

* **Default** — send renewals as regular `Purchase` events. No change in behavior; existing setups keep working.
* **Skip** — do not send renewals to Meta CAPI at all. Cleanest path for ROAS hygiene; you lose the LTV signal Meta could derive from renewals (most stores using value-based bidding don't rely on this anyway).
* **Tag** — still send a `Purchase`, but include `custom_data.customer_status = "subscription_renewal"` so you can filter renewals out in Events Manager / Custom Audiences / Custom Conversions.
* **Subscribe / SubscriptionRenewal events** — send Meta's standard `Subscribe` event for new sign-ups and a `SubscriptionRenewal` custom event for recurring orders. Renewals do not pollute the `Purchase` metric; advertisers who use Meta's LTV-bidding can opt into both events.

**Tag every Purchase with customer_status** (checkbox):

When enabled, every `Purchase` event (subscription or not) carries a `custom_data.customer_status` field with value `new_customer`, `returning_customer`, or `subscription_renewal`. Meta's Advantage+ Shopping Campaigns can use this signal to bid differently for new-customer acquisition vs. retention. The classification uses the customer's prior completed-or-processing order count; for guest checkouts it falls back to billing email lookup so a returning shopper without an account is still recognized.


== Upgrade Notice ==

= 3.5.3 =
Reliability fixes plus log enhancements. (1) Spurious AJAX `add_to_cart` events from WooCommerce sessionStorage fragment replay are eliminated — the dataLayer payload is now a self-clearing data attribute. (2) Per-platform queue retry: when one platform succeeds and the other fails transiently, only the failing side is retried on the next tick. (3) Event Log now captures the User Agent for each entry, supports date-range filtering, and the retention period (default 15 days) is configurable. No GTM template change; safe to update routinely.

= 3.5.2 =
**Critical: GTM template re-import required.** The bundled GTM container template has been fully migrated to modern GTM API schema. Older versions used legacy entity identifiers (`fbp` / `dlv` / `customEvent` / `googletag` / `eventParameters`) and pointed the Meta Pixel custom template at Facebook's archived `facebookarchive` GitHub org — current GTM no longer accepts any of these and rejects the import with "File format is invalid" or "Unknown entity type". After updating the plugin, re-download the template from the settings page and re-import it ("Merge" mode). The plugin shows a one-time admin notice with instructions. Server-side CAPI continues to work unchanged.

= 3.5.1 =
**Critical hotfix.** v3.5.0 introduced a CMP detection helper that called `class_exists()` without disabling autoload, which triggered third-party plugin autoloaders. The CookieYes / Cookie Law Info plugin's autoloader fatals on any non-namespaced argument, taking down the entire WP admin (white screen / "There has been a critical error" / autoloader stack trace). Update immediately if you have CookieYes or Cookie Law Info installed.

= 3.5.0 =
**GTM template re-import required.** This release fixes a schema bug that prevented the previous template from importing into Google Tag Manager (the "Unrecognized value [customEvent]" error). After updating, the plugin shows a one-time admin notice with re-import instructions. Adds Consent Mode v2 starter, CMP auto-block exemption attributes for CookieYes/Cookiebot/Complianz, and a WooCommerce Subscriptions integration that lets you skip renewals or send them as separate Subscribe / SubscriptionRenewal events to keep Purchase ROAS clean. Server-side CAPI continues to work unchanged.

= 3.4.2 =
**Action required for existing installs.** This release fixes a schema-conversion bug that caused Meta Events Manager to reject parameters from browser-side tags. Your existing GTM container needs to be updated to receive the fix. After updating, the plugin will show a one-time admin notice with two paths: (A) re-download the GTM template from the plugin settings page and re-import it with "Merge" mode, or (B) manually create the two new Custom JavaScript variables in GTM. Server-side CAPI was unaffected and continues to work as before.

== Changelog ==

= 3.5.3 =
* **Fix: spurious `add_to_cart` events from WooCommerce fragment cache.** The plugin previously returned an inline `<script>` inside the WC `add_to_cart` fragment to push the dataLayer event. WooCommerce caches rendered fragments in browser sessionStorage (`wc_fragments_*`) and re-injects them on every page load that renders the cart widget — re-executing the script and producing context-less AddToCart events on Home, Shop, Category pages. The fix moves the payload to a `data-mcapi-payload` attribute on a placeholder `<div>`; browser JS reads it once on WC's `added_to_cart` jQuery event then clears it, so subsequent fragment replays are silent. JSON encoded with `JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS` plus `esc_attr()` to keep the payload safe inside the attribute even under aggressive HTML optimizers.
* **Fix: per-platform retry tracking in the event queue.** When one platform returned 2xx but the other failed transiently (5xx / 429 / network timeout), the dispatcher kept the entire batch for retry and re-sent the succeeded side on the next tick. Both platforms dedupe by `event_id` so reporting was correct, but it wasted bandwidth and rate-limit budget. The queue table now carries a `sent_to` column (added via dbDelta — no manual migration). Per-row outcome: fully resolved rows (success or permanent 4xx) are deleted; partially resolved rows are flipped to `sent_to='meta'` or `sent_to='pinterest'` and retried only against the failing side; unresolved rows are left untouched. DELETE + bulk INSERT for the partial-success rows is wrapped in a MySQL transaction so a packet-size or deadlock failure rolls back instead of silently losing rows.
* **Optimization: dynamic queue batch size.** Backlog probed via `SELECT 1 ... LIMIT 501` (PRIMARY KEY scan, avoids the InnoDB `COUNT(*)` full-index-scan cost). Default batch stays at 100; on heavy backlog (>500 — e.g. WC Subscriptions mass renewal, traffic spike, post-outage drain) it jumps to 500. With batch >200 the dispatcher calls `set_time_limit(0)` so PHP's default `max_execution_time` doesn't truncate dispatch mid-batch (the 120s cron lock TTL still bounds runaway runs). The `mcapi_queue_batch_size` filter still wins for power users.
* **New: Event Log captures User Agent.** Every log entry now stores the originating browser/bot User Agent (new `user_agent` column on `wp_mcapi_logs`, varchar(500), added via dbDelta on update — no manual migration). The Logs admin table shows a truncated UA per row with the full string available on hover. Useful for diagnosing unexpected event volume — bot/scraper traffic now becomes identifiable in the log instead of guesswork.
* **New: date-range filter on the Event Log.** Two date inputs (From / To) added next to the existing event-name dropdown. Strict `YYYY-MM-DD` validation; values are converted from the site's local timezone to UTC before querying so the filter reflects what the merchant sees on screen, not the GMT-stored timestamps. Cache key includes the date range so different filter combinations don't collide.
* **New: configurable log retention period.** New "Event Log Settings" panel rendered inside the Event Log tab (same place as the log viewer, so admins don't have to context-switch). The retention period (default 15 days, range 1–90, with client-side clamp on out-of-range input) controls how long log rows are kept before the daily cleanup task removes them. The Event Log statistics table's third column ("Last 15 Days") and the cleanup-notice text under the log now reflect the configured value. Stored in its own option key so the standalone form's submit is isolated from the main settings sanitizer.
* **Hardening: remaining `class_exists()` calls now disable autoload.** v3.5.1 added `, false` as the second argument to detection helpers in `mcapi_detect_optimization_plugins()` and `mcapi_detect_cmp_plugins()` after the CookieYes autoloader fatal. Five other `class_exists()` calls in the plugin (HPOS compatibility check, WooCommerce gate, WC Subscriptions detection, YITH Wishlist hook, Contact Form 7 hook) were missed in that pass and could theoretically fatal in the same scenario if a buggy third-party autoloader is registered. All five now pass `, false` so detection observes loaded classes only — third-party loaders can't be triggered by our checks.
* Privacy & Consent settings description shortened — full background moved to the readme so the in-admin field copy stays scannable.
* No GTM template changes. No settings migration required for the bug fixes; the new retention setting initializes to 15 (preserving prior behavior) for stores that don't change it.

= 3.5.2 =
* **Critical fix: GTM container template migrated to modern GTM schema.** Real-world testing of the v3.5.x template against a fresh GTM workspace surfaced a cascade of schema-mismatch errors: GTM rejected the import outright with "File format is invalid", then "Unknown entity type (template public ID: googletag)", then "Unknown entity type (template public ID: dlv)", then "containerVersion.tag[1].vendorTemplate.parameter.measurementIdOverride: The value must not be empty", and finally complained that the customTemplate's `galleryReference` pointed at `facebookarchive` (Meta's archived GitHub org). v3.5.2 rebuilds the template against current GTM API:
  - Top-level `container` block (`usageContext: ["WEB"]`, full `features` map) and `builtInVariable` array added — modern GTM requires both even for placeholder containers.
  - Custom Template `galleryReference` repointed from `github.com/facebookarchive/...` to `github.com/facebook/...` with new commit hash and signature, matching the current "Meta Pixel by facebook" template.
  - Tag type `fbp` → `cvt_5RM3Q` (Gallery short-ID convention) for all 7 Meta Pixel tags. Parameter shape rebuilt: `pixelId`, `eventId`, `standardEventName` (or `customEventName` for ViewCategory), `useGA4Ecommerce: true` (Meta's template now auto-converts the GA4 ecommerce schema, replacing the hand-rolled CJS variable from 3.4.x), `consent: true` (Consent Mode v2 native), `disablePushState`, `enhancedEcommerce`, `dpoLDU`, `objectPropertiesFromVariable`, `advancedMatching`.
  - Tag type `googletag` → `googtag` for the GA4 configuration tag.
  - Variable type `dlv` → `v` for all 10 Data Layer Variables, with new required parameters `dataLayerVersion: 2` and `setDefaultValue: false`.
  - GA4 Event tag (`gaawe`) parameter shape: legacy `eventParameters` LIST/MAP (with `name`/`value` keys) replaced by modern `eventSettingsTable` LIST/MAP (with `parameter`/`parameterValue` keys). New `sendEcommerceData: true` flag added on the 9 ecommerce events. `measurementId` parameter renamed to `measurementIdOverride`.
  - New `CONST - GA4 Measurement ID` constant variable. The Google Tag and all 10 GA4 Event tags now reference it via `{{CONST - GA4 Measurement ID}}` instead of the previous tag-name-as-variable hack (which GTM cannot resolve).
  - Obsolete `CJS - Meta Object Properties` variable removed — the new Meta Pixel template's `useGA4Ecommerce` flag does the conversion natively.
* New: `Stop Losing 30% of Your Data to Cookie Banners` description now reflects the simpler architecture (no CJS conversion needed for Meta).
* New: `MANUAL SETUP` section in the readme rewritten for the new schema. Step 1 lists `CONST - Meta Pixel ID` + `CONST - GA4 Measurement ID` as the two constants merchants need to fill in. Step 3 (Meta) describes the new Meta Pixel template fields (Use GA4 Ecommerce data, Consent toggle, Event ID for deduplication).
* Note: this update applies only to the GTM template + readme. Plugin runtime (CAPI sender, queue, cron, settings, consent gating) is unchanged from 3.5.1.

= 3.5.1 =
* **Critical fix:** v3.5.0's CMP detection helper called `class_exists('CookieYes')` and similar without passing the second argument as `false`, so PHP triggered the target plugin's autoloader during detection. CookieYes / Cookie Law Info's autoloader (`class-autoloader.php`) builds a require path from the class name and fatals with `Failed opening required ''` when it receives a non-namespaced argument like `CookieYes`. The fatal fired from inside the `admin_notices` hook chain and broke every WP admin page on affected sites. All `class_exists()` calls in `mcapi_detect_cmp_plugins()` and `mcapi_detect_optimization_plugins()` now pass `false` to suppress autoload — detection helpers should observe loaded classes, never trigger third-party loading. Affects sites with CookieYes / Cookie Law Info / similar plugins that have a strict autoloader; unaffected sites can update routinely.

= 3.5.0 =
* Fix: GTM container template now imports cleanly. Previous versions emitted trigger types in the Tag Manager API format (`customEvent`, lowercase camelCase) which the container-import parser rejected with "Error deserializing enum type [EventType]. Unrecognized value [customEvent]". All ten triggers now use the container-import schema (`CUSTOM_EVENT`, UPPER_SNAKE_CASE). Existing manual GTM setups are unaffected.
* New: Consent Mode v2 support in the GTM template. A paused Custom HTML "Consent Defaults (Pre-CMP)" tag is included; merchants whose CMP does not call `gtag('consent', 'default', ...)` itself can enable it and route it to the built-in Consent Initialization trigger. Most modern CMPs (Cookiebot, CookieYes, Complianz, Iubenda, Termly, OneTrust) handle this automatically with a one-click toggle once their Consent Mode v2 integration is enabled. With Consent Mode v2 active, GA4 receives cookieless pings even when consent is denied — Google's machine learning models the conversions you would have measured (typical recovery: 20–50%). Meta Pixel reads the same `ad_user_data` / `ad_personalization` signals.
* New: CMP auto-block exemption attributes on every plugin-rendered inline script. CookieYes (`data-cookieyes="cookieyes-necessary"`), Cookiebot (`data-cookieconsent="ignore"`), and Complianz (`data-cmplz-no-cookielaw="1"`) auto-blockers were converting the plugin's first-party tracking-pipeline scripts to `type="text/plain"` and silently breaking the queue. The plugin's inline scripts only POST events to `/wp-json/mcapi/v1/event` on the same origin — they are not third-party trackers — so marking them as "necessary" is technically correct. New `mcapi_inline_script_attrs` filter lets users add attributes for less common CMPs (OneTrust, Quantcast).
* New: CMP detection admin notice. When CookieYes / Cookiebot / Complianz / Iubenda / Termly is detected, the plugin settings page shows a one-time, dismissable info notice pointing to the Consent Mode v2 setup section with per-CMP toggle locations.
* New: Optional **Strict server-side consent mode**. New "Privacy & Consent (Server-side)" settings section. When enabled, the plugin reads marketing-consent state from your CMP's cookie (Cookiebot, CookieYes, Complianz; others via the `mcapi_marketing_consent_granted` filter) and strips hashed PII (em / ph / fn / ln / ct / st / zp / country / external_id / fbp / fbc / Pinterest click ID) from CAPI payloads when consent is explicitly denied. The event still ships with `event_id` + non-PII context (IP, UA, value, currency, contents) so Meta's browser↔CAPI deduplication and conversion modeling continue to work — useful for EU stores that need GDPR-defensible CAPI behavior alongside Consent Mode v2's cookieless pings. Default OFF; backward compatible.
* Performance: Action Scheduler migration check no longer runs on every page load. Previous releases polled `as_has_scheduled_action()` (which hits the `actionscheduler_actions` table directly) on each `plugins_loaded` to migrate any leftover WP-Cron entries. v3.5.0 folds the migration into the version-bump block (priority 20, after WooCommerce loads AS), so it runs once per upgrade and never again on the same version. Eliminates two DB queries per page load on large catalogs.
* Fix: Server-side `_fbp` / `_fbc` cookie rewrite (Safari ITP bypass) now strips a leading `www.` from the cookie domain to match the Meta Pixel JS, which writes those cookies against the registrable domain (e.g. `example.com`, not `www.example.com`). Without this, sites served from `www.` produced two parallel `_fbp` values (one from the browser Pixel on apex, one from PHP on `www.`) and Meta's identity stitching split the visitor into two profiles, hurting EMQ.
* Fix: Uninstall now cleans up the per-user `mcapi_dismissed_cmp_notice` meta added by the v3.5.0 CMP detection notice. Previously this key would have been left as orphan metadata after plugin removal.
* New: WooCommerce Subscriptions integration. Auto-detected when WC Subscriptions is active. Adds two settings:
  - **Subscription Renewal Behavior** (radio): default / skip / tag / subscribe_event. "Skip" suppresses renewal Purchase events entirely (cleanest ROAS hygiene). "Tag" stamps `custom_data.customer_status="subscription_renewal"` so renewals can be filtered in Events Manager. "Subscribe / SubscriptionRenewal events" sends Meta's standard Subscribe event for new sign-ups and a SubscriptionRenewal custom event for renewals — keeps the Purchase metric uncontaminated by recurring revenue.
  - **Tag every Purchase with customer_status** (checkbox): adds `customer_status` (new_customer / returning_customer / subscription_renewal) to every Purchase event so Meta Advantage+ campaigns can optimize for new-customer acquisition.
* Fix: Pre-3.5.0 installs upgrading now see a single GTM update notice (the older 3.4.2-specific notice is suppressed when the broader 3.5.0 notice fires, avoiding double-banner). Fresh installs see no notice.
* Improvement: Two new FAQ entries: "tags fire in GTM Preview but Event Log empty" (CMP auto-block diagnostic) and "GTM template import fails with EventType enum error" (resolved by this release).

= 3.4.2 =
* Fix: GTM container template now ships with two Custom JavaScript Variables (`CJS - Meta Object Properties` and `CJS - Pinterest Contents`) that convert the plugin's GA4-schema dataLayer (`ecommerce.items[]`) into the `contents[]` shape Meta Pixel and Pinterest Tag expect (`id`, `quantity`, `item_price`). All Meta tag `objectProperties` now reference the new CJS variable instead of `{{DLV - ecommerce}}`. Without this conversion, Meta Events Manager would surface "missing parameter" warnings and Pinterest catalog matching would fail because the schemas do not align.
* Fix: Manual GTM setup instructions in the readme are updated to mirror the template change. Step 1 now lists every Data Layer Variable used across Meta, Pinterest, and GA4 sections (previously duplicated as section-specific prerequisites) plus the two new CJS variables. Step 3 (Meta) tags reference `{{CJS - Meta Object Properties}}` for Object Properties; Step 3 (Pinterest) tags reference `{{CJS - Pinterest Contents}}` for the `contents` and `content_ids` custom parameters.
* Fix: Pinterest event-name typos in the manual setup. `Pinterest - ViewContent` was instructed to fire `pagevisit` (causing it to dedupe with PageView server-side); now correctly `view_content`. `Pinterest - ViewCategory` was `viewcategory`; now `view_category`. Both match the plugin's CAPI event-name mapping, so server-side and browser-side events now share an `event_id` for the same Pinterest event name and can be properly deduped.
* Fix: Pinterest tag instructions were missing the `content_ids` custom parameter. Without it, Pinterest cannot match events to catalog products for retargeting and dynamic-product-ad attribution.

= 3.4.1 =
* Fix: dataLayer items now include `item_id` alongside `id`. GA4's Enhanced Ecommerce schema requires `item_id` — without it, the GA4 Items report showed "(not set)" for product breakdowns even though events were firing correctly. Meta CAPI continues to read `id` from `contents[]` as before, so server-side tracking is unchanged. Existing GTM tags configured to read `id` keep working; new tags can use the GA4-spec `item_id`.

= 3.4.0 =
* Fix: Event log timestamps were displayed up to a few hours ahead of real time on hosts where the PHP server timezone differs from the WordPress timezone (e.g. PHP set to UTC, WP set to Europe/Prague). Logs are now stored in UTC and converted to the WordPress timezone for display, matching the cleanup queries that already used UTC.
* Fix: GTM container template (`assets/gtm-template.json`) failed to import with "File format is invalid. Error deserializing enum type. Unrecognized value [EVENT]." All nine GA4 Event tag parameter blocks have been restructured from the invalid `EVENT`-typed stringified payload into the correct `LIST` of `MAP` entries with `name`/`value` keys, per the GTM container schema. The template now imports cleanly.
* Improvement: Bot and crawler traffic is now filtered before reaching the event queue. AhrefsBot, SemrushBot, Wordfence, Pingdom, GTmetrix, headless browsers (Puppeteer/Playwright), `curl`, `python-requests`, and similar self-identifying clients no longer generate phantom AddToCart, PageView, or other events. Purchase events are intentionally exempt to avoid losing real conversions to over-eager UA matching. Filterable via `mcapi_is_bot_request`.
* Improvement: Recurring tasks (event queue processor, daily log cleanup) now use Action Scheduler when available — the same library WooCommerce ships with. Action Scheduler runs reliably on low-traffic sites that rarely trigger WP-Cron, has built-in retry, and exposes a Tools → Scheduled Actions admin UI for debugging stuck jobs. Existing installs are migrated automatically on first load. WP-Cron is preserved as a fallback if Action Scheduler is unavailable.
* Improvement: SelectItem (item-list click) detection no longer depends on a hard-coded list of theme-specific wrapper classes. A new `mcapi-loop-item` class is injected onto every WooCommerce loop product via the `woocommerce_post_class` filter, providing a stable, theme-agnostic anchor for the click handler. The legacy class list is retained as a fallback for builders that bypass `post_class`.
* Improvement: Log entries written from the cron batch processor now record the original event time (when the visitor's action actually fired) rather than the cron processing time, eliminating the up-to-60-second drift in the event log timeline.
* Fix: Login event ID could collide on same-second logins (e.g. SSO double-callback). Now uses microsecond precision plus a six-character random suffix.

= 3.3.0 =
* New: REST API endpoint `/wp-json/mcapi/v1/event` for cache-safe browser-side tracking. No nonce required, so events continue to fire on pages served from a 7-day LiteSpeed/Varnish/Cloudflare cache. Secured by strict same-origin check, per-IP rate limit (50/min), global rate limit (1000/min), 16 KB body cap, and event-name whitelist.
* New: Inline bootstrap helper printed at top of `<head>` (priority 1). All three inline tracking scripts (PageView, ViewContent, ViewCategory) share one code path that tries REST first, falls back to admin-ajax.php with a hard-coded fallback nonce if REST is blocked by a WAF.
* Improvement: Reliable retries — transient API failures (HTTP 5xx, 429, network timeouts) no longer drop events from the queue. They stay queued and retry on the next cron tick. Permanent failures (4xx) still drop to avoid infinite retries. Queue retention extended to 24 hours to cover longer API outages.
* Improvement: ViewCart and ViewCategory events now respect the per-event enable checkboxes in settings. They were previously always-on.
* Improvement: `external_id` is now SHA-256 hashed when sent to Meta (was sent plain, which hurt Event Match Quality).
* Improvement: REST handler respects per-event enable checkboxes — prevents cached pages from sending events the merchant has turned off.
* Improvement: Description rewritten to highlight cache compatibility, REST/AJAX dual path, and retry semantics.
* Fix: Nested `<form>` in admin UI — "Refresh Log" button now actually submits (was silently dropped by browsers).
* Fix: `double-hash` on Pinterest external_id (since it's now hashed upstream) — passed through as-is.
* Fix: REST rate limiter now reads the real client IP via CF-Connecting-IP / X-Forwarded-For / X-Real-IP headers. Previously, sites behind Cloudflare or a load balancer saw every visitor as the proxy's IP and hit the per-IP 429 limit within seconds.
* Fix: Phone numbers now normalized toward E.164 using the billing country (leading "0" stripped, country dial code prepended). A Turkish shopper typing "0532..." now correctly matches against Meta's hashed phone index instead of being sent as an unmatched local-format number.
* Improvement: Cron lock on the queue processor — prevents two overlapping cron ticks from re-processing the same batch (which would have caused duplicate events on slow API responses).
* Improvement: Batch size is now filterable via `mcapi_queue_batch_size` — high-traffic stores can raise the 100-per-tick ceiling.
* Improvement: AJAX handler emits `nocache_headers()` so Cloudflare and other upstream caches do not cache the POST response.
* Improvement: Uninstall cleanup now removes all plugin transients (stats, log caches, rate-limit counters) and the optimizer-notice dismissal user meta.
* Improvement: Safari ITP bypass — `_fbp` and `_fbc` cookies are re-written server-side on every request with a 90-day TTL, which iOS/macOS Safari does NOT cap to 7 days (only JS-written cookies are capped). Long-window attribution for iOS shoppers is restored; often the single biggest EMQ lift for stores with heavy iOS traffic.
* Improvement: Guest `external_id` is now the cookie-backed UUID (not billing email) when an order is processed. Keeps the Meta user journey consistent across PageView → AddToCart → Purchase. Email is still sent separately in the `em` field, so Meta's matching is unchanged.
* Improvement: `mcapi_guest_external_id` cookie now HTTP-only, Secure (on HTTPS), SameSite=Lax, 1-year TTL.

= 3.2.6 =
* Improvement: Added `data-no-defer="1"` and `data-no-minify="1"` attributes to inline scripts so LiteSpeed Cache and other aggressive optimizers don't wrap them in `type="litespeed/javascript"` (which prevented the scripts from running).
* Improvement: Plugin now detects LiteSpeed Cache, WP Rocket, Autoptimize, WP Fastest Cache, and W3 Total Cache, and shows a one-time admin notice on the settings page with exact exclude-list instructions.
* Improvement: Full compatibility guide in the FAQ for cache/optimizer plugins.

= 3.2.5 =
* Fix: PageView event_id now includes browser-generated random component + timestamp. Previously the event_id was a path hash only, so every visitor to a full-page-cached URL sent the same event_id to Meta — which deduplicated them into a single event and dropped 95%+ of PageViews on LiteSpeed/WP Rocket/Varnish/Cloudflare sites.
* Fix: All JS-driven CAPI events (PageView, ViewContent, ViewCategory, SelectItem, AddShippingInfo, AddPaymentInfo) now send the real page URL as event_source_url instead of `/wp-admin/admin-ajax.php`. This was lowering Meta Event Match Quality (EMQ) scores and breaking attribution.
* Fix: ViewContent now captures product data on `woocommerce_before_single_product` (before related-products/upsells render) instead of reading `global $product` in wp_footer (where it was often overwritten by the last related product in the loop). Meta was receiving wrong product IDs on product pages with related/upsell sections.
* Fix: SelectItem click tracker no longer fires on unrelated clicks (menu, logo, footer, cart drawer). The DOM walk is now constrained to the clicked link's product container, using theme-agnostic selectors that cover Astra, Flatsome, Divi, Elementor, Bricks, and the block theme Products block.
* Fix: Removed the duplicate product-marker hook registration that injected 2 hidden spans per product in every shop loop. The SelectItem tracker now reuses the `data-mcapi-product-data` attribute already present on the add-to-cart button (no extra HTML output).
* Fix: Rejected attempts to spoof event_source_url from cross-origin values — server now validates that the POST'ed URL matches the site's host.
* Improvement: Added `data-cfasync="false"` and `data-no-optimize="1"` attributes to all plugin inline scripts. These signal Cloudflare Rocket Loader, LiteSpeed Cache, WP Rocket, and Autoptimize not to defer or combine the scripts — helps events fire on sites with aggressive JS optimizers.
* Improvement: Better block theme compatibility — events are tracked correctly on Twenty Twenty-Five and other FSE (block-based) themes that use the WooCommerce Products block.

= 3.2.4 =
* Fix: ViewContent event moved to JavaScript/AJAX to survive full-page cache (Varnish, WP Rocket, LiteSpeed, Cloudflare). Previously, cached product pages would fire the event only once when the cache was generated — every subsequent visitor produced zero CAPI calls and duplicate event_ids.
* Fix: AddToCart event_id now includes `uniqid()` alongside cart_item_key. Adding the same product twice no longer gets silently deduplicated by Meta (WooCommerce reuses cart_item_key when incrementing quantity on an existing cart item).
* Improvement: SelectItem click detection is now theme-agnostic. Works with Flatsome, Divi, Elementor, Bricks, Astra, Oxygen, and any custom theme/page builder. The plugin injects a hidden product marker via `woocommerce_before_shop_loop_item_title`, and the JS walks up the DOM from the clicked link to find it — no hard-coded CSS class dependencies.

= 3.2.3 =
* Architecture: AddToCart CAPI is now fired server-side from the `woocommerce_add_to_cart` hook for **all** flows (classic page-reload AND AJAX cart), instead of relying on frontend JS. This is what CAPI was designed for — reliable tracking immune to adblockers, tracker blockers, and theme incompatibilities. Maximum theme coverage.
* Improvement: Browser-side dataLayer push for AJAX add-to-cart is now delivered via `woocommerce_add_to_cart_fragments` filter, which injects the push script into the AJAX response. Works on any theme that uses WooCommerce's standard AJAX cart.
* Improvement: Shared `event_id` between server-side CAPI and browser-side dataLayer push (based on `cart_item_key`), guaranteeing Meta deduplication.
* Performance: All AJAX event handler calls now go through the batch queue instead of direct synchronous HTTP calls. Admin-ajax.php response times are no longer tied to Meta/Pinterest API latency.
* Cleanup: Removed the legacy `mcapi_ajax_fired_*` session flag and `DOING_AJAX` skip logic that was causing events to be lost when JS AJAX was blocked.
* Event Match Quality: Every `woocommerce_add_to_cart` trigger now reaches Meta/Pinterest, regardless of browser-side interference.

= 3.2.2 =
* Fix: GTM template - Meta tags now correctly use `eventName: "standard"` + `standardEventName` parameters. Previously all browser-side events were sent as PageView because the Facebook Pixel community template requires a radio button selector, not a direct event name.
* Fix: GTM template - Pixel ID now uses a shared constant variable (`CONST - Meta Pixel ID`) instead of referencing a tag name, which GTM cannot resolve.
* Improvement: GTM template setup simplified — users only need to update one variable for all Meta tags.

= 3.2.1 =
* Fix: PageView event now fires correctly on first page load — dataLayer push no longer blocked by deferred script loaders (WP Rocket, LiteSpeed, etc.). Meta Pixel Helper will detect the pixel immediately.
* Fix: GA4 product price field renamed from `item_price` to `price` to match GA4 standard. Fixes zero revenue in GA4 Item Revenue reports.
* Fix: Real client IP detection for sites behind Cloudflare, AWS CloudFront, or Nginx reverse proxy. Improves Meta Event Match Quality (EMQ) scores.
* Fix: Resolved plugin check warnings for unescaped DB parameters and missing translators comments.

= 3.2.0 =
* Improvement: Updated Meta Graph API from v21.0 to v25.0 for continued compatibility.
* Improvement: Meta API access token now sent via Authorization header instead of URL parameter (security best practice).
* Fix: Fixed Pinterest CAPI event mapping - ViewContent now correctly maps to `view_content` instead of `page_visit`.
* Fix: Fixed Pinterest CAPI ViewCategory event - now uses native `view_category` event instead of `search`.
* Fix: Fixed Pinterest CAPI custom_data field names to match official API spec (`contents` instead of `line_items`, correct sub-field names).
* Fix: Added Pinterest CAPI mappings for `add_payment_info` and `view_category` events.
* Fix: Fixed GTM container template (broken JSON due to trailing comma and 10 malformed GA4 variable references). Template import now works correctly.
* Security: Fixed all output escaping issues flagged by WordPress Plugin Check.
* Security: Improved input sanitization and wp_unslash handling for cookies and POST data.
* Security: Restructured admin request handlers to verify nonces before accessing POST data.
* Standards: Renamed global functions to use the `mcapi_` prefix per WordPress naming conventions.
* Standards: Added translators comments for all translatable strings with placeholders.
* Standards: Added proper phpcs:ignore annotations for legitimate direct database operations.
* Standards: Added version parameter to enqueued scripts.
* Standards: Fixed uninstall.php with ABSPATH check and prefixed global variables.
* Performance: CAPI events are now queued in a database table and sent in batches every 60 seconds. A single HTTP request can carry up to 100 events, reducing server load by ~98% on high-traffic sites. Falls back to synchronous if the queue table is unavailable.
* Improvement: Added block checkout compatibility for AddShippingInfo and AddPaymentInfo events. These now fire correctly on both classic and block-based WooCommerce checkout.
* Improvement: Block checkout user data capture (billing info) now works for improved Event Match Quality.
* Improvement: Added WooCommerce HPOS (High-Performance Order Storage) compatibility declaration.
* Improvement: Enhanced event log - failures now show platform name (Meta/Pinterest) and all event types are filterable.
* Improvement: Event statistics table now includes PageView and Search metrics.
* Improvement: Added jQuery as explicit script dependency for frontend events.
* Improvement: Removed review and donation prompts from the admin panel for a cleaner UI.
* Improvement: Added WooCommerce version headers to readme.txt.
* Improvement: Dataset quality report now dynamically uses the current plugin version.
* Fix: PageView and ViewCategory CAPI events now fire via JavaScript AJAX, making them compatible with full-page cache (Varnish, Redis, Cloudflare).
* Fix: Sanitization function now preserves numeric data types (float/int) instead of converting to strings, preventing API event rejections.
* Fix: Added WooCommerce dependency check - shows admin notice instead of fatal error if WooCommerce is deactivated.
* Fix: Log clearing now uses DELETE instead of TRUNCATE for compatibility with restricted database permissions on shared hosts.
* Fix: Added composite database index (event_name, event_time) for faster log queries on high-traffic sites.

= 3.1.1 =
* Fix: Minor bug fixes and stability improvements.

= 3.1.0 =
* Major Feature: Added detailed tracking for checkout funnel steps: 'Add Shipping Info' and 'Add Payment Info' events for deeper analysis of cart abandonment.
* Major Feature: Added 'Select Item' event tracking for analyzing product clicks from category and shop pages.
* Major Feature: Added a new admin setting to choose the Product Identifier for events (SKU with fallback to ID, or ID Only), making the plugin flexible for stores without SKUs.
* Improvement: Significantly enriched Pinterest CAPI user data to improve match rates, mirroring the data sent to Meta.
* Improvement: Corrected Pinterest CAPI event mapping for 'InitiateCheckout' to improve funnel accuracy.

= 3.0.0 =
* Major Improvement: Enriched the 'view_item_list' (Category View) event for GA4, now including the full list of products displayed on the page for advanced analytics.
* New Feature: Added comprehensive tracking for the 'remove_from_cart' event, providing deeper insights into cart abandonment.
* New Feature: Added tracking for the 'login' event to the dataLayer for GA4 and CAPI.
* Improvement: Enriched the 'Purchase' event dataLayer with coupon codes, payment method, and shipping method details.
* Improvement: Enhanced the Pinterest CAPI 'add_to_cart' event to include detailed product line items, improving data quality.
* Security: Hardened the AJAX event handler with recursive sanitization for all incoming data, improving overall security.

= 2.9.0 =
* Improvement: View Category and GA4 settings

= 2.8.0 =
* Fix: Pinterest external_id and click_id.

= 2.7.0 =
* Fix: Duplicate pageview fix.

= 2.6.0 =
* Fix: Pinterest line_items fix.

= 2.5.0 =
* Fix: AddToCart fix.

= 2.4.0 =
* Fix: Pinterest data format fix.

= 2.3.0 =
* Major Feature: Full Pinterest Conversions API (CAPI) and Pixel integration.
* Improvement: Added a robust AJAX-based tracking method for the 'AddToCart' event to ensure it fires correctly with all themes and caching systems.
* Improvement: Enhanced 'PageView' event tracking with a dedicated dataLayer push to ensure perfect deduplication between browser and server events.
* Improvement: Redesigned the admin panel by merging Meta and Pinterest settings into a unified tab.
* Improvement: Enhanced the event log to display the platform (Meta/Pinterest) for each successful event.
* Improvement: Added a manual "Refresh Log" button to the admin panel to bypass caching and view real-time event data.
* Housekeeping: General code refinements and WordPress standards alignment for repository submission.

= 2.2.0 =
* Improvement: Developments for improved Event Match Quality.

= 2.1.0 =
* Security: Added nonce checks for admin forms to prevent CSRF vulnerabilities.
* Security: Implemented late escaping for all echoed variables in the admin panel to prevent XSS.
* Security: Hardened database queries by ensuring all parts of the query are properly prepared.
* Security: Sanitized all `$_SERVER` superglobal inputs to prevent potential injection issues.
* Documentation: Added a required "External Services" section to the readme.txt to disclose the use of the Meta Conversion API.
* Housekeeping: Bumped version number.

= 2.0.0 =
* Major Feature: Added Google Tag Manager (GTM) container script injection. The plugin is now an all-in-one solution.
* Major Feature: Unified Data Layer for both CAPI and GTM to enable flawless event deduplication.
* Improvement: Added a GTM Container ID field to the admin settings.
* Housekeeping: Updated plugin name and description to reflect new GTM capabilities.

= 1.8.0 =
* Feature: Added first name, last name, and phone hashing (fn, ln, ph) for improved Event Match Quality.

= 1.7.0 =
* Improvement: Enhanced overall security.

= 1.6.0 =
* Improvement: Updated admin panel UI/UX for better usability.

= 1.5.0 =
* Feature: Added Dataset Quality API report submission on Pixel ID change.

= 1.4.0 =
* Feature: Added server-side PageView event tracking.

= 1.3.0 =
* Improvement: Optimized performance of event tracking hooks.

= 1.2.0 =
* Security: Hardened database queries and improved input sanitization.
* Standardization: Added full translation support and aligned code with WordPress.org best practices.

= 1.1.0 =
* Feature: Added dashboard with event statistics.

= 1.0.0 =
* Initial public release.