npm package: @rooguys/js
Description: Official Browser SDK for Rooguys API
Author: mowamed
Latest version: 1.0.0

# Rooguys JavaScript SDK

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

## Installation

### NPM
```bash
npm install @rooguys/js
```

### Direct Import (ES Modules)
```javascript
import Rooguys from './path/to/sdk/index.js';
```

## Initialization

```javascript
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`
   ```javascript
   // 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
   ```javascript
   // 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

```javascript
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

```javascript
// 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

```javascript
// 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

```javascript
const user = await client.users.create({
  userId: 'user_123',
  displayName: 'John Doe',
  email: 'john@example.com',
  firstName: 'John',
  lastName: 'Doe',
  metadata: { plan: 'premium' }
});
```

#### Update User Profile

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

#### Batch User Creation

```javascript
const response = await client.users.createBatch([
  { userId: 'user_1', displayName: 'User One', email: 'one@example.com' },
  { userId: 'user_2', displayName: 'User Two', email: 'two@example.com' },
  // ... up to 100 users
]);
```

#### Get User Profile with Field Selection

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

#### Search Users

```javascript
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

```javascript
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

```javascript
// 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

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

#### "Around Me" View

```javascript
// 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

```javascript
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

```javascript
// 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

```javascript
// 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:

```javascript
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:

```javascript
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:
```javascript
const response = await client._httpClient.get('/users/user_123');
console.log('Rate limit:', response.rateLimit);
// { limit: 1000, remaining: 950, reset: 1704067200 }
```

## Testing

```bash
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