Setup A/B Testing
Pack's A/B Testing is currently in opt-in beta, with potential fluctuations in APIs and features ahead. While A/B testing is accessible across all plans during beta, keep in mind that this could evolve. Your feedback and insights are invaluable to us as we navigate this phase.
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
andvisitorConsentCollected
. - ✅ Publishes
EXPERIMENT_EXPOSED
events when consent is granted. - ✅ Pushes experiment exposure data to
window.dataLayer
for GTM tracking. - ✅ Uses a
useRef
to maintain the latestpublish
function fromuseAnalytics
.
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
- Run the storefront locally.
- Visit an A/B test URL (e.g.,
/test-xyz
) with an active test set up in Pack Admin – A/B Tests. - Confirm that
handleTestExpose
is firing. You should see the console log:==== TEST EXPOSED ====
- Check GTM Debug Mode and GA4 DebugView to validate event tracking.
Notes
- If the
OneTrustGroupsUpdated
event doesn't include theC0003
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)
Name | Data Layer Variable Name |
---|---|
DLV - experiment_id | experiment_id |
DLV - experiment_name | experiment_name |
DLV - experiment_variant_id | experiment_variant_id |
DLV - experiment_variation | experiment_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 Parameter | Value |
---|---|
experiment_id | DLV - experiment_id |
experiment_name | DLV - experiment_name |
experiment_variant_id | DLV - experiment_variant_id |
experiment_variation | DLV - experiment_variation |
4. Configure GTM Consent Settings
- Enable additional consent checks.
- Require consent for:
ad_storage
,analytics_storage
, andpersonalization_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 Keys → Add Key → Create new key (choose JSON format).
- Save the downloaded
.json
file securely.
5. Link GA4 to BigQuery
- In GA4, navigate to Admin → BigQuery 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)
- GCP Project ID (e.g.,
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 ID | Scenario | Expected Result |
---|---|---|
C-01 | First-time visitor, no consent given | view_experiment event does not fire. No ==== TEST EXPOSED ==== log appears. |
C-02 | User grants consent | view_experiment event fires in GTM. Console log ==== TEST EXPOSED ==== appears. |
C-03 | User revokes consent | No further view_experiment events fire. No additional ==== TEST EXPOSED ==== logs. |
C-04 | User with existing consent revisits | view_experiment event fires normally. Console log ==== TEST EXPOSED ==== appears. |
Event Debugging & Validation
GTM Event Validation
Test Case ID | Scenario | Expected Result |
---|---|---|
GTM-01 | Consent not given (missing C0003 in OneTrustGroupsUpdated ) | view_experiment event does not appear in GTM Preview Mode. No ==== TEST EXPOSED ==== log. |
GTM-02 | Consent given (includes C0003 in OneTrustGroupsUpdated ) | view_experiment event fires, visible in GTM Preview Mode. Console log appears. |
GTM-03 | User toggles consent from denied to allowed mid-session | Consent status updates; subsequent view_experiment events fire correctly with matching logs. |
GTM-04 | GTM Consent Settings block analytics without consent | GA4 events do not fire when consent is denied. No ==== TEST EXPOSED ==== log appears. |
GA4 Debugging & Validation
Test Case ID | Scenario | Expected Result |
---|---|---|
GA4-01 | User grants consent and views an experiment | view_experiment event appears in GA4 DebugView with correct experiment details and console log. |
GA4-02 | User does not grant consent | view_experiment event does not appear in GA4 DebugView; no console log. |
GA4-03 | User toggles consent on and off | GA4 logs events only when consent is active and stops when revoked; console log behavior matches. |
GA4-04 | GA4 BigQuery export | BigQuery tables update daily with the correct event parameters. |
BigQuery Data Validation
Test Case ID | Scenario | Expected Result |
---|---|---|
BQ-01 | User grants consent and view_experiment fires | The event is recorded in BigQuery within 24 hours. |
BQ-02 | User does not grant consent and visits experiment | No experiment-related data is stored in BigQuery. |
BQ-03 | Data Layer Variables are missing | BigQuery 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
- Verify that
hasUserConsent
istrue
. - Ensure
handleTestExpose
is triggered correctly. - Confirm GTM receives the
view_experiment
event. - Check that GA4 logs the event.
- 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.