> ## Documentation Index
> Fetch the complete documentation index at: https://www.commercengine.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Analytics

> Use Commerce Engine's vendor-neutral analytics mappers with Hosted Checkout, custom SDK checkouts, or direct REST implementations.

Commerce Engine analytics has two independent layers:

1. **`@commercengine/analytics`** — a vendor-neutral, zero-runtime-dependency mapping package that turns canonical Commerce Engine entities into Segment/RudderStack-compatible ecommerce envelopes.
2. **Hosted Checkout analytics forwarding** — Hosted Checkout uses that package internally and sends the resulting envelopes to the parent application through `onAnalyticsEvent`.

The mapper package is not tied to Hosted Checkout, the Storefront SDK, Segment, or RudderStack. It can be used with:

* Hosted Checkout
* a custom checkout built with `@commercengine/storefront`
* a custom checkout calling the REST API directly
* browser analytics SDKs
* server-side CDP SDKs
* raw HTTP ingestion
* a fully custom analytics implementation

## Mental model

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
Commerce Engine entity or customer action
  → @commercengine/analytics mapper/builder
  → canonical track or identify envelope
  → your delivery adapter
  → Segment, RudderStack, another CDP, or your own pipeline
```

`@commercengine/analytics` performs only the mapping step. It does not:

* load a vendor SDK
* send network requests
* manage consent
* store identity
* persist an event queue
* prescribe where events must be delivered

That separation keeps event schemas consistent while leaving transport, consent, batching, retries, and destination ownership to the application.

## Install the mapper package

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @commercengine/analytics
```

It consumes canonical Commerce Engine types through a type-only peer dependency:

* Applications using `@commercengine/storefront` already satisfy that dependency.
* REST-only TypeScript applications should also install the generated SDK type package:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @commercengine/storefront-sdk
```

The analytics bundle itself has no runtime dependency on either SDK package; the peer exists so mapper inputs remain type-safe.

## Three supported integration modes

### Hosted Checkout

Hosted Checkout maps cart, coupon, checkout-step, payment, identity, and order interactions internally. The parent receives completed envelopes and only needs to forward them.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import type { AnalyticsEvent } from "@commercengine/checkout";
import { initCheckout } from "@commercengine/checkout";

function forwardCheckoutAnalytics(event: AnalyticsEvent) {
  if (event.type !== "track") return;
  analytics.track(event.event, event.properties);
}

initCheckout({
  storeId: "store_xxx",
  apiKey: "ak_xxx",
  onAnalyticsEvent: forwardCheckoutAnalytics,
});
```

Use `@commercengine/checkout` version `0.5.0` or later for `onAnalyticsEvent`. This minimal adapter forwards track events only; identity handling depends on which layer owns the Commerce Engine session and is covered below.

<Info>
  Do not rebuild the same checkout events in the parent application. Hosted Checkout already maps them from the canonical cart and order entities. Re-emitting them from both layers creates duplicate conversions and schema drift.
</Info>

### Custom checkout with the Storefront SDK

A custom checkout owns the customer interaction, so it decides when an event has occurred. Map the canonical entity returned by the SDK at that point.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import {
  orderProperties,
  toCartViewed,
  toCheckoutStarted,
  toOrderCompleted,
  trackArgs,
  trackEvent,
} from "@commercengine/analytics";

const { data: cartData, error: cartError } = await sdk.cart.getUserCart();
if (cartError || !cartData?.cart) {
  throw new Error(cartError?.message ?? "Cart not found");
}

analytics.track(...trackArgs(toCartViewed(cartData.cart)));
analytics.track(...trackArgs(toCheckoutStarted(cartData.cart)));

const { data: orderData, error: orderError } =
  await sdk.order.createOrder(orderInput);

if (orderError || !orderData?.order) {
  throw new Error(orderError?.message ?? "Order creation failed");
}

const order = orderData.order;
analytics.track(
  ...trackArgs(trackEvent("Order Created", orderProperties(order))),
);

if (!orderData.payment_required) {
  analytics.track(...trackArgs(toOrderCompleted(order)));
}
```

For a payment-required order, do not emit `Order Completed` after `createOrder()` or after one immediate status check. Provider callbacks are asynchronous, so the first response will commonly remain `pending`.

Run conversion tracking from the same payment-return flow that drives the customer-facing result, or from a server-side payment-success webhook. A browser return flow should wait through pending states with a bounded verifier:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const MAX_POLLS = 40;
const POLL_INTERVAL_MS = 3_000;

async function waitForPayment(orderNumber: string) {
  for (let attempt = 0; attempt < MAX_POLLS; attempt += 1) {
    const { data, error, response } =
      await sdk.order.getPaymentStatus(orderNumber);

    if (error) {
      const status = response?.status;
      const transient =
        status === undefined ||
        status === 429 ||
        status >= 500;

      if (!transient) throw new Error(error.message);
    } else if (
      data?.status === "success" ||
      data?.status === "failed" ||
      (data?.status !== "pending" && data?.status !== "partially_paid")
    ) {
      return data;
    }

    await new Promise((resolve) =>
      setTimeout(resolve, POLL_INTERVAL_MS),
    );
  }

  return null;
}

const payment = await waitForPayment(order.order_number);

if (payment?.status === "success") {
  const { data: completed, error: orderDetailError } =
    await sdk.order.getOrderDetails({
      order_number: order.order_number,
    });

  if (orderDetailError || !completed?.order) {
    throw new Error(
      orderDetailError?.message ?? "Completed order could not be retrieved",
    );
  }

  analytics.track(...trackArgs(toOrderCompleted(completed.order)));
}
```

Do not start a second polling loop solely for analytics. Reuse the authoritative payment-status flow that already drives the checkout return page. If the bounded check ends in `pending` or times out, do not emit the conversion; let a later payment-return visit, account refresh, or idempotent server webhook record completion once success is authoritative.

The package maps the entity; your application still owns the semantic trigger. For example, emit `Checkout Started` when the customer actually begins checkout, not merely because a cart happened to load.

### Custom checkout with direct REST calls

The package works the same way when the Commerce Engine entity came from REST rather than the TypeScript SDK.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import {
  orderProperties,
  toCheckoutStarted,
  toOrderCompleted,
  trackArgs,
  trackEvent,
} from "@commercengine/analytics";
import type { Cart, Order } from "@commercengine/storefront-sdk";

const cartResponse = await fetch(`${baseUrl}/carts/${cartId}`, {
  headers: { Authorization: `Bearer ${accessToken}` },
});

if (!cartResponse.ok) throw new Error("Unable to load cart");

const cartPayload = await cartResponse.json();
const cart = cartPayload.content.cart as Cart;
analytics.track(...trackArgs(toCheckoutStarted(cart)));

const orderResponse = await fetch(`${baseUrl}/orders`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${accessToken}`,
    "content-type": "application/json",
  },
  body: JSON.stringify(orderInput),
});

if (!orderResponse.ok) throw new Error("Unable to create order");

const orderPayload = await orderResponse.json();
const order = orderPayload.content.order as Order;
analytics.track(
  ...trackArgs(trackEvent("Order Created", orderProperties(order))),
);

if (!orderPayload.content.payment_required) {
  analytics.track(...trackArgs(toOrderCompleted(order)));
}
```

For payment-required REST orders, use the same bounded pending-state loop in the payment-return flow—or an idempotent server webhook—until `GET /orders/{order_number}/payment-status` reports `success`. Then retrieve `GET /orders/{order_number}` and map that current `Order` with `toOrderCompleted()`. One immediate status check and a redirect back from the payment provider are not proof of payment success.

Prefer generated Commerce Engine types even in REST integrations. Do not create parallel hand-written cart, product, and order interfaces that can drift from the API.

## Entity mappers

The package includes pure mappers for events that can be derived from canonical Commerce Engine entities.

| Mapper                    | Event                     | Canonical input                       |
| ------------------------- | ------------------------- | ------------------------------------- |
| `toProductsSearched`      | `Products Searched`       | Search string                         |
| `toProductViewed`         | `Product Viewed`          | `Product`, `ProductDetail`, or `Item` |
| `toProductClicked`        | `Product Clicked`         | `Product`, `ProductDetail`, or `Item` |
| `toProductListViewed`     | `Product List Viewed`     | Product entities                      |
| `toProductListFiltered`   | `Product List Filtered`   | Product entities                      |
| `toProductAdded`          | `Product Added`           | `CartItem`                            |
| `toProductRemoved`        | `Product Removed`         | `CartItem`                            |
| `toCartViewed`            | `Cart Viewed`             | `Cart`                                |
| `toCheckoutStarted`       | `Checkout Started`        | `Cart`                                |
| `toCheckoutStepViewed`    | `Checkout Step Viewed`    | `Cart`                                |
| `toCheckoutStepCompleted` | `Checkout Step Completed` | `Cart`                                |
| `toPaymentInfoEntered`    | `Payment Info Entered`    | `Cart`                                |
| `toOrderCompleted`        | `Order Completed`         | `Order`                               |
| `toOrderUpdated`          | `Order Updated`           | `Order`                               |
| `toOrderCancelled`        | `Order Cancelled`         | `Order`                               |
| `toOrderRefunded`         | `Order Refunded`          | `Order`                               |

The mappers derive currency and standard monetary fields from the entity. Product mappers also normalize differences between `Product`, `ProductDetail`, and flattened `Item` shapes.

### Selected variants

A parent product can represent several variants. Pass the selected variant when mapping a product interaction:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const event = toProductViewed(product, {}, {
  variant: selectedVariant,
});

analytics.track(...trackArgs(event));
```

The selected variant overrides the emitted SKU, price, image, variant name, variant ID, and variant slug. Without an explicit selection, the mapper uses the default variant when one exists.

### Product list events

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import {
  toProductListViewed,
  trackArgs,
} from "@commercengine/analytics";

analytics.track(
  ...trackArgs(
    toProductListViewed(items, {}, {
      listId: category.id,
      category: category.name,
    }),
  ),
);
```

Avoid rebuilding product arrays manually or hardcoding currency. Let the mapper normalize the Commerce Engine entities.

## Events without a single source entity

Use `trackEvent` for actions such as coupons, promotions, wishlist interactions, reviews, and custom business events.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { trackArgs, trackEvent } from "@commercengine/analytics";

analytics.track(
  ...trackArgs(
    trackEvent("Coupon Applied", {
      cart_id: cart.id,
      coupon_id: couponCode,
      discount: cart.coupon_discount_amount,
    }),
  ),
);
```

For custom events, reuse the package's property builders instead of reconstructing canonical cart or order fields.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import {
  cartProperties,
  orderProperties,
  trackArgs,
  trackEvent,
} from "@commercengine/analytics";

analytics.track(
  ...trackArgs(trackEvent("Cart Created", cartProperties(cart))),
);

analytics.track(
  ...trackArgs(trackEvent("Order Created", orderProperties(order))),
);
```

## Browser, server, and HTTP delivery

The mapper returns a complete Segment/RudderStack HTTP-style envelope. Adapt it to the delivery API you use.

### Browser SDK

Browser SDKs typically keep identity in ambient state and expect `(event, properties)`:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
analytics.track(...trackArgs(toProductViewed(product)));
```

<Warning>
  Do not pass the whole envelope to a Segment-style browser `track()` method. `analytics.track(eventEnvelope)` is interpreted as an event-name argument. Use `trackArgs` or pass `event.event` and `event.properties` separately.
</Warning>

### Server SDK

A server process has no ambient browser identity. Add identity to the mapper context and pass the resulting envelope fields to the server SDK.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const event = toOrderCompleted(order, {
  userId,
  anonymousId,
});

serverAnalytics.track({
  userId: event.userId,
  anonymousId: event.anonymousId,
  event: event.event,
  properties: event.properties,
});
```

### Raw HTTP

A compatible ingestion endpoint can accept the complete envelope unchanged:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
const event = toOrderCompleted(order, {
  userId,
  anonymousId,
});

await fetch(analyticsIngestionUrl, {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify(event),
});
```

## Identity ownership

Identity is separate from event mapping. Choose one owner for each application architecture.

### Storefront SDK + Hosted Checkout

When using `authMode: "provided"`, the parent Storefront SDK owns tokens and identity. A sound pattern is:

* Hosted Checkout forwards `track` envelopes
* Storefront SDK token updates drive `identify`
* Hosted Checkout `identify` envelopes are ignored to avoid a second identity path

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
function forwardCheckoutAnalytics(event: AnalyticsEvent) {
  if (event.type !== "track") return;
  analytics.track(event.event, event.properties);
}
```

### Checkout-only integration

When Hosted Checkout uses `authMode: "managed"`, it owns the session. The parent adapter can consume both track and identify envelopes.

Handle anonymous identity using the destination's supported anonymous-ID API. Do not call `identify(undefined, traits)`.

### Custom checkout

A custom checkout can use `toIdentify`, `identifyArgs`, or `userTraits` with either a full Commerce Engine `User` or the JWT-derived `UserInfo`.

```typescript theme={"theme":{"light":"github-light","dark":"github-dark"}}
import {
  identifyArgs,
  toIdentify,
} from "@commercengine/analytics";

const user = await sdk.getUserInfo();
if (user) {
  analytics.identify(...identifyArgs(toIdentify(user)));
}
```

The mapper distinguishes a true anonymous user from a known user. It does not equate “logged out” with “anonymous”; a logged-out user can still be a known Commerce Engine identity.

## Hosted Checkout events

Hosted Checkout currently uses the mapper package internally for customer actions including:

| Area            | Events                                                                |
| --------------- | --------------------------------------------------------------------- |
| Cart            | `Cart Created`, `Cart Viewed`, `Product Added`, `Product Removed`     |
| Recommendations | `Recommended Products Viewed`                                         |
| Checkout        | `Checkout Started`, `Checkout Step Viewed`, `Checkout Step Completed` |
| Payment         | `Payment Info Entered`                                                |
| Coupons         | `Coupon Entered`, `Coupon Applied`, `Coupon Removed`, `Coupon Denied` |
| Orders          | `Order Created`, `Order Completed`                                    |
| Identity        | Canonical `identify` envelope                                         |

Checkout-step events can include a numeric step, a more specific `step_name`, the selected shipping method, and the selected payment method.

Hosted Checkout suppresses duplicate emission for key session-level signals such as checkout start, recommendation views, and order completion. Downstream destinations should still apply their normal idempotency and deduplication policies.

## Data minimization

The standard property builders intentionally avoid copying large operational objects into analytics payloads. Raw addresses, shipments, inventory lots, seller details, payment authentication data, tokens, and OTP values are not included by default.

Add custom properties only when they serve a defined analytics purpose and comply with the application's consent and privacy policy.

## Common mistakes

| Mistake                                                                 | Consequence                                       | Correct approach                                |
| ----------------------------------------------------------------------- | ------------------------------------------------- | ----------------------------------------------- |
| Treating the package as a RudderStack client                            | No events are delivered                           | Add your own destination adapter                |
| Assuming it only works with Hosted Checkout                             | Unnecessary hand-built schemas in custom checkout | Map REST or SDK entities directly               |
| Recreating Hosted Checkout events in the parent                         | Duplicate conversions                             | Forward `onAnalyticsEvent`                      |
| Forwarding identify from both checkout and Storefront SDK               | Competing identity transitions                    | Choose one identity owner                       |
| Passing a full envelope to browser `track()`                            | Invalid invocation                                | Use `trackArgs`                                 |
| Hardcoding currency                                                     | Incorrect multi-currency data                     | Let mappers derive it                           |
| Emitting events because data loaded, rather than because the user acted | Misleading funnels                                | Let the application own semantic trigger timing |
| Throwing from instrumentation                                           | Analytics can disrupt commerce UX                 | Keep analytics non-blocking                     |

## Verification checklist

```text theme={"theme":{"light":"github-light","dark":"github-dark"}}
[ ] every customer action has one event owner
[ ] Hosted Checkout events are forwarded, not rebuilt
[ ] custom checkout events use canonical REST/SDK entities
[ ] browser destinations receive event + properties, not the whole envelope
[ ] server/HTTP destinations receive explicit identity context
[ ] currency and totals come from Commerce Engine entities
[ ] anonymous activity joins the known identity after login
[ ] no tokens, OTPs, raw card data, or complete addresses are emitted
[ ] analytics failures cannot interrupt checkout
[ ] consent and destination loading are controlled by the application
```

<Card title="@commercengine/analytics" icon="npm" href="https://npmx.dev/package/@commercengine/analytics/v/0.1.2">
  Browse the current package documentation, exported mappers, types, and source.
</Card>
