Custom e-commerce

If your storefront isn't a stock Shopify install, PennyLens still lights up the full visit → cart → checkout → purchase funnel — you just wire four events at the user-action sites in your own code. This guide is for headless storefronts, custom-built shops, BigCommerce / Magento / Salesforce Commerce Cloud, subscription products, and any e-commerce stack outside the auto-detect layer.

The four events below populate the same dashboards, detectors, and funnels that auto-detected Shopify stores see — cart abandonment, checkout dropoff, surprise-cost detection, average order value, conversion rate by source, and revenue attribution.

The four core events

// Wire each event at the user-action site:
pennylens.track('product_view', { product_id: 'sku-123', handle: 'wireless-mug-warmer' });
pennylens.track('cart_add',     { product_id: 'sku-123', qty: 1, total: 1999, currency: 'USD' });
pennylens.track('checkout_step',{ step_name: 'shipping_method' });
pennylens.track('purchase',     { total: 1999, currency: 'USD', order_id: 'ord-abcd' });

These four reserved event names drive every e-commerce surface in the PennyLens dashboard. You can emit additional custom events alongside them — they coexist freely — but the funnels, detectors, and revenue rollups key off these names specifically.

Money values are integers in the minor unit of the currency (cents for USD, EUR, GBP; whole units for JPY, KRW). 1999 means $19.99. Sending 19.99 will silently treat your store as 100× cheaper than it actually is.

Where each event belongs

Each event corresponds to a specific user action. Wire it at the point in your code that handles that action — not on a generic page-view, and not after a redirect.

product_view — product detail page

Fires once when a shopper lands on a product page. In a React/Vue/Svelte SPA, emit it from the product page component's mount effect. In a server-rendered store, emit it from the inline <script> block at the bottom of the product template.

pennylens.track('product_view', {
  product_id: 'sku-123',         // required — your stable product identifier
  handle: 'wireless-mug-warmer', // optional — URL slug
  name: 'Wireless Mug Warmer',   // optional — for readability in dashboards
  price: 1999,                   // optional — in minor units
  currency: 'USD',               // optional — required if price is present
  variant_id: 'sku-123-blue',    // optional — if the product has variants
  category: 'kitchen',           // optional — top-level category
});

product_view is the first step in the auto-populated visit → cart → checkout → purchase funnel, and anchors the cart-abandonment and checkout-dropoff detectors alongside cart_add, checkout_step, and purchase.

cart_add — add-to-cart action

Fires every time a shopper adds a line item to their cart. Emit it from your add-to-cart handler after the server confirms the line was added — not on optimistic UI updates that may get rolled back.

pennylens.track('cart_add', {
  product_id: 'sku-123',
  qty: 1,                  // required — quantity of this line item
  total: 1999,             // required — line total in minor units (qty × unit price)
  currency: 'USD',         // required
  variant_id: 'sku-123-blue', // optional
  name: 'Wireless Mug Warmer', // optional
});

If a shopper increases the quantity of an existing line, emit cart_add again — PennyLens treats each add as a discrete action. If they decrement or remove a line, no event is needed; the dashboard reconstructs cart state from the sequence of adds and the eventual checkout_step or purchase payload.

checkout_step — checkout funnel progress

Fires on every distinct step of a multi-step checkout. The step_name is freeform — use the names that match your UI. PennyLens autodetects the funnel from the sequence of step names a user moves through and builds the step-by-step drop-off chart.

pennylens.track('checkout_step', {
  step_name: 'shipping_method', // required — your step identifier
  step_index: 2,                // optional — explicit ordering if you have it
  total: 1999,                  // optional — current cart total
  currency: 'USD',              // optional
});

Common step_name values: cart_review, email, shipping_address, shipping_method, billing, payment, review, confirm. Stick to lowercase snake_case for consistency across stores.

For a single-page checkout with progressive disclosure, emit checkout_step whenever a user first interacts with a new section (focuses the email field, opens the shipping panel, etc.). For a buy-now flow that skips the cart, emit one checkout_step with step_name: 'express_checkout' so the conversion still lands in the funnel.

purchase — order completion

Fires once on the order confirmation / thank-you page. Emit it from server-rendered HTML, an inline <script> block, or your post-checkout client effect.

pennylens.track('purchase', {
  total: 1999,           // required — order total in minor units
  currency: 'USD',       // required
  order_id: 'ord-abcd',  // required — your order identifier
  tax: 160,              // optional — tax portion of total
  shipping: 500,         // optional — shipping portion of total
  discount: 0,           // optional — discount applied
  customer_id: 'cust-42',// optional — your internal customer id
  items: [               // optional — line-item breakdown
    { product_id: 'sku-123', qty: 1, total: 1999 },
  ],
});

order_id must be unique per order. If a customer hits the thank-you page twice (refresh, redirect-back), PennyLens deduplicates by order_id and counts the purchase exactly once.

For subscription products, emit purchase on the initial signup. Recurring renewals belong as their own subscription_renewal event — not as repeated purchase events, which would double-count revenue in the funnel.

Identifying shoppers

Anonymous tracking works out of the box — you don't need to identify anyone for the dashboards, funnels, or e-commerce detectors to populate. pennylens.identify() is accepted by the SDK today for forward compatibility, but the traits you pass to it aren't yet transmitted to PennyLens or attached to events — per-customer identity, lifetime-value, repeat-purchase, and cross-device stitching are on the roadmap, not live today.

What works without any identify() call: every reserved e-commerce event (product_view, cart_add, checkout_step, purchase) is tied to the anonymous visitor and session automatically, and that's enough to drive the funnel, the cart-abandonment / checkout-dropoff / surprise-cost detectors, and revenue rollups. For Shopify, connecting the order-creation webhook (see Shopify auto-detect) attaches a hashed customer identity server-side for per-customer purchase attribution — that's the reliable path today, not client-side identify().

See User Identification for the current state of identify() and what's on the roadmap.

Where to wire each event in real code

The four events look simple in the snippet above. Wiring them in a real codebase is where most teams get tripped up. Here's where they typically belong:

| Event | Wire it from | |-------|--------------| | product_view | Product page component's mount effect, or inline <script> in the product template's HTML | | cart_add | The promise resolution of your add-to-cart server call — after the server confirms | | checkout_step | Each checkout step component's mount (SPA) or template body (multi-page) | | purchase | The thank-you / order-received page's inline <script> or mount effect |

For a Next.js or Remix store with a /products/[handle] route, the product page's server component renders the product, and a small client component fires product_view on mount. For a headless Shopify Hydrogen site, do the same — Hydrogen doesn't trigger the standard /cart/add.js interceptor that auto-detect uses, so wire manually. For a server-rendered Rails/Django/Laravel store, drop an inline <script> block in the relevant template.

The events are queued client-side and flushed in small batches, so emitting them from any of these contexts is safe — you don't need to await anything.

Tracking the full shopping experience

The four reserved events cover the funnel and drive three built-in detectors — no configuration required:

  • Cart abandonment — sessions with a cart_add that never reach purchase, measured against your project's abandonment-rate threshold.
  • Checkout step drop-off — the funnel built from checkout_step events surfaces the exact step with the highest drop-off rate.
  • Surprise cost — elevated drop-off specifically at the shipping/tax-reveal steps (step_name: 'shipping_method' or 'shipping_address') signals costs that surprise shoppers late in checkout. Use those step_name values if your checkout reveals shipping or tax at a distinct step.
  • Rage clicks and dead clicks — auto-captured on every page including checkout, and feed the AI insight detectors. Session recordings show a seekable, color-coded timeline of every click, scroll, and form interaction so you don't scrub blind.
  • Session recordings on the checkout — subject to the same retention and masking as the rest of the site. Card-number, CVV, and address fields are masked by default. Use data-pl-ignore on any element (see Session Recordings) to exclude it entirely.

Pre-built auto-detect integrations

If your stack matches one of these, you don't need to wire the four events manually. PennyLens detects the storefront and emits them for you.

Shopify

window.Shopify.shop (a non-empty string) or a shopify-checkout-api-token <meta> tag trigger auto-detect. Covers stock Online Store 2.0 themes, Shopify Plus, and most Hydrogen sites that ship with the standard Shopify JS. See Shopify auto-detect for the full reference.

WooCommerce

Three signals trigger auto-detect on WooCommerce stores (WC 8.x, 9.x, 10.x):

  • woocommerce-checkout body class — present on the checkout page across virtually every theme.
  • window.wc.wcBlocksRegistry — present on stores using the Blocks checkout.
  • A /wp-content/plugins/woocommerce script tag — catches WC core scripts.

Any single positive enables the WooCommerce hooks. PennyLens listens to both the legacy jQuery added_to_cart event (Storefront, Astra, Divi, etc.) and the Blocks wc-blocks_added_to_cart DOM event. On Blocks-only stores, product_id may be null due to an upstream WC payload issue — visitor-level metrics still work correctly because they count visitors, not line items.

If your WooCommerce theme strips the woocommerce-checkout body class AND doesn't use Blocks AND doesn't load the /wp-content/plugins/woocommerce script on checkout, fall back to the manual snippet at the top of this page — usually a small block in functions.php that emits the four events from the relevant WC hooks.

Privacy

PennyLens captures only what's needed to populate the dashboards described in this guide:

  • Auto-captured: page URLs (with query strings stripped of obvious PII parameters like email, token), referrer, viewport, anonymous session ID, click coordinates, scroll depth, dwell time.
  • Never auto-captured: input field values (masked by default — including card numbers, CVV, addresses), localStorage / sessionStorage, cookies beyond the PennyLens anonymous session cookie.
  • Captured only when you emit it: the properties you pass to track() and identify(). PennyLens does not introspect your DOM for prices or product IDs — you control exactly what flows into the dashboard.

For GDPR / CCPA data subject requests, contact privacy@pennylens.com — see the Privacy Policy for the full deletion pipeline.

Zero-event troubleshooting

If 24 hours after install you don't see e-commerce events in the dashboard, walk these in order:

  1. Confirm the SDK is loaded. In your browser's developer console on a product page, run window.PennyLens. It should be defined. If it isn't, the snippet isn't in your theme's <head> block, or it's gated behind a consent banner that hasn't fired yet.

  2. Confirm events are firing locally. In the console, run window.PennyLens.debug = true and refresh. Each track() call logs the event name and payload. Walk through a product view → cart add → checkout step → purchase manually and verify each event appears.

  3. Confirm network requests reach PennyLens. In the Network tab, filter by pennylens.com or your custom collector domain. Each tracked event produces a POST to /v1/events (batched). If you see no requests, a Service Worker, ad blocker, or strict CSP is blocking them — add https://collect.pennylens.com to your CSP connect-src.

  4. Confirm the dashboard project key. In the snippet, pennylens.init('phc_...') must match the project's key in the dashboard's Settings → API Key tab. A typo here results in events being silently dropped at the collector.

  5. Confirm money values are minor units. If revenue numbers in the dashboard look 100× too small, you're sending 19.99 instead of 1999. Fix at the source — the dashboard does not retroactively rescale.

Next steps