{"id":11191,"date":"2026-05-27T18:30:05","date_gmt":"2026-05-27T10:30:05","guid":{"rendered":"https:\/\/aeosub.com\/variolab-ab-testing\/"},"modified":"2026-05-27T18:30:05","modified_gmt":"2026-05-27T10:30:05","slug":"variolab-ab-testing","status":"publish","type":"post","link":"https:\/\/aeosub.com\/zh\/variolab-ab-testing\/","title":{"rendered":"Variolab \u2013 A\/B Testing"},"content":{"rendered":"<div class=\"post-img\" style=\"margin-bottom: 25px\"><img decoding=\"async\" src=\"https:\/\/ps.w.org\/variolab-ab-testing\/assets\/banner-1544x500.png?rev=3550427\" alt=\"Variolab \u2013 A\/B Testing\" style=\"width: 100%;height: auto;border-radius: 12px\"><\/div>\n<div class=\"plugin-info-card\" style=\"border: 1px solid #e2e8f0;border-radius: 10px;overflow: hidden;margin-bottom: 30px;font-family: -apple-system, system-ui, sans-serif;background: #fff\">\n<table style=\"width: 100%;border-collapse: collapse;font-size: 14px\">\n<tr style=\"border-bottom: 1px solid #edf2f7\">\n<th style=\"padding: 15px 20px;text-align: left;width: 35%;color: #4a5568;background: #fcfcfc\"><img decoding=\"async\" src=\"https:\/\/ps.w.org\/variolab-ab-testing\/assets\/icon-256x256.png?rev=3550427\" style=\"width: 40px;height: 40px;border-radius: 6px;margin-right: 12px;vertical-align: middle\"> Developer<\/th>\n<td style=\"padding: 15px 20px;font-weight: 500\"><a href=\"https:\/\/profiles.wordpress.org\/lozit\/\" target=\"_blank\" rel=\"noopener\">Guillaume Ferrari<\/a><\/td>\n<\/tr>\n<tr style=\"border-bottom: 1px solid #edf2f7\">\n<th style=\"padding: 12px 20px;text-align: left;color: #4a5568;background: #fcfcfc\">Version<\/th>\n<td style=\"padding: 12px 20px\">v0.15.0<\/td>\n<\/tr>\n<tr style=\"border-bottom: 1px solid #edf2f7\">\n<th style=\"padding: 12px 20px;text-align: left;color: #4a5568;background: #fcfcfc\">Active Installs<\/th>\n<td style=\"padding: 12px 20px\">0 +<\/td>\n<\/tr>\n<tr style=\"border-bottom: 1px solid #edf2f7\">\n<th style=\"padding: 12px 20px;text-align: left;color: #4a5568;background: #fcfcfc\">Requires \/ Tested<\/th>\n<td style=\"padding: 12px 20px\">6.0 \/ 6.9.4<\/td>\n<\/tr>\n<tr style=\"border-bottom: 1px solid #edf2f7\">\n<th style=\"padding: 12px 20px;text-align: left;color: #4a5568;background: #fcfcfc\">Last Updated<\/th>\n<td style=\"padding: 12px 20px\">2026-05-27 9:58am GMT<\/td>\n<\/tr>\n<tr>\n<th style=\"padding: 12px 20px;text-align: left;color: #4a5568;background: #fcfcfc\">Official Link<\/th>\n<td style=\"padding: 12px 20px\"><a href=\"https:\/\/wordpress.org\/plugins\/variolab-ab-testing\/\" target=\"_blank\" style=\"text-decoration: none;color: #4a3bca;font-weight: bold\" rel=\"noopener\">View Details \u2192<\/a><\/td>\n<\/tr>\n<\/table><\/div>\n<div class=\"plugin-short-desc\" style=\"margin: 20px 0;padding: 12px 15px;background: #f9fafb;border-left: 4px solid #4a3bca;border-radius: 6px;font-style: italic;color: #4a5568\">Lightweight A\/B testing for pages: internal tracking, persistent-cookie 50\/50 split, GDPR-friendly. No third-party dependency.<\/div>\n<p>Run A\/B tests by pointing the plugin at two existing pages \u2014 one as the control (Variant A), one as the variant (Variant B). Visitors are split 50\/50 via a persistent cookie; once assigned, they always see the same variant.<\/p>\n<p>Tracking is fully internal: impressions and conversions land in a custom database table, and the wp-admin dashboard shows conversion rates, lift, and a basic statistical significance indicator (two-proportion z-test).<\/p>\n<p>Security-audited internally before every release (situated checklist + OWASP grid). See SECURITY.md on GitHub for the disclosure policy and the latest audit report (<code>docs\/security\/latest.md<\/code>).<\/p>\n<h4>Features<\/h4>\n<ul>\n<li>Page-level A\/B tests (entire page as variant \u2014 no Gutenberg surgery needed)<\/li>\n<li>Persistent cookie split (httponly, samesite=Lax)<\/li>\n<li>Internal tracking \u2014 no third-party dependency, no data leaving your site<\/li>\n<li>Conversion goals: URL visited or CSS selector clicked<\/li>\n<li>Auto bypass for logged-in editors and bots<\/li>\n<li>Two-proportion z-test for statistical significance<\/li>\n<li><code>abtest_event_logged<\/code> action hook ready for v2 GA4\/webhook integrations<\/li>\n<\/ul>\n<h3>Caching<\/h3>\n<p>A\/B testing breaks under page caching: the first variant served gets cached for everyone, all subsequent visitors get that same response, the 50\/50 split dies. The plugin handles most cases automatically.<\/p>\n<h4>What the plugin does automatically<\/h4>\n<ol>\n<li><strong>Sends <code>Cache-Control: no-store<\/code> headers<\/strong> on every page response under A\/B test. Respected by Cloudflare, Varnish, Kinsta edge cache, nginx page cache, and most server-level caches.<\/li>\n<li><strong>Hooks WP Rocket&#8217;s <code>rocket_cache_reject_uri<\/code> filter<\/strong> when WP Rocket is detected \u2014 your test URLs are auto-added to the never-cache list.<\/li>\n<li><strong>Hooks LiteSpeed Cache&#8217;s <code>litespeed_force_nocache_url<\/code> filter<\/strong> when LiteSpeed is detected \u2014 same idea.<\/li>\n<li><strong>Surfaces an admin notice<\/strong> when a cache plugin or known host (like Kinsta) is detected, with what to verify.<\/li>\n<\/ol>\n<h4>Hosting on Kinsta<\/h4>\n<p>Kinsta uses a two-layer cache (nginx server-cache + Cloudflare Enterprise edge cache). The plugin&#8217;s no-store headers bypass both \u2014 but for 100% safety, also add your test URLs to <strong>MyKinsta <span aria-hidden=\"true\" class=\"wp-exclude-emoji\">\u2192<\/span> Tools <span aria-hidden=\"true\" class=\"wp-exclude-emoji\">\u2192<\/span> Cache <span aria-hidden=\"true\" class=\"wp-exclude-emoji\">\u2192<\/span> Cache Bypass<\/strong> as URL Patterns (e.g. <code>^\/promo\/$<\/code>). After publishing a new test, <strong>purge the Kinsta cache<\/strong> to flush any version cached before the experiment started.<\/p>\n<p>Verify it works by inspecting headers on your test URL:<\/p>\n<pre><code>curl -I https:\/\/yoursite.com\/promo\/\n<\/code><\/pre>\n<p>Look for <code>X-Kinsta-Cache: BYPASS<\/code> (or <code>MISS<\/code>). If you see <code>HIT<\/code>, you&#8217;re getting the cached version and the split is broken \u2014 purge the cache.<\/p>\n<h4>Hosting on other CDNs \/ hosts<\/h4>\n<ul>\n<li><strong>Cloudflare APO<\/strong>: Cache-Control headers from origin override the cache. Should work out of the box. Verify with <code>curl -I<\/code> looking for <code>cf-cache-status: BYPASS<\/code> or <code>DYNAMIC<\/code>.<\/li>\n<li><strong>WP Engine<\/strong>: Add the test URLs to the &#8220;Cache Exclusions&#8221; list in your User Portal.<\/li>\n<li><strong>Pagely \/ Pantheon \/ Pressable<\/strong>: Cache-Control headers respected. Add manual URL exclusion in the host&#8217;s panel for safety.<\/li>\n<\/ul>\n<h4>Plugins not auto-supported<\/h4>\n<p>For W3 Total Cache, WP Super Cache, WP Fastest Cache, and Cache Enabler \u2014 the plugin shows a notice but does not automatically exclude URLs (no clean public API). Manually add your test URLs to that plugin&#8217;s cache exclusion list.<\/p>\n<h3>REST API<\/h3>\n<p>Pull stats programmatically from external tools (n8n, Make, Pipedream, dashboards).<\/p>\n<ul>\n<li>Endpoint: <code>GET \/wp-json\/abtest\/v1\/stats<\/code><\/li>\n<li>Auth: WP Application Passwords (Basic Auth). The user must have <code>manage_options<\/code>.<\/li>\n<li>Generate one in your WP profile <span aria-hidden=\"true\" class=\"wp-exclude-emoji\">\u2192<\/span> Application Passwords.<\/li>\n<\/ul>\n<p>Optional query params:<\/p>\n<ul>\n<li><code>url=\/promo\/<\/code> \u2014 filter to a single test URL.<\/li>\n<li><code>experiment_id=38<\/code> \u2014 fetch a single experiment by ID.<\/li>\n<li><code>status=running|paused|ended|draft<\/code> \u2014 filter by status.<\/li>\n<li><code>from=YYYY-MM-DD&amp;to=YYYY-MM-DD<\/code> \u2014 restrict event date range for the stats computation.<\/li>\n<li><code>breakdown=daily<\/code> \u2014 include per-day time series (for charting).<\/li>\n<\/ul>\n<p>Example:<\/p>\n<pre><code>curl -u 'admin:xxxx xxxx xxxx xxxx xxxx xxxx' 'https:\/\/yoursite.com\/wp-json\/abtest\/v1\/stats?status=running'\n<\/code><\/pre>\n<p>The response includes for each experiment: id, title, test_url, status, dates, control\/variant IDs, goal, and a stats block with A\/B impressions\/conversions\/rate, lift, p-value, significance, and 95% confidence interval bounds for both lift and absolute difference.<\/p>\n<h3>Privacy<\/h3>\n<p>The plugin stores no raw IP, no User-Agent, no email, no name, and no cross-site tracking identifier. The events table contains: experiment_id, variant, test_url, event_type, created_at, and a <code>visitor_hash<\/code> = first 16 hex chars (64 bits) of <code>sha256(IP + UA + wp_salt('auth'))<\/code> \u2014 non-reversible, single-site, salt-rotated, dedup-safe. Cookies are httponly, samesite=Lax, secure on HTTPS, value = a single letter (a\/b\/c\/d), 30-day TTL.<\/p>\n<p>A native WordPress privacy guide snippet is registered automatically \u2014 find it under Settings <span aria-hidden=\"true\" class=\"wp-exclude-emoji\">\u2192<\/span> Privacy <span aria-hidden=\"true\" class=\"wp-exclude-emoji\">\u2192<\/span> Policy Guide <span aria-hidden=\"true\" class=\"wp-exclude-emoji\">\u2192<\/span> Variolab \u2013 A\/B Testing to paste into your privacy policy.<\/p>\n<p>For consent-banner sites: enable &#8220;Require consent&#8221; in the plugin settings and wire your banner to the <code>abtest_visitor_has_consent<\/code> filter (return true to track, false\/null to block). Snippets for Complianz, CookieYes, and Cookiebot are in the README on GitHub.<\/p>\n<p>Right to erasure: because no reversible identifier is stored, individual deletion isn&#8217;t possible. Use <code>TRUNCATE wp_abtest_events<\/code> to erase all A\/B testing data.<\/p>\n<h3>External services<\/h3>\n<p>This plugin connects to one external service, <strong>only when the site administrator opts in<\/strong> through the plugin&#8217;s Settings <span aria-hidden=\"true\" class=\"wp-exclude-emoji\">\u2192<\/span> Google Analytics 4 panel by entering a Measurement ID and API Secret. With the GA4 integration disabled (default), no data leaves your site.<\/p>\n<h4>Google Analytics 4 (Measurement Protocol)<\/h4>\n<p>What it is and what it&#8217;s used for: when the GA4 integration is enabled, the plugin forwards A\/B-test impression and conversion events to Google Analytics 4 via the Measurement Protocol, so the test results can be analyzed alongside your existing GA4 reports.<\/p>\n<p>What data is sent and when: on each impression and each conversion logged by the plugin, a single fire-and-forget HTTPS request is sent to <code>https:\/\/www.google-analytics.com\/mp\/collect<\/code> with a JSON payload containing:<\/p>\n<ul>\n<li><code>client_id<\/code> \u2014 the plugin&#8217;s internal visitor hash (truncated salted SHA-256 of IP + User-Agent; never the raw IP or UA)<\/li>\n<li><code>events[].name<\/code> \u2014 <code>abtest_impression<\/code> or <code>abtest_conversion<\/code><\/li>\n<li><code>events[].params.experiment_id<\/code> \u2014 the WordPress post ID of the experiment<\/li>\n<li><code>events[].params.variant<\/code> \u2014 the variant served (<code>a<\/code>, <code>b<\/code>, <code>c<\/code>, or <code>d<\/code>)<\/li>\n<li><code>events[].params.test_url<\/code> \u2014 the URL path under test (e.g. <code>\/promo\/<\/code>)<\/li>\n<\/ul>\n<p>No raw IP address, User-Agent string, email, name, WordPress user ID, or page content is sent.<\/p>\n<p>Service provided by Google. Please review their terms and policies before enabling the integration:<\/p>\n<ul>\n<li>Google Analytics terms of service: https:\/\/marketingplatform.google.com\/about\/analytics\/terms\/us\/<\/li>\n<li>Google privacy policy: https:\/\/policies.google.com\/privacy<\/li>\n<\/ul>","protected":false},"excerpt":{"rendered":"<p>Lightweight A\/B testing for pages: internal tracking, persistent-cookie 50\/50 split, GDPR-friendly. No third-party dependency.<\/p>","protected":false},"author":1,"featured_media":9317,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"footnotes":""},"categories":[252],"tags":[1838,760,1495,1841,555],"class_list":["post-11191","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-trends","tag-a-b-testing","tag-analytics","tag-conversion","tag-split-testing","tag-wordpress-plugin"],"_links":{"self":[{"href":"https:\/\/aeosub.com\/zh\/wp-json\/wp\/v2\/posts\/11191","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/aeosub.com\/zh\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/aeosub.com\/zh\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/aeosub.com\/zh\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/aeosub.com\/zh\/wp-json\/wp\/v2\/comments?post=11191"}],"version-history":[{"count":0,"href":"https:\/\/aeosub.com\/zh\/wp-json\/wp\/v2\/posts\/11191\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/aeosub.com\/zh\/wp-json\/wp\/v2\/media\/9317"}],"wp:attachment":[{"href":"https:\/\/aeosub.com\/zh\/wp-json\/wp\/v2\/media?parent=11191"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/aeosub.com\/zh\/wp-json\/wp\/v2\/categories?post=11191"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/aeosub.com\/zh\/wp-json\/wp\/v2\/tags?post=11191"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}