IntegrationsShopify

Custom UI Tracking

Publish PSYKHE AI tracking events from custom Shopify collection UI through the PSYKHE AI Web Pixel.

Use this page when the PSYKHE AI Shopify app is installed, but the collection page product grid is rendered by custom theme code. The PSYKHE AI Web Pixel listens to Shopify analytics events and forwards them to PSYKHE AI when Shopify consent allows the pixel to run.

For headless storefronts where the Shopify Web Pixel does not run, use the Tracking API instead.

Publish Through Shopify Analytics

Publish events with Shopify's storefront analytics API:

type PsykheTrackingProduct = {
  productIdentifier: string;
  variantIdentifier?: string;
  name: string;
  price: number;
  position: number;
  currency: string;
};

type PsykheEventPayloads = {
  psykhe_list_view: {
    productList: string;
    recommendationId?: string;
    products: PsykheTrackingProduct[];
  };
  psykhe_list_click: {
    productList: string;
    recommendationId?: string;
    product: PsykheTrackingProduct;
  };
  psykhe_dwell_time: {
    duration: number;
    recommendationId?: string;
    product: PsykheTrackingProduct;
    pageType: "plp";
  };
};

function publishPsykheEvent<TEventName extends keyof PsykheEventPayloads>(
  eventName: TEventName,
  payload: PsykheEventPayloads[TEventName]
) {
  window.Shopify?.analytics?.publish?.(eventName, payload);
}

On a Shopify storefront, publish events when the UI action happens. Shopify manages Web Pixel consent handling and forwards eligible events to the PSYKHE AI Web Pixel.

When displayed products come from PSYKHE AI, pass the response recommendation_id as recommendationId. When displayed products come from Shopify rendering or custom merchandising code, publish the event without recommendationId.

Event Timing

EventPublish when
psykhe_list_viewA batch of products is inserted into the rendered list. For pagination or infinite scroll, send only the new batch.
psykhe_list_clickA shopper activates a product card or link from a rendered list.
psykhe_dwell_timeA product card has been visible long enough to count as dwell.

When filtering, sorting, paginating, or replacing a result set, publish a new psykhe_list_view for the newly rendered products. Use 1-based absolute positions in the rendered result order, so the first product on page 2 is 25 when page 1 rendered 24 products.

productList is a stable list or placement key defined by the storefront implementation. Use the same value for the matching list view, click, and dwell events. For collection pages, use the Shopify collection handle, such as new-arrivals. Do not use dynamic values such as the product title or recommendationId.

Product Payload

Use this product shape in list view, click, and dwell events:

FieldTypeNotes
productIdentifierstringShopify product ID as a string, without the gid://shopify/... prefix.
variantIdentifierstringOptional Shopify variant ID as a string, without the gid://shopify/... prefix.
namestringProduct title shown to the shopper.
pricenumberProduct price in major currency units, not cents. For $24.99, send 24.99.
positionnumber1-based position in the rendered list.
currencystringISO 4217 currency code, for example USD, EUR, CAD, or GBP.

If your data source returns Shopify Storefront API GIDs, strip the prefix before publishing. If your theme uses Shopify Liquid money fields that are stored in cents, divide by 100 before publishing:

function shopifyIdentifier(value: string | number) {
  return String(value).split("/").pop()!;
}

function optionalShopifyIdentifier(value: string | number | null | undefined) {
  return value == null ? undefined : shopifyIdentifier(value);
}

function shopifyCentsToMajorUnits(cents: number) {
  return cents / 100;
}

Collection Page Events

Use these events for Shopify collection pages such as /collections/new-arrivals.

List View

Publish psykhe_list_view when products are rendered or appended on a Shopify collection page. For /collections/new-arrivals, set productList to the collection handle: new-arrivals.

If PSYKHE AI returned the category results, include the response recommendation_id as recommendationId:

publishPsykheEvent("psykhe_list_view", {
  productList: "new-arrivals",
  recommendationId: response.recommendation_id,
  products: [
    {
      productIdentifier: "1234567890",
      variantIdentifier: "9876543210",
      name: "Red Silk Dress",
      price: 240,
      position: 1,
      currency: "USD",
    },
  ],
});

If the collection page renders Shopify products or another non-PSYKHE result set, publish the same event without recommendationId:

publishPsykheEvent("psykhe_list_view", {
  productList: "new-arrivals",
  products: [
    {
      productIdentifier: shopifyIdentifier(product.id),
      variantIdentifier: optionalShopifyIdentifier(
        product.selectedOrFirstAvailableVariant?.id
      ),
      name: product.title,
      price: shopifyCentsToMajorUnits(product.price),
      position: index + 1,
      currency: "USD",
    },
  ],
});

List Click

Publish psykhe_list_click when the shopper opens a product from a rendered list. Include recommendationId when the clicked item came from a PSYKHE AI result set.

Send the event for every product-card activation that shows shopper intent, including left click, right click / context menu, Shift-click, and Cmd-click. Publish before handing control to navigation so new-tab and context-menu flows are not lost.

publishPsykheEvent("psykhe_list_click", {
  productList: "new-arrivals",
  recommendationId: response.recommendation_id,
  product: {
    productIdentifier: "1234567890",
    variantIdentifier: "9876543210",
    name: "Red Silk Dress",
    price: 240,
    position: 1,
    currency: "USD",
  },
});

For non-PSYKHE lists, send the same payload without recommendationId.

For product links, publish from the product link event handlers without preventing the native link behavior. This covers left click, modified left click, middle click, and right click / context menu. In this example, productPayload is the PsykheTrackingProduct for the link:

type ProductLinkActivationEvent = {
  type: string;
  button: number;
};

function trackProductLinkActivation(event: ProductLinkActivationEvent) {
  const isLeftClick = event.type === "click" && event.button === 0;
  const isMiddleClick = event.type === "auxclick" && event.button === 1;
  const isContextMenu = event.type === "contextmenu";

  if (!isLeftClick && !isMiddleClick && !isContextMenu) return;

  publishPsykheEvent("psykhe_list_click", {
    productList: "new-arrivals",
    recommendationId: response.recommendation_id,
    product: productPayload,
  });
}

productLink.addEventListener("click", trackProductLinkActivation);
productLink.addEventListener("auxclick", trackProductLinkActivation);
productLink.addEventListener("contextmenu", trackProductLinkActivation);

Product Dwell

Publish psykhe_dwell_time when a product card remains visible long enough to count as dwell. Send the duration in milliseconds. Ignore accidental passes shorter than 300ms, and do not send events longer than 5 minutes.

Measure continuous visible time for a rendered product card. Use a consistent visibility threshold, such as at least half of the card visible, and send at most one dwell event per product per rendered list instance.

publishPsykheEvent("psykhe_dwell_time", {
  duration: 1200,
  recommendationId: response.recommendation_id,
  pageType: "plp",
  product: {
    productIdentifier: "1234567890",
    variantIdentifier: "9876543210",
    name: "Red Silk Dress",
    price: 240,
    position: 1,
    currency: "USD",
  },
});

A minimal implementation can use IntersectionObserver:

function observeProductDwell(
  productCard: Element,
  productPayload: PsykheTrackingProduct
) {
  const minDwellMs = 300;
  const maxDwellMs = 5 * 60 * 1000;
  let visibleStartedAt: number | undefined;
  let sent = false;

  function flushDwell() {
    if (visibleStartedAt == null || sent) return;

    const duration = Math.round(performance.now() - visibleStartedAt);
    visibleStartedAt = undefined;

    if (duration < minDwellMs || duration > maxDwellMs) return;

    sent = true;

    publishPsykheEvent("psykhe_dwell_time", {
      duration,
      recommendationId: response.recommendation_id,
      pageType: "plp",
      product: productPayload,
    });
  }

  const observer = new IntersectionObserver(
    ([entry]) => {
      const isVisible = entry.isIntersecting && entry.intersectionRatio >= 0.5;

      if (isVisible && !sent && visibleStartedAt == null) {
        visibleStartedAt = performance.now();
        return;
      }

      if (!isVisible) {
        flushDwell();
      }
    },
    { threshold: [0, 0.5] }
  );

  observer.observe(productCard);

  return () => {
    flushDwell();
    observer.disconnect();
  };
}

For non-PSYKHE lists, omit recommendationId.

On this page