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

@atlanwave/checkout-friction-detector

v1.2.0

Published

Lightweight browser library that detects checkout and form friction, reporting structured events to a backend API.

Readme

checkout-friction-detector

Lightweight, framework-agnostic browser library that detects checkout and form friction — hesitation, repeated edits, validation failures, abandonment, rage clicks, and more — and reports structured events to a backend API.

Installation

npm

npm install checkout-friction-detector
import CheckoutFrictionDetector from "checkout-friction-detector";

CheckoutFrictionDetector.init({
  publicKey: "pk_live_xxx",
});

Script tag

<script src="https://cfd.atlanwave.com/js/v1.0.0/checkout-friction-detector.umd.min.js"></script>
<script>
  CheckoutFrictionDetector.init({
    publicKey: "pk_live_xxx",
  });
</script>

Quick start

CheckoutFrictionDetector.init({
  publicKey: "pk_live_xxx",
  debug: true,
  formSelector: "form, .checkout-container",
  hesitationThresholdMs: 2000,
  hooks: {
    onSignal(event) {
      console.log(event.eventType, event.signal);
    },
  },
});

Public API

| Method | Description | | ---------------------------------- | -------------------------------------------------- | | init(config) | Initialize the detector with configuration options | | destroy() | Remove all listeners and clean up state | | pause() | Temporarily stop emitting signals | | resume() | Resume signal emission after pause | | trackCustomEvent(name, payload?) | Emit a custom event | | trackCartView(opts) | Emit a cart_view event with the current cart total | | trackOrderCompleted(opts) | Emit an order_completed event on the success page | | flush() | Manually flush the event queue to the backend | | getState() | Return safe diagnostic metadata |

Configuration

CheckoutFrictionDetector.init({
  // Required
  publicKey: "pk_live_xxx",

  // General
  debug: false, // Enable console logging
  autoTrack: true, // Auto-attach detectors on init

  // Form discovery
  formSelector: "form", // CSS selector for forms
  excludeSelector: "[data-cfd-ignore]", // Exclude matching elements
  trackContentEditable: false,

  // Multi-step forms
  stepSelectors: [".checkout-step", "[data-step]"],

  // Detection thresholds
  hesitationThresholdMs: 2000, // Focus-to-input delay
  longCompletionThresholdMs: 8000, // Time in a single field
  repeatedEditThreshold: 3, // Direction changes before emitting
  inactivityTimeoutMs: 180000, // Abandonment inactivity window

  // Rage clicks
  rageClickThreshold: {
    clicks: 3,
    windowMs: 1000,
    radiusPx: 30,
  },

  // Dead clicks
  deadClickWindowMs: 500,

  // Validation
  validationErrorClasses: ["error", "is-invalid", "input-error"],

  // Conversion detection
  successUrlPatterns: ["/thank-you", "/order-confirmation"],
  successElementSelectors: [".order-success", "[data-checkout-success]"],

  // Privacy (privacy-first defaults)
  maskFields: ['input[type="password"]', '[name*="card"]', '[name*="cvv"]'],
  allowValueCapture: false, // Never capture field values by default
  sensitiveFieldPatterns: [/password/i, /card/i, /cvv/i, /ssn/i],

  // Transport
  flushIntervalMs: 10000,
  batchSize: 20,
  requestTimeoutMs: 10000,
  retryCount: 3,
  maxQueueSize: 500,
  useLocalStorageQueue: false,

  // Hooks
  hooks: {
    beforeSend(eventBatch) {},
    afterSend(response) {},
    onSignal(event) {},
    isFieldSensitive(fieldInfo) {}, // Return boolean
    extractValidationState(fieldEl) {}, // Return 'invalid' or falsy
  },
});

Event types

| Event | Trigger | | ------------------------ | ---------------------------------------------------- | | form_discovered | Form element found on page or dynamically added | | form_started | First meaningful field interaction in a form | | field_hesitation | User focuses field, no input for threshold duration | | field_long_completion | Time in a single field exceeds threshold | | field_repeated_edits | Value changed (direction changes) exceeds threshold | | field_validation_error | Native invalid, aria-invalid, or CSS class detected | | step_viewed | Multi-step form step becomes visible | | step_revisited | User returns to a previously viewed step | | step_blocked | Submit attempt blocked on current step | | submit_attempt | Click on submit button detected | | rage_click | Rapid repeated clicks in small area | | dead_click | Click on control with no DOM change after window | | backtracking | User moves backward in field sequence | | form_abandoned | Started form not completed (page exit or inactivity) | | form_completed | Form successfully submitted or success detected | | cart_view | Cart total reported via trackCartView() | | order_completed | Order completion reported via trackOrderCompleted()| | custom_event | Manually tracked via trackCustomEvent() |

Event schema

Every event includes:

{
  "eventId": "uuid",
  "sessionId": "uuid",
  "pageViewId": "uuid",
  "formInstanceId": "uuid",
  "eventType": "field_hesitation",
  "eventTimestamp": 1712345678000,
  "page": {
    "url": "https://example.com/checkout",
    "path": "/checkout",
    "referrer": "https://example.com/cart",
    "title": "Checkout",
    "viewport": { "width": 1440, "height": 900 },
    "screen": { "width": 1920, "height": 1080 },
    "timezone": "America/New_York",
    "userAgent": "...",
    "language": "en-US",
    "deviceType": "desktop"
  },
  "form": {
    "cssPath": "form#checkout",
    "started": true,
    "submitAttempts": 0,
    "currentStep": "shipping"
  },
  "field": {
    "tagName": "input",
    "type": "email",
    "name": "email",
    "id": "email",
    "label": "Email Address",
    "cssPath": "form#checkout > input#email",
    "fingerprint": "a1b2c3d4"
  },
  "signal": {
    "hesitationDurationMs": 2000,
    "previouslyVisited": false
  },
  "libraryVersion": "1.0.0"
}

Revenue tracking

The library can attribute revenue impact to the friction it detects. Two events: cart_view (fired when a checkout page loads with a known cart total) and order_completed (fired on the post-purchase success page).

Both go through the existing /api/v1/ingest endpoint with the same session and pageView envelope.

valueMinor is the cart total in minor units (cents/pence) as an integer — never floats. Without it the backend can only show event-count framing in reports, not revenue impact.

API

CheckoutFrictionDetector.trackCartView({
  valueMinor: 4999, // preferred — integer in cents
  // value: 49.99,  // alternative — float, library will × 100
  currency: "USD",
  itemCount: 3, // optional
});

CheckoutFrictionDetector.trackOrderCompleted({
  valueMinor: 4999,
  currency: "USD",
  orderRef: "ord_abc123", // optional, must be a non-PII id
});

orderRef must be an opaque order identifier — never an email, name, or anything that identifies the customer. If the value looks like an email the library drops it and emits a console.warn.

Vanilla JS

<script src="https://cfd.atlanwave.com/js/v1.0.0/checkout-friction-detector.umd.min.js"></script>
<script>
  CheckoutFrictionDetector.init({ publicKey: "pk_live_xxx" });
  CheckoutFrictionDetector.trackCartView({
    valueMinor: 4999,
    currency: "USD",
    itemCount: 3,
  });
</script>

React

import { useEffect } from "react";
import CheckoutFrictionDetector from "checkout-friction-detector";

function CheckoutPage({ cart }) {
  useEffect(() => {
    CheckoutFrictionDetector.trackCartView({
      valueMinor: cart.totalCents,
      currency: cart.currency,
      itemCount: cart.items.length,
    });
  }, [cart.totalCents, cart.currency, cart.items.length]);

  return /* ... */;
}

function OrderSuccessPage({ order }) {
  useEffect(() => {
    CheckoutFrictionDetector.trackOrderCompleted({
      valueMinor: order.totalCents,
      currency: order.currency,
      orderRef: order.number,
    });
  }, [order.number]);

  return /* ... */;
}

Shopify Liquid

<script>
  CheckoutFrictionDetector.trackCartView({
    valueMinor: {{ cart.total_price }},
    currency: {{ cart.currency.iso_code | json }},
    itemCount: {{ cart.item_count }}
  });
</script>

{% comment %} On the order status page: {% endcomment %}
<script>
  CheckoutFrictionDetector.trackOrderCompleted({
    valueMinor: {{ checkout.total_price }},
    currency: {{ checkout.currency | json }},
    orderRef: {{ checkout.order_number | json }}
  });
</script>

Auto-detection (opt-in)

Set autoDetectCart: true in the init config and the library will make a best-effort attempt to fire cart_view / order_completed on init from:

  • Schema.org JSON-LD Product / Order blocks
  • Shopify checkout: window.Shopify.checkout.total_price (already in minor units)
  • Stripe Checkout success page: a ?session_id= query param triggers a presence-only order_completed (no value — your backend reconciles via the Stripe webhook)
CheckoutFrictionDetector.init({
  publicKey: "pk_live_xxx",
  autoDetectCart: true,
});

The library deliberately does not parse arbitrary DOM cart totals — too easy to grab the wrong element. The explicit trackCartView API is the recommended path.

Privacy model

Privacy-first by default:

  • Field values are never captured unless allowValueCapture: true is set
  • Even with value capture enabled, sensitive fields (password, card, CVV, SSN) are always masked
  • Fields matching maskFields selectors or sensitiveFieldPatterns are treated as sensitive
  • The isFieldSensitive hook allows custom sensitivity logic
  • No innerText capture beyond safe label extraction
  • No keystroke-level recording
  • Outgoing payloads are sanitized and size-limited

SPA support

The library automatically detects route changes via History API (pushState/replaceState) and popstate. On navigation:

  • pageViewId is refreshed
  • sessionId is preserved
  • Forms are re-scanned
  • Success URL patterns are re-checked

No additional configuration needed for most SPAs.

Development

npm install
npm run build       # Build ESM + UMD + UMD minified
npm test            # Run tests
npm run mock-server # Start mock backend on :3333

Examples

  • examples/simple-checkout.html — Basic single-page checkout form
  • examples/multi-step-checkout.html — Multi-step wizard with step tracking
  • examples/spa-checkout.html — SPA with client-side routing
  • examples/test-harness.html — Interactive friction scenario simulator

Start the mock server and open examples in your browser:

npm run mock-server
# Open http://localhost:3333/simple-checkout.html

Build outputs

| File | Format | Use case | | -------------------------------------------- | ------------ | -------------------------------- | | dist/checkout-friction-detector.esm.js | ESM | Bundlers (webpack, vite, rollup) | | dist/checkout-friction-detector.umd.js | UMD | Script tag, RequireJS | | dist/checkout-friction-detector.umd.min.js | UMD minified | Production script tag |

License

MIT