npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@commercengine/analytics

v0.1.2

Published

Zero-dependency mappers that transform canonical Commerce Engine entities into canonical e-commerce spec analytics events

Readme

@commercengine/analytics

Zero-dependency, fully-typed mappers that turn canonical Commerce Engine entities into events conforming to the Segment / RudderStack E-Commerce spec.

Every mapper is a pure function: canonical CE shape in → a Segment HTTP Tracking API envelope out ({ type: "track", event, properties, … }). Modify it, POST it unchanged to a CDP, or dispatch it through an analytics SDK.

With the browser SDKs (Segment analytics.js / RudderStack JS) identity is ambient — set once via identify(), then auto-attached to every track. So you pass no context here; just map and send:

import {
  toProductViewed,
  toCartViewed,
  toCheckoutStarted,
  toOrderCompleted,
  trackArgs,
} from "@commercengine/analytics";

const { data: product } = await sdk.catalog.getProduct(slug);
analytics.track(...trackArgs(toProductViewed(product)));

const { data: cart } = await sdk.cart.getCart();
analytics.track(...trackArgs(toCartViewed(cart)));
analytics.track(...trackArgs(toCheckoutStarted(cart)));

const { data: order } = await sdk.order.getOrder(orderNumber);
analytics.track(...trackArgs(toOrderCompleted(order)));

Currency is derived from each entity. Pass a ctx only when you need a mapping hook (productUrl, brand) or you're sending the envelope yourself (see below).

Sending events

The returned object is the Segment/RudderStack HTTP Tracking API payload, not the argument list of the browser SDKs. Dispatch it the way your target expects:

// 1. Browser SDK (Segment analytics.js / RudderStack JS): method takes
//    (name, properties). Identity is ambient (set via identify()), so no ctx.
analytics.track(...trackArgs(toProductViewed(product)));

// 2. Server SDK (@segment/analytics-node, @rudderstack/rudder-sdk-node):
//    no ambient session — pass identity via ctx so it lands on the envelope.
const event = toProductViewed(product, { userId });
analytics.track({ userId: event.userId, event: event.event, properties: event.properties });

// 3. Raw HTTP ingestion: POST the envelope unchanged.
await fetch("https://api.segment.io/v1/track", { method: "POST", body: JSON.stringify(event) });

analytics.track(event) (passing the whole envelope to a browser SDK) is wrong — it would be read as the event-name string. Use trackArgs or destructure event.event / event.properties.

Design

  • Pure & zero-dependency — no network, no auth, no storage. Just transforms.
  • Tree-shakeable — import only the mappers you use.
  • Canonical types by construction — input types are the generated CE types, imported type-only from @commercengine/storefront-sdk. The imports are erased at build (no runtime dependency — the bundle has zero imports), but the public types reference them, so @commercengine/storefront-sdk is a required peer dependency for TypeScript consumers. It's satisfied automatically if you use @commercengine/storefront (which depends on, and re-exports, the SDK); install @commercengine/storefront-sdk directly only if you use this package without the umbrella.
  • Spec types are the public contract — the Segment shapes in spec/segment-ecommerce.ts are hand-authored because they describe an external spec, not the CE API.

Mapping context

Every mapper accepts an optional MapContext. Its fields fall into two groups:

Mapping hooks — shape the event properties; useful in every setup:

| Field | Purpose | | ------------ | ---------------------------------------------------------- | | productUrl | (ref) => string to build a product page URL (url) | | brand | (ref) => string to resolve a brand (brand) | | currency | Last-resort currency fallback (normally derived — see below) |

Envelope identity/metadatauserId, anonymousId, timestamp, messageId, context. These populate the track/identify envelope and matter only when you send the envelope yourself (server SDK object form, or raw HTTP). With the browser SDKs they're redundant: identity and context are managed by the SDK, and trackArgs omits identity regardless. Leave them unset for browser usage.

Currency is derived, not passed. Mappers read it from the entity — Product.pricing.currency, Cart.currency, Order.currency. The only exception is Product Added/Product Removed, which take a bare CartItem (no currency of its own): pass the parent cart via { cart } and both cart_id and currency are derived from it. ctx.currency exists solely as a last-resort fallback and is rarely needed.

Identifying users

toIdentify maps either canonical user shape — the full API User (e.g. verifyOtp(...).data.user or the profile endpoint) or the trimmed JWT UserInfo (e.g. sdk.getUserInfo()) — to a Segment identify envelope; userTraits is the reusable traits bag; identifyArgs adapts to the browser SDK analytics.identify(userId, traits) call.

import { toIdentify, userTraits, identifyArgs } from "@commercengine/analytics";

const { data } = await sdk.auth.verifyOtp({ otp, otp_token, otp_action });

// Browser SDK — accepts a full `User`…
analytics.identify(...identifyArgs(toIdentify(data.user)));
// …or the trimmed JWT `UserInfo` (its anonymousId is used automatically)
const info = await sdk.getUserInfo();
analytics.identify(...identifyArgs(toIdentify(info)));

// Server SDK / HTTP — the envelope is the identify payload
const event = toIdentify(data.user, ctx);
// { type: "identify", userId, traits: {…} }

Standard traits (email, firstName, lastName, name, phone, avatar, createdAt, company, …) are mapped from the User, plus CE-specific additive traits: customer_id, customer_group_id, customer_group, country_code, email_verified, phone_verified.

Anonymous vs identified. CE, Segment, and RudderStack all distinguish a known userId from a pseudonymous anonymousId. toIdentify decides purely on is_anonymous/isAnonymousnot login state, since a user can be logged out yet still known. When not anonymous (even if logged out) it sets userId (and also anonymousId, so Segment links prior anonymous activity); when anonymous it leaves userId unset and sets only anonymousId — the user's CE id is never used as userId. An explicit ctx.userId overrides.

Entity mappers

Convenience functions that derive an event directly from a Commerce Engine shape:

| Mapper | Event | Input | | ----------------------- | ----------------------- | ----------- | | toProductsSearched | Products Searched | string | | toProductViewed | Product Viewed | Product | | toProductClicked | Product Clicked | Product | | toProductListViewed | Product List Viewed | Product[] | | toProductListFiltered | Product List Filtered | Product[] | | 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 |

Product sources & variant fields

The product mappers (toProductViewed, toProductClicked, toProductListViewed, toProductListFiltered) and productProperties accept any catalog shape — Product, ProductDetail, or Item — and normalize the field-name differences for you. Every emitted product carries the additive variant_id and variant_slug (beyond the spec's variant name) whenever available, plus the canonical CE identifiers that are useful for analytics joins: product_slug, variant_name, product_type, category ids/slugs, tags, stock/promotion/ subscription flags, line-level price/tax/discount fields, and compact product attributes keyed by CE attribute key/name. Empty slug fields are omitted rather than emitted as empty strings.

Item, CartItem, and OrderItem already reference a single variant inline. A Product/ProductDetail describes the parent product (its variants live in variants[]), so pass the selected one to report variant info:

// Explicit variant (object or id) — 3rd arg; ctx (2nd) is empty here
toProductViewed(product, {}, { variant: selectedVariantId });
toProductClicked(product, {}, { variant: selectedVariant });

When no variant is passed, the product's default variant (is_default) is used if present — including in list events. A resolved variant overrides variant/variant_id/variant_slug and sku, price, and image_url.

The mappers intentionally do not dump every nested CE object into analytics payloads. Large operational objects like addresses, shipments, raw promotion objects, inventory lots, and seller details are left out by default; add them in your app only when a destination actually needs them.

trackEvent — the rest of the spec, fully typed

Events with no natural source entity (promotions, coupons, checkout steps, wishlist, sharing, reviews) are covered by a single builder. For spec events the properties argument is type-checked against the event name; for custom event names it accepts an open object:

import { trackEvent, trackArgs } from "@commercengine/analytics";

analytics.track(...trackArgs(trackEvent("Promotion Viewed", { promotion_id: "promo_1", name: "Sale" })));
analytics.track(...trackArgs(trackEvent("Coupon Applied", { cart_id, coupon_id, discount: 5 })));

Reusable builders & custom events

The property bags are the reusable primitives — productProperties (product), cartProperties (cart), orderProperties (order). Compose them into events that aren't in the Segment spec but that you track internally (e.g. Cart Created, Cart Updated, Order Created, Payment Successful):

cartProperties and orderProperties include the core CE monetary and status fields: totals/subtotals, tax, shipping, coupon/promotion discount and savings breakdowns, loyalty/credit amounts, item counts, and cart/order status. They exclude addresses, metadata, shipments, and raw applied-promotion/coupon objects by default.

checkoutStepProperties and paymentInfoEnteredProperties derive checkout-step linking fields from the canonical Cart: checkout_id is the cart id, and cart_id is emitted separately. shipping_method is derived from cart.fulfillment_preference (delivery provider/courier, collect-in-store pickup location, or both for partial fulfillment). Commerce Engine has no separate checkout id, so order mappers emit the order's cart_id as both cart_id and the checkout_id proxy.

import { trackEvent, trackArgs, cartProperties, orderProperties } from "@commercengine/analytics";

analytics.track(...trackArgs(trackEvent("Cart Created", cartProperties(cart))));
analytics.track(...trackArgs(trackEvent("Order Created", orderProperties(order))));
analytics.track(...trackArgs(trackEvent("Payment Successful", {
  ...orderProperties(order),
  payment_method: "Visa",
})));

To make a custom event strongly typed, augment the spec map via declaration merging:

import type { OrderProperties } from "@commercengine/analytics";

declare module "@commercengine/analytics" {
  interface EcommerceEventProperties {
    "Cart Created": import("@commercengine/analytics").CartProperties;
    "Payment Successful": OrderProperties & { payment_method?: string };
  }
}

// Now `trackEvent("Payment Successful", …)` is type-checked.

This means every V2 spec event is representable — entity mappers for the derivable ones, trackEvent for the rest — and your own non-spec events reuse the exact same builders.