By Brian Danin |
A/B testing traditionally requires expensive third-party platforms, complex JavaScript integrations, or extensive custom development. But what if you could build a native, context-aware A/B testing system directly in Drupalβone that leverages your existing design system, automates variant creation, and uses AI to intelligently manage experiments?
By combining Figma’s design variants, Make’s automation capabilities, and Model Context Protocol (MCP) for intelligent orchestration, we can create a powerful A/B testing workflow that feels native to Drupal while remaining accessible to designers and marketers.
Figma Make β Drupal A/B Testing Architecture
Integration flow with MCP and first-party analytics
Figma Make
Design & build UI components
- β’ React components
- β’ Tailwind styling
- β’ Export variations
MCP
Model Context Protocol
- β’ API integration
- β’ Data transformation
- β’ Content mapping
Drupal
Native A/B Testing
- β’ Variant management
- β’ User segmentation
- β’ Test orchestration
First-Party Analytics (localStorage)
Client-side tracking without third-party cookies, ensuring privacy compliance and fast performance.
- β’ Page views
- β’ Click events
- β’ Conversions
- β’ Active variant ID
- β’ Exposure time
- β’ User cohort
- β’ Load times
- β’ Interaction metrics
- β’ Session duration
Data Flow
- Components exported from Figma Make
- MCP transforms to Drupal content types
- Drupal serves variant based on test rules
- User interactions logged to localStorage
- Analytics synced back to Drupal
A/B Test Workflow
- Create variants in Figma Make
- Push through MCP to Drupal
- Configure test parameters in Drupal
- Serve variants to user segments
- Track results in localStorage
- Analyze and optimize
The Traditional A/B Testing Problem
Most A/B testing solutions have significant drawbacks:
Third-Party Platforms (Optimizely, VWO, etc.)
- High cost: Enterprise pricing for features you may not need
- Performance impact: Additional JavaScript that slows page loads
- Limited control: Dependent on external systems and their uptime
- Data silos: Test results and analytics exist outside your CMS
- Privacy concerns: Sending user data to third parties
Custom Development
- High effort: Building variant systems, traffic splitting, and analytics from scratch
- Maintenance burden: Keeping custom code updated with Drupal core
- Designer disconnect: Designers can’t easily create and deploy variants
- Slow iteration: Each variant requires developer intervention
JavaScript-Heavy Solutions
- SEO challenges: Search engines may not see variants consistently
- Flash of content: Users see original before variant loads
- Accessibility issues: Client-side manipulation can break screen readers
- Fragile: Breaks when DOM structure changes
What we need is a system that:
- Lives natively in Drupal for full control and performance
- Integrates with existing design systems for consistency
- Allows designers to create variants without developer bottlenecks
- Uses intelligent context to determine which variants to show
- Captures analytics without relying on third-party platforms
- Respects user privacy with first-party data collection
The Architecture: Figma + Make + MCP + Drupal + localStorage Analytics
Our approach combines four powerful tools into a cohesive workflow:
1. Figma: Design Variant Source of Truth
Figma already supports component variants for design systems. By extending this pattern, designers can:
- Create A/B test variants as Figma component variations
- Annotate variants with test metadata (target audience, hypothesis, metrics)
- Export variant specifications through Figma’s API
- Maintain design consistency across all test variants
2. Make (formerly Integromat): Automation Bridge
Make connects Figma to Drupal, automating the variant creation process:
- Watches Figma files for new A/B test variants
- Extracts variant specifications and assets
- Creates corresponding Drupal entities (nodes, blocks, paragraphs)
- Triggers builds/deployments when variants are ready
- Sends notifications to relevant teams
3. MCP: Intelligent Context Layer
Model Context Protocol provides the intelligence layer:
- Understands user context (behavior, preferences, segments)
- Selects appropriate variant based on test rules and context
- Analyzes test performance and suggests optimizations
- Provides natural language queries for test results
- Helps identify which audiences benefit most from which variants
4. Drupal: Native Variant Management
Drupal handles the actual variant storage and delivery:
- Stores variant content as standard entities
- Manages test metadata (dates, traffic allocation, success metrics)
- Delivers variants server-side (no JavaScript flash)
- Tracks impressions and conversions natively
- Provides editorial interface for test management
5. localStorage: Privacy-Focused First-Party Analytics
Instead of relying on third-party analytics that compromise privacy and performance, we use browser localStorage for first-party data collection:
- No cookies required: GDPR/CCPA friendly without consent banners for analytics
- Lightning fast: No external script loading or network requests
- Complete data ownership: All analytics data stays within your control
- Privacy by default: Data never leaves the user’s browser until explicitly synced
- Offline capable: Track interactions even without network connectivity
- No ad blockers: First-party scripts aren’t blocked like Google Analytics
This approach aligns with the modern web’s privacy-first direction while providing the metrics needed for effective A/B testing.
Building the System: Step by Step
Part 1: Structuring Variants in Figma
Start by establishing a variant annotation system in your Figma design files.
1. Create a dedicated A/B Testing frame in your Figma file:
π Design System
ββ π¨ Components
ββ π§ͺ A/B Tests
β ββ Homepage Hero Test #1
β β ββ Variant A (Control)
β β ββ Variant B (Treatment)
β ββ CTA Button Color Test
β ββ Variant A (Blue)
β ββ Variant B (Green)
2. Add test metadata as Figma descriptions:
For each test, include structured metadata in the frame description:
test_id: homepage_hero_v1
hypothesis: "Emphasizing trust signals will increase form completions"
target_metric: form_submission_rate
traffic_split: 50/50
start_date: 2025-12-01
end_date: 2025-12-31
target_audience: new_visitors
3. Use Figma’s variant properties to define differences:
Each variant should be a proper Figma component variant with properties that describe the changes:
Component: Hero / CTA
Properties:
- emphasis: trust_signals | social_proof
- cta_text: "Get Started" | "Start Free Trial"
- image: testimonial | product_shot
Part 2: Automating Variant Export with Make
Create a Make scenario that monitors your Figma file and extracts variant data.
Make Scenario Flow:
1. [Trigger] Figma Webhook β File Updated
β
2. [Figma API] Get File Components
β
3. [Filter] Find frames with "test_id" in description
β
4. [Iterator] For each A/B test found
β
5. [Parser] Extract variant metadata (YAML)
β
6. [Figma API] Export variant images as PNG/SVG
β
7. [Router] Create variants in Drupal
β
8. [Drupal API] Create/update test entities
β
9. [Notification] Notify team via Slack/Email
Example Make Module: Extract Figma Variants
// Make JavaScript module
// Parses Figma frame descriptions for A/B test metadata
const frame = input.frame;
const description = frame.description;
// Extract YAML frontmatter from description
const yamlMatch = description.match(/```yaml\n([\s\S]*?)\n```/);
if (!yamlMatch) {
return { hasTest: false };
}
const yaml = require('js-yaml');
const metadata = yaml.load(yamlMatch[1]);
// Get variants from frame children
const variants = frame.children
.filter(child => child.name.startsWith('Variant'))
.map(variant => ({
id: variant.id,
name: variant.name,
type: variant.name.includes('Control') ? 'control' : 'treatment'
}));
return {
hasTest: true,
testId: metadata.test_id,
hypothesis: metadata.hypothesis,
targetMetric: metadata.target_metric,
trafficSplit: metadata.traffic_split,
startDate: metadata.start_date,
endDate: metadata.end_date,
targetAudience: metadata.target_audience,
variants: variants
};
Part 3: Drupal A/B Test Entity Structure
Create custom Drupal entities to store A/B tests and track results.
Install required modules:
composer require drupal/eck
composer require drupal/views
composer require drupal/paragraphs
drush en eck views paragraphs -y
Create a custom “AB Test” entity bundle via ECK:
Entity configuration (ab_test entity type):
# config/sync/eck.eck_entity_type.ab_test.yml
uuid: ...
langcode: en
status: true
dependencies: {}
id: ab_test
label: 'A/B Test'
created: true
changed: true
uid: true
title: true
Define fields for the AB Test entity:
// web/modules/custom/mindsing_ab_test/mindsing_ab_test.install
function mindsing_ab_test_install() {
// Create fields for AB Test entity
$fields = [
'field_test_id' => [
'type' => 'string',
'label' => 'Test ID',
'required' => TRUE,
],
'field_hypothesis' => [
'type' => 'text_long',
'label' => 'Hypothesis',
],
'field_target_metric' => [
'type' => 'string',
'label' => 'Target Metric',
],
'field_traffic_split' => [
'type' => 'string',
'label' => 'Traffic Split',
'default_value' => '50/50',
],
'field_start_date' => [
'type' => 'datetime',
'label' => 'Start Date',
],
'field_end_date' => [
'type' => 'datetime',
'label' => 'End Date',
],
'field_target_audience' => [
'type' => 'string',
'label' => 'Target Audience',
],
'field_status' => [
'type' => 'list_string',
'label' => 'Test Status',
'allowed_values' => [
'draft' => 'Draft',
'active' => 'Active',
'paused' => 'Paused',
'completed' => 'Completed',
],
],
'field_variants' => [
'type' => 'entity_reference_revisions',
'label' => 'Variants',
'target_type' => 'paragraph',
'target_bundles' => ['ab_test_variant'],
'cardinality' => -1,
],
];
// Create field storage and instances
foreach ($fields as $field_name => $config) {
_create_field_storage($field_name, 'ab_test', $config);
}
}
Create a Variant paragraph type:
// Paragraph type: ab_test_variant
// Fields:
// - field_variant_name: String (Control, Treatment A, Treatment B, etc.)
// - field_variant_type: List (control, treatment)
// - field_variant_content: Entity Reference (node, block, paragraph)
// - field_variant_weight: Integer (for traffic allocation)
// - field_figma_component_id: String (reference back to Figma)
// - field_impressions: Integer (counter)
// - field_conversions: Integer (counter)
Part 4: Make β Drupal Integration
Configure Make to create/update Drupal entities via REST API.
Drupal REST API setup:
# Enable necessary modules
drush en rest restui serialization hal basic_auth -y
# Create API user with appropriate permissions
drush user:create ab_test_api --password="SECURE_PASSWORD"
drush user:role:add "ab_test_manager" ab_test_api
Make HTTP Module configuration:
{
"url": "https://yoursite.com/entity/ab_test?_format=hal_json",
"method": "POST",
"headers": {
"Content-Type": "application/hal+json",
"Authorization": "Basic {{base64(username:password)}}"
},
"body": {
"_links": {
"type": {
"href": "https://yoursite.com/rest/type/ab_test/ab_test"
}
},
"title": [{"value": "{{testMetadata.title}}"}],
"field_test_id": [{"value": "{{testMetadata.test_id}}"}],
"field_hypothesis": [{"value": "{{testMetadata.hypothesis}}"}],
"field_target_metric": [{"value": "{{testMetadata.target_metric}}"}],
"field_status": [{"value": "draft"}],
"field_variants": "{{variants}}"
}
}
Part 5: Server-Side Variant Selection
Implement intelligent variant selection using Drupal middleware and session management.
Create a variant selection service:
<?php
// web/modules/custom/mindsing_ab_test/src/VariantSelector.php
namespace Drupal\mindsing_ab_test;
use Drupal\Core\Session\SessionManagerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
class VariantSelector {
protected $sessionManager;
protected $requestStack;
public function __construct(SessionManagerInterface $session_manager, RequestStack $request_stack) {
$this->sessionManager = $session_manager;
$this->requestStack = $request_stack;
}
/**
* Select variant for a given test and user.
*/
public function selectVariant($test_entity) {
$request = $this->requestStack->getCurrentRequest();
$session = $request->getSession();
// Check if user already has assigned variant
$test_id = $test_entity->get('field_test_id')->value;
$session_key = "ab_test.{$test_id}.variant";
if ($session->has($session_key)) {
return $session->get($session_key);
}
// Get user context for intelligent selection
$user_context = $this->getUserContext($request);
// Check if user matches target audience
$target_audience = $test_entity->get('field_target_audience')->value;
if (!$this->matchesAudience($user_context, $target_audience)) {
// User not in test audience, show control
return $this->getControlVariant($test_entity);
}
// Select variant based on traffic split
$variants = $test_entity->get('field_variants')->referencedEntities();
$selected = $this->weightedRandom($variants);
// Store in session for consistency
$session->set($session_key, $selected->id());
// Track impression
$this->trackImpression($selected);
return $selected;
}
/**
* Get user context for targeting.
*/
protected function getUserContext($request) {
return [
'is_new_visitor' => !$request->cookies->has('returning_visitor'),
'referrer' => $request->headers->get('referer'),
'user_agent' => $request->headers->get('user-agent'),
'ip' => $request->getClientIp(),
// Add more context as needed
];
}
/**
* Check if user matches target audience criteria.
*/
protected function matchesAudience($context, $audience_criteria) {
switch ($audience_criteria) {
case 'new_visitors':
return $context['is_new_visitor'];
case 'returning_visitors':
return !$context['is_new_visitor'];
case 'all':
default:
return TRUE;
}
}
/**
* Weighted random selection based on traffic split.
*/
protected function weightedRandom($variants) {
$total_weight = 0;
$weights = [];
foreach ($variants as $variant) {
$weight = $variant->get('field_variant_weight')->value ?? 1;
$weights[] = ['variant' => $variant, 'weight' => $weight];
$total_weight += $weight;
}
$random = mt_rand(1, $total_weight);
$current_weight = 0;
foreach ($weights as $item) {
$current_weight += $item['weight'];
if ($random <= $current_weight) {
return $item['variant'];
}
}
return $variants[0];
}
/**
* Track impression for analytics.
*/
protected function trackImpression($variant) {
$current = (int) $variant->get('field_impressions')->value;
$variant->set('field_impressions', $current + 1);
$variant->save();
}
}
Create a Twig function to render variants:
<?php
// web/modules/custom/mindsing_ab_test/src/TwigExtension/ABTestExtension.php
namespace Drupal\mindsing_ab_test\TwigExtension;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class ABTestExtension extends AbstractExtension {
protected $variantSelector;
public function __construct($variant_selector) {
$this->variantSelector = $variant_selector;
}
public function getFunctions() {
return [
new TwigFunction('ab_test', [$this, 'renderABTest'], ['is_safe' => ['html']]),
];
}
public function renderABTest($test_id) {
// Load test entity by test_id
$query = \Drupal::entityQuery('ab_test')
->condition('field_test_id', $test_id)
->condition('field_status', 'active');
$ids = $query->execute();
if (empty($ids)) {
return ''; // No active test
}
$test = \Drupal::entityTypeManager()
->getStorage('ab_test')
->load(reset($ids));
// Select variant for current user
$variant = $this->variantSelector->selectVariant($test);
// Render variant content
$content_reference = $variant->get('field_variant_content')->entity;
if ($content_reference) {
$view_builder = \Drupal::entityTypeManager()
->getViewBuilder($content_reference->getEntityTypeId());
return $view_builder->view($content_reference);
}
return '';
}
}
Use in Twig templates:
{# web/themes/custom/mindsing/templates/page--front.html.twig #}
<div class="hero-section">
{# Render A/B test variant if active, otherwise show default #}
{{ ab_test('homepage_hero_v1') }}
</div>
Part 6: Integrating MCP for Intelligent Management
Model Context Protocol can enhance your A/B testing system with intelligent analysis and recommendations.
Use cases for MCP in A/B testing:
Natural Language Test Queries
- “Which homepage variant performed better last month?”
- “Show me all active tests targeting mobile users”
- “What’s the conversion rate difference for the CTA test?”
Automated Insights
- MCP analyzes test results and identifies statistical significance
- Suggests when to end tests (winner declared or inconclusive)
- Recommends next tests based on patterns
Context-Aware Variant Selection
- Beyond simple rules, MCP can consider complex user context
- Learn from past behavior to predict best variant for each user
- Adapt traffic allocation based on real-time performance
Test Planning Assistance
- “What should we test next based on current conversion funnels?”
- “Suggest variant ideas for improving mobile signup rate”
- Generate hypothesis templates from industry best practices
Example MCP Server for A/B Testing:
// mcp-server/ab-test-server.js
// MCP server that exposes A/B test context to AI assistants
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
const server = new Server({
name: 'drupal-ab-test',
version: '1.0.0',
});
// Expose active tests as MCP resources
server.setRequestHandler('resources/list', async () => {
const tests = await fetchActiveTests(); // Fetch from Drupal API
return {
resources: tests.map(test => ({
uri: `ab-test://${test.test_id}`,
name: test.title,
mimeType: 'application/json',
description: test.hypothesis,
})),
};
});
// Provide test data when requested
server.setRequestHandler('resources/read', async (request) => {
const testId = request.params.uri.replace('ab-test://', '');
const testData = await fetchTestDetails(testId);
return {
contents: [{
uri: request.params.uri,
mimeType: 'application/json',
text: JSON.stringify(testData, null, 2),
}],
};
});
// Expose analytics tools
server.setRequestHandler('tools/list', async () => {
return {
tools: [
{
name: 'analyze_test_results',
description: 'Analyze A/B test performance and determine statistical significance',
inputSchema: {
type: 'object',
properties: {
test_id: { type: 'string', description: 'Test ID to analyze' },
},
required: ['test_id'],
},
},
{
name: 'suggest_next_test',
description: 'Suggest next A/B test based on current performance and goals',
inputSchema: {
type: 'object',
properties: {
goal: { type: 'string', description: 'Business goal to optimize for' },
},
},
},
],
};
});
// Implement tool handlers
server.setRequestHandler('tools/call', async (request) => {
if (request.params.name === 'analyze_test_results') {
const testId = request.params.arguments.test_id;
const analysis = await analyzeTestStatistics(testId);
return {
content: [{
type: 'text',
text: JSON.stringify(analysis, null, 2),
}],
};
}
if (request.params.name === 'suggest_next_test') {
const goal = request.params.arguments.goal;
const suggestion = await generateTestSuggestion(goal);
return {
content: [{
type: 'text',
text: suggestion,
}],
};
}
});
// Statistical analysis helper
async function analyzeTestStatistics(testId) {
const test = await fetchTestDetails(testId);
const variants = test.variants;
// Calculate conversion rates
const results = variants.map(v => ({
name: v.name,
impressions: v.impressions,
conversions: v.conversions,
conversion_rate: (v.conversions / v.impressions * 100).toFixed(2) + '%',
}));
// Simple statistical significance (chi-square test)
const isSignificant = calculateSignificance(variants);
const winner = isSignificant ? determineWinner(variants) : null;
return {
test_id: testId,
duration_days: calculateDuration(test.start_date),
results: results,
is_significant: isSignificant,
winner: winner,
recommendation: generateRecommendation(test, isSignificant, winner),
};
}
const transport = new StdioServerTransport();
await server.connect(transport);
Using MCP in your workflow:
With an MCP server running, you can ask AI assistants (like Claude) questions about your A/B tests in natural language:
You: "Analyze the homepage hero test and tell me if we have a winner"
Claude (via MCP): *accesses ab-test://homepage_hero_v1*
"Based on 14 days of data:
- Control: 2,450 impressions, 245 conversions (10.0%)
- Treatment: 2,380 impressions, 309 conversions (13.0%)
The treatment variant shows a 30% relative improvement in conversion rate
(10.0% β 13.0%). With a chi-square test, this difference is statistically
significant (p < 0.05).
Recommendation: Deploy the treatment variant as the new default. The
emphasis on trust signals is performing significantly better."
Part 7: First-Party Analytics with localStorage
A critical advantage of this native approach is implementing privacy-focused analytics using browser localStorage instead of third-party tracking services.
Why localStorage for A/B Test Analytics?
Traditional analytics solutions (Google Analytics, Mixpanel, etc.) have significant downsides:
- Require user consent (GDPR/CCPA compliance overhead)
- Blocked by ad blockers and privacy tools
- Send data to third parties (privacy concerns)
- Add page load overhead (external scripts)
- Subject to attribution loss (Safari ITP, etc.)
localStorage offers a better path:
- No external requests (instant, no latency)
- Works offline (Progressive Web App friendly)
- Complete data ownership (sync to Drupal on your terms)
- No consent required for first-party functional storage
- Immune to ad blockers (first-party code)
- Supports rich, structured data
Implementing the localStorage Analytics Layer:
// web/themes/custom/mindsing/js/ab-test-analytics.js
// First-party A/B test analytics using localStorage
class ABTestAnalytics {
constructor() {
this.storageKey = 'mindsing_ab_tests';
this.syncEndpoint = '/api/ab-test/analytics';
this.syncInterval = 60000; // Sync every 60 seconds
this.init();
}
init() {
// Initialize storage structure if it doesn't exist
if (!localStorage.getItem(this.storageKey)) {
this.resetStorage();
}
// Set up periodic sync to Drupal
setInterval(() => this.syncToDrupal(), this.syncInterval);
// Sync before page unload
window.addEventListener('beforeunload', () => this.syncToDrupal());
// Track page visibility changes (tab switching)
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.syncToDrupal();
}
});
}
/**
* Reset localStorage structure
*/
resetStorage() {
const structure = {
tests: {},
session: {
id: this.generateSessionId(),
start: Date.now(),
},
events: [],
synced_at: null,
};
localStorage.setItem(this.storageKey, JSON.stringify(structure));
}
/**
* Track when user is exposed to a variant
*/
trackVariantExposure(testId, variantId, variantName) {
const data = this.getData();
// Initialize test if first exposure
if (!data.tests[testId]) {
data.tests[testId] = {
variant_id: variantId,
variant_name: variantName,
exposed_at: Date.now(),
interactions: [],
conversions: [],
};
}
this.saveData(data);
// Log exposure event
this.trackEvent('variant_exposure', {
test_id: testId,
variant_id: variantId,
variant_name: variantName,
});
}
/**
* Track user interaction with variant content
*/
trackInteraction(testId, interactionType, metadata = {}) {
const data = this.getData();
if (!data.tests[testId]) {
console.warn(`Test ${testId} not found in analytics storage`);
return;
}
data.tests[testId].interactions.push({
type: interactionType,
metadata: metadata,
timestamp: Date.now(),
});
this.saveData(data);
// Log interaction event
this.trackEvent('interaction', {
test_id: testId,
interaction_type: interactionType,
...metadata,
});
}
/**
* Track conversion event
*/
trackConversion(testId, conversionGoal, value = null) {
const data = this.getData();
if (!data.tests[testId]) {
console.warn(`Test ${testId} not found in analytics storage`);
return;
}
data.tests[testId].conversions.push({
goal: conversionGoal,
value: value,
timestamp: Date.now(),
});
this.saveData(data);
// Log conversion event
this.trackEvent('conversion', {
test_id: testId,
goal: conversionGoal,
value: value,
});
// Immediate sync on conversion (important data)
this.syncToDrupal();
}
/**
* Track generic event
*/
trackEvent(eventType, eventData) {
const data = this.getData();
data.events.push({
type: eventType,
data: eventData,
timestamp: Date.now(),
url: window.location.href,
referrer: document.referrer,
});
// Keep only last 100 events to prevent storage bloat
if (data.events.length > 100) {
data.events = data.events.slice(-100);
}
this.saveData(data);
}
/**
* Sync analytics data to Drupal backend
*/
async syncToDrupal() {
const data = this.getData();
// Only sync if we have unsaved data
if (data.events.length === 0 && data.synced_at) {
return;
}
try {
const response = await fetch(this.syncEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
session: data.session,
tests: data.tests,
events: data.events,
user_agent: navigator.userAgent,
screen_resolution: `${window.screen.width}x${window.screen.height}`,
viewport: `${window.innerWidth}x${window.innerHeight}`,
}),
});
if (response.ok) {
// Clear events after successful sync
data.events = [];
data.synced_at = Date.now();
this.saveData(data);
}
} catch (error) {
console.error('Failed to sync analytics:', error);
// Keep data in localStorage for retry
}
}
/**
* Get data from localStorage
*/
getData() {
const stored = localStorage.getItem(this.storageKey);
return stored ? JSON.parse(stored) : this.resetStorage();
}
/**
* Save data to localStorage
*/
saveData(data) {
localStorage.setItem(this.storageKey, JSON.stringify(data));
}
/**
* Generate unique session ID
*/
generateSessionId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Get current variant for a test
*/
getCurrentVariant(testId) {
const data = this.getData();
return data.tests[testId] || null;
}
/**
* Get all test data
*/
getAllTests() {
return this.getData().tests;
}
/**
* Clear all analytics data (for testing/debugging)
*/
clearAll() {
this.resetStorage();
}
}
// Initialize analytics
window.abTestAnalytics = new ABTestAnalytics();
Usage in templates:
<!-- Inject variant data into page -->
<script>
// Track variant exposure when page loads
document.addEventListener('DOMContentLoaded', function() {
window.abTestAnalytics.trackVariantExposure(
'homepage_hero_v1',
'{{ variant_id }}',
'{{ variant_name }}'
);
});
</script>
<!-- Track interactions with variant elements -->
<div class="hero-cta">
<button
onclick="window.abTestAnalytics.trackInteraction('homepage_hero_v1', 'cta_click', {
button_text: this.textContent
})"
>
Get Started
</button>
</div>
<!-- Track conversions on form submission -->
<form id="signup-form" onsubmit="handleFormSubmit(event)">
<!-- form fields -->
</form>
<script>
function handleFormSubmit(event) {
event.preventDefault();
// Track conversion
window.abTestAnalytics.trackConversion(
'homepage_hero_v1',
'signup_form_submission'
);
// Submit form
event.target.submit();
}
</script>
Drupal Endpoint to Receive Analytics:
<?php
// web/modules/custom/mindsing_ab_test/src/Controller/AnalyticsController.php
namespace Drupal\mindsing_ab_test\Controller;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\JsonResponse;
class AnalyticsController extends ControllerBase {
/**
* Receive and process analytics data from localStorage.
*/
public function receiveAnalytics(Request $request) {
$data = json_decode($request->getContent(), TRUE);
if (!$data) {
return new JsonResponse(['error' => 'Invalid data'], 400);
}
// Process each test's analytics
foreach ($data['tests'] as $test_id => $test_data) {
$this->processTestAnalytics($test_id, $test_data, $data['session']);
}
// Log events for deeper analysis
foreach ($data['events'] as $event) {
$this->logEvent($event, $data['session']);
}
return new JsonResponse(['status' => 'success']);
}
/**
* Process analytics for a specific test.
*/
protected function processTestAnalytics($test_id, $test_data, $session_data) {
// Load test entity
$query = \Drupal::entityQuery('ab_test')
->condition('field_test_id', $test_id);
$ids = $query->execute();
if (empty($ids)) {
return;
}
$test = \Drupal::entityTypeManager()
->getStorage('ab_test')
->load(reset($ids));
// Find the variant
$variants = $test->get('field_variants')->referencedEntities();
$variant = null;
foreach ($variants as $v) {
if ($v->id() == $test_data['variant_id']) {
$variant = $v;
break;
}
}
if (!$variant) {
return;
}
// Update variant analytics
$current_impressions = (int) $variant->get('field_impressions')->value;
$current_conversions = (int) $variant->get('field_conversions')->value;
// Increment based on received data
$new_conversions = count($test_data['conversions']);
$variant->set('field_impressions', $current_impressions + 1);
$variant->set('field_conversions', $current_conversions + $new_conversions);
$variant->save();
// Store detailed interaction data
foreach ($test_data['interactions'] as $interaction) {
$this->logInteraction($test_id, $test_data['variant_id'], $interaction);
}
// Store conversion details
foreach ($test_data['conversions'] as $conversion) {
$this->logConversion($test_id, $test_data['variant_id'], $conversion);
}
}
/**
* Log interaction to database for detailed analysis.
*/
protected function logInteraction($test_id, $variant_id, $interaction) {
\Drupal::database()->insert('ab_test_interactions')
->fields([
'test_id' => $test_id,
'variant_id' => $variant_id,
'interaction_type' => $interaction['type'],
'metadata' => json_encode($interaction['metadata']),
'timestamp' => $interaction['timestamp'],
])
->execute();
}
/**
* Log conversion to database.
*/
protected function logConversion($test_id, $variant_id, $conversion) {
\Drupal::database()->insert('ab_test_conversions')
->fields([
'test_id' => $test_id,
'variant_id' => $variant_id,
'goal' => $conversion['goal'],
'value' => $conversion['value'],
'timestamp' => $conversion['timestamp'],
])
->execute();
}
/**
* Log generic event.
*/
protected function logEvent($event, $session_data) {
\Drupal::database()->insert('ab_test_events')
->fields([
'session_id' => $session_data['id'],
'event_type' => $event['type'],
'event_data' => json_encode($event['data']),
'url' => $event['url'],
'referrer' => $event['referrer'],
'timestamp' => $event['timestamp'],
])
->execute();
}
}
Key Benefits of localStorage Analytics:
Privacy-First Design
- Data stays in user’s browser until explicitly synced
- No third-party cookies or trackers
- Easy GDPR/CCPA compliance (functional storage, not tracking)
- Users maintain control over their data
Performance Advantages
- Zero network latency for tracking calls
- Works offline (PWA support)
- No external script blocking page load
- Minimal JavaScript overhead
Rich Data Collection
- Store complex, structured data
- Track user journey across sessions
- Maintain context without server round-trips
- Unlimited custom dimensions
Reliability
- Not blocked by ad blockers or privacy tools
- No cross-domain issues
- Survives page reloads
- Resilient to network failures (queued sync)
Complete Ownership
- All data in your Drupal database
- No vendor lock-in
- Custom analytics queries
- Integration with existing reporting
Handling Storage Limitations:
localStorage has a 5-10MB limit per domain. Manage this by:
// Automatically archive old data to server
pruneOldData() {
const data = this.getData();
const oneWeekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);
// Archive tests older than one week
Object.keys(data.tests).forEach(testId => {
const test = data.tests[testId];
if (test.exposed_at < oneWeekAgo) {
// Ensure it's synced
this.syncToDrupal();
// Remove from local storage
delete data.tests[testId];
}
});
this.saveData(data);
}
Real-World Benefits
This integrated approach delivers substantial advantages:
For Designers
- Design variants in familiar tool (Figma) without learning Drupal
- Immediate feedback when variants are deployed
- Maintain design system consistency across all test variants
- Visual test planning with annotations and metadata
For Developers
- No third-party JavaScript to integrate or maintain
- Native Drupal entities that work with existing workflows
- Server-side rendering for better performance and SEO
- Standard APIs for extending and customizing
For Marketers
- Faster iteration from idea to live test
- Natural language queries via MCP to analyze results
- No external platform costs or vendor lock-in
- Complete data ownership within your Drupal site
- Privacy-compliant analytics without consent overhead
For Organizations
- Democratized experimentation across design, development, and marketing
- Reduced tool sprawl by leveraging existing platforms
- Improved development workflows through automation
- Context-aware testing that adapts to user behavior
- First-party data strategy aligned with privacy regulations
- No ad blocker interference with analytics tracking
- Complete audit trail of all test data in your database
Getting Started
Implementing this system doesn’t require doing everything at once. Here’s a pragmatic rollout approach:
Phase 1: Foundation (Week 1-2)
- Set up AB Test entity structure in Drupal
- Create manual variant entry workflow (no automation yet)
- Implement basic variant selection service
- Test with one simple A/B test (e.g., CTA button text)
Phase 2: Figma Integration (Week 3-4)
- Establish Figma variant annotation conventions
- Set up Make account and basic Figma β Drupal scenario
- Test automated variant creation from Figma
- Document workflow for designers
Phase 3: MCP Intelligence (Week 5-6)
- Build basic MCP server exposing test data
- Implement analysis tools for statistical significance
- Connect to Claude or other MCP-compatible AI assistant
- Train team on natural language test queries
Phase 4: First-Party Analytics (Week 7-8)
- Implement localStorage analytics JavaScript module
- Create Drupal endpoint to receive analytics data
- Set up database tables for interaction/conversion logging
- Build analytics dashboard in Drupal admin
- Test data flow from browser to database
Phase 5: Advanced Features (Ongoing)
- Multi-variate testing (testing multiple elements simultaneously)
- Personalization based on user segments
- Automated variant optimization using ML
- Real-time analytics dashboards
- Cohort analysis and segmentation
Lessons from Implementation
Building this system reveals important insights about modern development workflows:
1. Systems Beat Tools
Individual tools (Figma, Make, MCP, Drupal) are powerful, but their value multiplies when connected into a coherent system. The whole becomes greater than the sum of parts.
2. Automation Enables Experimentation
When creating test variants requires developer time, you test less frequently. Automation removes friction and encourages a culture of experimentation.
3. Context is King
Generic A/B testing treats all users the same. By leveraging MCP’s context awareness, you can deliver more relevant experiences to each user segment.
4. Native Integration Wins
Third-party platforms seem easier initially but create long-term dependency, cost, and integration complexity. Native Drupal solutions offer more control and flexibility.
5. Privacy-First is Performance-First
Using localStorage for analytics isn’t just better for privacyβit’s faster, more reliable, and gives you complete control over your data. First-party strategies align business interests with user interests.
Beyond A/B Testing: The Bigger Picture
This Figma + Make + MCP + Drupal architecture extends beyond A/B testing:
- Personalization: Show different content variants based on user context
- Seasonal campaigns: Automate deployment of time-based content variations
- Multivariate testing: Test combinations of elements simultaneously
- Progressive rollouts: Gradually increase traffic to new designs
- Localization: Manage design variants for different regions/languages
The pattern of design in Figma β automate with Make β intelligently orchestrate with MCP β deliver natively in Drupal creates a powerful foundation for modern digital experiences.
The Future: AI-Designed Variants
As AI capabilities advance, we can imagine even more sophisticated workflows:
- AI-generated variant suggestions: MCP analyzes user behavior and suggests design variants likely to improve conversion
- Automated design adaptation: AI modifies Figma components based on test results
- Predictive analytics: MCP predicts which variant will perform best for each user before showing it
- Continuous optimization: System automatically creates, tests, and deploys improvements
Model Context Protocol positions us for this future by creating the context layer AI needs to make intelligent decisions about user experience.
Conclusion
Native Drupal A/B testing powered by Figma, Make, MCP, and localStorage analytics represents a new paradigm: intelligent, automated, context-aware, privacy-respecting experimentation without vendor lock-in or performance compromises.
By connecting your design system (Figma) to your automation platform (Make) to your intelligence layer (MCP) to your CMS (Drupal) and implementing first-party analytics (localStorage), you create a system that:
- Empowers designers to create and deploy variants independently
- Reduces developer burden through automation
- Provides intelligent insights via natural language queries
- Delivers better performance than JavaScript-heavy third-party solutions
- Maintains complete data ownership within your Drupal ecosystem
- Respects user privacy with first-party, consent-free analytics
- Survives ad blockers and browser privacy protections
- Works offline for Progressive Web App experiences
The future of A/B testing isn’t about external platformsβit’s about building intelligent, context-aware, privacy-first systems that integrate seamlessly with your existing workflows. Figma + Make + MCP + Drupal + localStorage shows us how.
Ready to implement intelligent A/B testing in your Drupal site? Contact us to discuss building custom experimentation workflows tailored to your organization’s needs.