Behio Storefront SDK
Advanced

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 (gtag present) → gtag("event", name, payload)
  • GTM (dataLayer present) → dataLayer.push({ event, ecommerce }) with the recommended ecommerce: null reset 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).

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 pagehide via 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).

Identity is three-tiered:

  1. Anonymous (no consent needed): the server computes a daily-rotating hash; the client sends nothing identifying.
  2. 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. Dispatch window.dispatchEvent(new CustomEvent("behio:consent-changed")) after recording or revoking consent so the tracker picks the change up immediately (the official ConsentBanner does this).
  3. Customer: pass analyticsVisitorId (from client.getAnalyticsVisitorId()) in checkout.createOrder and 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" }],
});

On this page