Engagement Adapter Pattern

The adapter pattern decouples analytics components from vendor-specific event APIs. Components call a generic interface and the adapter translates those calls into the vendor’s API. Storefront Next provides two built-in adapters for analytics event tracking that use Einstein and Active Data.

AdapterPurposeConfig key
EinsteinAnalytics event tracking (viewProduct, addToCart, etc.)engagement.adapters.einstein
Active DataAnalytics event tracking (dwac beacon)engagement.adapters.activeData

Both implement the EngagementAdapter interface and are registered in a shared store.

Architecture 

1┌─────────────────────────────────────────────────────────────┐
2│                     Component Layer                         │
3│  (PageViewTracker, AddToCartButton, etc.)                   │
4└──────────────────────────┬──────────────────────────────────┘
5                           │ sendEvent(event)
6
7┌─────────────────────────────────────────────────────────────┐
8│                     Adapter Store                           │
9│  Map<string, EngagementAdapter>                             │
10│  + ensureAdaptersInitialized() (lazy, idempotent)           │
11└──────────────────────────┬──────────────────────────────────┘
12
13
14┌─────────────────────────────────────────────────────────────┐
15│              Vendor Implementations                         │
16│  createEinsteinAdapter → POST to api.cquotient.com          │
17│  createActiveDataAdapter → pixel/beacon requests            │
18└─────────────────────────────────────────────────────────────┘

File Structure 

1src/lib/adapters/
2├── index.ts                        # Re-exports store, types, utils
3└── engagement/
4    ├── types.ts                    # EngagementAdapter interface, EngagementAdapterConfig
5    ├── store.ts                    # addAdapter, getAdapter, getAllAdapters, removeAdapter
6    ├── einstein.ts                 # createEinsteinAdapter factory (analytics events only)
7    ├── active-data.ts              # createActiveDataAdapter factory
8    ├── register.ts                 # initializeEngagementAdapters (reads config, creates + registers)
9    ├── initialize.ts               # ensureAdaptersInitialized (idempotent, lazy-loads register.ts)
10    ├── einstein-config.ts          # validateEinsteinConfig helper
11    └── utils.ts                    # hasConsent helper

Adapter Interface 

1// src/lib/adapters/engagement/types.ts
2export interface EngagementAdapter extends EventAdapter {
3  name: string;
4  sendEvent?: (
5    event: AnalyticsEvent,
6    siteInfo?: EventSiteInfo,
7    consentPreferences?: ConsentPreferences,
8  ) => Promise<unknown>;
9  send?: (url: string, options?: RequestInit) => Promise<Response>;
10}

Adapter Store 

A Map<string, EngagementAdapter> with functional accessors:

1addAdapter("einstein", adapter); // Register
2getAdapter("einstein"); // Retrieve by name
3getAllAdapters(); // All registered adapters
4removeAdapter("einstein"); // Unregister

Lazy Initialization 

Adapter code is dynamically imported to stay out of the initial bundle:

1// src/lib/adapters/engagement/initialize.ts
2export async function ensureAdaptersInitialized(appConfig: AppConfig): Promise<void> {
3  if (getAllAdapters().length > 0) return; // Already done
4  const { initializeEngagementAdapters } = await import("./register");
5  initializeEngagementAdapters(appConfig);
6}

The dynamic import() means that the Einstein or Active Data implementation modules are code-split into a separate chunk.

Configuration 

Adapters are configured in config.server.ts under engagement.adapters:

1engagement: {
2    adapters: {
3        einstein: {
4            enabled: true,
5            host: 'https://api.cquotient.com',
6            einsteinId: '<your-einstein-id>',
7            siteId: '<your-site-id>',
8            realm: '<realm>',
9            isProduction: false,
10            consentCategory: 'C0004',
11            eventToggles: { view_product: true, cart_item_add: true, ... },
12        },
13        activeData: {
14            enabled: true,
15            host: '<host>',
16            siteUUID: '<uuid>',
17            consentCategory: 'C0002',
18            eventToggles: { view_product: true, cart_item_add: true, ... },
19        },
20    },
21},

If an adapter’s enabled flag is false, it isn’t registered.

Testing 

Mocking Adapters in Tests 

1import { addAdapter, removeAdapter } from "@/lib/adapters";
2
3const mockAdapter: EngagementAdapter = {
4  name: "mock-einstein",
5  sendEvent: vi.fn().mockResolvedValue(undefined),
6};
7
8beforeEach(() => addAdapter("einstein", mockAdapter));
9afterEach(() => removeAdapter("einstein"));

Testing Initialization 

1import { resetAdaptersInitialization } from "@/lib/adapters/engagement/initialize";
2
3afterEach(() => resetAdaptersInitialization()); // Clear cached promise for clean state

Add an Engagement Adapter 

  1. Create src/lib/adapters/engagement/your-adapter.ts with a factory function returning EngagementAdapter.
  2. Register it in src/lib/adapters/engagement/register.ts inside initializeEngagementAdapters().
  3. Add configuration under engagement.adapters.yourAdapter in config.server.ts.