dataLayer & Google Tag Manager mit Shopify Checkout Extensibility

Der neue Checkout von Shopify mit seiner JavaScript Sandbox benötigt diese Anpassungen, um den Google Tag Manager und den dataLayer zu implementieren.

Justus

Justus

owntag Gründer

veröffentlicht am 30. Juli 2024

Ab dem 13. August 2024 ist es nicht mehr möglich, den HTML & JavaScript Codes des Checkouts von Shopify zu bearbeiten. Bislang ging das über Anpassungen in der Datei checkout.liquid.
Mit dem komplett neu entwickelten Checkout (“Checkout Extensibility”) geht das aber nicht mehr.

Für Shopify ist es ein nachvollziehbarer Schritt, mehr technische Kontrolle innerhalb des Checkouts zu erzwingen, für die Implementierung von Tracking-Code ist es aber eine echte Herausforderung.

Sandbox

Die einzige Möglichkeit, eigenen Code im Checkout zu implementieren, sind nun die “Web Pixels”.
Diese Codeschnipsel sind als Sandbox umgesetzt.
Das bedeutet, dass der Code in einem eigenen Bereich ausgeführt wird und absichtlich eingeschränkten Zugriff auf einige Browserfunktionen hat. Innerhalb der Web Pixel wird noch einmal unterschieden zwischen den App Pixeln, die durch Shopify Apps erstellt werden, und den Custom Web Pixeln, die manuell vom Shopbetreiber angelegt werden.

Custom Web Pixel

Die App Pixel sind noch stärker eingeschränkt und haben z. B. keinen Zugriff auf das window Objekt im Browser, das aber für viele Trackingskripte und auch für den Tag Manager essentiell ist.
Das führt u. a. dazu, dass Shopify Apps keine direkte Möglichkeit haben, den Google Tag Manager und dataLayer Events zu integrieren. Es gibt zwar einige, die es versprechen, schaffen es aber tatsächlich nur für die Seiten außerhalb des Checkouts und verweisen für den Checkout dann auf die unten näher beschriebene Lösung.
Für alle Events, die im Checkout ausgelöst werden, ist also die einzige mögliche Lösung das Anlegen eines Custom Web Pixels “per Hand”:

  1. Die “Customer Events” befinden sich in den Shopify Einstellungen unter “Settings” > “Customer events”
  1. Dort klickst du auf “Add custom pixel”
  1. Dann vergibst du noch einen eindeutigen Namen, z. B. “GTM & dataLayer”

Die Consent-Einstellungen beziehen sich auf das eigene Consent Management von Shopify. Falls du das nutzt, wähle hier die Einstellung aus, die zu dem passen, was du im GTM umsetzen möchtest.
Falls du ein eigenes Consent Management wie z. B. Cookiebot oder Usercentrics nutzt, kannst du die Einstellungen hier auf “Not required” und “Data collection does not qualify as data sale” setzen und die notwendigen Einwilligungen stattdessen in deinem Consent Management Tool einholen.

  1. Nun zum eigentlich wichtigen Teil: Der Code, der sowohl den Google Tag Manager lädt als auch den dataLayer implementiert.
    Er sorgt dafür, dass diese dataLayer Events ausgelöst werden:
  • init - Wird immer zuerst ausgelöst und enthält die grundlegenden Informationen zum Shop wie z. B. die Länderzuordnung des Shops
  • page_view - Seitenaufruf
  • view_item - Aufruf einer Produktdetailseite
  • add_to_cart - Hinzufügen eines Produkts zum Warenkorb
  • begin_checkout - Betreten des Checkouts
  • add_payment_info - Hinzufügen von Zahlungsinformationen
  • purchase - Kaufabschluss

Das init Event ist hier nur das Vehikel, um die grundlegenden Shop-Informationen in den dataLayer zu übermitteln, die dann bei den nachfolgenden Events weiterhin zur Verfügung stehen. Ebenfalls darin enthalten sind die gehashten Nutzerdaten im user_data Objekt, zumindest sofern der Nutzer eingeloggt ist.

javascript
// Google Tag Manager (GTM) & dataLayer integration for Shopify Checkout Extensibility
// provided by owntag.eu

// Configuration
const gtmContainerId = "GTM-XXXXXXX";
const gtmContainerUrl = "https://www.googletagmanager.com/gtm.js";

(function (w, d, s, l, i) {
	w[l] = w[l] || [];
	w[l].push({ "gtm.start": new Date().getTime(), event: "gtm.js" });
	var f = d.getElementsByTagName(s)[0],
		j = d.createElement(s),
		dl = l != "dataLayer" ? "&l=" + l : "";
	j.async = true;
	j.src = gtmContainerUrl + "?id=" + i + dl;
	f.parentNode.insertBefore(j, f);
})(window, document, "script", "dataLayer", gtmContainerId);

const pushToDataLayer = (eventName, eventData) => {
	window.dataLayer = window.dataLayer || [];
	window.dataLayer.push({
		event: eventName,
		...eventData,
	});
};

async function sha256(message) {
	const msgBuffer = new TextEncoder().encode(message);
	const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
	const hashArray = Array.from(new Uint8Array(hashBuffer));
	const hashHex = hashArray
		.map((b) => b.toString(16).padStart(2, "0"))
		.join("");
	return hashHex;
}

async function hashUserData(userData) {
	const hashPromises = Object.entries(userData).map(async ([key, value]) => {
		if (value) {
			return [key, await sha256(value)];
		}
		return [key, null];
	});
	return Object.fromEntries(await Promise.all(hashPromises));
}

analytics.subscribe("page_viewed", (event) => {
	const userData = {
		sha256_email_address: init.data.customer?.email || null,
		sha256_phone_number: init.data.customer?.phone || null,
		sha256_first_name: init.data.customer?.firstName || null,
		sha256_last_name: init.data.customer?.lastName || null,
	};

	hashUserData(userData).then((hashedUserData) => {
		pushToDataLayer("init", {
			shop_country: init.data.shop.countryCode,
			shop_name: init.data.shop.name,
			shop_domain: init.data.shop.myShopifyDomain,
			shop_storefront_url: init.data.shop.storefrontUrl,
			user_data: hashedUserData,
			user_id: init.data.customer?.id,
			orders_count: init.data.customer?.ordersCount,
		});

		pushToDataLayer("page_view", {
			page_location: event.context.window.location.href,
			page_title: event.context.document.title,
		});
	});
});

const createItemData = (productVariant) => {
	return {
		item_id: productVariant.sku,
		item_name: productVariant.product.title,
		item_variant: productVariant.title,
		currency: productVariant.price.currencyCode,
		item_brand: productVariant.product.vendor,
		price: productVariant.price.amount,
	};
};

const createLineItemsData = (lineItems) => {
	return lineItems.map((item) => {
		return {
			item_id: item.variant.sku,
			item_name: item.title,
			item_variant: item?.variant.title,
			currency: item.variant.price.currencyCode,
			item_brand: item.variant.product.vendor,
			price: item.variant.price.amount,
			quantity: item.quantity,
		};
	});
};

// view_item
analytics.subscribe("product_viewed", (event) => {
	const { productVariant } = event.data;
	const itemData = {
		item_id: productVariant.sku,
		item_name: productVariant.product.title,
		item_variant: productVariant.title,
		currency: productVariant.price.currencyCode,
		item_brand: productVariant.product.vendor,
		price: productVariant.price.amount,
	};

	pushToDataLayer("view_item", {
		ecommerce: {
			currency: productVariant.price.currencyCode,
			value: productVariant.price.amount,
			items: [itemData],
		},
		_clear: true,
	});
});

// add_to_cart
analytics.subscribe("product_added_to_cart", (event) => {
	const { cartLine } = event.data;
	const subtotalPrice = cartLine.cost.totalAmount.amount;
	const itemData = {
		item_id: cartLine.merchandise.sku,
		item_name: cartLine.merchandise.product.title,
		item_variant: cartLine.merchandise.title,
		currency: cartLine.merchandise.price.currencyCode,
		item_brand: cartLine.merchandise.product.vendor,
		price: cartLine.merchandise.price.amount,
	};

	pushToDataLayer("add_to_cart", {
		ecommerce: {
			currency: cartLine.merchandise.price.currencyCode,
			value: subtotalPrice.toFixed(2),
			items: [Object.assign(itemData, { quantity: cartLine.quantity })],
		},
		_clear: true,
	});
});

analytics.subscribe("checkout_started", (event) => {
	const checkoutData = ga4CheckoutEvents(event);
	pushToDataLayer("begin_checkout", checkoutData);
});

analytics.subscribe("payment_info_submitted", (event) => {
	const checkoutData = ga4CheckoutEvents(event);
	pushToDataLayer("add_payment_info", checkoutData);
});

analytics.subscribe("checkout_completed", (event) => {
	const checkoutData = ga4CheckoutEvents(event);
	const { checkout } = event.data;

	checkoutData.ecommerce.transaction_id =
		checkout.order?.id || checkout.token;
	checkoutData.ecommerce.shipping =
		checkout.shippingLine?.price.amount ||
		checkout.shipping_line?.price.amount ||
		0;
	checkoutData.ecommerce.tax = checkout.totalTax?.amount || 0;

	pushToDataLayer("purchase", checkoutData);
});

function ga4CheckoutEvents(event) {
	const { checkout } = event.data;
	const lineItems = createLineItemsData(checkout.lineItems);

	return {
		ecommerce: {
			currency: checkout.subtotalPrice.currencyCode,
			value: checkout.subtotalPrice.amount,
			items: lineItems,
		},
		_clear: true,
	};
}
Kompletten Code anzeigen

Damit auch wirklich dein GTM geladen wird, musst du in Zeile 5 noch deine Container-ID (GTM-…) einsetzen.
Die gtmContainerUrl kannst du optional anpassen, wenn du schon Server Side Tagging nutzt oder den Google First Party Mode verwendest und das Skript deines GTM Web Containers über eine eigene URL statt von Googles Servern laden möchtest.

Nach dem Einfügen des Codes musst du die Änderungen noch speichern und dann einmalig das Pixel über den “Connect” Button aktivieren:

Testen der dataLayer Events

Durch die Sandbox-Umgebung mit ihrem eingeschränkten Funktionsumfang funktioniert der praktische Preview Modus des Google Tag Managers nicht – daran führt leider kein Weg vorbei.
Welche Tags ausgelöst wurden, sieht man also nur an den ausgehenden HTTP Requests.

Eine Möglichkeit, immerhin die dataLayer Events zu sehen, die im Checkout ausgelöst werden, ist die JavaScript-Konsole des Browsers. Dort, wo standardmäßig “top” steht, was die oberste Ebene des Browserfensters ist, kannst du einen anderen Frame auswählen, dessen Name dem Muster web-pixel-sandbox-CUSTOM-…-LAX folgt:

Falls du neben dem GTM/dataLayer Pixel noch andere Custom Web Pixels angelegt hast, musst du evtl. mehrere ausprobieren bis du den richtigen gefunden hast.
Der richtige ist der, in dem dataLayer definiert ist und du eine ähnliche Ausgabe wie hier siehst, wenn das dataLayer in der JS Konsole eingibst und Enter drückst:

Testen der ausgehenden Requests

Wenn im Google Tag Manager Tags eingerichtet sind, durch die dataLayer Events erfolgreich ausgelöst werden und die Daten an externe Dienste wie z. B. GA4 versenden, kann man diese HTTP Requests wie gewohnt in der Developer Console des Browsers sehen.

Aber Achtung: Wenn Shopify den Nutzer zuverlässig erkannt hat und das firmeneigene “Shop Pay” als Zahlungsmethode aktiviert ist, wird man evtl. direkt zur Domain “shop.app” weitergeleitet. Das kann nach Checkout aussehen, ist aber keiner. In dem Fall muss man zunächst “Als Gast auschecken” am Seitenende anklicken.

Werde zum Server Side Tagging Profi mit owntag

Übernimm die Kontrolle über deine digitale Datenerhebung mit Server Side Tagging und dem Server Side GTM – einfach gehostet mit owntag.

App screenshot