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_addthat never reachpurchase, measured against your project's abandonment-rate threshold. - Checkout step drop-off — the funnel built from
checkout_stepevents 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 thosestep_namevalues 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-ignoreon 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-checkoutbody class — present on the checkout page across virtually every theme.window.wc.wcBlocksRegistry— present on stores using the Blocks checkout.- A
/wp-content/plugins/woocommercescript 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()andidentify(). 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:
-
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. -
Confirm events are firing locally. In the console, run
window.PennyLens.debug = trueand refresh. Eachtrack()call logs the event name and payload. Walk through a product view → cart add → checkout step → purchase manually and verify each event appears. -
Confirm network requests reach PennyLens. In the Network tab, filter by
pennylens.comor your custom collector domain. Each tracked event produces aPOSTto/v1/events(batched). If you see no requests, a Service Worker, ad blocker, or strict CSP is blocking them — addhttps://collect.pennylens.comto your CSPconnect-src. -
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. -
Confirm money values are minor units. If revenue numbers in the dashboard look 100× too small, you're sending
19.99instead of1999. Fix at the source — the dashboard does not retroactively rescale.
Next steps
- Event Tracking — the full
track()API and custom event reference - User Identification — linking sessions to known customers
- JavaScript SDK — the complete
window.PennyLensAPI - Shopify auto-detect — drop-in detection for Shopify stores
- Session Recordings — replay with checkout-safe masking
- Dashboard reference — the data model behind the e-commerce surfaces