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.accessToken] 2
    name = "Access token"
    description = "The access token used to push events to Coveo"
    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 an API key or search token that grants the privileges to push UA data to the target Coveo organization. See Authenticate commerce requests.
  2. Install the required dependencies, namely @coveo/relay and @coveo/relay-event-types.

    // package.json
    
    {
      // ...
      "dependencies": {
        // ...
        "@coveo/relay": "^1.1.0",
        "@coveo/relay-event-types": "^13.1.3"
      }
    }
  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";
    import { v5 } from "uuid";
    
    const uuidNamespace = "c2db3701-991e-424d-b117-03e30d43b651"; 1
    const clientIdFromShopifyCookie = (shopifyCookie: string) => v5(shopifyCookie, uuidNamespace);
    const isCoveoClientIdCookie = (key: string) => key === "visitorId" || key === "clientId";
    const coveoCookiePrefix = "coveo_";
    
    export const webPixelRelayEnvironment = (
      { init, browser }: ExtensionApi,
      shopifyCookie: string,
    ): CustomEnvironment => ({
      generateUUID: () => clientIdFromShopifyCookie(shopifyCookie),
      getLocation: () => init.context.document.location.href,
      getReferrer: () => init.context.document.referrer,
      getUserAgent: () => init.context.navigator.userAgent,
      send: (url, token, event) => 2
        fetch(`${url}?access_token=${token}`, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          keepalive: true,
          body: JSON.stringify([event]),
        }),
      storage: { 3
        getItem: (key) => (isCoveoClientIdCookie(key) ? clientIdFromShopifyCookie(shopifyCookie) : null),
        removeItem: (key) => {
          if (!isCoveoClientIdCookie(key)) {
            browser.cookie.set(`${coveoCookiePrefix}${key}`, `expires=${new Date(0).toUTCString()}`);
          }
        },
        setItem: (key: string, data: string) => {
          if (key !== "visitorId" && key !== "clientId") {
            browser.cookie.set(`${coveoCookiePrefix}${key}`, data);
          }
        },
      },
    });
    1 Make sure to use this same UUID namespace everywhere, so that the client ID is generated in a way consistent with the Coveo Event Protocol.
    2 Implement the Relay send function using the access token created in this file.
    3 Implement the Relay storage functions to manage the client ID and context, working around the restrictive nature of web pixels.
  5. Populate the index.ts file to register the web pixel extension.

    // src/index.ts
    
    import { ExtensionApi, register } from "@shopify/web-pixels-extension";
    import { createRelay, Relay } from "@coveo/relay";
    import { ecPurchaseEvent } from "./ecEventBuilder.js";
    import { webPixelRelayEnvironment } from "./relayEnvironment.js";
    import { name as webPixelPackageName, version as webPixelPackageVersion } from "../package.json";
    
    register((extensionAPI: ExtensionApi) => {
      const { analytics, browser, settings } = extensionAPI;
    
      const relayInstance: Promise<Relay> = (async () =>
        createRelay({
          token: settings.accessToken,
          trackingId: "market_17962041446",1
          url: "https://my-org.analytics.org.coveo.com/rest/organizations/my-org/events/v1", 2
          environment: webPixelRelayEnvironment(extensionAPI, await browser.cookie.get("_shopify_y")),3
          source: [`${webPixelPackageName}@${webPixelPackageVersion}`],
        }))();
    
      analytics.subscribe("checkout_completed", async (event) =>
        (await relayInstance).emit("ec.purchase", ecPurchaseEvent(event.data.checkout)),
      );
    
    });
    1 In a real implementation, you would most likely retrieve the tracking ID programmatically, depending on the market.
    2 The destination URL for sending Relay requests. It follows this format: https://<ORG_ID>.analytics.org.coveo.com/rest/organizations/<ORG_ID>/events/v1, where you replace <ORG_ID> with your organization ID.
    3 Use the webPixelRelayEnvironment function to manage the client ID and context.
  6. When activating your web pixel, pass an API key or a search token that grants the privileges to push UA data to the target Coveo organization. See Authenticate commerce requests.

    mutation {
      webPixelCreate(webPixel: { settings: "{\"accessToken\":\"xx602de46b-0902-442b-87e1-8e53f4f0c578\"}" }) {
        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.accessToken] 2
    name = "Access token"
    description = "The access token used to push events to Coveo"
    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 an API key or search token that grants the privileges to push UA data to the target Coveo organization. See Authenticate commerce requests.
  2. Install the required dependencies, namely @coveo/relay and @coveo/relay-event-types.

    // package.json
    
    {
      // ...
      "dependencies": {
        // ...
        "@coveo/relay": "^1.1.0",
        "@coveo/relay-event-types": "^13.1.3"
      }
    }
  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";
    import { v5 } from "uuid";
    
    const uuidNamespace = "c2db3701-991e-424d-b117-03e30d43b651"; 1
    const clientIdFromShopifyCookie = (shopifyCookie: string) => v5(shopifyCookie, uuidNamespace);
    const isCoveoClientIdCookie = (key: string) => key === "visitorId" || key === "clientId";
    const coveoCookiePrefix = "coveo_";
    
    export const webPixelRelayEnvironment = (
      { init, browser }: ExtensionApi,
      shopifyCookie: string,
    ): CustomEnvironment => ({
      generateUUID: () => clientIdFromShopifyCookie(shopifyCookie),
      getLocation: () => init.context.document.location.href,
      getReferrer: () => init.context.document.referrer,
      getUserAgent: () => init.context.navigator.userAgent,
      send: (url, token, event) => 2
        fetch(`${url}?access_token=${token}`, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          keepalive: true,
          body: JSON.stringify([event]),
        }),
      storage: { 3
        getItem: (key) => (isCoveoClientIdCookie(key) ? clientIdFromShopifyCookie(shopifyCookie) : null),
        removeItem: (key) => {
          if (!isCoveoClientIdCookie(key)) {
            browser.cookie.set(`${coveoCookiePrefix}${key}`, `expires=${new Date(0).toUTCString()}`);
          }
        },
        setItem: (key: string, data: string) => {
          if (key !== "visitorId" && key !== "clientId") {
            browser.cookie.set(`${coveoCookiePrefix}${key}`, data);
          }
        },
      },
    });
    1 Make sure to use this same UUID namespace everywhere, so that the client ID is generated in a way consistent with the Coveo Event Protocol.
    2 Implement the Relay send function using the access token created in this file.
    3 Implement the Relay storage functions to manage the client ID and context, working around the restrictive nature of web pixels.
  5. Populate the index.ts file to register the web pixel extension.

    // src/index.ts
    
    import { ExtensionApi, register } from "@shopify/web-pixels-extension";
    import { createRelay, Relay } from "@coveo/relay";
    import { ecAddToCartEvent, ecProductViewEvent, ecPurchaseEvent, ecRemoveFromCartEvent } from "./ecEventBuilder.js";
    import { webPixelRelayEnvironment } from "./relayEnvironment.js";
    import { name as webPixelPackageName, version as webPixelPackageVersion } from "../package.json";
    
    register((extensionAPI: ExtensionApi) => {
      const { analytics, browser, settings } = extensionAPI;
    
      const relayInstance: Promise<Relay> = (async () =>
        createRelay({
          token: settings.accessToken,
          trackingId: "market_17962041446", 1
          url: "https://my-org.analytics.org.coveo.com/rest/organizations/my-org/events/v1", 2
          environment: webPixelRelayEnvironment(extensionAPI, await browser.cookie.get("_shopify_y")), 3
          source: [`${webPixelPackageName}@${webPixelPackageVersion}`],
        }))();
    
      analytics.subscribe("checkout_completed", async (event) =>
        (await relayInstance).emit("ec.purchase", ecPurchaseEvent(event.data.checkout)),
      );
    
      analytics.subscribe("product_viewed", async (event) =>
        (await relayInstance).emit("ec.productView", ecProductViewEvent(event.data.productVariant)),
      );
    
      analytics.subscribe("product_added_to_cart", async (event) => {
        if (!event.data.cartLine) {
          return;
        }
        (await relayInstance).emit("ec.cartAction", ecAddToCartEvent(event.data.cartLine));
      });
    
      analytics.subscribe("product_removed_from_cart", async (event) => {
        if (!event.data.cartLine) {
          return;
        }
        (await relayInstance).emit("ec.cartAction", ecRemoveFromCartEvent(event.data.cartLine));
      });
    
    });
    1 In a real implementation, you would most likely programmatically retrieve the tracking ID depending on the market.
    2 The destination URL for sending Relay requests. It follows this format: https://<ORG_ID>.analytics.org.coveo.com/rest/organizations/<ORG_ID>/events/v1, where you replace <ORG_ID> with your organization ID.
    3 Use the webPixelRelayEnvironment function to manage the client ID and context.
  6. When activating your web pixel, pass an API key or a search token that grants the privileges to push UA data to the target Coveo organization. See Authenticate commerce requests.

    mutation {
      webPixelCreate(webPixel: { settings: "{\"accessToken\":\"xx602de46b-0902-442b-87e1-8e53f4f0c578\"}" }) {
        userErrors {
          code
          field
          message
        }
        webPixel {
          settings
          id
        }
      }
    }