Setup A/B Testing

This guide details how to implement A/B testing and consent management in your Pack storefront.

You can refer to the following example A/B testing branch.


1. Update Packages

Update the following packages to their latest ab-test versions:

npm

npm install @pack/hydrogen@ab-test
npm install @pack/react@ab-test

2. Add PackTestProvider to layout.tsx

Integrate PackTestProvider to manage A/B test exposure and ensure tracking fires only after user consent.

Import PackTestProvider

Add this import at the top of layout.tsx:

import { PackTestProvider } from '@pack/hydrogen';

Retrieve Consent & Test Exposure Handler

Use the useTestExpose hook to obtain hasUserConsent and handleTestExpose:

const { handleTestExpose, hasUserConsent } = useTestExpose();

Wrap the Storefront in PackTestProvider

Wrap your main layout inside PackTestProvider:

<>
  <Analytics />
  <PackTestProvider hasUserConsent={hasUserConsent} testExposureCallback={handleTestExpose}>
    <div>
      <Header />

      <main>
        {children}
      </main>

      <Footer />

      <Cart />

      <Search />

      <Modal />
    </div>
  </PackTestProvider>
</>

3. Implement the useTestExpose hook in Layout.tsx

The useTestExpose hook manages user consent for A/B testing and sends exposure events to analytics.

View the full implementation on GitHub

Usage Example

import { useTestExpose } from '~/hooks/useTestExpose';

export function Layout({children}: {children: ReactNode}) {

  const { handleTestExpose, hasUserConsent } = useTestExpose();

    return (
    <>
      <PackTestProvider
        hasUserConsent={hasUserConsent}
        testExposureCallback={handleTestExpose}
      >
        <div>
          <Header />
          <main>{children}</main>
          <Footer />
          <Cart />
          <Search />
          <Modal />
        </div>
      </PackTestProvider>
    </>
  );
};

Features

  • ✅ Tracks user consent via OneTrustGroupsUpdated and visitorConsentCollected.
  • ✅ Publishes EXPERIMENT_EXPOSED events when consent is granted.
  • ✅ Pushes experiment exposure data to window.dataLayer for GTM tracking.
  • ✅ Uses a useRef to maintain the latest publish function from useAnalytics.

API

handleTestExpose(test: any)

Fires an experiment exposure event only if consent is granted.

  • Parameters:
    • test.handle (string) – Experiment name.
    • test.testVariant.handle (string) – Active variation.

hasUserConsent: boolean

Indicates whether the user has consented to functional cookies and analytics.

Implementation Details

  • Listens to OneTrustGroupsUpdated to check for functional cookie consent.
  • Captures visitorConsentCollected from Shopify’s Customer Privacy API.
  • Uses a setTimeout(1000ms) delay to ensure proper event tracking.

4. Add PackTestRoute to All Relevant Storefront Routes

Integrate PackTestRoute to register test-related routes in your storefront (e.g., ($locale)._index.tsx, ($locale).articles.$handle.tsx, ($locale).blogs.$handle.tsx, ($locale).collections.$handle.tsx, ($locale).pages.$handle.tsx, ($locale).products.$handle.tsx).

Import PackTestRoute Component

import { PackTestRoutes } from '@pack/hydrogen';

Add PackTestRoute to Your Routes

For example, in app/routes/($locale)._index.tsx:

import { useLoaderData } from '@remix-run/react';
import { json } from '@shopify/remix-oxygen';
import type { LoaderFunctionArgs } from '@shopify/remix-oxygen';
import { RenderSections } from '@pack/react';
import { PackTestRoute } from '@pack/hydrogen';

import { PAGE_QUERY } from '~/data/queries';

export async function loader({ context, params, request }: LoaderFunctionArgs) {
  const { data, packTestInfo } = await context.pack.query(PAGE_QUERY, {
    variables: { handle: '/' },
    cache: context.storefront.CacheLong(),
  });

  if (!data?.page) throw new Response(null, { status: 404 });

  return json({
    page: data.page,
    url: request.url,
    packTestInfo,
  });
}

export default function Index() {
  const { page } = useLoaderData<typeof loader>();

  return (
    <>
      <PackTestRoute />
      <RenderSections content={page} />;
    </>
  );
}

5. Update Analytics Component and Relevant Integrations

The original PackAnalytics component is now renamed to Analytics.

Add EXPERIMENT_EXPOSED to Analytics Constants

Update app/components/Analytics/constants.ts:

export const AnalyticsEvent = {
  ...HydrogenAnalyticsEvent,
  PRODUCT_VARIANT_SELECTED: 'custom_product_variant_selected',
  PRODUCT_ITEM_CLICKED: 'custom_product_item_clicked',
  PRODUCT_QUICK_SHOP_VIEWED: 'custom_product_quick_shop_viewed',
  CUSTOMER: 'custom_customer',
  CUSTOMER_SUBSCRIBED: 'custom_customer_subscribed',
  CUSTOMER_LOGGED_IN: 'custom_customer_logged_in',
  CUSTOMER_REGISTERED: 'custom_customer_registered',
  // New event for A/B testing experiment exposure
  EXPERIMENT_EXPOSED: 'custom_experiment_exposed',
} as typeof HydrogenAnalyticsEvent & {
  PRODUCT_VARIANT_SELECTED: 'custom_product_variant_selected';
  PRODUCT_ITEM_CLICKED: 'custom_product_item_clicked';
  PRODUCT_QUICK_SHOP_VIEWED: 'custom_product_quick_shop_viewed';
  CUSTOMER: 'custom_customer';
  CUSTOMER_SUBSCRIBED: 'custom_customer_subscribed';
  CUSTOMER_LOGGED_IN: 'custom_customer_logged_in';
  CUSTOMER_REGISTERED: 'custom_customer_registered';
  EXPERIMENT_EXPOSED: 'custom_experiment_exposed';
};

Add experimentExposedEvent to Integrations

For example, in app/components/Analytics/GA4Events:

// app/components/Analytics/GA4Events/events.ts

const experimentExposedEvent = ({
  debug,
  ...data
}: Record<string, any> & { debug?: boolean }) => {
  const analyticsEvent = AnalyticsEvent.EXPERIMENT_EXPOSED;

  try {
    if (debug) logSubscription({ data, analyticsEvent });

    const { test, customer } = data;

    if (!test) throw new Error('`test` parameter is missing.');

    const event = {
      event: 'view_experiment',
      user_properties: generateUserProperties({ customer }),
      experiment_id: test?.id,
      experiment_name: test?.handle,
      experiment_variant_id: test?.testVariant.id,
      experiment_variation: test?.testVariant.handle,
    };

    emitEvent({ event, debug });
  } catch (error) {
    logError({
      analyticsEvent,
      message: error instanceof Error ? error.message : error,
    });
  }
};

export {
  experimentExposedEvent,
  // ... additional exports
};

And in app/components/Analytics/GA4Events/GA4Events.tsx:

import { useEffect, useState } from 'react';

import { AnalyticsEvent } from '../constants';
import {
  experimentExposedEvent, // newly added
  ANALYTICS_NAME,
} from './events';

export function GA4Events({ ga4TagId, register, subscribe, customer, debug = false }) {
  // ... initialization and script loading logic

  useEffect(() => {
    if (!scriptLoaded) return;

    subscribe(AnalyticsEvent.EXPERIMENT_EXPOSED, (data: Data) => {
      experimentExposedEvent({ ...data, customer, debug });
    });

    // ... additional subscriptions
  }, [scriptLoaded, customer?.id, debug]);

  // ... rest of the code
}

5. Verification Steps

  1. Run the storefront locally.
  2. Visit an A/B test URL (e.g., /test-xyz) with an active test set up in Pack Admin – A/B Tests.
  3. Confirm that handleTestExpose is firing. You should see the console log:
    ==== TEST EXPOSED ====
    
  4. Check GTM Debug Mode and GA4 DebugView to validate event tracking.

Notes

  • If the OneTrustGroupsUpdated event doesn't include the C0003 group, handleTestExpose will not publish events.
  • The setTimeout delay (1000ms) ensures the event is captured properly.
  • OneTrust is customer-specific. If using a different cookie consent service, modify the event listener logic accordingly.

GA4 / GTM Integration

GA4 is typically integrated via Google Tag Manager (GTM). Since all Pack Hydrogen sites use GTM, integrating GA4 through GTM enhances consent management.

Tag Setup in GTM

1. Create 4 User-Defined Data Layer Variables (DLV)

NameData Layer Variable Name
DLV - experiment_idexperiment_id
DLV - experiment_nameexperiment_name
DLV - experiment_variant_idexperiment_variant_id
DLV - experiment_variationexperiment_variation

2. Create a GA4 Event Tag for view_experiment

  • Tag Type: GA4 Event
  • Measurement ID: Replace with your actual GA4 ID
  • Event Name: view_experiment
  • Event Settings Variable: GA4 Event Settings

3. Configure Event Parameters

Event ParameterValue
experiment_idDLV - experiment_id
experiment_nameDLV - experiment_name
experiment_variant_idDLV - experiment_variant_id
experiment_variationDLV - experiment_variation

4. Configure GTM Consent Settings

  • Enable additional consent checks.
  • Require consent for: ad_storage, analytics_storage, and personalization_storage.

5. Create a GTM Trigger

  • Trigger Type: Custom Event
  • Event Name: view_experiment
  • Fires On: All Custom Events

BigQuery Integration with GA4 & PackDigital

1. Create or Use an Existing GCP Project

  • Create a new project or use an existing one via the Google Cloud Console.

2. Enable BigQuery API

  • Enable the BigQuery API for your project.

3. Create a Service Account

  • Navigate to Service Accounts → Create Service Account.
  • Assign the roles BigQuery Job User and BigQuery Data Viewer.
  • Complete the creation process.

4. Generate a Service Account Key

  • Open the created Service Account.
  • Go to Manage KeysAdd KeyCreate new key (choose JSON format).
  • Save the downloaded .json file securely.

5. Link GA4 to BigQuery

  • In GA4, navigate to AdminBigQuery Links.
  • Click Create Link and select your BigQuery project.
  • Confirm your selection.

6. Connect BigQuery to PackDigital

  • In PackDigital Admin, navigate to Store > Settings > Integrations.
  • Click Create New Integration and select BigQuery.
  • Provide:
    • GCP Project ID (e.g., g4a-bigquery-pack-data)
    • Dataset Name (e.g., analytics_GA4PROPERTYID)
    • Service Account JSON File (paste the JSON key from Step 4)

Example JSON Service Account Key:

{
  "type": "service_account",
  "project_id": "g4a-bigquery-pack-data",
  "private_key_id": ".....",
  "private_key": "....",
  "client_email": "pack-bigquery-access@g4a-bigquery-pack-data.iam.gserviceaccount.com",
  "client_id": "XXXXX",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/pack-bigquery-access%40g4a-bigquery-pack-data.iam.gserviceaccount.com",
  "universe_domain": "googleapis.com"
}
  • Click Save.

QA & Testing Plan

Key Considerations for Cookie Consent Testing

  • Testing cookie consent in preview environments is challenging.
  • Shopify’s consent banner may trigger only in production.
  • OneTrust requires a permanent staging URL and test scripts for QA before production. Set PUBLIC_ONETRUST_DATA_DOMAIN_SCRIPT in the .env file if you use OneTrust.

QA Test Cases

Functional Cookie Consent Testing:

Test Case IDScenarioExpected Result
C-01First-time visitor, no consent givenview_experiment event does not fire. No ==== TEST EXPOSED ==== log appears.
C-02User grants consentview_experiment event fires in GTM. Console log ==== TEST EXPOSED ==== appears.
C-03User revokes consentNo further view_experiment events fire. No additional ==== TEST EXPOSED ==== logs.
C-04User with existing consent revisitsview_experiment event fires normally. Console log ==== TEST EXPOSED ==== appears.

Event Debugging & Validation

GTM Event Validation

Test Case IDScenarioExpected Result
GTM-01Consent not given (missing C0003 in OneTrustGroupsUpdated)view_experiment event does not appear in GTM Preview Mode. No ==== TEST EXPOSED ==== log.
GTM-02Consent given (includes C0003 in OneTrustGroupsUpdated)view_experiment event fires, visible in GTM Preview Mode. Console log appears.
GTM-03User toggles consent from denied to allowed mid-sessionConsent status updates; subsequent view_experiment events fire correctly with matching logs.
GTM-04GTM Consent Settings block analytics without consentGA4 events do not fire when consent is denied. No ==== TEST EXPOSED ==== log appears.

GA4 Debugging & Validation

Test Case IDScenarioExpected Result
GA4-01User grants consent and views an experimentview_experiment event appears in GA4 DebugView with correct experiment details and console log.
GA4-02User does not grant consentview_experiment event does not appear in GA4 DebugView; no console log.
GA4-03User toggles consent on and offGA4 logs events only when consent is active and stops when revoked; console log behavior matches.
GA4-04GA4 BigQuery exportBigQuery tables update daily with the correct event parameters.

BigQuery Data Validation

Test Case IDScenarioExpected Result
BQ-01User grants consent and view_experiment firesThe event is recorded in BigQuery within 24 hours.
BQ-02User does not grant consent and visits experimentNo experiment-related data is stored in BigQuery.
BQ-03Data Layer Variables are missingBigQuery records should not contain null values for required parameters.

Debugging Tools

  • Console Log: Look for the ==== TEST EXPOSED ==== message.
  • GTM Debug Mode: Append ?gtm_debug=x to the URL.
  • GA4 DebugView: Accessible via Admin > DebugView in GA4.
  • Chrome DevTools: Inspect Console > Network > Analytics Requests.
  • BigQuery: Run queries like:
    SELECT * FROM analytics_GA4PROPERTYID.events_* ORDER BY event_timestamp DESC
    

Reporting & Fixing Issues

  1. Verify that hasUserConsent is true.
  2. Ensure handleTestExpose is triggered correctly.
  3. Confirm GTM receives the view_experiment event.
  4. Check that GA4 logs the event.
  5. Debug in BigQuery if GA4 logs but data isn’t received.

Summary

  • ==== TEST EXPOSED ==== appears when an experiment is correctly logged.
  • ✅ GTM Preview Mode shows view_experiment when consent is granted.
  • ✅ GA4 DebugView confirms event logging.
  • ✅ BigQuery records event data daily.

QA OPEN ITEM

  • Testing cookie consent in preview environments is challenging.
    • Shopify’s consent banner may only fire in production.
    • OneTrust requires a permanent staging URL and test scripts for QA before production.

Was this page helpful?