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
| Event | Publish when |
|---|---|
psykhe_list_view | A batch of products is inserted into the rendered list. For pagination or infinite scroll, send only the new batch. |
psykhe_list_click | A shopper activates a product card or link from a rendered list. |
psykhe_dwell_time | A 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:
| Field | Type | Notes |
|---|---|---|
productIdentifier | string | Shopify product ID as a string, without the gid://shopify/... prefix. |
variantIdentifier | string | Optional Shopify variant ID as a string, without the gid://shopify/... prefix. |
name | string | Product title shown to the shopper. |
price | number | Product price in major currency units, not cents. For $24.99, send 24.99. |
position | number | 1-based position in the rendered list. |
currency | string | ISO 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.