# Shopify customer event pixel

URL: https://docs.ablyft.com/de/developers/tracking-and-events/shopify/customer-event/

> Tracke Revenue, Checkout-Schritte und Goals auf Produktebene in Shopify mit einem Custom Pixel.



Mit einem Custom Pixel (Shopify **Customer events**) erfasst ABlyft die clientseitigen Events von Shopify über die Web Pixel
API. So kannst du auch die Schritte des Checkouts verfolgen, nicht nur den Kauf. Hintergründe findest du in der Shopify-Dokumentation zu
[Customer Events und Pixeln](https://help.shopify.com/en/manual/promoting-marketing/pixels).

> Customer Events können blockiert werden und sind weniger zuverlässig als der Webhook. Siehe den
> [Vergleich](https://docs.ablyft.com/de/developers/tracking-and-events/shopify/#which-method-should-i-choose), bevor du beide kombinierst.

## Einrichtung

1. Öffne in Shopify **Settings → Customer events**.
2. Wähle **Add custom pixel** und gib ihm einen Namen, zum Beispiel `ABlyft Tracking`.
3. Füge den [Code unten](#code-to-copy) ein und ändere die `projectId` auf die ID deines ABlyft-Projects. Das ist erforderlich.
4. Passe den `eventName` jedes Goals im Code an, falls er von den API names in ABlyft abweicht.
5. Füge den Pixel hinzu, speichere ihn und verbinde ihn.

Um Käufe und ähnliche Events zu tracken, prüfe die **Customer privacy**-Einstellungen des Pixels in Shopify. Events werden nur
gesendet, wenn die Einstellung es für den Visitor erlaubt.

## Code zum Kopieren

Einstellungen am Anfang des Codes:

| Konstante               | Bedeutung                                                                                                               |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `projectId`             | Die ID deines ABlyft-Projects. Ersetze den Platzhalter.                                                                 |
| `trackingCurrency`      | Die Währung, in der Revenue getrackt wird. Standard `"EUR"`. Bestellungen in einer anderen Währung werden umgerechnet.  |
| `maxOrderValueFallback` | Standardmäßig `false`. Gibst du eine Zahl ein, zum Beispiel `250.50`, wird ein höheres Revenue auf diesen Wert gekappt. |

```js
/*
IMPORTANT: Edit the Project ID
*/

const projectId = 123456789; // ← Your project id

//Further options (do not change if not necessary)
const trackingCurrency  = "EUR"; // ← currency you want to track in
const maxOrderValueFallback = false; // ← if a number (i.e., 250.50) is set, a revenue is capped to that

analytics.subscribe("all_events", async ({ name, data, context }) => {
    if (!(name.startsWith("checkout_") || name === "product_added_to_cart")) {
        return;
    }
    console.log("ABlyft - Shopify pixel event received:", name);

    const snippetVersion = 2.30;
    const d = document;
    const w = window;

    w.insideShopifyPixel = true;
    w.ablyftAllowIframe = true;
    w.shopifyEventContext = context;
    let j = d.createElement("script");
    j.async = true;
    j.src = `https://cdn.ablyft.com/s/${projectId}.js`;

    j.onload = async () => {
        const url = context.window.location.href;
        const { checkout } = data;
        let events = [{ eventType:"custom", eventName:`shopify_event_${name}`}];

        //currency conversion
        try {
            await new Promise((resolve, reject) => {
                const timeout = setTimeout(() => {
                    reject(new Error("ABlyft - Timeout loading currency script"));
                }, 1000); // 1 sec timeout
                const fxScript = document.createElement("script");
                fxScript.src = "https://cdn.shopify.com/s/javascripts/currencies.js";
                fxScript.onload = () => {
                    clearTimeout(timeout);
                    if (typeof Currency?.convert === "function") {
                        resolve();
                    } else {
                        reject(new Error("ABlyft - Currency.convert impossible"));
                    }
                };
                fxScript.onerror = () => {
                    clearTimeout(timeout);
                    reject(new Error("ABlyft - Currency script could not be loaded"));
                };
                document.head.appendChild(fxScript);
            });
        } catch (error) {
            console.warn("ABlyft - Currency conversion not available:", error);
        }

        //event handling
        if(name === "checkout_completed"){
            const { amount: rawAmount, currencyCode: sourceCurrency } = checkout.totalPrice;
            const count = checkout.lineItems.reduce((sum, {quantity}) => sum + quantity, 0);
            let revenue = rawAmount;
            let conversionFactor = 1;
            if(sourceCurrency !== trackingCurrency) {
                try {
                    if (typeof Currency?.convert === "function") {
                        revenue = Currency.convert(rawAmount, sourceCurrency, trackingCurrency);
                        conversionFactor = revenue / rawAmount;
                        console.log(`ABlyft - Currency conversion successful: from ${sourceCurrency} ${rawAmount} to ${trackingCurrency} ${parseFloat(revenue.toFixed(2))}`);
                    } else {
                        throw new Error("ABlyft - Currency.convert not available");
                    }
                } catch (e) {
                    console.warn("ABlyft - Currency conversion failed – using rawAmount", e);
                    revenue = rawAmount;
                }
            }
            if(typeof maxOrderValueFallback === "number"){
                if(revenue > maxOrderValueFallback) revenue = maxOrderValueFallback;
            }
            //use your own eventNames here
            events = events.concat([
                { eventType:"revenue", eventName:"revenue", eventValue: parseFloat(revenue.toFixed(2)) },
                { eventType:"custom",  eventName:"products-count", eventValue: count },
                { eventType:"custom",  eventName:"purchase" }
            ]);
            //custom product-related goals
            let lineItemsMap = {};
            checkout.lineItems.forEach(function(item) {
                const productId = item.variant?.product?.id || item.id;
                const amount =
                    item.finalLinePrice?.amount ??
                    item.totalPrice?.amount ??
                    item.linePrice?.amount ??
                    item.discountedTotalPrice?.amount ??
                    item.originalTotalPrice?.amount ??
                    0;
                const revenue = Number(amount) || 0;
                if (!lineItemsMap[productId]) {
                    lineItemsMap[productId] = { productId, revenue: 0 };
                }
                lineItemsMap[productId].revenue += revenue;
            });
            let lineItemsArray = Object.values(lineItemsMap);
            lineItemsArray.forEach(function(item) {
                events.push({ eventType: "revenue", eventName: "revenue-product-" + item.productId, eventValue: parseFloat((item.revenue*conversionFactor).toFixed(2)) });
                events.push({ eventType: "custom", eventName: "purchase-product-" + item.productId });
            });
        }
        console.log("ABlyft - Events to send:", events);

        let storageType = w.ablyft.get("data").storage;
        if(storageType === "default") storageType = "cookie";

        async function getStorageItem(key) {
            if(storageType === "cookie"){
                return await browser.cookie.get(key);
            }
            return await browser[storageType].getItem(key);
        }

        async function setStorageItem(key, value) {
            if(storageType === "cookie"){
                return await browser.cookie.set(key, value);
            }
            return await browser[storageType].setItem(key, value);
        }

        let userData = await getStorageItem("ablyft_uvs");

        const user = JSON.parse(userData);
        let visitor_type = "new";
        if (user && user.sessions && user.sessions > 1) {
            visitor_type = "returning";
        }

        const attributes = {
            device_type: w.ablyft.get("deviceType"),
            visitor_type,
            url,
            user_agent: w.navigator.userAgent
        };

        let experimentsBucketData = await getStorageItem("ablyft_exps");
        if(!experimentsBucketData){
            console.log("ABlyft - Empty user bucketing - aborting");
            return;
        }
        const experimentsBucket = JSON.parse(experimentsBucketData);

        const experimentIds = Object.keys(experimentsBucket);
        let variationIds = [];
        for (let i = 0; i < experimentIds.length; i++) { variationIds.push(experimentsBucket[experimentIds[i]]); }

        function createLockId() {
            const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
            let text = "";
            for (let i = 0; i < 10; i++) { text += possible.charAt(Math.floor(Math.random() * possible.length)); }
            return text;
        }

        const goals = w.ablyft.get("goals");
        const tgoalsData = await getStorageItem("ablyft_tgoals");
        const triggeredGoals = tgoalsData ? JSON.parse(tgoalsData) : {};

        const postObj = {
            projectId: projectId,
            source: "shopify-webpixel",
            snippetVersion,
            user: {
                attributes,
                experimentsBucket
            },
            events: events.filter(event => {
                const goalObject = goals.find(goal => goal.api_name === event.eventName);
                if(!goalObject) return false;
                return !(goalObject.counting_method === "one" && triggeredGoals[goalObject.id]);
            }).map(event => {
                const goalObject = goals.find(goal => goal.api_name === event.eventName);
                return {
                    type: event.eventType,
                    value: event.eventValue,
                    timestamp: Math.floor(Date.now() / 1000),
                    locked: Math.floor(Date.now() / 1000),
                    lockId: createLockId(),
                    goal: {
                        id: goalObject.id,
                        variations: variationIds
                    }
                };
            })
        }

        if(postObj.events.length === 0) return;
        const sent = await sendWithRetry(postObj);

        if(sent){
            postObj.events.forEach(event => {
                const goalId = event.goal.id;
                const existing = triggeredGoals[goalId] || [];
                triggeredGoals[goalId] = Array.from(new Set([...existing, ...variationIds]));
            });
            await setStorageItem("ablyft_tgoals", JSON.stringify(triggeredGoals));
        }

        async function sendWithRetry(data, maxRetries = 3) {
            const payload = JSON.stringify(data);
            for (let attempt = 1; attempt <= maxRetries; attempt++) {
                try {
                    const response = await fetch("https://log.ablyft.com", {
                        method: "POST",
                        headers: {
                            "Content-Type": "application/json"
                        },
                        body: payload
                    });
                    if (response.ok) {
                        console.log(`ABlyft - Request successful on attempt ${attempt}`);
                        return true;
                    } else if (response.status >= 400 && response.status < 500) {
                        console.warn(`ABlyft - Client error (${response.status}) – no retry`);
                        return false;
                    } else {
                        console.warn(`ABlyft - Server error (${response.status}) – retrying...`);
                    }
                } catch (error) {
                    console.warn(`ABlyft - Network error on attempt ${attempt}:`, error);
                }
                if (attempt < maxRetries) {
                    await new Promise((res) => setTimeout(res, 500)); //delay in ms
                }
            }

            console.error("ABlyft - All retry attempts failed.");
            return false;
        }
    };

    d.head.appendChild(j);
});
```

## Was der Pixel sendet

* Er reagiert auf die Events `product_added_to_cart` und alle `checkout_*`-Events, lädt dein ABlyft-Snippet und sendet die
  Events an ABlyft. Er sendet nur Events für Goals, die in deinem Project existieren (zugeordnet über den **API name**).
* Bei `checkout_completed` sendet er die Goals `revenue`, `products-count` und `purchase` sowie die unten beschriebenen
  Goals auf Produktebene.
* Der Visitor muss bereits Experiments zugewiesen sein (der Pixel liest die Zuweisung aus dem ABlyft-Storage). Gibt es
  keine, wird nichts gesendet.
* Eine fehlgeschlagene Anfrage wird bis zu dreimal wiederholt (nicht bei Client-Fehlern).

## Weitere Events

Erstelle ein **Custom** Goal und verwende einen dieser Namen als API name:

| API name                                         | Wird ausgelöst, wenn                                                             |
| ------------------------------------------------ | -------------------------------------------------------------------------------- |
| `shopify_event_product_added_to_cart`            | ein Produkt in den Warenkorb gelegt wird                                         |
| `shopify_event_checkout_started`                 | der Checkout gestartet wird                                                      |
| `shopify_event_checkout_contact_info_submitted`  | Kontaktdaten abgesendet werden                                                   |
| `shopify_event_checkout_address_info_submitted`  | die Adresse abgesendet wird                                                      |
| `shopify_event_checkout_shipping_info_submitted` | Versanddaten abgesendet werden                                                   |
| `shopify_event_checkout_completed`               | der Checkout abgeschlossen wird (alternativ `purchase` oder `revenue` verwenden) |

## Events auf Produktebene

Für die Messung pro Produkt sendet der Pixel für jedes Produkt einer abgeschlossenen Bestellung zwei zusätzliche Events:

| API name                       | Bedeutung                           |
| ------------------------------ | ----------------------------------- |
| `revenue-product-[productId]`  | Revenue dieses Produkts             |
| `purchase-product-[productId]` | Käufe, die dieses Produkt enthalten |

Erstelle ein Goal und ersetze den Platzhalter durch die **Shopify product ID**, zum Beispiel `revenue-product-1234567890`.
Die Werte werden pro Produkt summiert, nicht pro Variante: Die Revenues aller Varianten werden unter einer Produkt-ID gesammelt.

- [Shopify webhook](https://docs.ablyft.com/de/developers/tracking-and-events/shopify/webhook/): Zuverlässiges serverseitiges Kauf-Tracking.
- [Consent Manager](https://docs.ablyft.com/de/developers/consent-and-security/consent-manager/): Wie sich Consent auf das Tracking auswirkt.

