A/B Testing with Google Analytics 4: Implementation and Measurement

Drupal

A/B Testing with Google Analytics 4: Implementation and Measurement

Build an A/B test with stable variant assignment, consent-aware GA4 exposure and conversion events, Google Tag Manager integration, and reliable experiment analysis.

A different button label can improve a landing page, but a higher click count alone does not prove it. A useful A/B test randomly assigns eligible visitors to two experiences, records which experience they actually see, and compares a predefined outcome over a planned measurement window.

This guide uses a newsletter call to action: control A says Subscribe, while treatment B says Get weekly tutorials. The primary outcome is a successfully accepted newsletter subscription. Button clicks are a diagnostic metric.

What Google Analytics does in an A/B test

Google Optimize and Optimize 360 stopped operating on September 30, 2023. Do not start a new implementation with an Optimize container. GA4 collects and reports experiment events; your application or an experimentation platform assigns and renders variants. See Google's Optimize retirement notice.

Eligible visitor + consent
  -> stable assignment: A or B
  -> render assigned experience
  -> record experiment_exposure when visible
  -> record generate_lead after successful subscription
  -> compare outcomes for exposed visitors

There are three practical implementation choices:

Approach How to implement Advantages Tradeoffs
Browser assignment Persist a random variant, update a small component, send GA4 events Small implementation; useful for copy experiments Possible visual flicker; browser storage and consent affect eligibility
Server or edge assignment Assign before rendering; carry the variant through the response and conversion Consistent first render; supports larger changes Requires assignment infrastructure and correct cache variation
Experimentation platform Configure targeting and allocation in the platform; forward its actual exposure callback to GA4 Central management, rollouts, and analysis Subscription and integration work; verify identity and exposure semantics

The examples below are an implementation pattern, not an experiment already running on this website.

1. Write the experiment contract before writing code

Record these decisions in the issue or experiment brief:

  • ID: newsletter_cta_v1. A materially changed experiment gets a new ID.
  • Population: eligible, consented visitors to the newsletter component; exclude staff and automated test traffic consistently.
  • Assignment unit: one browser for this example, split 50/50. An authenticated account is a different possible unit.
  • Primary metric: unique exposed browsers with an accepted subscription divided by unique exposed browsers, within a fixed seven-day conversion window.
  • Guardrails: form errors, page performance, and unsubscribe or complaint rates where available.
  • Stopping rule: choose the minimum detectable improvement, sample size, analysis method, and end date before launch. Allow the last enrolled visitors their full conversion window.

A browser is not a person: clearing storage or switching devices creates a new assignment. If you need account-level conclusions, assign by an internal account key in a trusted system and define the analytics identity strategy separately. Never send email addresses as experiment identifiers.

2. Assign a stable variant and track actual exposure

Install the Google tag once, or use your existing integration. The example assumes gtag is configured for the intended property. Google documents the event API in Set up events.

The markup retains a usable control experience when scripting or consent is unavailable:

<a href="/newsletter" data-newsletter-cta>Subscribe</a>

Place this JavaScript in a theme library or application bundle. Call startNewsletterExperiment() after the consent manager confirms analytics and the storage needed for this experiment are permitted, after the Google tag is ready, and after the component exists.

const experimentId = 'newsletter_cta_v1';
const assignmentKey = `experiment:${experimentId}`;
let experimentState = null;
let exposureObserver = null;

function startNewsletterExperiment() {
  if (experimentState) return;
  const cta = document.querySelector('[data-newsletter-cta]');
  if (!cta || typeof window.gtag !== 'function') return;

  let variant;
  try {
    variant = localStorage.getItem(assignmentKey);
    if (variant !== 'A' && variant !== 'B') {
      const random = crypto.getRandomValues(new Uint32Array(1))[0];
      variant = random < 2147483648 ? 'A' : 'B';
      localStorage.setItem(assignmentKey, variant);
    }
  } catch {
    // Leave the control experience outside the measured experiment.
    return;
  }

  cta.textContent = variant === 'A' ? 'Subscribe' : 'Get weekly tutorials';
  experimentState = { variant, exposed: false };
  exposureObserver = new IntersectionObserver((entries) => {
    if (!experimentState || experimentState.exposed) return;
    if (document.visibilityState !== 'visible') return;
    if (!entries.some((entry) => entry.isIntersecting)) return;
    experimentState.exposed = true;
    window.gtag('event', 'experiment_exposure', {
      experiment_id: experimentId,
      variant_id: variant,
    });
    exposureObserver.disconnect();
  }, { threshold: 0 });
  exposureObserver.observe(cta);
}

function stopNewsletterExperiment() {
  exposureObserver?.disconnect();
  experimentState = null;
  try { localStorage.removeItem(assignmentKey); } catch {}
}

An exposure here means the component intersects the viewport in a visible document. It does not prove the visitor read it. Add a visibilitychange handler that re-observes the component when the tab becomes visible if your page can first load in the background. For a single-page application, disconnect observers on component removal and initialize them for the next eligible view.

Connect consent withdrawal to stopNewsletterExperiment() and your existing analytics consent update. Stop conversion tracking at the same time. Decide whether to restore the control label immediately or on the next view; avoid surprising a visitor during an interaction. Do not replay pre-consent interactions.

This emits at most one exposure per initialization, not one per person for the entire experiment. Repeated page visits legitimately produce repeated events. Analysis must deduplicate by the chosen assignment unit rather than divide raw event counts.

3. Attach experiment context to a real conversion

A click on Submit is not a subscription. Call the following helper from the application success handler only after the backend confirms acceptance. Your existing consent integration must supply the current consent state on every invocation.

function recordAcceptedSubscription(analyticsAllowed) {
  if (!analyticsAllowed || typeof window.gtag !== 'function') return;
  let variant;
  try { variant = localStorage.getItem(assignmentKey); } catch { return; }
  if (variant !== 'A' && variant !== 'B') return;

  window.gtag('event', 'generate_lead', {
    experiment_id: experimentId,
    variant_id: variant,
    lead_type: 'newsletter',
  });
}

Load the shared experiment constants and conversion helper on the confirmation flow as well. Assignment in storage is not proof of exposure: the final analysis must join conversions to an earlier exposure for the same experiment and variant. Make the success handler idempotent for the accepted submission; a refresh of a thank-you URL must not generate another success event.

Keep email addresses, form text, and other personal data out of event parameters. If your existing code already emits generate_lead, enrich that event instead of emitting it twice. With several simultaneous experiments, design an explicit schema for their contexts rather than letting one global variant_id overwrite another.

4. Use Google Tag Manager as an alternative delivery path

If GTM owns your analytics tags, replace each direct gtag event call with a data-layer event:

window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  event: 'experiment_exposure',
  experiment_id: experimentId,
  variant_id: variant,
});

In GTM, create Data Layer Variables for experiment_id and variant_id, a Custom Event trigger for experiment_exposure, and a GA4 Event tag using those variables as parameters. Repeat for the conversion event and its additional parameter. Apply your site's consent requirements to those tags, then test in Preview before publishing the container.

Choose one delivery path per event. Running both the direct call and a matching GTM tag duplicates measurement. Keep consent checks at the application boundary as well as in your tag configuration.

5. Make experiment parameters reportable in GA4

In the property's Admin area, create event-scoped custom dimensions under Custom definitions:

Display name Event parameter Example
Experiment ID experiment_id newsletter_cta_v1
Variant ID variant_id B
Lead type lead_type newsletter

Use low-cardinality labels, not a separate dimension value for every visitor. Register definitions before collecting the production experiment; they do not retroactively populate historical reports. Reporting can take 24–48 hours after setup. See GA4 custom dimensions and metrics.

Use DebugView in a test property to inspect exposure and conversion parameters; enable debug_mode: true only for test events. Verify A and B independently, including a returning visit, a failed submission, consent withdrawal, and unavailable storage. Network delivery alone does not establish that the intended GA4 property received the correct event.

6. Compare the right denominator

For an initial GA4 Exploration, use Experiment ID and Variant ID as rows, filter to this experiment, and inspect exposure and conversion events in separate tabs. Use total users to inspect unique-user counts rather than interpreting event count as participants. A sequence funnel from exposure to conversion is useful for diagnosis.

For the planned seven-day metric, use your experimentation engine or a carefully validated BigQuery analysis: select each eligible unit's first exposure, join subsequent accepted conversions within seven days on the same unit, experiment, and variant, and count each unit once. Exclude or investigate units observed in both variants. Define handling for missing identifiers and confirm that the export covers the experiment's start.

Illustrative results:

Variant Unique exposed units Units that converted Conversion rate
A 2,000 100 5.0%
B 2,000 120 6.0%

The observed absolute change is 1 percentage point; relative lift is 20%. That arithmetic is not proof of a winning treatment. Apply the prespecified statistical method, report uncertainty, and verify that every cohort has completed its conversion window. Do not repeatedly stop a fixed-horizon test the first time a dashboard looks favorable.

Check the actual allocation for an unexpected imbalance, often called sample ratio mismatch. Missing exposure events, inconsistent consent behavior, and a caching bug can create a false lift. Analytics measures the observable, consented population; do not assume its result automatically generalizes to all visitors.

Drupal implementation details

Attach experiment behavior through a Drupal library and Drupal.behaviors, using once() to avoid duplicate initialization when AJAX reattaches behaviors. Use the site's consent integration and existing analytics event service if one is available; update any event and parameter allowlists before expecting new payloads to pass through.

Browser assignment lets the initial HTML remain cacheable. Server assignment requires more care: create and apply a bounded cache context for the experiment variant to the affected render array, and ensure any CDN also partitions or bypasses the relevant response cache. Otherwise Drupal or the CDN may serve one visitor's treatment to everyone. Avoid varying all cache entries by an entire visitor cookie when only two variants are needed.

For Drupal forms, trigger the accepted-subscription event from a verified success response. Form validation failures and AJAX retries should not count as conversions. Verify the anonymous experience through the real cache path, not just an administrator's uncached page.

Launch and retirement

Run an A/A test first: both assignments show identical content while the full measurement path operates. Investigate assignment imbalance, missing events, or surprising conversion differences before changing the copy.

Then launch the planned A/B test, monitor guardrails, and allow the measurement window to complete. If the result is inconclusive, record that outcome. Once a decision is made, remove the losing code and obsolete assignment storage, retain the experiment definition and analysis, and use a fresh ID for the next hypothesis.

Keep reading

Drupal Sep 7, 2026 6 min read

Build a Drupal Chatbot with Local AI

Learn how a Drupal chatbot finds relevant published articles, uses Ollama to generate answers, and displays source links while keeping inference on your own hardware.

Dockerfile Best Practices for Drupal Applications
Drupal Jul 10, 2026 7 min read

Dockerfile Best Practices for Drupal Applications

A practical guide to writing Dockerfiles for Drupal: multi-stage builds, Composer dependencies, PHP extensions, WebP support, Apache docroot, .dockerignore, file permissions, secrets, and CI image verification.