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

@rooguys/js

v1.0.0

Published

Official Browser SDK for Rooguys API

Readme

Rooguys JavaScript SDK

The official Browser SDK for the Rooguys Gamification API. Lightweight and dependency-free (uses native fetch).

Installation

NPM

npm install @rooguys/js

Direct Import (ES Modules)

import Rooguys from './path/to/sdk/index.js';

Initialization

import Rooguys from '@rooguys/js';

const client = new Rooguys('YOUR_API_KEY', {
  baseUrl: 'https://api.rooguys.com/v1',
  timeout: 10000,
  // Rate limit handling
  onRateLimitWarning: (info) => {
    console.warn(`Rate limit warning: ${info.remaining}/${info.limit} remaining`);
  },
  autoRetry: true,
  maxRetries: 3,
});

Migration Guide (v1.x to v2.x)

Breaking Changes

  1. Event Tracking Endpoint: The SDK now uses /v1/events instead of /v1/event

    // Old (deprecated, still works with warning)
    client.events.trackLegacy('event_name', 'user_id', properties);
       
    // New (recommended)
    client.events.track('event-name', 'user_id', properties);
  2. Global Leaderboard Endpoint: Now uses /v1/leaderboards/global with timeframe query parameter

    // Both signatures work
    client.leaderboards.getGlobal('weekly', 1, 10);
    client.leaderboards.getGlobal({ timeframe: 'weekly', page: 1, limit: 10 });
  3. Response Format: All responses now follow standardized format { success: true, data: {...} }

New Features

  • Batch event tracking (events.trackBatch)
  • User management (users.create, users.update, users.createBatch)
  • User search (users.search)
  • Field selection for user profiles
  • Leaderboard filters (persona, level range, date range)
  • "Around me" leaderboard view (leaderboards.getAroundUser)
  • Health check endpoints
  • Rate limit handling with auto-retry
  • Typed error classes

Usage Examples

Events

Track a Single Event

const response = await client.events.track('level-completed', 'user_123', {
  difficulty: 'hard',
  score: 1500
}, {
  includeProfile: true,
  idempotencyKey: 'unique-request-id'
});

console.log('Event tracked:', response.status);
if (response.profile) {
  console.log('Updated points:', response.profile.points);
}

Track Events with Custom Timestamp

// Track historical events (up to 7 days in the past)
const response = await client.events.track('purchase', 'user_123', {
  amount: 99.99
}, {
  timestamp: new Date('2024-01-15T10:30:00Z')
});

Batch Event Tracking

// Track up to 100 events in a single request
const response = await client.events.trackBatch([
  { eventName: 'page-view', userId: 'user_123', properties: { page: '/home' } },
  { eventName: 'button-click', userId: 'user_123', properties: { button: 'signup' } },
  { eventName: 'purchase', userId: 'user_456', properties: { amount: 50 }, timestamp: new Date() }
], {
  idempotencyKey: 'batch-123'
});

// Check individual results
response.results.forEach((result, index) => {
  if (result.status === 'queued') {
    console.log(`Event ${index} queued successfully`);
  } else {
    console.error(`Event ${index} failed:`, result.error);
  }
});

Users

Create a New User

const user = await client.users.create({
  userId: 'user_123',
  displayName: 'John Doe',
  email: '[email protected]',
  firstName: 'John',
  lastName: 'Doe',
  metadata: { plan: 'premium' }
});

Update User Profile

// Partial update - only sends provided fields
const updated = await client.users.update('user_123', {
  displayName: 'Johnny Doe',
  metadata: { plan: 'enterprise' }
});

Batch User Creation

const response = await client.users.createBatch([
  { userId: 'user_1', displayName: 'User One', email: '[email protected]' },
  { userId: 'user_2', displayName: 'User Two', email: '[email protected]' },
  // ... up to 100 users
]);

Get User Profile with Field Selection

// Only fetch specific fields
const user = await client.users.get('user_123', {
  fields: ['points', 'level', 'badges']
});

Search Users

const results = await client.users.search('john', {
  page: 1,
  limit: 20,
  fields: ['userId', 'displayName', 'points']
});

results.users.forEach(user => {
  console.log(`${user.displayName}: ${user.points} points`);
});

Access Enhanced Profile Data

const user = await client.users.get('user_123');

// Activity summary
if (user.activitySummary) {
  console.log(`Last active: ${user.activitySummary.lastEventAt}`);
  console.log(`Total events: ${user.activitySummary.eventCount}`);
  console.log(`Days active: ${user.activitySummary.daysActive}`);
}

// Streak information
if (user.streak) {
  console.log(`Current streak: ${user.streak.currentStreak} days`);
  console.log(`Longest streak: ${user.streak.longestStreak} days`);
}

// Inventory summary
if (user.inventory) {
  console.log(`Items owned: ${user.inventory.itemCount}`);
  console.log(`Active effects: ${user.inventory.activeEffects.join(', ')}`);
}

Leaderboards

Global Leaderboard with Filters

// Using options object (recommended)
const leaderboard = await client.leaderboards.getGlobal({
  timeframe: 'weekly',
  page: 1,
  limit: 10,
  persona: 'competitor',
  minLevel: 5,
  maxLevel: 20,
  startDate: new Date('2024-01-01'),
  endDate: new Date('2024-01-31')
});

// Access cache metadata
if (leaderboard.cacheMetadata) {
  console.log(`Cached at: ${leaderboard.cacheMetadata.cachedAt}`);
  console.log(`TTL: ${leaderboard.cacheMetadata.ttl}s`);
}

// Rankings include percentile
leaderboard.rankings.forEach(entry => {
  console.log(`#${entry.rank} ${entry.userId}: ${entry.score} pts (top ${entry.percentile}%)`);
});

Custom Leaderboard with Filters

const customLb = await client.leaderboards.getCustom('leaderboard_id', {
  page: 1,
  limit: 10,
  persona: 'achiever',
  minLevel: 10
});

"Around Me" View

// Get entries around a specific user
const aroundMe = await client.leaderboards.getAroundUser(
  'leaderboard_id',
  'user_123',
  5  // 5 entries above and below
);

aroundMe.rankings.forEach(entry => {
  const marker = entry.userId === 'user_123' ? '→' : ' ';
  console.log(`${marker} #${entry.rank} ${entry.userId}: ${entry.score}`);
});

Get User Rank with Percentile

const rank = await client.leaderboards.getUserRank('leaderboard_id', 'user_123');

console.log(`Rank: #${rank.rank}`);
console.log(`Score: ${rank.score}`);
console.log(`Percentile: top ${rank.percentile}%`);

Health Checks

// Full health check
const health = await client.health.check();
console.log(`Status: ${health.status}`);
console.log(`Version: ${health.version}`);

// Quick availability check
const isReady = await client.health.isReady();
if (isReady) {
  console.log('API is ready');
}

Aha Score

// Declare user activation milestone (1-5)
const result = await client.aha.declare('user_123', 4);
console.log(result.message);

// Get user's aha score
const score = await client.aha.getUserScore('user_123');
console.log(`Current Score: ${score.data.current_score}`);
console.log(`Status: ${score.data.status}`);

API Reference

Events

| Method | Description | |--------|-------------| | track(eventName, userId, properties?, options?) | Track a single event | | trackBatch(events, options?) | Track multiple events (max 100) | | trackLegacy(eventName, userId, properties?, options?) | Deprecated - Use track() |

Users

| Method | Description | |--------|-------------| | create(userData) | Create a new user | | update(userId, userData) | Update user profile (partial update) | | createBatch(users) | Create multiple users (max 100) | | get(userId, options?) | Get user profile with optional field selection | | search(query, options?) | Search users with pagination | | getBulk(userIds) | Get multiple user profiles | | getBadges(userId) | Get user's badges | | getRank(userId, timeframe?) | Get user's global rank | | submitAnswers(userId, questionnaireId, answers) | Submit questionnaire answers |

Leaderboards

| Method | Description | |--------|-------------| | getGlobal(timeframeOrOptions?, page?, limit?, options?) | Get global leaderboard with filters | | list(pageOrOptions?, limit?, search?) | List all leaderboards | | getCustom(leaderboardId, pageOrOptions?, limit?, search?, options?) | Get custom leaderboard with filters | | getUserRank(leaderboardId, userId) | Get user's rank in leaderboard | | getAroundUser(leaderboardId, userId, range?) | Get entries around a user |

Badges

| Method | Description | |--------|-------------| | list(page?, limit?, activeOnly?) | List all badges |

Levels

| Method | Description | |--------|-------------| | list(page?, limit?) | List all levels |

Questionnaires

| Method | Description | |--------|-------------| | get(slug) | Get questionnaire by slug | | getActive() | Get active questionnaire |

Aha Score

| Method | Description | |--------|-------------| | declare(userId, value) | Declare aha score (1-5) | | getUserScore(userId) | Get user's aha score |

Health

| Method | Description | |--------|-------------| | check() | Get full health status | | isReady() | Quick availability check |

Error Handling

The SDK provides typed error classes for different error scenarios:

import Rooguys, {
  ValidationError,
  AuthenticationError,
  NotFoundError,
  ConflictError,
  RateLimitError,
  ServerError
} from '@rooguys/js';

try {
  await client.users.create({ userId: 'user_123', email: 'invalid-email' });
} catch (error) {
  if (error instanceof ValidationError) {
    console.error('Validation failed:', error.message);
    console.error('Field errors:', error.fieldErrors);
    console.error('Error code:', error.code);
  } else if (error instanceof AuthenticationError) {
    console.error('Invalid API key');
  } else if (error instanceof NotFoundError) {
    console.error('Resource not found');
  } else if (error instanceof ConflictError) {
    console.error('Resource already exists');
  } else if (error instanceof RateLimitError) {
    console.error(`Rate limited. Retry after ${error.retryAfter} seconds`);
  } else if (error instanceof ServerError) {
    console.error('Server error:', error.message);
  }
  
  // All errors include requestId for debugging
  console.error('Request ID:', error.requestId);
}

Error Types

| Error Class | HTTP Status | Description | |-------------|-------------|-------------| | ValidationError | 400 | Invalid input data | | AuthenticationError | 401 | Invalid or missing API key | | ForbiddenError | 403 | Insufficient permissions | | NotFoundError | 404 | Resource not found | | ConflictError | 409 | Resource already exists | | RateLimitError | 429 | Rate limit exceeded | | ServerError | 500+ | Server-side error |

Error Properties

All errors include:

  • message - Human-readable error message
  • code - Machine-readable error code (e.g., INVALID_EMAIL, USER_NOT_FOUND)
  • requestId - Unique request identifier for debugging
  • statusCode - HTTP status code

ValidationError also includes:

  • fieldErrors - Array of { field, message } for field-level errors

RateLimitError also includes:

  • retryAfter - Seconds until rate limit resets

Rate Limiting

The SDK provides built-in rate limit handling:

const client = new Rooguys('YOUR_API_KEY', {
  // Get notified when 80% of rate limit is consumed
  onRateLimitWarning: (info) => {
    console.warn(`Rate limit: ${info.remaining}/${info.limit} remaining`);
    console.warn(`Resets at: ${new Date(info.reset * 1000)}`);
  },
  
  // Automatically retry rate-limited requests
  autoRetry: true,
  maxRetries: 3
});

Rate limit info is available in response metadata:

const response = await client._httpClient.get('/users/user_123');
console.log('Rate limit:', response.rateLimit);
// { limit: 1000, remaining: 950, reset: 1704067200 }

Testing

npm test              # Run all tests
npm run test:coverage # Run with coverage report

The SDK maintains >90% test coverage with:

  • Unit tests for all API methods
  • Property-based tests using fast-check
  • Error handling validation
  • Rate limit handling tests

Requirements

  • Browser with support for:
    • fetch API
    • Promise
    • AbortController (for timeouts)
  • Most modern browsers support these features

License

MIT