flusterduck
v0.7.18
Published
Rage click detection, dead click tracking, and form abandonment monitoring across 132 UX friction signals. Privacy-first analytics without session replay.
Maintainers
Keywords
Readme
flusterduck
Browser SDK for UX friction monitoring. Detects and classifies rage clicks, dead clicks, form abandonment, navigation loops, and 20+ other behavioral signals locally in the browser. No configuration required beyond your publishable key.
Installation
npm install flusterduckOr load via script tag:
<script src="https://flusterduck.com/d.js" data-key="fd_pub_..." data-dnt="false" async></script>Basic usage
import { init } from 'flusterduck';
init({ key: 'fd_pub_...' });Call init once on page load. It's idempotent; calling it again does nothing. The SDK attaches all signal detectors, starts batching events, and observes route changes automatically.
Manual signals
import { signal, track, identify } from 'flusterduck';
// Custom friction signal with optional weight (0-100, default 15)
signal('checkout_error', { element: '#pay-btn', weight: 30 });
// Business event for correlation
track('checkout_started', { plan: 'scale' });
// Segment for filtering in the dashboard
identify({ plan: 'scale', role: 'admin' });Configuration
init({
key: 'fd_pub_...',
// Tag events by environment
environment: 'production',
// Only track a fraction of sessions (0-1)
sampleRate: 0.5,
// Cookieless mode: memory-only session ID, no Flusterduck cookie
cookieless: false,
// Respect DoNotTrack and GlobalPrivacyControl (default: true)
respectDoNotTrack: true,
// Map URL patterns to logical page names
pageRules: [
{ pattern: '/checkout/*', label: '/checkout' },
{ pattern: '/blog/*', label: '/blog' },
],
// Segment all events from this init call
segment: { plan: 'scale' },
// Control individual detectors
signals: {
rageClick: { enabled: true },
deadClick: { enabled: true },
formHesitation: { enabled: true, threshold: 3000 },
loopNav: { enabled: true },
helpHunt: { enabled: false },
},
// Skip tracking entirely on these pages
ignorePages: ['/internal', '/admin'],
// Skip signal detection on matching elements
ignoreElements: ['.no-track', '[data-fd-ignore]'],
// DOM evidence capture: 'off' | 'metadata' | 'snapshot'
domMode: 'metadata',
// Event batching
batchInterval: 2000,
batchMaxSize: 50,
});Consent and opt-out
import { setConsent, optOut } from 'flusterduck';
// Pass true after the user accepts your cookie banner
setConsent(true);
// Permanently opt out: clears session and stops all tracking
optOut();init defers automatically if respectDoNotTrack is enabled and the user's browser signals DNT or Global Privacy Control. Call setConsent(true) explicitly to override.
Signal detectors
Automatic detectors (all on by default):
| Signal | Description |
|---|---|
| rageClick | 3+ clicks on the same target within a 2s window, clustered within 24px |
| deadClick | Click on an element that isn't a real control and doesn't visually look clickable |
| speedFrustration | Click on a control with no DOM response within 3s |
| thrashCursor | High-velocity erratic mouse movement: 3+ direction reversals in a 2s window |
| loopNav | Repeated back-and-forth navigation, revisiting the same pages in a cycle |
| scrollBounce | Scroll past 50% of the page depth then bounce back toward the top |
| formHesitation | Pause over 5s in a form field |
| formAbandon | Form interaction without submission |
| formValidationLoop | 3+ failed validation attempts on the same field |
| errorEncounter | Uncaught JS errors, unhandled promise rejections, failed network requests, or captured console errors |
| scrollHijack | 4+ rapid scroll direction reversals in a row |
| scrollDepthAbandon | Scroll past 15% of the page then leave without clicking a link or button |
| helpHunt | 3+ clicks on help/support elements within 60s |
| closeClick | Modal, overlay, banner, or toast dismissed via a close/skip control |
| filterSpiral | 3+ toggles of the same filter/sort control within 10s |
| copyFrustration | 3+ text copies within 15s |
| navigationConfusion | 5+ distinct pages visited within 30s, each with a short dwell time |
| deadClickTrapZone | 3+ dead clicks clustered in the same screen region |
Mobile-only (activates automatically on touch devices):
| Signal | Description |
|---|---|
| mobileTapMiss | Tap that lands within 20px of a nearby interactive element instead of on it |
| pinchZoom | Repeated pinch-to-zoom outside map/canvas/gallery elements |
| swipeFrustration | Horizontal swipe gesture on a page that has swipeable elements, but not on one |
Keyboard:
| Signal | Description |
|---|---|
| tabThrash | 10+ Tab key presses within a 5s window |
| focusTrap | Focus stuck cycling inside a dialog/menu for 5+ Tab or Escape presses |
| keyboardNavFrustration | 15+ arrow key presses inside a listbox, menu, or select within 5s |
Listening for signals
import { onSignal } from 'flusterduck';
const unsubscribe = onSignal((signal) => {
console.log(signal.name, signal.element, signal.weight);
});
// Later, stop listening
unsubscribe();onSignal fires synchronously the moment a signal is detected, before it's batched and sent. You can also pass an onSignal callback directly to init(), or set emitSignalEvents: true to get a flusterduck:signal CustomEvent on window instead.
Lifecycle
import { destroy } from 'flusterduck';
// Tears down all detectors and flushes the event queue
destroy();Route changes in SPAs are detected automatically via History API patching and popstate. You don't need to call anything on navigation.
Key format
The SDK only accepts publishable keys (fd_pub_). If a secret key (fd_sec_) is passed, it logs an error and aborts. Nothing is tracked.
