Skip to main content
Illustration of two page variants side by side feeding results into a pair of analytics bars
Topic: Technology & innovation

Native Drupal A/B testing with Figma, Make, and MCP

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

1

Figma Make

Design & build UI components

  • • React components
  • • Tailwind styling
  • • Export variations
2

MCP

Model Context Protocol

  • • API integration
  • • Data transformation
  • • Content mapping
3

Drupal

Native A/B Testing

  • • Variant management
  • • User segmentation
  • • Test orchestration
Analytics Layer

First-Party Analytics (localStorage)

Client-side tracking without third-party cookies, ensuring privacy compliance and fast performance.

Event Tracking
  • • Page views
  • • Click events
  • • Conversions
Variant Data
  • • Active variant ID
  • • Exposure time
  • • User cohort
Performance
  • • Load times
  • • Interaction metrics
  • • Session duration

Data Flow

  1. Components exported from Figma Make
  2. MCP transforms to Drupal content types
  3. Drupal serves variant based on test rules
  4. User interactions logged to localStorage
  5. Analytics synced back to Drupal

A/B Test Workflow

  1. Create variants in Figma Make
  2. Push through MCP to Drupal
  3. Configure test parameters in Drupal
  4. Serve variants to user segments
  5. Track results in localStorage
  6. 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:

  1. 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?"

  2. 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

  3. 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

  4. 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:

  1. 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

  2. Performance Advantages - Zero network latency for tracking calls - Works offline (PWA support) - No external script blocking page load - Minimal JavaScript overhead

  3. Rich Data Collection - Store complex, structured data - Track user journey across sessions - Maintain context without server round-trips - Unlimited custom dimensions

  4. Reliability - Not blocked by ad blockers or privacy tools - No cross-domain issues - Survives page reloads - Resilient to network failures (queued sync)

  5. 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)

  1. Set up AB Test entity structure in Drupal
  2. Create manual variant entry workflow (no automation yet)
  3. Implement basic variant selection service
  4. Test with one simple A/B test (e.g., CTA button text)

Phase 2: Figma integration (week 3-4)

  1. Establish Figma variant annotation conventions
  2. Set up Make account and basic Figma → Drupal scenario
  3. Test automated variant creation from Figma
  4. Document workflow for designers

Phase 3: MCP intelligence (week 5-6)

  1. Build basic MCP server exposing test data
  2. Implement analysis tools for statistical significance
  3. Connect to Claude or other MCP-compatible AI assistant
  4. Train team on natural language test queries

Phase 4: First-party analytics (week 7-8)

  1. Implement localStorage analytics JavaScript module
  2. Create Drupal endpoint to receive analytics data
  3. Set up database tables for interaction/conversion logging
  4. Build analytics dashboard in Drupal admin
  5. 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.

More from Technology & innovation

View all

Ready to get started?

Contact us to discuss your business needs