GA4 Analytics Events
Fire standard GA4 e-commerce events (view_item, add_to_cart, begin_checkout, purchase) from your storefront
The SDK ships a tiny, dependency-free helper that fires standard GA4 e-commerce events into whatever analytics runtime the shop has configured. It is available from both entry points:
import { trackEcommerceEvent } from "@behio/storefront-sdk";
// or
import { trackEcommerceEvent } from "@behio/storefront-sdk/react";How it works
trackEcommerceEvent never loads any script itself. It only talks to runtimes
already present on the page, which is exactly what <StorefrontScripts />
injects based on the merchant's admin configuration:
- direct GA4 (
gtagpresent) →gtag("event", name, payload) - GTM (
dataLayerpresent) →dataLayer.push({ event, ecommerce })with the recommendedecommerce: nullreset push first - neither (no analytics configured, or the visitor has not granted analytics consent so the gated script never loaded) → silent no-op
Consent stays the script layer's job: if the merchant marked their GA4/GTM
script as consent-required, no runtime exists until the visitor accepts, and
every trackEcommerceEvent call before that is a no-op. You never need to
check consent yourself before calling it.
Events
import { trackEcommerceEvent } from "@behio/storefront-sdk";
// Product detail page
trackEcommerceEvent("view_item", {
currency: "CZK",
value: 1290,
items: [{ item_id: product.id, item_name: product.name, price: 1290, quantity: 1 }],
});
// After a successful add to cart
trackEcommerceEvent("add_to_cart", {
currency: cart.currency,
value: item.price * quantity,
items: [{ item_id: productId, item_name: name, price: item.price, quantity }],
});
// Checkout page mount
trackEcommerceEvent("begin_checkout", {
currency: cart.currency,
value: cart.grandTotal,
items: cart.items.map((it) => ({
item_id: it.productId,
item_name: it.product.name,
price: it.unitPrice,
quantity: it.quantity,
})),
});
// Order confirmation page (deduplicate per order - the page gets revisited)
trackEcommerceEvent("purchase", {
transaction_id: order.orderNumber,
currency: order.currency,
value: order.grandTotal,
shipping: order.shippingTotal,
items: order.items.map((it) => ({
item_id: it.sku || it.productName,
item_name: it.productName,
price: it.unitPrice,
quantity: it.quantity,
})),
});Supported event names: view_item, view_item_list, select_item,
add_to_cart, remove_from_cart, view_cart, add_to_wishlist,
view_promotion, select_promotion, begin_checkout, add_shipping_info,
add_payment_info, search, generate_lead, variant_selected,
newsletter_signup, purchase.
Non-item events carry GA4 params instead of items: search uses
search_term (+ props.resultsCount / props.zeroResults for the
demand-you-cannot-serve signal), add_shipping_info uses shipping_tier,
add_payment_info uses payment_type, and view_item_list / select_item
use item_list_id (shop | category:<slug> | search |
rail:crossSell | rail:related). newsletter_signup maps to GA4's
generate_lead on the GA sink while staying a distinct Behio signal. Any
props object is forwarded verbatim to Behio Analytics (ignored by GA4).
// Search results page: search + zero-results signal
trackEcommerceEvent("search", {
search_term: query,
props: { query, resultsCount, zeroResults: resultsCount === 0 },
});
// Shipping / payment method selected in checkout
trackEcommerceEvent("add_shipping_info", { currency, value: cartTotal, shipping_tier: method.name });
trackEcommerceEvent("add_payment_info", { currency, value: cartTotal, payment_type: method.name });
// Product grid / rail impression + card click attribution
trackEcommerceEvent("view_item_list", {
item_list_id: "rail:crossSell",
items: products.map((p) => ({ item_id: p.id, item_name: p.name, price: p.price })),
});Never emit order_created, order_paid, return_created (or other
server-only names) from the client. The public ingest silently drops them.
Revenue and fulfillment truth comes from trusted server hooks; the client's
only purchase signal is GA4's purchase (GA sink only, never double-counted
in Behio).
Consent + visitor id
The SDK owns the persistent behio_visitor_id. The consent banner calls
grantAnalyticsConsent(client) on accept and revokeAnalyticsConsent(client)
on reject (both from @behio/storefront-sdk); the SDK generates + stores the
id, records consent server-side and dispatches behio:consent-changed so the
tracker starts (or stops) attaching the id. Do not hand-roll the id.
Purchase deduplication
Confirmation pages get revisited (payment gateway returns, reloads, "where is
my order" checks). Guard the purchase event per order, e.g. with
sessionStorage:
"use client";
import { useEffect } from "react";
import { trackEcommerceEvent, type EcommerceItem } from "@behio/storefront-sdk";
export function TrackPurchase({ orderNumber, items, currency, value }: {
orderNumber: string; items: EcommerceItem[]; currency?: string; value?: number;
}) {
useEffect(() => {
const key = `behio.purchase.${orderNumber}`;
try {
if (sessionStorage.getItem(key)) return;
sessionStorage.setItem(key, "1");
} catch {}
trackEcommerceEvent("purchase", { transaction_id: orderNumber, currency, value, items });
}, [orderNumber]);
return null;
}The official storefront template wires all four events out of the box (product page, add-to-cart button, checkout page, order confirmation), so shops generated by the Behio builder measure e-commerce conversions in GA4 without any extra work.
Behio Analytics (first-party, cookieless)
Beyond GA4, the SDK ships a built-in first-party analytics tracker that feeds the shop's Behio admin dashboard. Mount it once in the root layout:
"use client";
import { BehioAnalyticsTracker } from "@behio/storefront-sdk/react";
export function BehioAnalytics() {
return <BehioAnalyticsTracker />;
}What it does:
- pageviews on load and on every SPA route change
- dwell time per page (visibility-aware), flushed on route change and
pagehidevia a keepalive request - UTM + referrer attribution on landing
Privacy model: the tracker is cookieless and sends no identity. The visitor hash is computed server-side from a daily-rotating salt, and the IP is never stored, so basic traffic analytics work without a consent banner and are not blocked by ad blockers (first-party domain).
Visitor identity and consent
Identity is three-tiered:
- Anonymous (no consent needed): the server computes a daily-rotating hash; the client sends nothing identifying.
- Consented: once the visitor grants analytics consent in the shop's
consent banner (keyed by the existing
behio_visitor_id), the tracker automatically attaches that persistent first-party id, which enables returning-visitor metrics and the customer journey. Dispatchwindow.dispatchEvent(new CustomEvent("behio:consent-changed"))after recording or revoking consent so the tracker picks the change up immediately (the official ConsentBanner does this). - Customer: pass
analyticsVisitorId(fromclient.getAnalyticsVisitorId()) incheckout.createOrderand the backend links the journey to the customer and the order server-side.
Clicks on interactive elements (a, button, [role=button]) are captured
automatically. Name important actions explicitly with the
data-behio-event attribute:
<button data-behio-event="newsletter_signup">Odebírat novinky</button>For one-off events you can also call the low-level method directly:
const behio = useBehioClient();
void behio.sendAnalyticsEvents({
events: [{ type: "custom", name: "newsletter_signup", path: "/newsletter" }],
});