<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Hamdi Bk</title>
    <description>The latest articles on DEV Community by Hamdi Bk (@opti-behavior).</description>
    <link>https://dev.to/opti-behavior</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4058277%2Fec0380d9-d438-4b8c-9ec4-de36c714d1a5.jpg</url>
      <title>DEV Community: Hamdi Bk</title>
      <link>https://dev.to/opti-behavior</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/opti-behavior"/>
    <language>en</language>
    <item>
      <title>WooCommerce Checkout Not Working? 9 Checks to Find the Cause</title>
      <dc:creator>Hamdi Bk</dc:creator>
      <pubDate>Sun, 01 Nov 2026 00:38:57 +0000</pubDate>
      <link>https://dev.to/opti-behavior/woocommerce-checkout-not-working-9-checks-to-find-the-cause-2hff</link>
      <guid>https://dev.to/opti-behavior/woocommerce-checkout-not-working-9-checks-to-find-the-cause-2hff</guid>
      <description>&lt;p&gt;A broken WooCommerce checkout rarely shows an error to the store owner: orders just stop. This is the checklist we published on optiuser.com, in the order we run it. Disclosure: we build Opti-Behavior, the plugin mentioned in some sections.&lt;/p&gt;

&lt;p&gt;If your WooCommerce checkout is not working, the cause is almost always one of five things: a JavaScript error on the checkout page, a plugin or theme conflict, a cached checkout page, a payment gateway problem, or a broken HTTPS or session setup. This guide shows the signs, then 9 checks to run in order, most of them without any extra plugin, and how to confirm the fix on your live store.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signs your WooCommerce checkout is not working
&lt;/h2&gt;

&lt;p&gt;A broken checkout rarely shows a clear error. Look for these signs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Place order button does nothing.&lt;/strong&gt; The shopper clicks, nothing moves, no message appears.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The spinner never stops.&lt;/strong&gt; The checkout form greys out after the click and stays that way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An error appears after the click&lt;/strong&gt;, often a vague one such as “There was an error processing your order” or a session expired message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orders pile up in Pending payment or Failed&lt;/strong&gt; in WooCommerce &amp;gt; Orders, while paid orders drop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cart empties&lt;/strong&gt; after a product is added, or the checkout page shows “Your cart is currently empty”.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The checkout page is blank&lt;/strong&gt; or shows only part of the form.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note when it started: after a plugin, theme or WooCommerce update, after a new cache or CDN rule, or after a change of payment gateway. That date narrows the search more than anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the WooCommerce checkout page is not working: 9 checks in order
&lt;/h2&gt;

&lt;p&gt;Run these checks in this order. The first ones take two minutes and catch most cases.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Place an order yourself, on desktop and on a phone
&lt;/h3&gt;

&lt;p&gt;Use a private browser window, add a product, and go to the end of the checkout with each payment method you offer. Then do it again on a phone. A failure that only happens on mobile, or only with one payment method, already tells you where to look.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Place order button not working? Open the browser console
&lt;/h3&gt;

&lt;p&gt;On the checkout page, open the developer tools (F12, or right-click &amp;gt; Inspect), then the &lt;strong&gt;Console&lt;/strong&gt; tab, and click Place order. A red error that mentions a plugin or theme file is the most common reason the button does nothing: one broken script can stop the checkout script that sends the order.&lt;/p&gt;

&lt;p&gt;Then open the &lt;strong&gt;Network&lt;/strong&gt; tab and click again. With the classic checkout, look for the request that contains &lt;code&gt;wc-ajax=checkout&lt;/code&gt;; with the Checkout block, look for the request to &lt;code&gt;/wp-json/wc/store/&lt;/code&gt;. A 500 status points to a PHP error on the server, a 403 often to a security plugin or firewall rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Exclude cart, checkout and my-account from the cache
&lt;/h3&gt;

&lt;p&gt;A cached checkout page can serve an expired security token or someone else’s empty cart, and the order then fails or the cart looks empty. Most cache plugins exclude these pages by default; check again if you added a CDN, a server cache or custom rules. If you also use a JavaScript optimisation feature (combine, defer or delay JavaScript), exclude the checkout scripts from it or turn it off for the checkout page, then test again. We wrote more about cache side effects in &lt;a href="https://optiuser.com/blog/heatmap-not-working-cache-plugin/" rel="noopener noreferrer"&gt;our guide to cache plugins and tracking&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Checkout page not working after an update: test for a conflict
&lt;/h3&gt;

&lt;p&gt;Install the free Health Check &amp;amp; Troubleshooting plugin and turn on its troubleshooting mode: it disables plugins and switches the theme for your own session only, so shoppers are not affected. Keep only WooCommerce and your payment gateway, switch to the Storefront theme, and place a test order. If it works, turn the other plugins back on one by one until the checkout breaks again. Checkout field editors, cache and optimisation plugins, security plugins and custom theme templates are the usual suspects. If you prefer, do the same test on a staging copy of the site.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Read the WooCommerce logs
&lt;/h3&gt;

&lt;p&gt;Go to WooCommerce &amp;gt; Status &amp;gt; Logs. Open the log of your payment gateway and the fatal-errors log for the day of the problem. A PHP fatal error during checkout, a declined API call or an invalid API key shows up here even when the shopper only sees a spinner. For more detail, turn on &lt;code&gt;WP_DEBUG_LOG&lt;/code&gt; on a staging site and repeat the order.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Check the payment gateway settings
&lt;/h3&gt;

&lt;p&gt;Many “checkout not working” cases are a gateway still in test or sandbox mode, live keys pasted in the test fields, an expired API key, or a webhook that no longer reaches your site. Open the gateway settings, check which mode is active, and use the gateway’s own dashboard to see whether the payment attempts reached it at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Read your Pending payment and Failed orders
&lt;/h3&gt;

&lt;p&gt;In WooCommerce &amp;gt; Orders, filter on Pending payment and on Failed. Pending payment means the order was created but the payment was never confirmed; Failed means the payment was refused or returned an error. A sudden rise after an update points to a technical problem, not to shoppers changing their mind. Open a few of these orders and read the order notes: the gateway often writes the reason there.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Check HTTPS, the site address and sessions
&lt;/h3&gt;

&lt;p&gt;In Settings &amp;gt; General, the WordPress Address and Site Address must both use https and the same domain (with or without www). A mix of http and https, or of two domains, can lose the WooCommerce session between the cart and the checkout, which empties the cart. A browser warning about mixed content on the checkout page is a sign of the same problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. Check the checkout page itself
&lt;/h3&gt;

&lt;p&gt;Go to WooCommerce &amp;gt; Settings &amp;gt; Advanced and check that the Checkout page setting points to the right page. Open that page in the editor: it must contain the Checkout block or the `` shortcode, and only one of them. A page builder template that replaced the checkout content is a frequent cause of a blank or partial checkout.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to see where the checkout breaks on your live store
&lt;/h2&gt;

&lt;p&gt;Here is what this looks like on real data. On our WooCommerce test store, the suggested Purchase Funnel recorded, from 28 August to 25 September 2026, 126 visitors entering at the shop, 75 reaching the cart, 69 reaching the checkout and 27 reaching Order received. The biggest loss is the last step: 42 of the 69 visitors who reached the checkout never saw the confirmation page. That Checkout to Order received step is the one to read first. On a store where the checkout works, a gap there means shoppers stopping at the form; if it suddenly widens after an update, test the checkout before anything else.&lt;/p&gt;

&lt;p&gt;The checks above find the cause when you can reproduce the problem. The hard cases are the ones you cannot reproduce: the checkout fails only for some shoppers, on some devices, some days. For those, you need numbers from real visits.&lt;/p&gt;

&lt;p&gt;The simplest measure works in any analytics tool: over the same days, compare visits to the checkout page with visits to the order-received page. If checkout visits stay normal and confirmations fall close to zero, the checkout is broken, not your shoppers. Our &lt;a href="https://optiuser.com/blog/woocommerce-conversion-tracking/" rel="noopener noreferrer"&gt;guide to WooCommerce conversion tracking&lt;/a&gt; shows how to measure every step from product page to paid order.&lt;/p&gt;

&lt;p&gt;Opti-Behavior is our WordPress plugin, so we are not neutral here. This is how we use it for this problem:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Suggested WooCommerce funnels (Free).&lt;/strong&gt; On the &lt;a href="https://optiuser.com/opti-behavior/ob-feature-funnels/" rel="noopener noreferrer"&gt;Funnels page&lt;/a&gt;, Opti-Behavior detects WooCommerce and suggests a Purchase Funnel (Shop, Product page, Cart, Checkout, Order received) and a Cart Recovery Funnel (Cart, Checkout, Order received), filled with your real shop addresses. Confirm one, wait a few days of visits, and read the Checkout to Order received step: a step that falls after an update is your broken checkout. Split it by device to see if only phones are hit.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error tracking (Pro).&lt;/strong&gt; Opti-Behavior Pro logs JavaScript errors and failed network requests on your pages, with the page where they happened. Look at the errors seen on your checkout page around the date the funnel dropped: that is often the script that stops the Place order button. See &lt;a href="https://optiuser.com/opti-behavior/ob-feature-errors/" rel="noopener noreferrer"&gt;WordPress error tracking&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checkout form analytics and &lt;a href="https://optiuser.com/opti-behavior/ob-feature-recordings/" rel="noopener noreferrer"&gt;session recordings&lt;/a&gt; (Pro).&lt;/strong&gt; When the checkout works but shoppers still stop, see the field where they quit, and watch a few replays of visits that left at the checkout. Our &lt;a href="https://optiuser.com/blog/woocommerce-abandoned-cart/" rel="noopener noreferrer"&gt;guide to WooCommerce checkout abandonment&lt;/a&gt; explains that side.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuy1n6gr90f6ctxelnjb5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuy1n6gr90f6ctxelnjb5.png" alt="Opti-Behavior Smart Insights panel listing two Funnel Drop-off Detected cards with drop-off rate, sessions, priority and confidence" width="477" height="670"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Free: Smart Insights turns a funnel into a card that names the step where visitors leave, ranked by priority (demo data).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The reports are stored in your own WordPress database. See what is in each version on the &lt;a href="https://optiuser.com/opti-behavior/pricing/" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;, or &lt;a href="https://wordpress.org/plugins/opti-behavior/?utm_source=optiuser&amp;amp;utm_medium=blog&amp;amp;utm_campaign=woocommerce-checkout-not-working" rel="noopener noreferrer"&gt;install the free plugin from WordPress.org&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Find the step where your orders stop
&lt;/h3&gt;

&lt;p&gt;Install the free plugin, open Funnels, confirm the suggested WooCommerce funnel and read the Checkout to Order received step after a few days of visits.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check after the fix
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Place a test order again on desktop and on a phone, with every payment method, and refund it.&lt;/li&gt;
&lt;li&gt;Check that the order arrives as Processing (or Completed for virtual products), not Pending payment.&lt;/li&gt;
&lt;li&gt;Check that the customer and admin e-mails for the order were sent.&lt;/li&gt;
&lt;li&gt;Clear every cache layer (plugin, server, CDN) and test once more from a private window.&lt;/li&gt;
&lt;li&gt;For the next 7 days, compare checkout visits with order-received visits, or read the Checkout to Order received step of your funnel, against the 7 days before the problem.&lt;/li&gt;
&lt;li&gt;Write down which plugin or setting caused it, so you test that one first after the next update.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the checkout works but shoppers still leave, the problem is no longer technical: read &lt;a href="https://optiuser.com/blog/woocommerce-abandoned-cart/" rel="noopener noreferrer"&gt;why WooCommerce shoppers abandon their cart&lt;/a&gt; and how to find the step where they leave.&lt;/p&gt;

&lt;h2&gt;
  
  
  WooCommerce checkout not working: FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why does my Place order button not work?
&lt;/h3&gt;

&lt;p&gt;Most often a JavaScript error on the checkout page stops the script that sends the order. Open the browser console, click Place order and read the red errors; then test for a plugin or theme conflict with troubleshooting mode.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is my WooCommerce checkout spinning forever?
&lt;/h3&gt;

&lt;p&gt;The order request was sent but the answer never came back correctly: a PHP error on the server, a security rule that blocks the request, or a payment gateway that does not respond. Check the Network tab for the status of the checkout request, then the WooCommerce logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why are my WooCommerce orders stuck in Pending payment?
&lt;/h3&gt;

&lt;p&gt;The order was created but the payment was never confirmed. Either the shopper left the payment page, or the gateway could not tell your site that the payment succeeded, often because of a webhook problem. Read the order notes and the gateway dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can a cache plugin break the WooCommerce checkout?
&lt;/h3&gt;

&lt;p&gt;Yes. A cached cart or checkout page, or a JavaScript optimisation applied to the checkout, can break orders. Exclude cart, checkout and my-account from every cache layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why does my WooCommerce cart empty itself?
&lt;/h3&gt;

&lt;p&gt;The WooCommerce session is lost between pages: a cached cart page, a mix of http and https or of www and non-www addresses, or a plugin that blocks the session cookie. Fix the site address and the cache exclusions first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Find where your checkout breaks
&lt;/h3&gt;

&lt;p&gt;Install the free plugin, then confirm the suggested Purchase Funnel and read the Checkout to Order received step.&lt;/p&gt;

&lt;p&gt;The Opti-Behavior Team&lt;/p&gt;

&lt;p&gt;Full checklist: &lt;a href="https://optiuser.com/go/devto-checkout-not-working" rel="noopener noreferrer"&gt;https://optiuser.com/go/devto-checkout-not-working&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Drafted with AI assistance, reviewed by the Opti-Behavior Team.&lt;/p&gt;

</description>
      <category>woocommerce</category>
      <category>wordpress</category>
      <category>ecommerce</category>
      <category>debugging</category>
    </item>
    <item>
      <title>What Is a Good Conversion Rate? (And Why the Average Can Fool You)</title>
      <dc:creator>Hamdi Bk</dc:creator>
      <pubDate>Sat, 31 Oct 2026 09:42:49 +0000</pubDate>
      <link>https://dev.to/opti-behavior/what-is-a-good-conversion-rate-and-why-the-average-can-fool-you-5279</link>
      <guid>https://dev.to/opti-behavior/what-is-a-good-conversion-rate-and-why-the-average-can-fool-you-5279</guid>
      <description>&lt;p&gt;"Is 2 % good?" is the question every site owner asks once. This is the guide we published on optiuser.com: why the average can fool you, how to find a good rate for your own site, and how to raise it. Disclosure: we build Opti-Behavior, the plugin mentioned in some sections.&lt;/p&gt;

&lt;p&gt;Imagine Nadia. She sells online yoga classes. Last month, 3,000 people visited her sales page and 45 bought a class. She typed "what is a good conversion rate" into Google and found an article that said "the average is 2.35%". Hers was 1.5%. She felt terrible.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Nadia is an example: her site and figures are made up to show the method step by step.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Then she found another article that said "a good conversion rate is 10%". Now she felt even worse.&lt;/p&gt;

&lt;p&gt;Here is the truth: both articles were talking about other people’s websites. A &lt;strong&gt;good conversion rate&lt;/strong&gt; depends on what you sell, who visits, and what you count as a conversion. In this guide we will work out a good rate for Nadia’s site, step by step, and then find ways to raise it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; A good conversion rate is one that is better than your own rate last month, measured the same way. Industry averages are rough hints, not goals. Measure one clear action, per visitor, for each type of page, and improve the weakest step first.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What is a conversion, exactly?
&lt;/h2&gt;

&lt;p&gt;A conversion is when a visitor does the thing you want. It can be big or small:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Big (macro) conversion&lt;/td&gt;
&lt;td&gt;A purchase, a paid sign-up, a booked call, a quote request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small (micro) conversion&lt;/td&gt;
&lt;td&gt;Adding to cart, starting a form, signing up to a newsletter, watching a demo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Small conversions matter because they lead to big ones. If nobody adds to cart, nobody buys.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to calculate your conversion rate
&lt;/h2&gt;

&lt;p&gt;The formula is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Conversion rate = conversions ÷ visitors × 100&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Nadia: 45 purchases ÷ 3,000 visitors × 100 = &lt;strong&gt;1.5%&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But watch out. Tools do not all divide by the same thing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per visitor:&lt;/strong&gt; 45 buyers out of 3,000 people. This is the honest one for sales.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per session:&lt;/strong&gt; 45 buyers out of 4,200 visits. The rate looks lower, because many people visit twice before buying.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per page:&lt;/strong&gt; conversions on one page divided by that page’s visitors. Best for comparing pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you compare numbers, always use the same method. Many "benchmark" articles do not say which one they used.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what is a good conversion rate?
&lt;/h2&gt;

&lt;p&gt;Here are ranges you often see quoted in marketing reports. They come from different studies and change every year, so treat them as rough hints:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type of page or site&lt;/th&gt;
&lt;th&gt;Often quoted range&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Online shop (all visitors)&lt;/td&gt;
&lt;td&gt;1% to 3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shop, visitors who add to cart&lt;/td&gt;
&lt;td&gt;30% to 60% reach checkout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lead form on a service site&lt;/td&gt;
&lt;td&gt;2% to 5%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dedicated landing page from an ad&lt;/td&gt;
&lt;td&gt;5% to 15%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Newsletter sign-up box on a blog&lt;/td&gt;
&lt;td&gt;1% to 3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free trial sign-up (software)&lt;/td&gt;
&lt;td&gt;3% to 10%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Why are the ranges so wide? Because a "good" rate depends on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Price.&lt;/strong&gt; A $5 ebook converts more than a $900 course.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic source.&lt;/strong&gt; Visitors from your newsletter already trust you. Visitors from a cold ad do not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Device.&lt;/strong&gt; Mobile visitors often browse and buy later on a computer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Intent.&lt;/strong&gt; Someone searching "buy yoga class online" is ready. Someone searching "is yoga good for back pain" is learning.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So Nadia’s 1.5% was not bad for a paid course with mostly cold traffic. The real question was: &lt;strong&gt;which part of her journey was weak?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Split the rate into steps
&lt;/h2&gt;

&lt;p&gt;One big conversion rate hides where the problem is. Split the path into steps. This is called a &lt;strong&gt;funnel&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Nadia’s path was: Sales page, Pricing, Checkout, Thank-you page. In Opti-Behavior, you can create this funnel in a minute: go to &lt;strong&gt;Opti-Behavior &amp;gt; Funnels&lt;/strong&gt;, click &lt;strong&gt;Build New Funnel&lt;/strong&gt;, and add each step with a URL rule, like "URL Contains /pricing/". Opti-Behavior also suggests funnels built from the pages of your site.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgion9bvi0f03slrci91o.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgion9bvi0f03slrci91o.webp" alt="Good conversion rate: funnel showing step-by-step conversion rate" width="800" height="629"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Each step shows how many visitors continue and how many leave.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Here is what Nadia’s funnel showed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Visitors&lt;/th&gt;
&lt;th&gt;Continue to next step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sales page&lt;/td&gt;
&lt;td&gt;3,000&lt;/td&gt;
&lt;td&gt;28%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pricing&lt;/td&gt;
&lt;td&gt;840&lt;/td&gt;
&lt;td&gt;22%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Checkout&lt;/td&gt;
&lt;td&gt;185&lt;/td&gt;
&lt;td&gt;24%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thank-you&lt;/td&gt;
&lt;td&gt;45&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The big leak was at checkout: 185 people started to pay, only 45 finished. That is 76% lost at the very last step. Her sales page was fine. Her checkout was not.&lt;/p&gt;

&lt;p&gt;With Pro, each funnel step links to the recordings of the people who left right there. Nadia watched eight of them. On phones, her checkout asked people to create an account with a password before paying. Most gave up at the password field.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3bgxtzkdafhpp3pgs3ou.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3bgxtzkdafhpp3pgs3ou.webp" alt="Watch recordings of visitors who left a funnel step" width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In Pro, each step links to the recordings of the visitors who dropped there.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure what counts as a win
&lt;/h2&gt;

&lt;p&gt;When you change something, you want to know if it worked. That is what A/B tests are for: half your visitors see the old version, half see the new one, at the same time.&lt;/p&gt;

&lt;p&gt;In Opti-Behavior, a test has goals. The first goal is the one that picks the winner.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Goal&lt;/th&gt;
&lt;th&gt;Counts a conversion when&lt;/th&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Page visit&lt;/td&gt;
&lt;td&gt;The visitor reaches a page, like a thank-you page&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Click&lt;/td&gt;
&lt;td&gt;The visitor clicks an element you pick on the page&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form submit&lt;/td&gt;
&lt;td&gt;A form on the page is sent&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scroll depth&lt;/td&gt;
&lt;td&gt;The visitor scrolls to a depth you set&lt;/td&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time on page&lt;/td&gt;
&lt;td&gt;The visit lasts longer than a threshold&lt;/td&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Revenue&lt;/td&gt;
&lt;td&gt;WooCommerce orders from the test&lt;/td&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkvxt3znrrktgpl46b8ag.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkvxt3znrrktgpl46b8ag.webp" alt="Choosing a conversion goal for an A/B test" width="800" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Pick the action that counts as a conversion. The first goal decides the winner.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The free version runs up to 3 tests at the same time, with up to 3 versions each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Careful: a higher conversion rate is not always better
&lt;/h2&gt;

&lt;p&gt;This surprises a lot of people. Look at this real-style example from our A/B testing docs:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Visitors&lt;/th&gt;
&lt;th&gt;Orders&lt;/th&gt;
&lt;th&gt;Conversion rate&lt;/th&gt;
&lt;th&gt;Average order&lt;/th&gt;
&lt;th&gt;Revenue per visitor&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A (current)&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;3.0%&lt;/td&gt;
&lt;td&gt;60 €&lt;/td&gt;
&lt;td&gt;1.80 €&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B (free shipping over 50 €)&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;2.7%&lt;/td&gt;
&lt;td&gt;78 €&lt;/td&gt;
&lt;td&gt;2.11 €&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Version B has a &lt;strong&gt;lower&lt;/strong&gt; conversion rate, but people spend more, so it earns 17% more per visitor. If you only looked at the conversion rate, you would pick the wrong version. For shops, watch &lt;strong&gt;revenue per visitor&lt;/strong&gt; too. Pro can measure it with a Revenue goal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Know what one conversion is worth
&lt;/h2&gt;

&lt;p&gt;Once you know the value of one conversion, every leak has a price. In Smart Insights settings, you can type the value of one conversion (Free and Pro). Smart Insights then estimates what a problem costs you. With Pro, WooCommerce orders set that value for you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuwboow3if5gzfet7bcl3.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuwboow3if5gzfet7bcl3.webp" alt="Setting the value of one conversion in Smart Insights" width="800" height="140"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Type the value of one conversion, or, with Pro, let WooCommerce orders price the losses.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For Nadia, one class was worth 60 €. Losing 140 people at checkout every month was not "a bad conversion rate". It was up to 8,400 € a month walking away. That made the fix very easy to prioritize.&lt;/p&gt;

&lt;h2&gt;
  
  
  8 ways to raise your conversion rate
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fix the weakest step first.&lt;/strong&gt; A 10% gain on your worst step beats a 10% gain on your best one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove forced account creation.&lt;/strong&gt; Let people buy as guests. Offer an account after payment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show the price and what is included early.&lt;/strong&gt; Surprises at checkout kill sales.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the main button obvious.&lt;/strong&gt; One clear action per page, visible without scrolling on mobile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer doubts next to the button.&lt;/strong&gt; Refunds, delivery time, support. One line each.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed up mobile.&lt;/strong&gt; A slow checkout on a phone loses people fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check for broken things.&lt;/strong&gt; Rage clicks and JavaScript errors on a checkout page are pure lost money. Read our guide on &lt;a href="https://optiuser.com/blog/rage-click-dead-click-detection-wordpress/" rel="noopener noreferrer"&gt;rage clicks&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test, do not guess.&lt;/strong&gt; Run an A/B test on one change at a time. Our WordPress A/B testing guide shows how.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Nadia’s new conversion rate
&lt;/h2&gt;

&lt;p&gt;Nadia allowed guest checkout and moved the account creation to the thank-you page. Six weeks later, 44% of people who reached checkout finished paying, instead of 24%. Her overall conversion rate went from 1.5% to 2.7%, with the same traffic.&lt;/p&gt;

&lt;p&gt;She still does not know if 2.7% is "good" for the whole yoga industry. She does not care anymore. It is &lt;strong&gt;better than last month&lt;/strong&gt;, and she knows exactly which step to work on next.&lt;/p&gt;

&lt;h3&gt;
  
  
  Find your weakest conversion step
&lt;/h3&gt;

&lt;p&gt;Opti-Behavior is free: funnels, click heatmaps and A/B tests on WordPress. Pro adds drop-off recordings and revenue goals, with a 6-month free trial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Good conversion rate FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a good conversion rate?
&lt;/h3&gt;

&lt;p&gt;It depends on your site, price and traffic. Rough ranges often quoted are 1% to 3% for online shops, 2% to 5% for lead forms and 5% to 15% for focused landing pages. The best benchmark is your own rate last month, measured the same way.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I calculate my conversion rate?
&lt;/h3&gt;

&lt;p&gt;Divide the number of conversions by the number of visitors and multiply by 100. For example, 45 sales from 3,000 visitors is a 1.5% conversion rate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is a 2% conversion rate good?
&lt;/h3&gt;

&lt;p&gt;For an online shop with mixed traffic, 2% is often considered solid. For a landing page that only receives ad visitors, it may be low.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is my conversion rate low?
&lt;/h3&gt;

&lt;p&gt;Usually one step loses most people: an unclear offer, a hidden button, a surprise cost, a long form or a broken checkout. A funnel shows which step, and recordings show why.&lt;/p&gt;

&lt;h3&gt;
  
  
  Should I trust average conversion rate benchmarks?
&lt;/h3&gt;

&lt;p&gt;Only as a rough hint. Studies measure different sites in different ways. Track your own rate per page and per step, and try to beat it.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Opti-Behavior Team&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Full guide: &lt;a href="https://optiuser.com/go/devto-conversion-rate" rel="noopener noreferrer"&gt;https://optiuser.com/go/devto-conversion-rate&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Drafted with AI assistance, reviewed by the Opti-Behavior Team.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>analytics</category>
      <category>webdev</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Add to Cart Rate: How to Find the WooCommerce Products That Lose Buyers</title>
      <dc:creator>Hamdi Bk</dc:creator>
      <pubDate>Mon, 26 Oct 2026 04:38:23 +0000</pubDate>
      <link>https://dev.to/opti-behavior/add-to-cart-rate-how-to-find-the-woocommerce-products-that-lose-buyers-54ic</link>
      <guid>https://dev.to/opti-behavior/add-to-cart-rate-how-to-find-the-woocommerce-products-that-lose-buyers-54ic</guid>
      <description>&lt;p&gt;Add to cart rate is the first number to read when a WooCommerce product gets visits but few sales. This is the guide we published on optiuser.com: how to measure it per product, how to find the pages that lose buyers, and how to fix them. Disclosure: we build Opti-Behavior, the plugin mentioned in some sections.&lt;/p&gt;

&lt;p&gt;Your &lt;strong&gt;add to cart rate&lt;/strong&gt; is the share of product page views that end with a click on Add to cart. On a WooCommerce store, it is the first number to check when you have visitors but few orders: if shoppers never put a product in the cart, no checkout fix will help. This guide shows how to calculate it, how to find the products that lose the most buyers, and what to change on those product pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; add to cart rate = adds to cart ÷ product page views × 100. Measure it per product, not only for the whole store. Sort your products by visits lost (viewed, not added to cart), then work on the product page at the top of the list: price and delivery cost visible, clear photos, simple variation choice, and a button that is easy to reach on a phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is the add to cart rate, and how do you calculate it?
&lt;/h2&gt;

&lt;p&gt;There are two common ways to count it. Both are useful, for different questions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per product:&lt;/strong&gt; adds to cart of one product ÷ views of that product’s page × 100. Example: a product page viewed 400 times with 24 adds to cart has an add to cart rate of 6%. This tells you &lt;em&gt;which page&lt;/em&gt; to fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per session:&lt;/strong&gt; sessions with at least one add to cart ÷ all sessions × 100. Many analytics tools report this one. It tells you whether the store as a whole is getting better or worse.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The two numbers are not comparable with each other, and two tools rarely count the same way. Pick one method, write it down, and compare the store with itself over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a good add to cart rate?
&lt;/h2&gt;

&lt;p&gt;There is no single good number. Published benchmarks vary with the industry, the price of the products, the device and the way each vendor counts. A €15 accessory and a €900 sofa will never have the same rate.&lt;/p&gt;

&lt;p&gt;Three comparisons are more useful than any benchmark:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Each product against your store average.&lt;/strong&gt; A product far below the others, with similar traffic, has a page problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phones against desktop.&lt;/strong&gt; A rate that collapses only on phones usually points to layout, photos or the button, not to the price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;This month against last month.&lt;/strong&gt; A sudden drop after a theme, plugin or price change is a strong clue.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to measure your add to cart rate in WooCommerce
&lt;/h2&gt;

&lt;h3&gt;
  
  
  In WooCommerce itself
&lt;/h3&gt;

&lt;p&gt;The WooCommerce Analytics reports show orders, items sold and net sales per product. They do not show product page views or adds to cart, so you cannot calculate the rate there. You need an analytics tool that records both.&lt;/p&gt;

&lt;h3&gt;
  
  
  With Google Analytics 4
&lt;/h3&gt;

&lt;p&gt;If your store sends the ecommerce events &lt;code&gt;view_item&lt;/code&gt; and &lt;code&gt;add_to_cart&lt;/code&gt; (for example with Google’s WooCommerce integration), the Ecommerce purchases report lists items viewed and items added to cart for each product. Divide the second by the first. It works once the event setup is in place.&lt;/p&gt;

&lt;h3&gt;
  
  
  With Opti-Behavior (free plugin)
&lt;/h3&gt;

&lt;p&gt;Opti-Behavior adds a &lt;strong&gt;Shop&lt;/strong&gt; screen to WordPress on WooCommerce sites (Opti-Behavior &amp;gt; Shop). It counts product views, adds to cart, checkouts and paid orders, and lists the visits each product loses (viewed, not added to cart), worst product first. No event setup is needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why shoppers view a product but do not add it to cart
&lt;/h2&gt;

&lt;p&gt;Before changing anything, check the product at the top of your list against these common causes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The full cost is unclear.&lt;/strong&gt; Price visible, but shipping cost and delivery time only appear at checkout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The photos do not answer the question.&lt;/strong&gt; No close-up, no scale, no photo of the product in use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The variation choice blocks the button.&lt;/strong&gt; On a variable product, the button stays inactive until size and colour are chosen, and shoppers do not see why.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stock is unclear.&lt;/strong&gt; “Out of stock” or “available on backorder” with no date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The button is hard to reach on a phone.&lt;/strong&gt; Below a long description, under a sticky banner or a cookie notice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing reassures.&lt;/strong&gt; No reviews, no returns policy, no way to ask a question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Something is broken.&lt;/strong&gt; A JavaScript error or a failed request stops the button from working for some browsers. Repeated clicks on the button are a typical sign: see &lt;a href="https://optiuser.com/blog/rage-click-dead-click-detection-wordpress/" rel="noopener noreferrer"&gt;rage and dead clicks&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The wrong visitors arrive.&lt;/strong&gt; A campaign or a search result sends people who were looking for something else.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Product page optimization: 7 changes worth testing
&lt;/h2&gt;

&lt;p&gt;Work on one product page at a time, the one that loses the most visits. To see the other steps where your store loses buyers, from cart to paid order, read our guide to &lt;a href="https://optiuser.com/blog/woocommerce-conversion-tracking/" rel="noopener noreferrer"&gt;WooCommerce conversion tracking&lt;/a&gt;. Change one thing, then compare the same number of days before and after.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Show the delivery cost and delivery time next to the price.&lt;/strong&gt; If you offer free shipping above an amount, say it here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the Add to cart button where a phone user sees it&lt;/strong&gt; without scrolling past the whole description.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make variation choices obvious:&lt;/strong&gt; swatches or clear labels, and a short message when a choice is missing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add the photos shoppers ask for:&lt;/strong&gt; close-up, scale, in use. Check what they click on the page first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Answer the top three questions in the first lines&lt;/strong&gt; of the description (size, material, compatibility, what is in the box).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bring reassurance near the button:&lt;/strong&gt; returns policy, delivery promise, a contact link, reviews if you have them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the page yourself on a phone&lt;/strong&gt; with each variation, after every theme or plugin update.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When shoppers do add to cart but still do not buy, the problem has moved to the next step: see our guides to the &lt;a href="https://optiuser.com/blog/woocommerce-abandoned-cart/" rel="noopener noreferrer"&gt;WooCommerce abandoned cart&lt;/a&gt; and to &lt;a href="https://optiuser.com/blog/form-analytics-wordpress-woocommerce-checkout-abandonment/" rel="noopener noreferrer"&gt;WooCommerce checkout abandonment&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Opti-Behavior finds the product that loses buyers, and what to fix
&lt;/h2&gt;

&lt;p&gt;Opti-Behavior is our WordPress plugin, so we are not neutral here. The data stays in your WordPress site, and the work follows three steps: the modules collect, Smart Insights ranks the problems, and a report tells you what to fix.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The modules collect the signals
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shop for WooCommerce (Free):&lt;/strong&gt; views, adds to cart, checkouts and paid orders, and the visits lost per product, worst product first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://optiuser.com/opti-behavior/ob-feature-funnels/" rel="noopener noreferrer"&gt;Funnels&lt;/a&gt; (Free):&lt;/strong&gt; on a WooCommerce site, Opti-Behavior suggests ready-made funnels built from your own addresses, such as Product page → Cart → Checkout → Order received.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://optiuser.com/opti-behavior/ob-feature-heatmaps/" rel="noopener noreferrer"&gt;Click heatmaps&lt;/a&gt; (Free):&lt;/strong&gt; see what shoppers click on the product page instead of Add to cart: photos, tabs, the size guide, a link away.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Smart Insights ranks what costs the most
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://optiuser.com/opti-behavior/ob-feature-smart-insights/" rel="noopener noreferrer"&gt;Smart Insights&lt;/a&gt; turns the data into one card per problem, ranked by visitors lost, with a confidence level based on the visits measured. In Pro, a WooCommerce card flags a product page whose visitors rarely reach the cart, and funnel cards name the step where visitors leave.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The report says what to fix, in order
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Page X-Ray (Pro)&lt;/strong&gt; opens one product page: a health score, what the page is for and how often visitors reach its main button, a page audit (buttons, trust, content) and an action plan. The free plugin shows a preview of your three most visited pages, each with its health score and first problem. More on &lt;a href="https://optiuser.com/blog/why-is-my-page-not-converting/" rel="noopener noreferrer"&gt;what Page X-Ray checks&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Site report and scheduled e-mail (Free):&lt;/strong&gt; the three most costly issues of the period and, on WooCommerce sites, the product that loses the most visits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shop in Pro:&lt;/strong&gt; an estimated range of lost sales per product, from your conversion rate and average order, and shop events (add to cart, checkout, payment failure) inside session recordings, recorded only in Full Tracking mode after the visitor’s consent.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Check that the fix worked
&lt;/h3&gt;

&lt;p&gt;When your change is live, mark the Smart Insights card as resolved. Opti-Behavior measures the period after your change and shows the result, so you know whether the product page really improved.&lt;/p&gt;

&lt;h3&gt;
  
  
  Find the product that loses your buyers
&lt;/h3&gt;

&lt;p&gt;Install the free plugin, open Opti-Behavior &amp;gt; Shop after a few days of visits, and start with the first product of the list. See what is in each version on the &lt;a href="https://optiuser.com/opti-behavior/pricing/" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add to cart rate FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  How do I calculate the add to cart rate?
&lt;/h3&gt;

&lt;p&gt;Divide the number of adds to cart by the number of product page views, then multiply by 100. Per product, this shows which page to fix. Per session (sessions with an add to cart ÷ all sessions), it shows the trend of the whole store.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does WooCommerce show the add to cart rate?
&lt;/h3&gt;

&lt;p&gt;Not in its built-in Analytics reports, which show orders and items sold but not product views or adds to cart. Use an analytics tool that records both, such as GA4 with ecommerce events or the Shop screen of Opti-Behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between add to cart rate and conversion rate?
&lt;/h3&gt;

&lt;p&gt;The add to cart rate measures the first step: product page to cart. The conversion rate measures the whole journey to a paid order. A store can have a good add to cart rate and a poor conversion rate when the problem is in the cart or the checkout.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is my add to cart rate low only on mobile?
&lt;/h3&gt;

&lt;p&gt;Usually because of the page, not the product: the button sits far below the photos and description, a banner covers it, or the variation choice is hard to use on a small screen. Test the page on a real phone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is the Shop screen free in Opti-Behavior?
&lt;/h3&gt;

&lt;p&gt;Yes. Product views, adds to cart, checkouts, paid orders and visits lost per product are in the free plugin. The estimated lost sales per product, Page X-Ray reports and session recordings are in Pro.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Opti-Behavior Team&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Full guide: &lt;a href="https://optiuser.com/go/devto-add-to-cart" rel="noopener noreferrer"&gt;https://optiuser.com/go/devto-add-to-cart&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Drafted with AI assistance, reviewed by the Opti-Behavior Team.&lt;/p&gt;

</description>
      <category>woocommerce</category>
      <category>wordpress</category>
      <category>ecommerce</category>
      <category>analytics</category>
    </item>
    <item>
      <title>WooCommerce Conversion Tracking: Find the Step That Loses Sales</title>
      <dc:creator>Hamdi Bk</dc:creator>
      <pubDate>Sat, 24 Oct 2026 15:11:30 +0000</pubDate>
      <link>https://dev.to/opti-behavior/woocommerce-conversion-tracking-find-the-step-that-loses-sales-1dng</link>
      <guid>https://dev.to/opti-behavior/woocommerce-conversion-tracking-find-the-step-that-loses-sales-1dng</guid>
      <description>&lt;p&gt;Most WooCommerce stores know what sold. Few know where the buyers who did not buy stopped. Below is the guide we published on optiuser.com: the five steps to track, how to set them up with GA4 or inside WordPress, and the four leaks they usually reveal. Disclosure: we build Opti-Behavior, the plugin mentioned in some sections.&lt;/p&gt;

&lt;p&gt;WooCommerce conversion tracking means counting how many visitors reach each step of a purchase, from the product page to the paid order, so you can see where sales are lost. WooCommerce reports tell you what sold. They do not tell you who almost bought. This guide shows how to set up WooCommerce conversion tracking with the tools you already have, then the four problems it usually reveals and how to fix each one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt;  track five steps (product page, add to cart, cart, checkout, order received), compare each step with the one before, and work on the biggest drop first. Google Analytics 4 can do it once your store sends ecommerce events; a WordPress funnel can do it from page visits. Then check the four usual leaks: products viewed but not added, checkouts started but not paid, payments that fail, and the product pages that lose the most sales.&lt;/p&gt;

&lt;h2&gt;
  
  
  What WooCommerce reports show, and what they miss
&lt;/h2&gt;

&lt;p&gt;The WooCommerce Analytics menu lists revenue, orders, products, categories, coupons and stock. It answers “what did we sell?”. It does not count the visits that did not become an order, so it cannot give you a conversion rate per step.&lt;/p&gt;

&lt;p&gt;A store owner usually feels the problem as “traffic is fine, orders are not”. Conversion tracking turns that feeling into four precise questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which product pages are seen but rarely added to cart?&lt;/li&gt;
&lt;li&gt;At which step between cart and order do buyers leave?&lt;/li&gt;
&lt;li&gt;Do payments fail, with which payment method and which error?&lt;/li&gt;
&lt;li&gt;Which of these problems costs the most?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to set up WooCommerce conversion tracking
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Write down the steps
&lt;/h3&gt;

&lt;p&gt;Use the same five steps whatever the tool: product page, add to cart, cart page, checkout page, order received page. For the last step, use a rule like “URL contains order-received”, because the real confirmation address contains the order number.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. With Google Analytics 4
&lt;/h3&gt;

&lt;p&gt;GA4 needs the ecommerce events &lt;code&gt;view_item&lt;/code&gt;, &lt;code&gt;add_to_cart&lt;/code&gt;, &lt;code&gt;begin_checkout&lt;/code&gt; and &lt;code&gt;purchase&lt;/code&gt;. Google’s official WooCommerce integration sends them. Then build a Funnel exploration in the Explore section with those events as steps. If you also run ads, Google Ads and Meta each need their own purchase conversion, usually sent by their official WooCommerce extensions. Check how each one fits your consent setup before you turn it on.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Inside WordPress, from page visits
&lt;/h3&gt;

&lt;p&gt;A funnel built from page visits needs no event setup: it counts the visitors who reach each page in order. In &lt;a href="https://optiuser.com/blog/wordpress-conversion-funnel-plugin/" rel="noopener noreferrer"&gt;our guide to WordPress conversion funnels&lt;/a&gt; we show how it works. Opti-Behavior, our plugin, detects WooCommerce and suggests a Purchase Funnel (Shop, Product page, Cart, Checkout, Order received) with your real cart and checkout addresses filled in. This is in the free plugin.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Test the tracking
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Place a test order on a phone and on a computer, and check that each step counts it once.&lt;/li&gt;
&lt;li&gt;Exclude your own visits and your team’s, or they will hide the real drop.&lt;/li&gt;
&lt;li&gt;Compare the order count of the tracking with WooCommerce &amp;gt; Orders for the same days. A large gap means a step is not measured.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Problem 1: products viewed but not added to cart
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The sign:&lt;/strong&gt;  a product gets visits from search or ads, but sells little.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check by hand:&lt;/strong&gt;  for each product, divide adds to cart by product page views over the same period, then sort from the lowest. Open the worst pages on a phone: the price, the delivery cost, the stock and the add to cart button should be visible without scrolling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Opti-Behavior shows (Free):&lt;/strong&gt;  the Shop screen of our &lt;a href="https://optiuser.com/opti-behavior/ob-feature-shop/" rel="noopener noreferrer"&gt;WooCommerce analytics&lt;/a&gt; lists, for each product, product views, adds to cart, paid orders and lost visits (product page views that did not lead to an add to cart), worst product first. A product with adds to cart but no measured product view shows “Not measured” instead of 0. The click heatmap of the product page, also free, shows whether visitors click the gallery, the tabs or the button.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: checkouts started but not paid
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The sign:&lt;/strong&gt;  carts are filled, the checkout page gets visits, few orders arrive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check by hand:&lt;/strong&gt;  over the same days, compare visits to the checkout page with visits to the order received page. Then filter WooCommerce &amp;gt; Orders on Pending payment and Failed. A sudden drop after a plugin or theme update points to a broken checkout, not to buyers changing their mind. Our &lt;a href="https://optiuser.com/blog/woocommerce-abandoned-cart/" rel="noopener noreferrer"&gt;WooCommerce abandoned cart guide&lt;/a&gt; lists the settings to check first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Opti-Behavior shows:&lt;/strong&gt;  the funnel shows where visitors drop off between steps, with a device filter (Free). &lt;a href="https://optiuser.com/opti-behavior/ob-feature-smart-insights/" rel="noopener noreferrer"&gt;Smart Insights&lt;/a&gt; turns that funnel into a card that names the step where visitors leave, ranked by visitors lost (Free). Opti-Behavior Pro adds dedicated WooCommerce cards (product page to cart, cart to checkout, checkout to purchase), and &lt;a href="https://optiuser.com/opti-behavior/ob-feature-recordings/" rel="noopener noreferrer"&gt;session recordings&lt;/a&gt; show the shop events (add to cart, checkout, paid order, payment failure) in the list and the player, with a “Shop action” filter. Session-linked shop events are recorded only in Full tracking mode, after the visitor’s consent.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkfehofwo8xtx0aii65jn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkfehofwo8xtx0aii65jn.png" alt="Smart Insights (Free) flags the funnel step where visitors drop off and says what to open next." width="800" height="420"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Smart Insights (Free) flags the funnel step where visitors drop off and says what to open next.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 3: payments that fail
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The sign:&lt;/strong&gt;  orders in Failed status, buyers who write “your payment does not work”, a payment method that suddenly sells nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check by hand:&lt;/strong&gt;  open WooCommerce &amp;gt; Status &amp;gt; Logs and read the log of each payment gateway for the days of the problem. Place a test order with each payment method, on a phone too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Opti-Behavior shows (Pro):&lt;/strong&gt;  on the Shop screen, payment errors grouped by error type and payment method, with the failed attempts, the orders concerned and their estimated cost (orders not paid later). In Full tracking mode, a recording of a failed payment can be watched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 4: which product loses the most sales
&lt;/h2&gt;

&lt;p&gt;A product with many lost visits is not always the most costly one: a cheap product with high traffic can cost less than an expensive one with fewer visits. Rank by money, not by visits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check by hand:&lt;/strong&gt;  lost visits × your conversion rate × your average order value. Example with round numbers: 400 lost visits × 2 % × 50 = about 400 in sales at stake for the period. It is an estimate, not a promise: use it to choose what to fix first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What Opti-Behavior shows (Pro):&lt;/strong&gt;  the Shop screen gives the estimated lost sales, as an indicative range, per product and for the period, from the lost visits, your conversion rate and your average order. The Shop screen shows lost sales as a range, or "Not enough data" when there are too few orders.&lt;/p&gt;

&lt;h2&gt;
  
  
  WooCommerce reports by e-mail
&lt;/h2&gt;

&lt;p&gt;Conversion tracking only helps if someone reads it. Instead of opening the admin every day, get the figures sent to you. In Opti-Behavior, the &lt;a href="https://optiuser.com/opti-behavior/ob-feature-reports/" rel="noopener noreferrer"&gt;scheduled e-mail report&lt;/a&gt; (daily, weekly or monthly) is free and includes a Shop block with the store’s figures. In Pro, the same block adds “Money at stake”, the estimated lost sales of the period, marked as an estimate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free or Pro: summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Problem&lt;/th&gt;
&lt;th&gt;What you get&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Products viewed but not added&lt;/td&gt;
&lt;td&gt;Shop screen: views, adds to cart, paid orders, lost visits per product&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where buyers leave&lt;/td&gt;
&lt;td&gt;Suggested purchase funnel, drop-off per step, click heatmaps&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A purchase step with a large drop&lt;/td&gt;
&lt;td&gt;Smart Insights card naming the step (Free); dedicated WooCommerce cards per purchase step (Pro)&lt;/td&gt;
&lt;td&gt;Free / Pro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Why buyers leave&lt;/td&gt;
&lt;td&gt;Recordings with shop events (Full mode, after consent) and the “Shop action” filter&lt;/td&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payments that fail&lt;/td&gt;
&lt;td&gt;Payment errors by type and payment method, with estimated cost&lt;/td&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What each product costs&lt;/td&gt;
&lt;td&gt;Estimated lost sales per product (indicative range)&lt;/td&gt;
&lt;td&gt;Pro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reports by e-mail&lt;/td&gt;
&lt;td&gt;Shop block with figures / “Money at stake”&lt;/td&gt;
&lt;td&gt;Free / Pro&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The full list of each version is on the &lt;a href="https://optiuser.com/opti-behavior/pricing/" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check after a fix
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Change one thing at a time: the product page, the cart, the checkout or a payment method.&lt;/li&gt;
&lt;li&gt;Compare the same number of days before and after, on the same step of the funnel.&lt;/li&gt;
&lt;li&gt;Split by device: a fix on desktop can leave the problem on phones.&lt;/li&gt;
&lt;li&gt;Check WooCommerce &amp;gt; Orders too: more checkouts without more paid orders means the leak moved to the payment step.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Does WooCommerce track conversions on its own?
&lt;/h3&gt;

&lt;p&gt;It records orders, revenue and products sold. It does not count the visitors who saw a product or started a checkout without buying, so it cannot show a conversion rate per step without another tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need Google Analytics for WooCommerce conversion tracking?
&lt;/h3&gt;

&lt;p&gt;No. GA4 works once your store sends ecommerce events. A funnel built from page visits inside WordPress gives the step-by-step drop without that setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between conversion tracking and WooCommerce reports?
&lt;/h3&gt;

&lt;p&gt;WooCommerce reports count what was sold. Conversion tracking counts what happened before: visits, adds to cart, checkouts and payments, step by step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which WooCommerce analytics are free in Opti-Behavior?
&lt;/h3&gt;

&lt;p&gt;The Shop screen, the purchase funnel, click heatmaps, the Smart Insights card that names the funnel step where visitors leave, and the Shop block of the e-mail report. Dedicated WooCommerce Smart Insights cards, estimated lost sales, payment errors and recordings with shop events are in Pro.&lt;/p&gt;

&lt;h3&gt;
  
  
  Find the step where your store loses sales
&lt;/h3&gt;

&lt;p&gt;Install the free plugin, open Shop to see the products viewed but not added, then confirm the suggested purchase funnel.&lt;/p&gt;

&lt;p&gt;Try it on your store: &lt;a href="https://optiuser.com/blog/woocommerce-conversion-tracking/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=oct26-problem-keyword&amp;amp;utm_content=woocommerce-conversion-tracking" rel="noopener noreferrer"&gt;https://optiuser.com/blog/woocommerce-conversion-tracking/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=oct26-problem-keyword&amp;amp;utm_content=woocommerce-conversion-tracking&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Drafted with AI assistance, reviewed by the Opti-Behavior Team.&lt;/p&gt;

</description>
      <category>woocommerce</category>
      <category>wordpress</category>
      <category>ecommerce</category>
      <category>analytics</category>
    </item>
    <item>
      <title>Heatmap Not Working After Installing a Cache Plugin? Here's Exactly What Breaks It</title>
      <dc:creator>Hamdi Bk</dc:creator>
      <pubDate>Tue, 13 Oct 2026 10:52:28 +0000</pubDate>
      <link>https://dev.to/opti-behavior/heatmap-not-working-after-installing-a-cache-plugin-heres-exactly-what-breaks-it-37ha</link>
      <guid>https://dev.to/opti-behavior/heatmap-not-working-after-installing-a-cache-plugin-heres-exactly-what-breaks-it-37ha</guid>
      <description>&lt;p&gt;&lt;em&gt;Last verified: August 2026. Filter names and behaviours below were checked against each optimizer’s own documentation or source. Re-check before relying on them — optimizers change these between releases.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Heatmap not working after installing a cache plugin? Your heatmap page is empty. The session counter says zero. There is nothing in the browser console, no failed request in the Network tab, no warning in your dashboard. As far as every screen you can look at is concerned, nothing is wrong.&lt;/p&gt;

&lt;p&gt;It isn’t your analytics tool. It’s your optimizer.&lt;/p&gt;

&lt;p&gt;Cache and performance plugins rewrite, merge, defer and delay JavaScript. Tracking scripts are JavaScript. When an optimizer decides your tracker is just another file to bundle, the tracker stops executing — and because nothing errors, nothing tells you. You keep browsing your own site, the pages load faster than ever, and you lose weeks of behavioural data before you happen to open the heatmap and find it blank.&lt;/p&gt;

&lt;p&gt;This applies to every tool in the category. Hotjar, Microsoft Clarity, Matomo, Opti-Behavior, your own custom beacon — same mechanism, same silence.&lt;/p&gt;

&lt;p&gt;Click around a real dashboard before you install anything&lt;/p&gt;

&lt;p&gt;A live WordPress site with real traffic, real heatmaps and real recordings. No signup, no credit card, no email.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://demo.optiuser.com/" rel="noopener noreferrer"&gt;Open the live demo →&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Heatmap Not Working? The 60-Second Diagnostic
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3dzrs9z92eb5c1infok5.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3dzrs9z92eb5c1infok5.gif" width="700" height="394"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A heatmap not working right after a cache plugin install is almost always a script problem, so start here. Open your site in a private window, as a logged-out visitor, and run these three checks in order. Each one narrows the failure to a different cause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Is the script in the HTML at all?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;View source (Ctrl+U — not the DevTools Elements tab, which shows the DOM after JavaScript has run). Search for your tracker’s filename.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not there&lt;/strong&gt; → the script was never enqueued. This is not an optimizer problem. Check that the plugin is active, that you aren’t excluded from tracking as a logged-in admin, and that consent (if required) has been granted.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There, but the &lt;code&gt;src&lt;/code&gt; points at something like &lt;code&gt;/wp-content/cache/min/1/xxxx.js&lt;/code&gt;&lt;/strong&gt; → your tracker was merged into a combined bundle. Go to the Minify/Combine section below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There, but the &lt;code&gt;type&lt;/code&gt; attribute is not &lt;code&gt;text/javascript&lt;/code&gt;&lt;/strong&gt; — you see &lt;code&gt;type="rocketlazyloadscript"&lt;/code&gt;, &lt;code&gt;type="lazyload"&lt;/code&gt;, &lt;code&gt;data-src&lt;/code&gt; instead of &lt;code&gt;src&lt;/code&gt;, or similar → the script is being &lt;em&gt;delayed&lt;/em&gt;. Go to the Delay section.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There, with &lt;code&gt;defer&lt;/code&gt; or &lt;code&gt;async&lt;/code&gt; added&lt;/strong&gt; → go to the Defer section.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;2. Does the request actually leave the browser?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;DevTools → Network → filter by XHR/Fetch, then click around and scroll for ten seconds. You are looking for the POST that ships events back to your site (&lt;code&gt;admin-ajax.php&lt;/code&gt;, a REST route, or your vendor’s endpoint).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No request at all&lt;/strong&gt; → the tracker never initialised. Confirmed script-level problem, continue below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request fires but returns 4xx&lt;/strong&gt; → different problem: expired nonce or blocked REST route, usually caused by a &lt;em&gt;page&lt;/em&gt; cache serving stale HTML rather than a JS optimizer. See “The page-cache trap”.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request fires and returns 200&lt;/strong&gt; → your tracking works. Your problem is elsewhere: filtering, bot exclusion, or admin-user exclusion.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. Is the config object defined?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In the console, type the name of the global config variable your tracker uses. Most tools localize their settings into a global (&lt;code&gt;window.hj&lt;/code&gt;, &lt;code&gt;window.clarity&lt;/code&gt;, &lt;code&gt;window.optiBehaviorHeatmapConfig&lt;/code&gt;, etc.).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;undefined&lt;/code&gt; while the script file itself is present&lt;/strong&gt; → this is the failure almost everyone misses. The external &lt;code&gt;.js&lt;/code&gt; file was correctly excluded from optimization, but the small &lt;strong&gt;inline&lt;/strong&gt; &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; block containing its settings was not. The tracker loads, finds no configuration, and exits silently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That third case is worth stating plainly, because it wastes more hours than the other two combined: &lt;strong&gt;excluding your tracker’s file is only half the job. The inline config block has to be excluded too, and it is matched by a different rule.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The four ways an optimizer kills a tracker
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Delay JavaScript until user interaction
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F95wntrfn638h0vwyo9j9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F95wntrfn638h0vwyo9j9.png" width="799" height="646"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The most destructive one, and the most popular — it’s the single biggest PageSpeed win an optimizer can sell you. All non-essential JS is held back until the visitor moves the mouse, scrolls, or touches the screen.&lt;/p&gt;

&lt;p&gt;For a tracker, that is fatal by design: everything that happens &lt;em&gt;before&lt;/em&gt; the first interaction is exactly what you were trying to measure. Landing, above-the-fold reading time, immediate bounces, and any click that lands before the delay releases — all lost. A visitor who arrives and leaves in four seconds without touching anything is invisible.&lt;/p&gt;

&lt;h3&gt;
  
  
  Defer
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;defer&lt;/code&gt; moves execution to after HTML parsing. Less brutal than delay, but it inverts load order. If your tracker depends on another script (an ID broker, a consent layer, a queue stub) and only one of the two is deferred, the dependent one runs first and finds nothing.&lt;/p&gt;

&lt;p&gt;The characteristic symptom is not empty data — it’s &lt;em&gt;wrong&lt;/em&gt; data. A tracker whose session-ID source isn’t ready when it runs will usually fall back to minting a fresh random ID. You get one brand-new “session” per page view: inflated visitor counts, average session duration collapsing toward zero, funnels that never link two steps together. Everything looks alive, and everything is wrong.&lt;/p&gt;

&lt;p&gt;Empty heatmaps get noticed. This doesn’t. Check for it specifically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Minify and combine
&lt;/h3&gt;

&lt;p&gt;Several JS files are concatenated into one bundle and rewritten. Trackers break here for two reasons: the rewrite can mangle the code, and the file the browser now loads has a different URL — so any exclusion rule you wrote based on the original filename no longer matches anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Concatenate / lazy-load / render-blocking removal
&lt;/h3&gt;

&lt;p&gt;Feature names differ, mechanism is the same as the two above. Jetpack Boost concatenates. Cloudflare Rocket Loader defers everything through its own scheduler. NitroPack does the work on an external service, so no local rule can reach it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The page-cache trap (bonus, and it’s a real one)
&lt;/h3&gt;

&lt;p&gt;Full-page caches — WP Super Cache, Cache Enabler, and the page-cache half of &lt;a href="https://wp-rocket.me/" rel="noopener noreferrer"&gt;WP Rocket&lt;/a&gt; or LiteSpeed — don’t touch your JavaScript at all. They cause a different failure: &lt;strong&gt;stale HTML with an expired security nonce&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Your tracker’s POST includes a nonce that WordPress generated when the page was rendered. If that page has been sitting in the cache for two days, the nonce is dead and the server rejects every event with a 403. Same silence, completely different cause. If diagnostic step 2 showed a 4xx, this is your problem — not JS optimization.&lt;/p&gt;

&lt;p&gt;The same trap ruins &lt;a href="https://optiuser.com/blog/self-hosted-ab-testing-wordpress-technical-guide/" rel="noopener noreferrer"&gt;A/B tests&lt;/a&gt;. A page cached while showing variant B gets served to every subsequent visitor, including the ones assigned variant A. Your test results become noise. The fix is a cache-vary cookie (below), not an exclusion rule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixes, plugin by plugin
&lt;/h2&gt;

&lt;p&gt;Add these to your child theme’s &lt;code&gt;functions.php&lt;/code&gt; or a small mu-plugin. Replace &lt;code&gt;your-tracker.js&lt;/code&gt; and &lt;code&gt;yourTrackerConfig&lt;/code&gt; with your actual filename and inline config variable name.&lt;/p&gt;

&lt;p&gt;If you’d rather not write code: every one of these plugins exposes the same lists in its settings UI (usually under &lt;em&gt;File Optimization&lt;/em&gt; → &lt;em&gt;Excluded files&lt;/em&gt; or similar). The code path is more reliable because a plugin update or a settings reset can’t quietly wipe it.&lt;/p&gt;

&lt;h3&gt;
  
  
  WP Rocket
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Minify/Combine JS and Defer JS — regex fragments matched against the URL.&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'rocket_exclude_js'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'my_tracker_excludes'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'rocket_exclude_defer_js'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'my_tracker_excludes'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;my_tracker_excludes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$excluded&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$excluded&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'(.*)your-tracker\.js'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$excluded&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Delay JavaScript execution — plain strings, matched against BOTH&lt;/span&gt;
&lt;span class="c1"&gt;// external script URLs and inline script content.&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'rocket_delay_js_exclusions'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$excluded&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$excluded&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your-tracker.js'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$excluded&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'yourTrackerConfig'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// the inline config block&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$excluded&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Inline config blocks, for Defer and Combine separately.&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'rocket_defer_inline_exclusions'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'yourTrackerConfig'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'rocket_excluded_inline_js_content'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'yourTrackerConfig'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The trap that costs people weeks:&lt;/strong&gt; &lt;code&gt;rocket_exclude_js&lt;/code&gt; and &lt;code&gt;rocket_exclude_defer_js&lt;/code&gt; entries are &lt;strong&gt;not&lt;/strong&gt; self-delimited regex patterns. WP Rocket joins your entries with &lt;code&gt;|&lt;/code&gt; and wraps the whole set in its own &lt;code&gt;#(...)#&lt;/code&gt; delimiters before matching. Write &lt;code&gt;/my-tracker\.js/&lt;/code&gt; with slashes, as you would for &lt;code&gt;preg_match()&lt;/code&gt;, and those slashes become &lt;strong&gt;literal characters the URL must contain&lt;/strong&gt;. A real enqueued URL ends in &lt;code&gt;.js?ver=1.2.3&lt;/code&gt;, never in a literal &lt;code&gt;/&lt;/code&gt;, so the pattern silently matches nothing — and you will swear you excluded the file, because you did.&lt;/p&gt;

&lt;p&gt;Emit bare fragments. No delimiters.&lt;/p&gt;

&lt;h3&gt;
  
  
  LiteSpeed Cache
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'litespeed_optimize_js_excludes'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your-tracker.js'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Defer exclusion — matched against inline content too, so feed both.&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'litespeed_optm_js_defer_exc'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your-tracker.js'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'yourTrackerConfig'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  SiteGround Optimizer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'sgo_javascript_combine_exclude'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'my_tracker_handles'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'sgo_js_minify_exclude'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="s1"&gt;'my_tracker_handles'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'sgo_js_async_exclude'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="s1"&gt;'my_tracker_handles'&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;my_tracker_handles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$exclude_list&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$exclude_list&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your-tracker-handle'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// enqueue HANDLE, not filename&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$exclude_list&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'sgo_javascript_combine_excluded_inline_content'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'yourTrackerConfig'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The trap:&lt;/strong&gt; all three SG Optimizer filters compare against the &lt;strong&gt;&lt;code&gt;wp_enqueue_script&lt;/code&gt; handle&lt;/strong&gt;, not the filename. Their own documentation shows &lt;code&gt;$exclude_list[] = 'script-handle'&lt;/code&gt;. Pass &lt;code&gt;your-tracker.js&lt;/code&gt; and the filter runs, returns cleanly, and excludes precisely nothing. This one is invisible in testing because the code &lt;em&gt;looks&lt;/em&gt; correct.&lt;/p&gt;

&lt;p&gt;If you don’t know your handle: search the plugin’s source for &lt;code&gt;wp_enqueue_script(&lt;/code&gt; — it’s the first argument.&lt;/p&gt;

&lt;h3&gt;
  
  
  Autoptimize
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'autoptimize_filter_js_exclude'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$exclude&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$exclude&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;', your-tracker.js'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the format: a &lt;strong&gt;comma-separated string&lt;/strong&gt;, not an array. Returning an array here breaks the filter.&lt;/p&gt;

&lt;h3&gt;
  
  
  Perfmatters
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="k"&gt;array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'delay'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'defer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'minify'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$stage&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s2"&gt;"perfmatters_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$stage&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_js_exclusions"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your-tracker.js'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  WP-Optimize
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'wp-optimize-minify-default-exclusions'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your-tracker.js'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The trap:&lt;/strong&gt; matched case-insensitively as a substring &lt;strong&gt;against the full script URL&lt;/strong&gt;. Enqueue handles never appear in a URL, so passing a handle here silently does nothing. The mirror image of the SiteGround mistake — which is exactly why people who fix one site break the next.&lt;/p&gt;

&lt;h3&gt;
  
  
  W3 Total Cache
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'w3tc_minify_js_do_tag_minification'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$do&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nb"&gt;strpos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$tag&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'your-tracker.js'&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$do&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Breeze
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'breeze_js_dontmove'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'your-tracker.js'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Jetpack Boost
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'js_do_concat'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$do_concat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$handle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'your-tracker-handle'&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nv"&gt;$handle&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$do_concat&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Concatenation is filterable. &lt;strong&gt;Render-blocking/defer is not&lt;/strong&gt; — Boost’s only documented exclusion mechanism there is the &lt;code&gt;data-jetpack-boost="ignore"&lt;/code&gt; tag attribute. See below.&lt;/p&gt;

&lt;h3&gt;
  
  
  NitroPack, WP Fastest Cache, FlyingPress, Hummingbird, Swift Performance
&lt;/h3&gt;

&lt;p&gt;No reliable PHP exclusion filter is publicly documented for these. NitroPack in particular optimizes on its own external service, so no local hook can intercept it. Use the settings UI, and rely on tag attributes.&lt;/p&gt;

&lt;h3&gt;
  
  
  The universal fallback: tag attributes
&lt;/h3&gt;

&lt;p&gt;When no filter exists, mark the script tag itself. Six attributes cover most of the ecosystem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'script_loader_tag'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$handle&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'your-tracker-handle'&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$handle&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$tag&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;str_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'&amp;lt;script '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'&amp;lt;script nowprocket data-cfasync="false" data-noptimize="1" '&lt;/span&gt;
        &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s1"&gt;'data-no-optimize="1" data-no-defer="1" data-jetpack-boost="ignore" '&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nv"&gt;$tag&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;nowprocket&lt;/code&gt; — WP Rocket&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data-cfasync="false"&lt;/code&gt; — Cloudflare Rocket Loader&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data-noptimize="1"&lt;/code&gt; — Autoptimize, Breeze (no hyphen)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data-no-optimize="1"&lt;/code&gt; — LiteSpeed and others (with hyphen — yes, you need both)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data-no-defer="1"&lt;/code&gt; — generic&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;data-jetpack-boost="ignore"&lt;/code&gt; — Jetpack Boost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Important limitation:&lt;/strong&gt; attributes only protect optimizations that inspect the rendered tag. Optimizations that match against the &lt;strong&gt;URL&lt;/strong&gt; — WP Rocket’s minify and defer among them — never see these attributes. Attributes are a safety net, not a substitute for the filters above.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fixing the page-cache trap
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// WP Rocket: vary the page cache by a cookie your tracker sets.&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'rocket_cache_dynamic_cookies'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$cookies&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$cookies&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'my_ab_variant'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$cookies&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// LiteSpeed equivalent.&lt;/span&gt;
&lt;span class="nf"&gt;add_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'litespeed_vary_cookies'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$cookies&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$cookies&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'my_ab_variant'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$cookies&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For expired nonces on long-cached pages, the durable fix belongs in your tracker: retry once on a 403 after re-reading a fresh nonce. If you’re using a hosted tool, you can only shorten the cache lifetime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the fix properly
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjzwb809se864723oy098.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjzwb809se864723oy098.png" alt="Heatmap not working after installing a cache plugin" width="799" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Before you trust any fix for a heatmap not working, verify it properly. Applying an exclusion and seeing data appear proves nothing on its own — you may simply have generated traffic while logged in, or the page you tested may not have been cached yet. Do this instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Purge every cache.&lt;/strong&gt; Plugin cache, host cache (SiteGround, Kinsta, WP Engine all have their own), and Cloudflare if it’s in front.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reload as a logged-out visitor&lt;/strong&gt; in a private window.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;View source and confirm three things:&lt;/strong&gt; your tracker’s &lt;code&gt;src&lt;/code&gt; points at the original file, not a &lt;code&gt;/cache/min/&lt;/code&gt; bundle; the &lt;code&gt;type&lt;/code&gt; is &lt;code&gt;text/javascript&lt;/code&gt;; the inline config block is present and unmangled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In the console, check the config global is defined.&lt;/strong&gt; Not &lt;code&gt;undefined&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;In the Network tab, confirm the event POST fires and returns 200.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Then check the dashboard&lt;/strong&gt; — with the same date filter as your test, and with any “exclude admins” setting in mind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-test on the pages that matter&lt;/strong&gt;, not just the homepage. Checkout, product, and landing pages often have different scripts and different cache rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Re-test after your next optimizer update.&lt;/strong&gt; Exclusion lists get reset by major version upgrades more often than you’d expect.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Choosing a tool? See our &lt;a href="https://optiuser.com/blog/wordpress-heatmap-plugins-comparison/" rel="noopener noreferrer"&gt;WordPress heatmap plugin comparison&lt;/a&gt; of 8 options.&lt;/p&gt;

&lt;h2&gt;
  
  
  You should never have had to read this
&lt;/h2&gt;

&lt;p&gt;A heatmap not working for weeks with no warning is the real failure here. Every step above is diagnosis you performed because your tool stayed quiet. That’s the real defect. Not the optimizer — optimizers are doing exactly what you installed them to do. The defect is that a tracking tool can lose 100% of its data for six weeks and report nothing but an empty chart.&lt;/p&gt;

&lt;p&gt;There’s no technical reason for that silence. The tool knows the site had traffic. It knows which of its modules are enabled. If traffic is arriving and a module’s event table stays at zero, it has everything it needs to raise its hand.&lt;/p&gt;

&lt;p&gt;That’s the check we built into Opti-Behavior in 1.7.1. Every six hours it compares “the site received page views in the last 24 hours” against “this module recorded at least one event”, per module. If traffic is flowing and events aren’t, it shows an admin notice, names which modules went dark, and identifies the active cache or optimization plugin most likely responsible — from a list of nineteen. Sites with no traffic never alert, so a quiet site can’t trigger a false alarm. It reads nothing but &lt;code&gt;COUNT(*)&lt;/code&gt; aggregates, adds no frontend code, and runs at most once every six hours.&lt;/p&gt;

&lt;p&gt;Click around a real dashboard before you install anything&lt;/p&gt;

&lt;p&gt;A live WordPress site with real traffic, real heatmaps and real recordings. No signup, no credit card, no email.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://demo.optiuser.com/" rel="noopener noreferrer"&gt;Open the live demo →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Alongside it, the exclusion rules on this page ship pre-registered for all the optimizers listed above — including the handle-versus-filename distinction that makes the SiteGround and WP-Optimize filters no-ops when you get them backwards. We had both of those backwards ourselves until July 2026. That’s how we know how invisible it is.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://optiuser.com/" rel="noopener noreferrer"&gt;Opti-Behavior&lt;/a&gt; is a self-hosted WordPress plugin: heatmaps, funnels, session recordings, A/B testing and form analytics, with the data staying in your own database. &lt;a href="https://wordpress.org/plugins/opti-behavior/" rel="noopener noreferrer"&gt;See the plugin on WordPress.org →&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Whatever you use, apply the exclusions, and then go check the last thirty days of data you assumed was being collected. Using Opti-Behavior? Run &lt;a href="https://optiuser.com/docs/verify-tracking-first-10-minutes/" rel="noopener noreferrer"&gt;First 10 minutes: verify tracking is working&lt;/a&gt;, then see &lt;a href="https://optiuser.com/docs/cache-plugin-compatibility/" rel="noopener noreferrer"&gt;Cache plugin compatibility&lt;/a&gt; for what is handled automatically.&lt;/p&gt;

&lt;p&gt;This article was drafted with the help of AI tools and reviewed by The Opti-Behavior Team.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>performance</category>
    </item>
    <item>
      <title>Cookieless Analytics for WordPress: How Session-Based Tracking Works, and How to Prove a Plugin Sets Zero Cookie</title>
      <dc:creator>Hamdi Bk</dc:creator>
      <pubDate>Sat, 05 Sep 2026 10:38:02 +0000</pubDate>
      <link>https://dev.to/opti-behavior/cookieless-analytics-for-wordpress-how-session-based-tracking-works-and-how-to-prove-a-plugin-16go</link>
      <guid>https://dev.to/opti-behavior/cookieless-analytics-for-wordpress-how-session-based-tracking-works-and-how-to-prove-a-plugin-16go</guid>
      <description>&lt;p&gt;Every WordPress analytics plugin says "GDPR compliant" on its page. Most of them still leave a cookie. Some leave nothing in the cookie jar and hide a note in &lt;code&gt;localStorage&lt;/code&gt; instead. A few build a fingerprint of your device.&lt;/p&gt;

&lt;p&gt;This post explains the one design that really needs no consent banner, and shows you how to test any plugin yourself in one minute.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzbvcv04n5jly0xz7i7q7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzbvcv04n5jly0xz7i7q7.png" alt=" wp Plugin Sets Zero Cookie" width="800" height="430"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The cookie is the problem, not the counting
&lt;/h2&gt;

&lt;p&gt;Europe's privacy rule (ePrivacy, Art. 5(3)) is simple: if you &lt;strong&gt;write or read something on the visitor's device&lt;/strong&gt;, you must ask first. Counting pageviews is not the problem. Leaving a note in the browser is.&lt;/p&gt;

&lt;p&gt;Three ways to leave a note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a cookie (&lt;code&gt;document.cookie&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;other browser storage: &lt;code&gt;localStorage&lt;/code&gt;, &lt;code&gt;sessionStorage&lt;/code&gt;, IndexedDB&lt;/li&gt;
&lt;li&gt;a fingerprint: nothing is written, but the script reads your screen size, fonts and browser details and mixes them into a secret ID. Privacy authorities treat this as worse than a cookie, because you can't see it or delete it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A plugin that does none of the three can count visitors without a banner. France's privacy authority (CNIL) already allows this kind of audience measurement without consent, under conditions.&lt;/p&gt;

&lt;h2&gt;
  
  
  How session-based tracking works
&lt;/h2&gt;

&lt;p&gt;The idea: the visitor's ID lives only in the page's memory, while the tab is open. It is never saved anywhere.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified. Runs once per page load.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;     &lt;span class="c1"&gt;// exists only in this variable&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendBeacon&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/wp-json/analytics/v1/event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;referrer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pageview&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pageX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pageY&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Close the tab, the ID disappears. There is nothing left to ask permission for.&lt;/p&gt;

&lt;p&gt;On the server, WordPress writes the data into its own database. Before saving, it cuts the visitor's internet address (IP) short, so nobody can be identified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified. IPv4: zero the last number. IPv6: keep the first 48 bits.&lt;/span&gt;
&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;ob_anonymize_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$ip&lt;/span&gt; &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;filter_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;FILTER_VALIDATE_IP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;FILTER_FLAG_IPV4&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;preg_replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s1"&gt;'/\.\d+$/'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'.0'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$ip&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;$bin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;inet_pton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$ip&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;inet_ntop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nb"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="nv"&gt;$bin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;str_repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\0&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You still know which country the visitor is from. You can't find the person.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you lose:&lt;/strong&gt; the plugin can't be sure that today's visitor is the same person as yesterday. It makes a good guess. For traffic, funnels and heatmaps, that's enough. For following one person over months, it isn't, and you shouldn't do that without asking anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you gain:&lt;/strong&gt; the 35–45% of European visitors who click "Reject" are now counted. And they're not just anyone: Firefox and Brave users, people with ad blockers, developers. If your visitors are technical, cookie-based analytics is measuring the wrong crowd.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test any plugin in 60 seconds
&lt;/h2&gt;

&lt;p&gt;Open your site in a private window, press F12, and paste this in the console:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Run before and after the analytics script loads&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cookie&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionStorage&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;indexedDB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;databases&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dbs&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IndexedDB:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dbs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open the Network tab and look at what the plugin's script sends out. If you see scrambled &lt;code&gt;navigator&lt;/code&gt; or canvas data leaving, that's a fingerprint.&lt;/p&gt;

&lt;p&gt;Try it on Google Analytics, Hotjar or Clarity: you'll see &lt;code&gt;_ga&lt;/code&gt;, &lt;code&gt;_hjSession*&lt;/code&gt;, &lt;code&gt;_clck&lt;/code&gt;. Try it on a real cookieless plugin: every list is empty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where I land
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://optiuser.com/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=cookieless-session" rel="noopener noreferrer"&gt;Opti-Behavior&lt;/a&gt;) on exactly this model: ID in memory only, no &lt;code&gt;localStorage&lt;/code&gt;, no fingerprint, IP cut short before saving, everything stored in your WordPress database with no outside server, a ~15 KB script that never slows the page, robot filtering. Heatmaps and funnels are free. Session recordings (replaying one visitor) are Pro and run on a separate track, because replaying a person does need a "yes", cookies or not.&lt;/p&gt;

&lt;p&gt;Full write-up, including what the CNIL says and why fingerprinting is a trap: &lt;a href="https://optiuser.com/blog/wordpress-visitor-tracking-without-cookies/?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=cookieless-session" rel="noopener noreferrer"&gt;How to Track WordPress Visitors Without Cookies (GDPR Compliant)&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Run the 60-second test on your current setup and post what comes back. I'm collecting examples of "cookieless" plugins that aren't.&lt;/p&gt;

</description>
      <category>wordpress</category>
      <category>privacy</category>
      <category>javascript</category>
      <category>php</category>
    </item>
    <item>
      <title>Your A/B test has three goals and they disagree. Now what?</title>
      <dc:creator>Hamdi Bk</dc:creator>
      <pubDate>Sat, 01 Aug 2026 18:37:51 +0000</pubDate>
      <link>https://dev.to/opti-behavior/your-ab-test-has-three-goals-and-they-disagree-now-what-4hj4</link>
      <guid>https://dev.to/opti-behavior/your-ab-test-has-three-goals-and-they-disagree-now-what-4hj4</guid>
      <description>&lt;p&gt;Every A/B testing tutorial ends the same way: run the test, wait for significance, ship the winner.&lt;/p&gt;

&lt;p&gt;Then you run a real test and variant B converts 12% better on newsletter signups, brings in 4% less revenue per visitor, and bounce is flat. Nothing is significant except the signups. Ship it?&lt;/p&gt;

&lt;p&gt;I spent an embarrassing amount of time on this question while building an A/B engine, and most of what I read online didn't help, because most of it assumes one metric. This post is what I ended up with. It's not novel — the statistics are decades old — but I couldn't find it written down in one place with working code, so here it is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the p-value doesn't answer the question you're asking
&lt;/h2&gt;

&lt;p&gt;Two problems, and the second one is the bad one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multiple comparisons.&lt;/strong&gt; Three metrics at α = 0.05 means roughly a 14% chance of at least one false positive if nothing is actually different. Bonferroni fixes this, but now you need α = 0.017 per metric and your test needs to run three times as long. On a site doing 300 conversions a month that's not a fix, it's a refusal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The p-value is answering a different question.&lt;/strong&gt; It tells you the probability of your data assuming no difference exists. What you actually want to know is: if I ship B, how much do I expect to lose if I'm wrong? Those are not the same question and no amount of Bonferroni turns one into the other.&lt;/p&gt;

&lt;p&gt;There's also the peeking problem — everyone checks the dashboard daily and stops when it goes green, which quietly inflates the false positive rate well past whatever α you wrote down. I'll come back to that, because Bayesian methods do &lt;em&gt;not&lt;/em&gt; magically solve it, whatever you may have read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Posterior first, decision second
&lt;/h2&gt;

&lt;p&gt;For a conversion rate, the Beta-Binomial conjugate pair gives you the posterior in one line. With a uniform prior, after &lt;code&gt;c&lt;/code&gt; conversions out of &lt;code&gt;n&lt;/code&gt; visitors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight r"&gt;&lt;code&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;~&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Beta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. No closed-form comparison between two Betas that's worth implementing, so sample.&lt;/p&gt;

&lt;p&gt;PHP has no Beta sampler in core, and no Gamma sampler either, so you build one. Marsaglia–Tsang, which needs a normal sampler underneath:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;normal_sample&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Box–Muller&lt;/span&gt;
    &lt;span class="nv"&gt;$u1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;mt_rand&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;mt_getrandmax&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$u2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;mt_rand&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;mt_getrandmax&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$u1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="no"&gt;M_PI&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$u2&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;gamma_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="nv"&gt;$shape&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$shape&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;mt_rand&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;mt_getrandmax&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;gamma_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$shape&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;$shape&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;$d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$shape&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$c&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;9.0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;normal_sample&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nv"&gt;$v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$c&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$x&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="nv"&gt;$v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$v&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$v&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nv"&gt;$u&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;mt_rand&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nb"&gt;mt_getrandmax&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$u&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.0331&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$v&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$d&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$v&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;beta_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gamma_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$a&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;gamma_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$x&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the single-metric comparison:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$draws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$wins&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$loss_if_ship_b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$draws&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$pA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;beta_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$cA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$nA&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$cA&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$pB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;beta_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$cB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$nB&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$cB&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$pB&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$pA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$wins&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;$loss_if_ship_b&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$pA&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$pB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$prob_b_better&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$wins&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;$draws&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$expected_loss&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$loss_if_ship_b&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;$draws&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two numbers instead of one. &lt;code&gt;$prob_b_better&lt;/code&gt; is the intuitive one everybody quotes. &lt;strong&gt;&lt;code&gt;$expected_loss&lt;/code&gt; is the one that should drive the decision&lt;/strong&gt;: it's the average amount of conversion rate you give up, across the whole posterior, in the worlds where B is actually worse. If that number is 0.0004 and you genuinely don't care about four hundredths of a percentage point, ship B and stop thinking about it, even at 88% probability.&lt;/p&gt;

&lt;p&gt;Pick that threshold &lt;em&gt;before&lt;/em&gt; the test. Write it in the test config. It's the smallest effect you'd bother shipping for, and it forces a conversation about what the test is actually for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Revenue per visitor is not a Beta
&lt;/h2&gt;

&lt;p&gt;This is where I initially got it wrong. I treated revenue per visitor as a conversion-like quantity and got posteriors that were far too confident.&lt;/p&gt;

&lt;p&gt;Revenue per visitor is zero-inflated (most visitors buy nothing) and heavy-tailed (one enterprise order distorts everything). A Beta is wrong, a Normal is wrong, a log-normal is closer but still assumes away the zeros.&lt;/p&gt;

&lt;p&gt;I use a bootstrap instead. Resample the observed per-visitor revenue values with replacement, &lt;code&gt;$draws&lt;/code&gt; times, take the mean of each resample. That's your posterior-ish distribution, and it inherits whatever ugly shape your real data has without you having to name it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;bootstrap_means&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$draws&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$values&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$d&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$draws&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$d&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$sum&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nv"&gt;$values&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;mt_rand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)];&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$sum&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's O(draws × n) and it will hurt on large samples. Precompute a cumulative array and sample indices in blocks, or bootstrap on a fixed random subsample of 5,000 visitors per arm — the extra Monte Carlo noise is small compared to the sampling noise you're already living with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combining metrics without lying to yourself
&lt;/h2&gt;

&lt;p&gt;Now the actual question. Three metrics, three posteriors, one decision.&lt;/p&gt;

&lt;p&gt;The standard answer is an OEC — Overall Evaluation Criterion, from Kohavi's work at Microsoft. One weighted composite, agreed in advance, that the test is scored against. The two things people get wrong:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Weight relative uplift, not raw values.&lt;/strong&gt; A conversion rate lives in [0,1], revenue per visitor might be €3.40, bounce rate is a percentage. Summing those with weights is meaningless. Convert each to relative uplift first, so everything is dimensionless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Compute the composite inside the loop.&lt;/strong&gt; This one matters and it's easy to miss. If you compute &lt;code&gt;P(conv_B &amp;gt; conv_A)&lt;/code&gt;, &lt;code&gt;P(rev_B &amp;gt; rev_A)&lt;/code&gt; separately and then combine the summary numbers, you've thrown away the correlation between metrics. The draws are paired: draw &lt;code&gt;i&lt;/code&gt; represents one coherent hypothetical world. Score the composite in that world, then aggregate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$draws&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$w&lt;/span&gt;      &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'conv'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'rev'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'bounce'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// must sum to 1&lt;/span&gt;
&lt;span class="nv"&gt;$rev_A&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bootstrap_means&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$revenue_A&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$draws&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$rev_B&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bootstrap_means&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$revenue_B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$draws&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$oec_positive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$oec_loss&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nv"&gt;$draws&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$pA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;beta_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$cA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$nA&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$cA&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$pB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;beta_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$cB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$nB&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$cB&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$bA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;beta_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$bounceA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$nA&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$bounceA&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$bB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;beta_sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$bounceB&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$nB&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$bounceB&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$u_conv&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$pB&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$pA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;$pA&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nv"&gt;$u_rev&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt;  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$rev_B&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$rev_A&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;$rev_A&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nv"&gt;$u_bounce&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nv"&gt;$bB&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nv"&gt;$bA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;$bA&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// lower bounce is better, so flip&lt;/span&gt;

    &lt;span class="nv"&gt;$oec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'conv'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$u_conv&lt;/span&gt;
         &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'rev'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$u_rev&lt;/span&gt;
         &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$w&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'bounce'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nv"&gt;$u_bounce&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$oec&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$oec_positive&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nv"&gt;$oec_loss&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nv"&gt;$oec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;$prob_b_better&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$oec_positive&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;$draws&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$expected_loss&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$oec_loss&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nv"&gt;$draws&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// in relative-uplift units&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ship B when &lt;code&gt;$expected_loss&lt;/code&gt; is below your threshold of caring. Not when &lt;code&gt;$prob_b_better&lt;/code&gt; crosses 95%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix the weights before you look at any data.&lt;/strong&gt; Otherwise you will — I promise you will — nudge them until your preferred variant wins. If you can't decide the weights before the test, you don't have a hypothesis, you have a hope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardrails are not goals
&lt;/h2&gt;

&lt;p&gt;Some metrics don't belong in the composite at all. Page load time, JS error rate, checkout failures — these are guardrails, and no amount of signup uplift should be allowed to buy a regression in them.&lt;/p&gt;

&lt;p&gt;They get a veto, not a weight:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$p95_load_B&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$p95_load_A&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Decision&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BLOCKED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// no composite, no discussion&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mixing a guardrail into the OEC is how you ship a variant that converts 8% better and breaks checkout for Safari.&lt;/p&gt;

&lt;h2&gt;
  
  
  The peeking problem is still there
&lt;/h2&gt;

&lt;p&gt;You will read that Bayesian methods let you monitor continuously without penalty. That's half true and the half that's false will burn you.&lt;/p&gt;

&lt;p&gt;Expected loss is a decision-theoretic quantity, so stopping when it falls under your threshold isn't the same statistical sin as stopping when a p-value dips below 0.05. But with tiny samples the posterior is wide and jumpy, and "expected loss happens to be low right now" is a thing that occurs by chance early in a test.&lt;/p&gt;

&lt;p&gt;The pragmatic fix I settled on: a hard minimum before any decision is offered at all. Both arms need a floor of visitors &lt;em&gt;and&lt;/em&gt; a floor of conversions — conversions are what actually carry the information, and 50,000 visitors with 11 conversions tells you almost nothing. Below the floor the UI shows the posterior and refuses to say anything else.&lt;/p&gt;

&lt;p&gt;Not elegant. Works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance, since this runs on shared hosting
&lt;/h2&gt;

&lt;p&gt;20,000 draws × 4 Beta samples × 2 arms is a lot of &lt;code&gt;mt_rand()&lt;/code&gt; calls in PHP, and it is nowhere near free on a €4/month host.&lt;/p&gt;

&lt;p&gt;What made it survivable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run it on a scheduled job, not on dashboard load. Cache the decision payload.&lt;/li&gt;
&lt;li&gt;Aggregate counts (&lt;code&gt;$nA&lt;/code&gt;, &lt;code&gt;$cA&lt;/code&gt;, …) in a summary table updated incrementally. Never scan the raw events table to compute a posterior.&lt;/li&gt;
&lt;li&gt;Drop to 10,000 draws. The Monte Carlo error on &lt;code&gt;$prob_b_better&lt;/code&gt; at 10k draws is about ±0.5 percentage points, which is invisible next to the uncertainty in your data.&lt;/li&gt;
&lt;li&gt;Bail out early when the result isn't close. If the two 99% intervals don't overlap, you don't need the simulation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On my setup, the full three-metric run at 20k draws takes &lt;strong&gt;[X] ms&lt;/strong&gt; for &lt;strong&gt;[N]&lt;/strong&gt; visitors per arm, down from &lt;strong&gt;[Y] ms&lt;/strong&gt; before the summary table. Replace these with your real numbers or delete the paragraph — made-up benchmarks are worse than no benchmarks.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell past me
&lt;/h2&gt;

&lt;p&gt;The statistics were the easy part. The hard part was accepting that "which variant won?" is a badly formed question, and that the honest output of an A/B test is a distribution plus a decision rule you committed to in advance — not a green checkmark.&lt;/p&gt;

&lt;p&gt;The engine got simpler once I stopped trying to produce certainty and started producing an expected cost of being wrong.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm the author of Opti-Behavior, a self-hosted analytics plugin, and the decision engine described here is what's running inside its A/B testing module — so take my enthusiasm for this approach with the appropriate grain of salt. The maths is standard and framework-agnostic; port it wherever you like. Happy to argue about the weighting scheme in the comments, it's the part I'm least sure about.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>statistics</category>
      <category>privacy</category>
      <category>php</category>
      <category>wordpress</category>
    </item>
  </channel>
</rss>
