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
});
PennyLens uses product_view to compute browse depth, product affinity scoring, and the most-viewed-but-never-purchased report.
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 to populate. But identifying shoppers when you have the data turns your funnel from "what happened" into "who did what," and unlocks the lifetime-value, repeat-purchase, and cross-device stitch reports.
Call pennylens.identify() at any of these moments:
// On login or account creation — link the anonymous session to a known user
pennylens.identify('cust-42', {
email: 'shopper@example.com',
name: 'Sam Rivera',
plan: 'regular',
signup_at: '2026-01-15',
});
// On checkout when the email field is filled — links the in-progress cart
pennylens.identify('cust-42', { email: 'shopper@example.com' });
// On order completion — guarantees the purchase is tied to a known customer
pennylens.identify(order.customer_id, { email: order.email });
The first argument is your stable customer identifier. The second is a traits object that PennyLens stores on the customer record. Once identified, every subsequent event in this session — and every future session on this device — is linked to that customer.
See User Identification for masking, logout, and GDPR deletion flows.
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. To surface deeper UX insights, combine them with PennyLens's auto-tracked behavior layer:
- Product browsing patterns — page views, scroll depth, and dwell time on every product page are auto-captured. The dashboard's "most-viewed, never-purchased" report joins these against
purchaseevents without any manual wiring. - Cart manipulation — every
cart_addis captured. For cart removals or quantity edits, emitcart_update(optional) withproduct_id,qty_delta, andtotal_after. The cart abandonment detector uses both to compute net-cart-value at the moment of exit. - Checkout step drop-off — the funnel built from
checkout_stepevents shows the largest leak in the dashboard's checkout dropoff detector. Hovering any step shows the recordings of visitors who exited there. - Surprise-cost detection — when the total at
checkout_stepjumps significantly above the cart total atcart_add(shipping, tax, fees added late), PennyLens flags it as a surprise-cost leak. No configuration required — just includetotalon both events. - Rage clicks, dead clicks, form errors — auto-captured on every page including checkout. Recordings auto-jump to the friction moment so you don't scrub.
- Session recordings on the checkout — disabled by default for compliance. Enable per-property with Session Recordings. Card-number, CVV, and address fields are masked by default even when recordings are on.
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, /cdn/shopify* scripts, and template-* body classes 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, customer data attached via identify() is deletable via the dashboard's data subject request flow. See User Identification 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