TypeScript SDK for experiment-plus feature flags and A/B testing
npm install @sos-workspace/experiment-plusTypeScript SDK for experiment-plus feature flags and A/B testing.
``bash`
npm install @experiment-plus/sdkor
pnpm add @experiment-plus/sdkor
yarn add @experiment-plus/sdk
`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();
`
`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)
});
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.
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();
`
- Publishable keys (pk_live_..., pk_test_...): Safe for client-side use, can only evaluate flags and track eventssk_live_...
- Secret keys (, sk_test_...): Server-side only, have full API access
`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();
`
The SDK automatically retries failed requests using exponential backoff with jitter.
| 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%)
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
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
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();
`
Rules let you override percentage-based distribution for specific user segments.
``
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.
| 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"] |
`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();
`
`typescript`
// Get currently evaluated flags
const evaluated = client.getEvaluatedFlags();
console.log(evaluated); // { 'flag-id-1': 'variant-a', 'flag-id-2': 'control' }
`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);API error: ${error.message}
} else if (error instanceof EvaluationLimitError) {
console.log('Monthly evaluation limit exceeded');
} else if (error instanceof ExperimentPlusError) {
console.log();``
}
}
MIT