Custom implementations: Track events without the Coveo app for Shopify

This is for:

Developer
In this article

usage analytics events are crucial for analyzing your storefront performance and power Coveo Machine Learning (Coveo ML) models. This page explains how to track commerce events if you’re not using the Coveo AI Search and Discovery app. If you’re using the app, see Track Shopify events using the Coveo AI Search & Discovery app.

Types of events

There are four types of commerce events:

Note

Under the Coveo Event Protocol (EP), the Commerce API logs search events server-side automatically for you.

Headless

The appropriate Headless controllers log click, cart, and product view events.

Headless can’t log purchase events in your store. To log purchase events, implement an app web pixel that uses Coveo Relay. Whenever your store logs a checkout_completed event, the pixel triggers a corresponding purchase event, as in the following implementation example.

  1. Create a web pixel extension and configure it as follows:

    # shopify.extension.toml
    
    name = "coveo-analytics-web-pixel"
    type = "web_pixel_extension"
    
    runtime_context = "strict"
    
    [customer_privacy]
    analytics = true 1
    marketing = false
    preferences = false
    sale_of_data = "disabled"
    
    [settings]
    type = "object"
    
    [settings.fields.organizationId] 2
    name = "Organization ID"
    description = "Your Coveo organization ID."
    type = "single_line_text_field"
    validations = [{ name = "min", value = "1" }]
    1 Set the analytics setting to true because the app web pixel will log analytics events.
    2 When activating your web pixel, you’ll pass your Coveo organization ID in the organizationId setting.
  2. Install the required dependencies, namely @coveo/relay, @coveo/relay-event-types, and @coveo/shopify.

    // package.json
    
    {
      // ...
      "dependencies": {
        // ...
        "@coveo/relay": "^1.2.0",
        "@coveo/relay-event-types": "^13.1.3",
        "@coveo/shopify": "1.3.0",
      }
    }
  3. Create a helper file to build the target Coveo Relay events.

    // src/ecEventsBuilders.ts
    
    import { CurrencyCodeISO4217, ProductQuantity } from "@coveo/relay-event-types";
    import { Checkout, CheckoutLineItem } from "@shopify/web-pixels-extension";
    import { Ec } from "@coveo/relay-event-types";
    
    export const ecPurchaseEvent = (checkout: Checkout): Ec.Purchase => ({ 1
      currency: checkout.currencyCode?.toUpperCase() as CurrencyCodeISO4217,
      products: checkout.lineItems.map(lineItemObject),
      transaction: {
        id: checkout.token ?? "",
        revenue: checkout.totalPrice?.amount ?? 0,
      },
    });
    
    const lineItemObject = (lineItem: CheckoutLineItem): ProductQuantity => ({
      quantity: lineItem.quantity,
      product: {
        productId: lineItem.id ?? "",
        name: lineItem.title ?? "",
        price: lineItemPrice(lineItem),
      },
    });
    
    const lineItemPrice = (lineItem: CheckoutLineItem): number =>
      (lineItem.finalLinePrice?.amount ?? 0) / lineItem.quantity;
    1 Build the purchase event.
  4. Create a Relay environment helper file for client ID and context management. Outside of Shopify web pixels, Relay can manage the client ID and context automatically, but the restrictive nature of web pixels requires a custom implementation.

    // relayEnvironment.ts
    
    import type { CustomEnvironment } from "@coveo/relay";
    import { ExtensionApi } from "@shopify/web-pixels-extension";
    
    export const webPixelRelayEnvironment = ({ init }: ExtensionApi, clientId: string): CustomEnvironment => {
      return {
        generateUUID: () => clientId, 1
        getLocation: () => init.context.document.location.href,
        getReferrer: () => init.context.document.referrer,
        getUserAgent: () => init.context.navigator.userAgent,
        send: (url, token, event) =>
          fetch(`${url}?access_token=${token}`, {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            keepalive: true,
            body: JSON.stringify([event]),
          }),
      };
    };
    1 This client ID will be retrieved from a coveo_shopify_config custom event emitted by the storefront, which will be handled in the next step.
  5. Create a getCoveoShopifyEvent.ts file that listens to the custom event emitted by your storefront and extracts the relevant data to build the Relay instance. You need to manually emit the coveo_shopify_config event in your Hydrogen storefront, with the relevant data.

    // front-end code example
    
    window.Shopify.analytics.publish("coveo_shopify_config", {
      "accessToken": "<ACCESS_TOKEN>",
      "organizationId": "<ORGANIZATION_ID>",
      "trackingId": "<TRACKING_ID>",
      "clientId": "<CLIENT_ID>",
    })
    // getCoveoShopifyEvent.ts
    
    import { ExtensionApi } from "@shopify/web-pixels-extension";
    import { COVEO_SHOPIFY_CONFIG_KEY } from "@coveo/shopify/utilities"; 1
    import { createStorage } from "./storage"; 2
    
    export interface CoveoShopifyEvent { 3
      accessToken: string;
      organizationId: string;
      trackingId: string;
      clientId: string;
    }
    
    export const registerCoveoShopifyEventListener = ({ analytics, browser }: ExtensionApi) => {
      const storage = createStorage(browser);
    
      let resolveCoveoShopifyEvent: (value: CoveoShopifyEvent) => void; 4
      const coveoShopifyEventPromise = new Promise<CoveoShopifyEvent>((resolve) => {
        resolveCoveoShopifyEvent = resolve;
      });
    
      analytics.subscribe(COVEO_SHOPIFY_CONFIG_KEY, async (event) => { 5
        const settings = event.customData as unknown as CoveoShopifyEvent; 6
        if (validateCoveoShopifyEvent(settings)) { 7
          resolveCoveoShopifyEvent(settings);
          await storage.set(COVEO_SHOPIFY_CONFIG_KEY, settings);
        }
      });
    
      const getCoveoShopifyEvent = async () => {
        const settings = await storage.get<CoveoShopifyEvent>(COVEO_SHOPIFY_CONFIG_KEY);
        if (validateCoveoShopifyEvent(settings)) { 8
          return settings;
        }
        return coveoShopifyEventPromise; 9
      };
    
      return { getCoveoShopifyEvent };
    };
    
    const validateCoveoShopifyEvent = (settings: unknown): settings is CoveoShopifyEvent => {
      const coveoShopifyEvent = settings as CoveoShopifyEvent;
    
      if (
        coveoShopifyEvent &&
        typeof coveoShopifyEvent === "object" &&
        typeof coveoShopifyEvent.organizationId === "string" &&
        coveoShopifyEvent?.organizationId?.length &&
        typeof coveoShopifyEvent.trackingId === "string" &&
        coveoShopifyEvent?.trackingId?.length &&
        typeof coveoShopifyEvent.accessToken === "string" &&
        coveoShopifyEvent?.accessToken?.length &&
        typeof coveoShopifyEvent.clientId === "string" &&
        coveoShopifyEvent?.clientId?.length
      ) {
        return true;
      } else {
        console.warn(`Received Coveo Shopify event with invalid settings: ${JSON.stringify(settings)}`);
        return false;
      }
    };
    1 The @coveo/shopify package exports the COVEO_SHOPIFY_CONFIG_KEY constant, which is used to identify the custom event name emitted by the storefront.
    2 The createStorage helper function will be defined next.
    3 The interface for the custom event data that will be emitted by the storefront and used to create the Relay instance.
    4 A promise that will be resolved with the Coveo Shopify event data when the custom event is received.
    5 Subscribe to the custom event emitted by the storefront.
    6 The custom data of the event is expected to be of type CoveoShopifyEvent.
    7 Validate the custom data to ensure it contains the required properties. Then, resolve the promise with the settings and store them in the browser storage.
    8 If the settings are valid, return them.
    9 If the settings aren’t valid or not available, return the promise that will be resolved when the custom event is received. This allows the web pixel to wait for the custom event to be emitted by the storefront before proceeding with the Relay instance creation and event logging.
  6. Create the helper storage file.

    // src/storage.ts
    
    import { ExtensionApi } from "@shopify/web-pixels-extension";
    
    export const createStorage = (browser: ExtensionApi["browser"]) => ({
      get: async <T>(key: string) => {
        const value = (await browser.cookie.get(key)) || (await browser.localStorage.getItem(key));
        if (value) {
          try {
            return JSON.parse(value) as T;
          } catch {
            return;
          }
        }
      },
      set: async <T>(key: string, value: T) => { 1
        const encodedValue = JSON.stringify(value);
        await browser.cookie.set(key, encodedValue);
        await browser.localStorage.setItem(key, encodedValue);
      },
    });
    1 Store the value in both the cookie and local storage to improve reliability.
  7. Populate the index.ts file to register the web pixel extension.

    // src/index.ts
    
    import { createRelay } from "@coveo/relay";
    import { ExtensionApi, register } from "@shopify/web-pixels-extension";
    
    import { ecAddToCartEvent, ecProductViewEvent, ecPurchaseEvent, ecRemoveFromCartEvent } from "./ecEventBuilders.ts";
    import { webPixelRelayEnvironment } from "./relayEnvironment";
    import { name as webPixelPackageName, version as webPixelPackageVersion } from "../package.json";
    import { registerCoveoShopifyEventListener, CoveoShopifyEvent } from "./getCoveoShopifyEvent.ts";
    
    register((extensionAPI: ExtensionApi) => {
      const { analytics } = extensionAPI;
      const { getCoveoShopifyEvent } = registerCoveoShopifyEventListener(extensionAPI);
    
      const relayPromise = createRelayPromise(extensionAPI, getCoveoShopifyEvent);
    
      analytics.subscribe("checkout_completed", async (event) =>
        (await relayPromise).emit("ec.purchase", ecPurchaseEvent(event.data.checkout)),
      );
    });
    
    const createRelayPromise = async (
      extensionAPI: ExtensionApi,
      getCoveoShopifyEvent: () => Promise<CoveoShopifyEvent>,
    ) => {
      const { clientId, trackingId, accessToken } = await getCoveoShopifyEvent();
      const relay = createRelay({
        trackingId,
        token: accessToken,
        url: `https://${settings.organizationId}.analytics.org.coveo.com/rest/organizations/${settings.organizationId}/events/v1`, 1
        environment: webPixelRelayEnvironment(extensionAPI, clientId),
        source: [`${webPixelPackageName}@${webPixelPackageVersion}`],
      });
      return relay;
    };
    1 Use the organization ID in the web pixel settings to configure the endpoint. It would also be possible to retrieve this ID from the custom event data.
  8. When activating your web pixel, pass your Coveo organization ID in the organizationId setting.

    mutation {
      webPixelCreate(webPixel: { settings: "{\"organizationId\":\"myshoprzh5nbge\"}" }) {
        userErrors {
          code
          field
          message
        }
        webPixel {
          settings
          id
        }
      }
    }

Atomic

Atomic logs click events automatically.

We recommend creating an app web pixel with Coveo Relay to track cart, product, and purchase events, when the following events are logged from your store, respectively:

The following implementation example explains how to do so.

  1. Create a web pixel extension and configure it as follows:

    # shopify.extension.toml
    
    name = "coveo-analytics-web-pixel"
    type = "web_pixel_extension"
    
    runtime_context = "strict"
    
    [customer_privacy]
    analytics = true 1
    marketing = false
    preferences = false
    sale_of_data = "disabled"
    
    [settings]
    type = "object"
    
    [settings.fields.organizationId] 2
    name = "Organization ID"
    description = "Your Coveo organization ID."
    type = "single_line_text_field"
    validations = [{ name = "min", value = "1" }]
    1 Set the analytics setting to true because the app web pixel will log analytics events.
    2 When activating your web pixel, you’ll pass your Coveo organization ID in the organizationId setting.
  2. Install the required dependencies, namely @coveo/relay, @coveo/relay-event-types, and @coveo/shopify.

    // package.json
    
    {
      // ...
      "dependencies": {
        // ...
        "@coveo/relay": "^1.2.0",
        "@coveo/relay-event-types": "^13.1.3",
        "@coveo/shopify": "1.3.0",
      }
    }
  3. Create a helper file to build the target Coveo Relay events.

    // src/ecEventsBuilders.ts
    
    import { CurrencyCodeISO4217, Product, ProductQuantity } from "@coveo/relay-event-types";
    import { CartLine, ProductVariant, Checkout, CheckoutLineItem } from "@shopify/web-pixels-extension";
    import { Ec } from "@coveo/relay-event-types";
    
    export const ecProductViewEvent = (product: ProductVariant): Ec.ProductView => ({ 1
      currency: product.price.currencyCode.toUpperCase() as CurrencyCodeISO4217,
      product: productObject(product),
    });
    
    export const ecAddToCartEvent = (cartLine: CartLine): Ec.CartAction => ({ 2
      action: "add",
      currency: cartLine?.cost.totalAmount.currencyCode.toUpperCase() as CurrencyCodeISO4217,
      quantity: cartLine?.quantity,
      product: productObject(cartLine.merchandise),
    });
    
    export const ecRemoveFromCartEvent = (cartLine: CartLine): Ec.CartAction => ({ 3
      action: "remove",
      currency: cartLine.cost.totalAmount.currencyCode.toUpperCase() as CurrencyCodeISO4217,
      quantity: cartLine.quantity,
      product: productObject(cartLine.merchandise),
    });
    
    const productObject = (product: ProductVariant): Product => ({
      productId: product.id || "",
      name: product.title || "",
      price: product.price.amount || 0,
    });
    
    export const ecPurchaseEvent = (checkout: Checkout): Ec.Purchase => ({ 4
      currency: checkout.currencyCode?.toUpperCase() as CurrencyCodeISO4217,
      products: checkout.lineItems.map(lineItemObject),
      transaction: {
        id: checkout.token ?? "",
        revenue: checkout.totalPrice?.amount ?? 0,
      },
    });
    
    const lineItemObject = (lineItem: CheckoutLineItem): ProductQuantity => ({
      quantity: lineItem.quantity,
      product: {
        productId: lineItem.id ?? "",
        name: lineItem.title ?? "",
        price: lineItemPrice(lineItem),
      },
    });
    
    const lineItemPrice = (lineItem: CheckoutLineItem): number =>
      (lineItem.finalLinePrice?.amount ?? 0) / lineItem.quantity;
    1 Build the product view event.
    2 Build the add to cart event.
    3 Build the remove from cart event.
    4 Build the purchase event.
  4. Create a Relay environment helper file for client ID and context management. Outside of Shopify web pixels, Relay can manage the client ID and context automatically, but the restrictive nature of web pixels requires a custom implementation.

    // relayEnvironment.ts
    
    import type { CustomEnvironment } from "@coveo/relay";
    import { ExtensionApi } from "@shopify/web-pixels-extension";
    
    export const webPixelRelayEnvironment = ({ init }: ExtensionApi, clientId: string): CustomEnvironment => {
      return {
        generateUUID: () => clientId, 1
        getLocation: () => init.context.document.location.href,
        getReferrer: () => init.context.document.referrer,
        getUserAgent: () => init.context.navigator.userAgent,
        send: (url, token, event) =>
          fetch(`${url}?access_token=${token}`, {
            method: "POST",
            headers: {
              "Content-Type": "application/json",
            },
            keepalive: true,
            body: JSON.stringify([event]),
          }),
      };
    };
    1 This client ID will be retrieved from a coveo_shopify_config custom event emitted by the storefront, which will be handled in the next step.
  5. Create a getCoveoShopifyEvent.ts file that listens to the custom event emitted by your storefront and extracts the relevant data to build the Relay instance. Use the init function from the @coveo/shopify library to initialize your web pixel. The init function emits a custom event named coveo_shopify_config with the relevant data.

    // getCoveoShopifyEvent.ts
    
    import { ExtensionApi } from "@shopify/web-pixels-extension";
    import { COVEO_SHOPIFY_CONFIG_KEY } from "@coveo/shopify/utilities"; 1
    import { createStorage } from "./storage"; 2
    
    export interface CoveoShopifyEvent { 3
      accessToken: string;
      organizationId: string;
      trackingId: string;
      clientId: string;
    }
    
    export const registerCoveoShopifyEventListener = ({ analytics, browser }: ExtensionApi) => {
      const storage = createStorage(browser);
    
      let resolveCoveoShopifyEvent: (value: CoveoShopifyEvent) => void; 4
      const coveoShopifyEventPromise = new Promise<CoveoShopifyEvent>((resolve) => {
        resolveCoveoShopifyEvent = resolve;
      });
    
      analytics.subscribe(COVEO_SHOPIFY_CONFIG_KEY, async (event) => { 5
        const settings = event.customData as unknown as CoveoShopifyEvent; 6
        if (validateCoveoShopifyEvent(settings)) { 7
          resolveCoveoShopifyEvent(settings);
          await storage.set(COVEO_SHOPIFY_CONFIG_KEY, settings);
        }
      });
    
      const getCoveoShopifyEvent = async () => {
        const settings = await storage.get<CoveoShopifyEvent>(COVEO_SHOPIFY_CONFIG_KEY);
        if (validateCoveoShopifyEvent(settings)) { 8
          return settings;
        }
        return coveoShopifyEventPromise; 9
      };
    
      return { getCoveoShopifyEvent };
    };
    
    const validateCoveoShopifyEvent = (settings: unknown): settings is CoveoShopifyEvent => {
      const coveoShopifyEvent = settings as CoveoShopifyEvent;
    
      if (
        coveoShopifyEvent &&
        typeof coveoShopifyEvent === "object" &&
        typeof coveoShopifyEvent.organizationId === "string" &&
        coveoShopifyEvent?.organizationId?.length &&
        typeof coveoShopifyEvent.trackingId === "string" &&
        coveoShopifyEvent?.trackingId?.length &&
        typeof coveoShopifyEvent.accessToken === "string" &&
        coveoShopifyEvent?.accessToken?.length &&
        typeof coveoShopifyEvent.clientId === "string" &&
        coveoShopifyEvent?.clientId?.length
      ) {
        return true;
      } else {
        console.warn(`Received Coveo Shopify event with invalid settings: ${JSON.stringify(settings)}`);
        return false;
      }
    };
    1 The @coveo/shopify package exports the COVEO_SHOPIFY_CONFIG_KEY constant, which is used to identify the custom event name emitted by the storefront.
    2 The createStorage helper function will be defined next.
    3 The interface for the custom event data that will be emitted by the storefront and used to create the Relay instance.
    4 A promise that will be resolved with the Coveo Shopify event data when the custom event is received.
    5 Subscribe to the custom event emitted by the storefront.
    6 The custom data of the event is expected to be of type CoveoShopifyEvent.
    7 Validate the custom data to ensure it contains the required properties. Then, resolve the promise with the settings and store them in the browser storage.
    8 If the settings are valid, return them.
    9 If the settings aren’t valid or not available, return the promise that will be resolved when the custom event is received. This allows the web pixel to wait for the custom event to be emitted by the storefront before proceeding with the Relay instance creation and event logging.
  6. Create the helper storage file.

    // src/storage.ts
    
    import { ExtensionApi } from "@shopify/web-pixels-extension";
    
    export const createStorage = (browser: ExtensionApi["browser"]) => ({
      get: async <T>(key: string) => {
        const value = (await browser.cookie.get(key)) || (await browser.localStorage.getItem(key));
        if (value) {
          try {
            return JSON.parse(value) as T;
          } catch {
            return;
          }
        }
      },
      set: async <T>(key: string, value: T) => { 1
        const encodedValue = JSON.stringify(value);
        await browser.cookie.set(key, encodedValue);
        await browser.localStorage.setItem(key, encodedValue);
      },
    });
    1 Store the value in both the cookie and local storage to improve reliability.
  7. Populate the index.ts file to register the web pixel extension.

    // src/index.ts
    
    import { createRelay } from "@coveo/relay";
    import { ExtensionApi, register } from "@shopify/web-pixels-extension";
    
    import { ecAddToCartEvent, ecProductViewEvent, ecPurchaseEvent, ecRemoveFromCartEvent } from "./ecEventBuilders.ts";
    import { webPixelRelayEnvironment } from "./relayEnvironment";
    import { name as webPixelPackageName, version as webPixelPackageVersion } from "../package.json";
    import { registerCoveoShopifyEventListener, CoveoShopifyEvent } from "./getCoveoShopifyEvent.ts";
    
    register((extensionAPI: ExtensionApi) => {
      const { analytics } = extensionAPI;
      const { getCoveoShopifyEvent } = registerCoveoShopifyEventListener(extensionAPI);
    
      const relayPromise = createRelayPromise(extensionAPI, getCoveoShopifyEvent);
    
      analytics.subscribe("checkout_completed", async (event) =>
        (await relayPromise).emit("ec.purchase", ecPurchaseEvent(event.data.checkout)),
      );
    
      analytics.subscribe("product_viewed", async (event) =>
        (await relayPromise).emit("ec.productView", ecProductViewEvent(event.data.productVariant)),
      );
    
      analytics.subscribe("product_added_to_cart", async (event) => {
        if (!event.data.cartLine) {
          return;
        }
        (await relayPromise).emit("ec.cartAction", ecAddToCartEvent(event.data.cartLine));
      });
    
      analytics.subscribe("product_removed_from_cart", async (event) => {
        if (!event.data.cartLine) {
          return;
        }
        (await relayPromise).emit("ec.cartAction", ecRemoveFromCartEvent(event.data.cartLine));
      });
    });
    
    const createRelayPromise = async (
      extensionAPI: ExtensionApi,
      getCoveoShopifyEvent: () => Promise<CoveoShopifyEvent>,
    ) => {
      const { clientId, trackingId, accessToken } = await getCoveoShopifyEvent();
      const relay = createRelay({
        trackingId,
        token: accessToken,
        url: `https://${settings.organizationId}.analytics.org.coveo.com/rest/organizations/${settings.organizationId}/events/v1`, 1
        environment: webPixelRelayEnvironment(extensionAPI, clientId),
        source: [`${webPixelPackageName}@${webPixelPackageVersion}`],
      });
      return relay;
    };
    1 Use the organization ID in the web pixel settings to configure the endpoint. It would also be possible to retrieve this ID from the custom event data.
  8. When activating your web pixel, pass your Coveo organization ID in the organizationId setting.

    mutation {
      webPixelCreate(webPixel: { settings: "{\"organizationId\":\"myshoprzh5nbge\"}" }) {
        userErrors {
          code
          field
          message
        }
        webPixel {
          settings
          id
        }
      }
    }