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

puppr-js

v0.1.6

Published

A library to detect multiple types of user engagement signals, including on-page and navigational patterns.

Readme

Puppr

A lightweight JavaScript library to detect user engagement signals and behavioral patterns that indicate confusion, frustration, or high engagement. Perfect for triggering support chat, collecting UX insights, and improving user experience.

Features

  • 🔥 Rage Click Detection: Detects when users click rapidly in the same area, indicating frustration
  • 🖱️ Erratic Mouse Movement: Detects erratic mouse movements that indicate user confusion
  • 📜 Scroll Thrash Detection: Detects rapid scrolling up and down, indicating content confusion
  • 🚀 Rapid Navigation Detection: Detects when users navigate between pages very quickly
  • 🔄 Revisiting Detection: Detects when users repeatedly visit the same pages
  • 💬 Custom Event Triggering: Programmatically trigger your own engagement events
  • ⚙️ Cooldown System: Prevents event spam with configurable global and event-specific cooldowns
  • 📊 Real-time Statistics: Track total events, blocked events, and engagements
  • 🎯 Event Subscription System: Subscribe to engagement events and stat changes with simple callbacks

Installation

Via CDN

<script src="https://cdn.jsdelivr.net/npm/puppr-js@latest/dist/puppr.umd.js"></script>
<script>
  // Puppr is available as a global variable
  const puppr = new Puppr.Puppr({
    rageClick: true,
    erraticMouse: true,
    scrollThrash: true,
    rapidNavigation: true,
    revisiting: true
  });
  puppr.start();
</script>

Via npm

npm install puppr-js
import { Puppr } from 'puppr-js';

const puppr = new Puppr({
    rageClick: true,
    erraticMouse: true,
    scrollThrash: true,
    rapidNavigation: true,
    revisiting: true
});
puppr.start();

Quick Start

Basic Setup

// Initialize Puppr with all detectors enabled
const puppr = new Puppr({
    rageClick: true,
    erraticMouse: true,
    scrollThrash: true,
    rapidNavigation: true,
    revisiting: true
});

// Start detection
puppr.start();

Listening for Events

Puppr provides two ways to listen for engagement events:

Method 1: Event Subscription (Recommended)

// Subscribe to engagement events
const unsubscribeEngagement = puppr.onEngagement((detail) => {
    console.log('Engagement detected:', detail);
    // detail.engagementType: 'CONFUSION' | 'FRUSTRATION' | 'ENGAGEMENT'
    // detail.eventType: 'rage-click' | 'erratic-mouse-movement' | etc.
    // detail.message: Human-readable description
    // detail.timestamp: Unix timestamp
    // detail.data: Additional event-specific data
});

// Subscribe to stat changes
const unsubscribeStats = puppr.onStatsChange((stats) => {
    console.log('Stats updated:', stats);
    // { totalEvents: 15, blockedEvents: 3, engagements: 12 }
});

// Clean up subscriptions when done
unsubscribeEngagement();
unsubscribeStats();

Method 2: DOM Event Listener

// Listen for engagement events via DOM events
window.addEventListener('user_engagement_detected', (event) => {
    const detail = event.detail;
    console.log('Engagement detected:', detail.engagementType, detail.eventType, detail.message);
});

Triggering Your Own Events

You can programmatically trigger custom engagement events for any scenario. This is perfect for triggering support chat when users encounter errors, validation failures, or other engagement opportunities.

// Trigger a custom engagement event
puppr.triggerEngagement(
    'ENGAGEMENT',              // engagementType: 'CONFUSION' | 'FRUSTRATION' | 'ENGAGEMENT'
    'custom-engagement',      // eventType: Your custom event identifier
    'User needs assistance',  // message: Human-readable description
    {                          // data: Optional additional data
        userId: '123',
        page: '/checkout',
        error: 'Payment failed'
    }
);

Example: Trigger Support Chat on Form Validation Error

function validateForm() {
    const value = parseFloat(document.getElementById('amount').value);
    
    if (value < 1 || value > 1000) {
        // Trigger engagement event to open support chat
        puppr.triggerEngagement(
            'ENGAGEMENT',
            'form-validation-error',
            'Invalid amount entered. Support chat triggered.',
            { 
                value: value, 
                validRange: { min: 1, max: 1000 } 
            }
        );
        
        // Open your support chat (e.g., Crisp, Intercom, etc.)
        if (window.$crisp) {
            window.$crisp.push(['do', 'chat:open']);
        }
    }
}

Example: Handle Different Engagement Types

puppr.onEngagement((detail) => {
    switch(detail.engagementType) {
        case 'FRUSTRATION':
            // User is frustrated - show help immediately
            showHelpTooltip(detail.message);
            break;
        case 'CONFUSION':
            // User is confused - offer guided tour
            offerGuidedTour();
            break;
        case 'ENGAGEMENT':
            // High engagement - open support chat
            openSupportChat();
            break;
    }
});

Configuration

Individual Detector Options

Customize the sensitivity of each detector:

const puppr = new Puppr({
    rageClick: {
        clickCountThreshold: 5,    // Number of clicks to trigger (default: 5)
        timeThresholdMs: 1000,     // Time window for clicks in ms (default: 1000)
        distanceThresholdPx: 50     // Max distance between clicks in pixels (default: 50)
    },
    erraticMouse: {
        reversalThreshold: 4,      // Number of direction changes (default: 4)
        timeThresholdMs: 500       // Time window for movements in ms (default: 500)
    },
    scrollThrash: {
        reversalThreshold: 3,      // Number of scroll reversals (default: 3)
        timeThresholdMs: 2000      // Time window for thrashing in ms (default: 2000)
    },
    rapidNavigation: {
        pageCountThreshold: 5,     // Number of pages to visit (default: 5)
        timeThresholdMs: 5000      // Time window for navigation in ms (default: 5000)
    },
    revisiting: {
        visitCountThreshold: 3,    // Number of visits to same page (default: 3)
        historySize: 10             // Size of navigation history (default: 10)
    }
});

Cooldown Configuration

Puppr includes a cooldown system to prevent event spam. By default, there's a 3-second global cooldown between any engagement events.

const puppr = new Puppr();

// Set global cooldown (affects all engagement events)
puppr.setGlobalCooldown(5000); // 5 seconds

// Set event-specific cooldown (overrides global cooldown)
puppr.setEventTypeCooldown('scroll-thrash', 8000); // 8 seconds for scroll thrash
puppr.setEventTypeCooldown('rage-click', 2000);    // 2 seconds for rage clicks

// Clear all cooldowns
puppr.clearAllCooldowns();

// Set deduplication window (default: 100ms)
puppr.setDedupWindow(200); // 200ms

// Clear deduplication cache
puppr.clearDedupCache();

// Get current deduplication window
const windowMs = puppr.getDedupWindow();

Event Types

Puppr detects the following engagement signals:

| Event Type | Engagement Type | Description | |------------|----------------|-------------| | rage-click | FRUSTRATION | User clicked rapidly in the same area | | erratic-mouse-movement | FRUSTRATION | User moved the mouse erratically | | scroll-thrash | FRUSTRATION | User scrolled up and down rapidly | | rapid-navigation | CONFUSION | User navigated between pages very quickly | | revisiting-page | CONFUSION | User repeatedly visited the same page |

Event Detail Structure

{
    engagementType: 'CONFUSION' | 'FRUSTRATION' | 'ENGAGEMENT',
    eventType: string,      // Event type (e.g., 'rage-click')
    message: string,        // Human-readable description
    timestamp: number,      // Unix timestamp
    data: any              // Additional event-specific data
}

Examples

Complete Integration Example

// Initialize Puppr
const puppr = new Puppr({
    rageClick: true,
    erraticMouse: true,
    scrollThrash: true,
    rapidNavigation: true,
    revisiting: true
});

// Subscribe to engagement events
puppr.onEngagement((detail) => {
    console.log(`${detail.engagementType}: ${detail.eventType} - ${detail.message}`);
    
    // Handle different engagement types
    if (detail.engagementType === 'FRUSTRATION') {
        // Show help tooltip or open support chat
        showHelpMessage(detail.message);
    } else if (detail.engagementType === 'CONFUSION') {
        // Offer guided tour or help documentation
        offerHelp(detail.message);
    } else if (detail.engagementType === 'ENGAGEMENT') {
        // High engagement - open support chat
        openSupportChat();
    }
});

// Subscribe to stat changes
puppr.onStatsChange((stats) => {
    console.log('Stats:', stats);
    // Update your analytics dashboard
    updateAnalytics(stats);
});

// Start detection
puppr.start();

Statistics and Monitoring

const puppr = new Puppr();

// Get current stats
const stats = puppr.getStats();
console.log('Current stats:', stats);
// {
//   totalEvents: 15,
//   blockedEvents: 3,
//   engagements: 12
// }

// Subscribe to stat changes
puppr.onStatsChange((stats) => {
    console.log('Stats updated:', stats);
    // Update your dashboard or send to analytics
});

Integration with Support Chat (Crisp, Intercom, etc.)

const puppr = new Puppr();

puppr.onEngagement((detail) => {
    // Open support chat on engagement events
    if (detail.engagementType === 'ENGAGEMENT') {
        // Crisp
        if (window.$crisp) {
            window.$crisp.push(['do', 'chat:open']);
        }
        
        // Intercom
        if (window.Intercom) {
            window.Intercom('show');
        }
        
        // Custom chat widget
        openChatWidget();
    }
});

puppr.start();

API Reference

Puppr Class

Constructor

new Puppr(options?: PupprOptions)

Core Methods

start()
Start all configured detectors.

puppr.start();

stop()
Stop all running detectors.

puppr.stop();

isRunning()
Check if Puppr is currently running.

const isRunning = puppr.isRunning(); // returns boolean

Event Management

onEngagement(callback)
Subscribe to engagement events. Returns an unsubscribe function.

const unsubscribe = puppr.onEngagement((detail) => {
    console.log('Engagement:', detail);
});
// Later, to unsubscribe:
unsubscribe();

onStatsChange(callback)
Subscribe to stat changes. Returns an unsubscribe function.

const unsubscribe = puppr.onStatsChange((stats) => {
    console.log('Stats:', stats);
});

triggerEngagement(engagementType, eventType, message, data?)
Manually trigger an engagement event.

puppr.triggerEngagement(
    'ENGAGEMENT',
    'custom-event',
    'User needs help',
    { userId: '123' }
);

Configuration Methods

setGlobalCooldown(ms)
Set global cooldown for all engagement events (in milliseconds).

puppr.setGlobalCooldown(5000); // 5 seconds

setEventTypeCooldown(eventType, ms)
Set cooldown for a specific event type (overrides global cooldown).

puppr.setEventTypeCooldown('scroll-thrash', 8000); // 8 seconds

clearAllCooldowns()
Clear all cooldowns (global and event-specific).

puppr.clearAllCooldowns();

setDedupWindow(ms)
Set deduplication window time (default: 100ms).

puppr.setDedupWindow(200); // 200ms

getDedupWindow()
Get current deduplication window time.

const windowMs = puppr.getDedupWindow();

clearDedupCache()
Clear the deduplication cache.

puppr.clearDedupCache();

Statistics Methods

getStats()
Get current statistics.

const stats = puppr.getStats();
// Returns: { totalEvents: 15, blockedEvents: 3, engagements: 12 }

Statistics

Puppr provides real-time statistics about detected events:

const stats = puppr.getStats();
// {
//   totalEvents: 15,      // Total number of events processed
//   blockedEvents: 3,     // Number of events blocked by cooldowns
//   engagements: 12       // Number of successful engagement events dispatched
// }

Browser Support

  • Modern browsers with ES6+ support
  • Chrome, Firefox, Safari, Edge (latest versions)
  • IE11+ (with polyfills)

License

ISC