npm package: @setbit-io/node
Description: Official Node.js SDK for SetBit - Simple feature flags and A/B testing
Author: SetBit
Latest version: 0.1.1

# SetBit Node.js SDK

Official Node.js SDK for [SetBit](https://setbit.io) - Simple feature flags and A/B testing for server-side JavaScript.

## Features

- ✅ **Boolean Flags** - Simple on/off feature toggles
- 🧪 **A/B Testing** - Weighted variant distribution for experiments
- 📊 **Conversion Tracking** - Track events and conversions
- 🏷️ **Tag-Based Targeting** - Target by environment, app, team, region, etc.
- 🚀 **Fail-Open Design** - Returns defaults if API is unreachable
- 🪶 **Zero Dependencies** - Uses only Node.js built-in modules
- ⚡ **Fast** - Built-in timeouts and connection handling
- 📘 **TypeScript Support** - Full TypeScript definitions included

## Installation

```bash
npm install @setbit-io/node
```

Or with Yarn:

```bash
yarn add @setbit-io/node
```

## Quick Start

```javascript
const SetBitClient = require('@setbit-io/node');

// Initialize the client
const client = new SetBitClient({
  apiKey: 'pk_your_api_key_here',
  tags: {
    env: 'production',
    app: 'api',
  }
});

// Check a boolean flag (user ID required for analytics)
const userId = getCurrentUserId();
const enabled = await client.enabled('new-checkout', userId, false);
if (enabled) {
  showNewCheckout();
} else {
  showOldCheckout();
}

// Get A/B test variant
const variant = await client.variant('pricing-experiment', userId, 'control');
switch (variant) {
  case 'variant_a':
    showPrice99();
    break;
  case 'variant_b':
    showPrice149();
    break;
  default:
    showPrice129();
}

// Track conversions (pass variant for proper attribution)
await client.track('purchase', userId, {
  flagName: 'pricing-experiment',
  variant: variant,
  metadata: { amount: 99.99, currency: 'USD' }
});
```

## TypeScript Usage

```typescript
import SetBitClient, { SetBitConfig } from '@setbit-io/node';

const config: SetBitConfig = {
  apiKey: 'pk_your_api_key',
  tags: {
    env: 'production',
    app: 'api'
  }
};

const client = new SetBitClient(config);

// All methods are fully typed
const enabled: boolean = await client.enabled('feature', userId, false);
const variant: string = await client.variant('experiment', userId, 'control');
await client.track('event', userId, { flagName: 'flag', variant, metadata: { key: 'value' } });
```

## API Reference

### Constructor

```javascript
const client = new SetBitClient({
  apiKey: 'pk_your_api_key',
  tags: {
    env: 'production',
    app: 'api',
    region: 'us-east'
  },
  baseURL: 'https://flags.setbit.io', // Optional, custom endpoint
  timeout: 3000,                       // Optional, request timeout in ms
  silent: true                         // Optional, suppress error logs
});
```

**Config Parameters:**
- `apiKey` (string, **required**): Your SetBit API key
- `tags` (object, optional): Tags for targeting flags
- `baseURL` (string, optional): API endpoint URL (useful for self-hosted instances)
- `timeout` (number, optional): Request timeout in milliseconds (default: 3000)
- `silent` (boolean, optional): Suppress error logs (default: true)

**Returns:**
- `SetBitClient`: SetBit client instance

**Throws:**
- `Error`: If API key is missing

---

### `enabled(flagName, userId, defaultValue)`

Check if a flag is enabled. Returns `true` if the flag is globally enabled, `false` otherwise.

**Parameters:**
- `flagName` (string): Name of the flag
- `userId` (string, **required**): User identifier (required for analytics and billing)
- `defaultValue` (boolean): Value to return if flag not found (default behavior on error)

**Returns:** `Promise<boolean>` - `true` if enabled, `false` otherwise

**Note:** This method returns whether the flag is globally enabled. For rollout flags, use `variant()` to check which rollout group the user is in.

**Example:**
```javascript
// Check if flag is enabled
if (await client.enabled('new-feature', userId, false)) {
  // Show new feature
}

// With default true (fail open)
if (await client.enabled('beta-access', userId, true)) {
  // Grant beta access
}
```

---

### `variant(flagName, userId, defaultVariant)`

Get the variant for an A/B test experiment or percentage rollout.

**Parameters:**
- `flagName` (string): Name of the experiment flag
- `userId` (string, **required**): User identifier
- `defaultVariant` (string): Default variant if flag not found (e.g., "control")

**Returns:** `Promise<string>` - Variant name (e.g., "control", "variant_a", "variant_b")

**Note:** Returns the `defaultVariant` if the flag is disabled or API is unreachable.

**Example:**
```javascript
// A/B test
const variant = await client.variant('pricing-experiment', userId, 'control');
switch (variant) {
  case 'variant_a':
    // Show variant A
    break;
  case 'variant_b':
    // Show variant B
    break;
  default:
    // Show control
}

// Percentage rollout
const variant = await client.variant('new-api-rollout', userId, 'control');
if (variant === 'enabled') {
  useNewAPI();
} else {
  useOldAPI();
}
```

---

### `track(eventName, userId, options)`

Track a conversion event or user action.

**Parameters:**
- `eventName` (string): Name of the event (e.g., "purchase", "signup", "click")
- `userId` (string, **required**): User identifier
- `options` (object, optional): Tracking options
  - `flagName` (string): Flag name to associate with event
  - `variant` (string): Variant the user was assigned to (for A/B test attribution)
  - `metadata` (object): Additional metadata

**Returns:** `Promise<void>` - Resolves when tracking completes (fails silently)

**Example:**
```javascript
// Track conversion with variant attribution (recommended for A/B tests)
const variant = await client.variant('pricing-experiment', userId, 'control');
// ... user interacts with the variant ...
// ... later when they convert ...
await client.track('purchase', userId, {
  flagName: 'pricing-experiment',
  variant: variant,
  metadata: { amount: 99.99, currency: 'USD' }
});

// Track without flag association
await client.track('signup', userId, {
  metadata: { source: 'organic' }
});

// Track simple event
await client.track('button_click', userId);
```

## Framework Examples

### Express.js

```javascript
const express = require('express');
const SetBitClient = require('@setbit-io/node');

const app = express();
const flagClient = new SetBitClient({
  apiKey: process.env.SETBIT_API_KEY,
  tags: { env: process.env.NODE_ENV }
});

app.get('/api/checkout', async (req, res) => {
  const userId = req.user.id;

  // Check feature flag
  const useNewCheckout = await flagClient.enabled('new-checkout', userId, false);

  if (useNewCheckout) {
    res.json({ version: 'v2', features: ['express-checkout', 'saved-cards'] });
  } else {
    res.json({ version: 'v1', features: [] });
  }
});

app.post('/api/purchase', async (req, res) => {
  const userId = req.user.id;
  const amount = req.body.amount;
  const variant = req.body.variant; // Pass from frontend or re-evaluate

  // Process purchase...

  // Track conversion with variant attribution
  await flagClient.track('purchase', userId, {
    flagName: 'new-checkout',
    variant: variant,
    metadata: { amount }
  });

  res.json({ success: true });
});

app.listen(3000);
```

### Next.js API Routes

```javascript
// pages/api/features.js
import SetBitClient from '@setbit-io/node';

const client = new SetBitClient({
  apiKey: process.env.SETBIT_API_KEY,
  tags: { env: process.env.NODE_ENV }
});

export default async function handler(req, res) {
  const userId = req.headers['x-user-id'];

  const features = {
    newUI: await client.enabled('new-ui', userId, false),
    betaFeatures: await client.enabled('beta-features', userId, false),
    experiment: await client.variant('pricing-experiment', userId, 'control')
  };

  res.status(200).json(features);
}
```

### Next.js Server Components

```javascript
// app/dashboard/page.tsx
import SetBitClient from '@setbit-io/node';

const client = new SetBitClient({
  apiKey: process.env.SETBIT_API_KEY!
});

export default async function DashboardPage() {
  const userId = 'user_123'; // From auth

  const showNewDashboard = await client.enabled('new-dashboard', userId, false);

  if (showNewDashboard) {
    return <NewDashboard />;
  }

  return <OldDashboard />;
}
```

### Fastify

```javascript
const fastify = require('fastify')();
const SetBitClient = require('@setbit-io/node');

const flagClient = new SetBitClient({
  apiKey: process.env.SETBIT_API_KEY
});

fastify.get('/api/features', async (request, reply) => {
  const userId = request.user.id;

  const variant = await flagClient.variant('ui-experiment', userId, 'control');

  return {
    theme: variant === 'dark' ? 'dark-mode' : 'light-mode',
    variant
  };
});

fastify.listen({ port: 3000 });
```

## Error Handling

The SDK is designed to **fail open** - if the API is unreachable or returns an error, methods return the specified default values rather than blocking your application.

```javascript
// If API fails, returns false (safe default)
const enabled = await client.enabled('risky-feature', userId, false);

// If API fails, returns "control" (safe default)
const variant = await client.variant('experiment', userId, 'control');

// Track fails silently (logs error but doesn't block)
await client.track('event', userId, { flagName: 'flag', metadata: { key: 'value' } });
```

## Best Practices

### 1. User ID is Required
Always provide a user ID for all flag evaluations. This is required for:
- Analytics and reporting
- Billing (MAU tracking)
- Consistent variant assignment in A/B tests

```javascript
// ✅ Good - user ID provided
if (await client.enabled('feature', userId, false)) {
  // ...
}

// ❌ Bad - empty user ID
if (await client.enabled('feature', '', false)) {
  // This will still work but won't be tracked properly
}
```

### 2. Choose Appropriate Defaults

```javascript
// For new features, default to false (conservative)
if (await client.enabled('experimental-feature', userId, false)) {
  // Only enable if API confirms
}

// For stable features, default to true (progressive)
if (await client.enabled('stable-feature', userId, true)) {
  // Enable even if API is down
}
```

### 3. Reuse the Client Instance
Create one client instance and reuse it throughout your application:

```javascript
// config/flags.js
const SetBitClient = require('@setbit-io/node');

const flagClient = new SetBitClient({
  apiKey: process.env.SETBIT_API_KEY,
  tags: {
    env: process.env.NODE_ENV
  }
});

module.exports = flagClient;

// Use in other files
const flagClient = require('./config/flags');

async function handler(req, res) {
  if (await flagClient.enabled('feature', req.user.id, false)) {
    // ...
  }
}
```

### 4. Use Tags for Organization

```javascript
const client = new SetBitClient({
  apiKey: 'pk_your_api_key',
  tags: {
    env: 'production',     // Environment
    app: 'api',           // Application/service name
    team: 'backend',      // Team ownership
    region: 'us-east-1'   // Geographic region
  }
});
```

## Testing

Run the test suite:

```bash
npm test
```

## Requirements

- Node.js 18.0.0 or higher
- No external dependencies (uses only Node.js built-in modules)

## License

MIT License - see [LICENSE](LICENSE) file for details

## Support

- 📖 [Documentation](https://docs.setbit.io)
- 💬 [GitHub Issues](https://github.com/setbit-io/setbit-node/issues)
- 📧 Email: support@setbit.io

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.