npm package: @sos-workspace/experiment-plus
Description: TypeScript SDK for experiment-plus feature flags and A/B testing
Latest version: 0.4.7

# @experiment-plus/sdk

TypeScript SDK for experiment-plus feature flags and A/B testing.

## Installation

```bash
npm install @experiment-plus/sdk
# or
pnpm add @experiment-plus/sdk
# or
yarn add @experiment-plus/sdk
```

## Quick Start

```typescript
import { ExperimentPlus } from '@experiment-plus/sdk';

// Initialize the client
const client = new ExperimentPlus({
  apiKey: 'pk_live_your_api_key',
});

// Identify the user (required before evaluating flags)
client.identify('user-123', {
  plan: 'pro',
  country: 'US',
});

// Evaluate a feature flag (synchronous)
const variant = client.getFeatureFlag('new-checkout-flow');
console.log(`User sees: ${variant}`); // 'control' | 'variant-a' | 'variant-b'

// Check if a boolean flag is enabled
const isEnabled = client.isFeatureEnabled('dark-mode');

// Get flag with payload
const result = client.getFeatureFlagWithPayload('pricing-experiment');
if (result.payload) {
  console.log(`Discount: ${result.payload.discount}%`);
}

// Track events for A/B test analysis
client.track('button_clicked', { properties: { button: 'checkout' } });
client.track('purchase_completed', { value: 99.99 });

// Flush events before page unload
await client.shutdown();
```

## Configuration Options

```typescript
const client = new ExperimentPlus({
  apiKey: 'pk_live_...',              // Required: Your API key
  baseUrl: 'https://api.example.com', // Optional: Custom API URL
  pollIntervalMs: 30000,              // Optional: Flag polling interval in ms (default: 30000)
  cache: {
    ttl: 60_000,                      // Cache TTL in ms (default: 60000)
    maxSize: 100,                     // Max cached flags (default: 100)
  },
  batching: {
    flushInterval: 5_000,             // Event flush interval in ms (default: 5000)
    maxBatchSize: 100,                // Max events per batch (default: 100)
  },
  timeout: 30_000,                    // Request timeout in ms (default: 30000)
  retries: 3,                         // Max retries on failure (default: 3)
});
```

## User Identification

You must call `identify()` before evaluating flags:

```typescript
// Identify user with optional properties
client.identify('user-123', {
  plan: 'pro',
  country: 'US',
});

// Update properties without changing distinctId
client.setProperties({
  lastLogin: new Date().toISOString(),
});

// Get current context
const context = client.getContext();
```

If you evaluate flags without calling `identify()`, the SDK will return `'control'` and log a warning.

## Flag Evaluation Methods

All flag evaluation methods are **synchronous** - they use locally cached flag configurations.

```typescript
// Get variant key
const variant = client.getFeatureFlag('my-flag');

// Check if boolean flag is enabled (returns true for 'on', 'true', 'enabled')
const isEnabled = client.isFeatureEnabled('dark-mode');

// Get variant with payload
const { variant, payload } = client.getFeatureFlagWithPayload('pricing-test');

// Get all evaluated flags
const allFlags = client.getAllFlags();
```

## API Keys

- **Publishable keys** (`pk_live_...`, `pk_test_...`): Safe for client-side use, can only evaluate flags and track events
- **Secret keys** (`sk_live_...`, `sk_test_...`): Server-side only, have full API access

## Server-Side Usage (Node.js)

```typescript
import { ExperimentPlus } from '@experiment-plus/sdk';

const client = new ExperimentPlus({
  apiKey: process.env.EXPERIMENT_PLUS_SECRET_KEY,
});

// Wait for initial flag fetch
await client.ready();

// Identify and evaluate per-request
client.identify('user-123', { plan: 'enterprise' });
const variant = client.getFeatureFlag('feature-x');

// Track events
client.track('api_request', { properties: { endpoint: '/users' } });

// On shutdown
await client.shutdown();
```

## Automatic Retries

The SDK automatically retries failed requests using exponential backoff with jitter.

### Default Behavior

| Setting | Default |
|---------|---------|
| Max retries | 3 |
| Initial delay | 1 second |
| Max delay | 10 seconds |
| Backoff multiplier | 2x |

Example timing: 1s → 2s → 4s (with random jitter up to 25%)

### Retryable Errors

These errors are automatically retried:
- Rate limit errors (HTTP 429, except billing limits)
- Server errors (HTTP 500, 502, 503, 504)
- Network connectivity issues
- Request timeouts

### Non-Retryable Errors

These errors fail immediately without retry:
- Bad request (400) - fix your request parameters
- Unauthorized (401) - check your API key
- Forbidden (403) - verify permissions
- Not found (404) - resource doesn't exist
- Evaluation limit exceeded (429) - upgrade your plan

## Background Polling

The SDK fetches flag configurations on startup and polls for updates in the background.

```typescript
// Default: polls every 30 seconds
const client = new ExperimentPlus({
  apiKey: '...',
  pollIntervalMs: 60000, // Poll every 60 seconds instead
});

// Force immediate refresh
await client.refresh();

// Stop background polling
client.stopPolling();
```

## Targeting Rules

Rules let you override percentage-based distribution for specific user segments.

### How Rules Work

```
User visits
    │
    ▼
┌─────────────────────────┐
│ 1. Is the flag enabled? │ ── NO ──→ Return default
└───────────┬─────────────┘
            │ YES
            ▼
┌─────────────────────────┐
│ 2. Do any RULES match?  │ ── YES ──→ Return rule's variant
└───────────┬─────────────┘
            │ NO
            ▼
┌─────────────────────────┐
│ 3. Hash-based percentage│ ──────────→ Return calculated variant
└─────────────────────────┘
```

**Rules always win over percentages.**

### Condition Operators

| Operator | Description | Example |
|----------|-------------|---------|
| `equals` | Exact match | `country = "US"` |
| `not_equals` | Not equal | `plan != "free"` |
| `contains` | Substring match | `email contains "@company.com"` |
| `not_contains` | No substring | `email not contains "test"` |
| `greater_than` | Numeric > | `age > 18` |
| `less_than` | Numeric < | `cart_value < 100` |
| `greater_than_or_equal` | Numeric >= | `purchases >= 5` |
| `less_than_or_equal` | Numeric <= | `days_since_signup <= 30` |
| `regex` | Pattern match | `email matches ".*@(gmail\|yahoo)\\.com"` |
| `is_set` | Property exists | `premium_tier is set` |
| `is_not_set` | Property missing | `payment_method is not set` |
| `in` | In array | `country in ["US", "CA", "UK"]` |
| `not_in` | Not in array | `role not in ["admin", "moderator"]` |

## Event Tracking

```typescript
// Track event (batched, auto-flushes periodically)
client.track('button_clicked', {
  properties: { button: 'checkout' },
  value: 99.99,
});

// Track event immediately (no batching)
await client.trackImmediate('purchase_completed', {
  value: 149.99,
});

// Manually flush queued events
await client.flush();
```

## Debugging

```typescript
// Get currently evaluated flags
const evaluated = client.getEvaluatedFlags();
console.log(evaluated); // { 'flag-id-1': 'variant-a', 'flag-id-2': 'control' }
```

## Error Handling

```typescript
import {
  ExperimentPlusError,
  RateLimitError,
  EvaluationLimitError,
} from '@experiment-plus/sdk';

try {
  await client.ready();
} catch (error) {
  if (error instanceof RateLimitError) {
    console.log(`Rate limited, retry after ${error.retryAfter}ms`);
  } else if (error instanceof EvaluationLimitError) {
    console.log('Monthly evaluation limit exceeded');
  } else if (error instanceof ExperimentPlusError) {
    console.log(`API error: ${error.message}`);
  }
}
```

## License

MIT