Greenshift A/B Variant Block
Lightweight, privacy-friendly A/B testing block for the WordPress block editor (FSE compatible).
by Greenshift · github.com/tquinonero/greenshift-ab-testing · website
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/tquinonero/greenshift-ab-testing/archive/refs/heads/main.zipA/B Variant Block for Greenshift
Lightweight, privacy-friendly A/B testing block for WordPress with Full Site Editing (FSE) support. No SaaS. No third-party scripts. Everything stays inside WordPress.
Table of Contents
- How it works
- Features
- Requirements
- Installation
- How to use the block
- Visitor assignment logic
- Testing your setup
- Stats dashboard
- Conversion triggers
- Inspector options reference
- Compatibility notes
- Developer hooks
- REST endpoint
- License
How it works
Editor canvas Frontend
────────────────────── ──────────────────────────────────────────
[A/B Variant] render.php
└─ [Variant A] (your blocks) reads cookie / IP hash → picks 0 or 1
└─ [Variant B] (your blocks) renders ONLY that child → <div data-gsab-…>
↓ wp_footer
tracking.js (~500 bytes)
→ gsabTrack() via sendBeacon / fetch
→ POST /wp-json/greenshift/v1/ab-track
→ wp_gsab_events table
↓
Tools → A/B Testing dashboard
Key point: only one variant's HTML is ever sent to the browser. There is no hidden DOM, no CSS toggling, no layout shift.
Features
- Server-side rendering — one variant rendered per request, zero hidden markup
- Cookie-based assignment — same visitor always sees the same variant
- Deterministic IP-hash fallback — works even when cookies are blocked
- Query-param override (
?ab=0/?ab=1) — for testing and QA - Logged-in user bypass — show editors a fixed variant so they don't skew results
- Configurable split ratio — any percentage, not just 50/50
- Conversion tracking — click, form submit, URL visit, or custom JS event
- REST tracking endpoint — nonce-verified, no external services
- Custom DB table — all data stored locally in your WordPress database
- Admin dashboard — stats table with z-score significance indicator and CSV export
- Zero frontend footprint — when the block is not on a page, nothing loads
- Four developer hooks — for full extensibility
Requirements
| Requirement | Minimum |
|---|---|
| WordPress | 6.3+ |
| PHP | 7.4+ |
| MySQL / MariaDB | Any version supported by your WP install |
| Block editor | Must be active (Classic Editor plugin will break this) |
| Node.js | 18+ (only needed to rebuild JS from source) |
Installation
Option A — Upload zip (recommended)
- Download or build
greenshift-ab-testing.zip - In wp-admin go to Plugins → Add New → Upload Plugin
- Upload the zip and click Activate
The database table (wp_gsab_events) is created automatically on activation.
Option B — Build from source
cd greenshift-ab-testing
npm install
npm run build
Then zip and upload, or symlink directly into wp-content/plugins/:
ln -s /path/to/greenshift-ab-testing /path/to/wp-content/plugins/greenshift-ab-testing
wp plugin activate greenshift-ab-testing
Development (watch mode)
npm start # rebuilds automatically on file save
How to use the block
Step 1 — Insert the block
In the block editor click +, search for A/B Variant and insert it. You will see both variants side by side in the editor canvas:
┌─ A/B Variant Block ─────── A: 50% / B: 50% ─┐
│ ┌──── Variant A ────┐ ┌──── Variant B ────┐ │
│ │ │ │ │ │
│ │ (add blocks) │ │ (add blocks) │ │
│ │ │ │ │ │
│ └───────────────────┘ └───────────────────┘ │
└──────────────────────────────────────────────┘
Both variants are visible in the editor only so you can build and edit them. On the live site, each visitor sees exactly one.
Step 2 — Build your two variants
Click inside Variant A and add whatever blocks you want — headings, paragraphs, images, buttons, forms, etc. Do the same for Variant B with your alternative content.
Common use cases:
- Two different CTA button texts ("Buy Now" vs "Get Started")
- Two hero images or hero layouts
- Long-form copy vs short-form copy
- Testimonial block vs pricing table
Step 3 — Configure the split
Click the outer A/B Variant block to select it, then open the Inspector panel on the right.
- Set Variant A traffic % — e.g.
50for a 50/50 test,80if Variant B is a risky change - Choose a Conversion trigger (see Conversion triggers below)
- Toggle Track impressions to record a view event on every page load (recommended on)
Step 4 — Publish
Publish the page normally. A unique block ID is auto-generated and saved with the post. That ID is how all stats are scoped.
Visitor assignment logic
Assignment is deterministic — the same visitor always sees the same variant. It is never random on a per-request basis.
Priority chain
1. Logged-in bypass (if enabled in inspector)
↓
2. ?ab= query param (if override enabled in inspector)
↓
3. Cookie gs_ab_{block_id} (set for 30 days)
↓
4. IP hash fallback hash( IP + block_id + date ) → bucket 0–99
What each step means
| Step | Behaviour |
|---|---|
| Logged-in bypass | All logged-in users see whichever variant you choose. Keeps editors from appearing in stats. |
| ?ab= override | ?ab=0 forces Variant A, ?ab=1 forces Variant B. Used for QA and demos. |
| Cookie | Once assigned, a cookie locks the visitor to their variant for 30 days. Returning visitors always see the same version. |
| IP hash | First-time visitors with no cookie are assigned by hashing their IP + block ID + today's date into a 0–99 bucket. If bucket < ratio → Variant A, otherwise → Variant B. The date component means the hash rotates daily, which helps avoid caching bias. |
Who sees what — quick reference
| Visitor type | Gets |
|---|---|
| First visit, no cookie | Assigned by IP hash based on your ratio |
| Returning visitor | Same variant as before (from cookie) |
?ab=0 in URL |
Always Variant A |
?ab=1 in URL |
Always Variant B |
| Logged-in user (bypass on) | Whatever you set in the inspector |
| Cookies blocked | IP hash on every visit (still stable within the same day) |
Testing your setup
Method 1 — Query parameter (recommended)
Add ?ab=0 or ?ab=1 to your page URL:
https://yoursite.com/your-page/?ab=0 ← see Variant A
https://yoursite.com/your-page/?ab=1 ← see Variant B
This overrides everything and lets you instantly switch between variants in the same browser session.
Method 2 — Check the rendered HTML
View the page source (Ctrl+U in your browser) and search for gsab-variant-wrap. You will find something like:
<div class="gsab-variant-wrap wp-block-greenshift-ab-variant"
data-gsab-id="abc123"
data-gsab-variant="0"
...>
data-gsab-variant="0"→ Variant A was rendereddata-gsab-variant="1"→ Variant B was rendered
Also verify that only one child block class appears in the source:
wp-block-greenshift-ab-variant-a→ Variant A content is presentwp-block-greenshift-ab-variant-b→ Variant B content is present
They should never both appear in the same page response.
Method 3 — Inspect the cookie
Open browser DevTools → Application → Cookies → your domain.
Look for gs_ab_XXXXXXXXXX. Its value is 0 (Variant A) or 1 (Variant B). Delete it and reload to get reassigned.
Method 4 — Check via curl (server-level confirmation)
# Should show: data-gsab-variant="0"
curl -sk "https://yoursite.com/your-page/?ab=0" | grep 'data-gsab-variant'
# Should show: data-gsab-variant="1"
curl -sk "https://yoursite.com/your-page/?ab=1" | grep 'data-gsab-variant'
Stats dashboard
Go to Tools → A/B Testing in wp-admin.
| Column | Description |
|---|---|
| Block ID | Unique identifier for each A/B block on your site |
| Impr. A / B | How many visitors saw each variant |
| Conv. A / B | How many triggered your conversion event |
| CR A / B | Conversion rate — conversions ÷ impressions (%) |
| Significance | Statistical confidence using z-score |
Significance levels
| Label | Meaning |
|---|---|
| ≥95% confidence | Strong evidence one variant outperforms the other. Safe to act on. |
| ≥90% confidence | Moderate evidence. Consider running longer to confirm. |
| Not significant | No reliable difference detected yet. |
| Not enough data | Fewer than 30 impressions on one variant. Too early to measure. |
Rule of thumb: wait until you have at least 100–200 impressions per variant before drawing conclusions.
Click Export CSV to download all raw event rows (block_id, variant, event, timestamp).
Conversion triggers
Set in the Conversion Trigger section of the Inspector panel.
| Trigger | When it fires | Extra field |
|---|---|---|
| Click on selector | Visitor clicks a specific element inside the block | CSS selector, e.g. .wp-block-button a |
| Form submit | Any <form> inside the block is submitted |
— |
| URL visit | The page loading counts as a conversion | — |
| Custom JS event | Your own code dispatches a named JS event | Event name, e.g. gsab_purchase |
Custom event example
Dispatch from anywhere in your JS:
document.dispatchEvent( new Event( 'gsab_purchase' ) );
Set the event name gsab_purchase in the block inspector under Custom JS event.
Inspector options reference
| Option | Default | Description |
|---|---|---|
| Variant A traffic % | 50 | Percentage of visitors assigned to Variant A |
| Track impressions | On | Record a view event on every page load |
| Allow ?ab= override | On | Enable QA bypass via query parameter |
| Conversion trigger | click | What counts as a conversion |
| CSS selector | — | Element to watch for click trigger |
| Custom event name | — | JS event name for custom_event trigger |
| Show fixed variant to logged-in users | Off | Bypass split for logged-in users |
| Variant for logged-in users | A | Which variant logged-in users always see |
Compatibility notes
| Scenario | Status |
|---|---|
| FSE / Site Editor templates | ✅ Fully compatible |
| Classic themes | ✅ Fully compatible |
| WooCommerce | ✅ No conflicts |
| Full-page caching (WP Rocket, LiteSpeed, etc.) | ⚠️ Exclude A/B-tested pages from page cache, or the same cached variant will be served to everyone |
| Classic Editor plugin | ❌ Block editor must be active |
| REST API disabled (some security plugins) | ❌ Tracking calls will fail silently (content still renders correctly) |
GDPR cookie consent blocking gs_ab_* |
⚠️ Falls back to IP hash — still deterministic but not sticky across days |
| WordPress Multisite | ✅ Works per-subsite (table is blog-specific via $wpdb->prefix) |
| SQLite (WP 6.3 experimental) | ✅ Uses dbDelta() which is SQLite-compatible |
Developer hooks
greenshift_ab_assignment
Filters the variant assigned to a visitor before rendering.
add_filter( 'greenshift_ab_assignment', function( $variant, $block_id, $attrs ) {
// Always show Variant A to logged-in users.
return is_user_logged_in() ? 0 : $variant;
}, 10, 3 );
add_filter( 'greenshift_ab_assignment', function( $variant, $block_id, $attrs ) {
// Show Variant B to visitors from a specific country (requires geo plugin).
if ( my_geo_plugin_get_country() === 'DE' ) {
return 1;
}
return $variant;
}, 10, 3 );
greenshift_ab_conversion_triggers
Add custom trigger types to the inspector dropdown.
add_filter( 'greenshift_ab_conversion_triggers', function( $triggers ) {
$triggers['video_play'] = __( 'Video Play', 'my-plugin' );
return $triggers;
} );
greenshift_ab_track_event
Filter or cancel a tracking event before it is stored. Return false to cancel.
add_filter( 'greenshift_ab_track_event', function( $data, $request ) {
// Don't track known bots.
if ( my_is_bot() ) {
return false;
}
return $data;
}, 10, 2 );
greenshift_ab_storage_adapter
Replace the default DB storage with any custom backend. Return true after your own storage to skip the default table insert.
add_filter( 'greenshift_ab_storage_adapter', function( $handled, $block_id, $variant, $event ) {
my_analytics_sdk()->push( $block_id, $variant, $event );
return true; // skip wp_gsab_events table insert
}, 10, 4 );
REST endpoint
Tracking calls are made automatically by the frontend script. This documents the endpoint for custom integrations.
POST /wp-json/greenshift/v1/ab-track
Content-Type: application/json
X-WP-Nonce: <wp_rest nonce>
{
"block_id": "abc123",
"variant": 0,
"event": "conversion"
}
Parameters:
| Field | Type | Values |
|---|---|---|
| block_id | string | The block's unique ID (auto-generated) |
| variant | integer | 0 = Variant A, 1 = Variant B |
| event | string | "impression" or "conversion" |
Response:
{ "recorded": true }
File structure
greenshift-ab-testing/
├── greenshift-ab-testing.php Main plugin entry point
├── includes/
│ ├── class-gsab-database.php Custom DB table (install, record, aggregate, export)
│ ├── class-gsab-tracking.php REST tracking route
│ ├── class-gsab-admin.php Admin dashboard + CSV export + significance
│ └── class-gsab-hooks.php Developer hook documentation
├── blocks/
│ ├── ab-variant/
│ │ ├── block.json Parent block definition
│ │ └── render.php Server-side render (assignment + output)
│ ├── ab-variant-a/
│ │ ├── block.json
│ │ └── render.php Transparent pass-through
│ └── ab-variant-b/
│ ├── block.json
│ └── render.php Transparent pass-through
├── src/
│ ├── ab-variant/ Parent block editor JS + SCSS
│ ├── ab-variant-a/ Variant A editor JS
│ ├── ab-variant-b/ Variant B editor JS
│ ├── tracking/ Frontend tracker (window.gsabTrack)
│ └── admin/ Admin dashboard JS
├── build/ Compiled JS/CSS (generated by npm run build)
├── assets/css/admin.css Admin dashboard styles
├── webpack.config.js
└── package.json
License
GPL-2.0-or-later