UNPKG

@varia-bly/variably-sdk

Version:

Official JavaScript/TypeScript SDK for Variably feature flags, experimentation, LLM experiments with React hooks, and real-time dynamic configurations

565 lines (454 loc) 15.2 kB
# Variably JavaScript/TypeScript SDK Official JavaScript/TypeScript SDK for Variably feature flags and experimentation platform. ## Installation ```bash npm install @varia-bly/variably-sdk # or yarn add @varia-bly/variably-sdk ``` ## Quick Start ```typescript import { VariablyClient } from '@varia-bly/variably-sdk'; // Initialize the client const client = new VariablyClient({ apiKey: 'your-api-key', baseUrl: 'https://graphql.variably.tech', // optional, defaults to production GraphQL endpoint environment: 'production' // optional }); // Evaluate a boolean feature flag const userContext = { userId: 'user-123', email: 'user@example.com', country: 'US' }; const isFeatureEnabled = await client.evaluateFlagBool( 'new-checkout-flow', false, // default value userContext ); if (isFeatureEnabled) { // Show new checkout flow } // Evaluate a feature gate const hasAccess = await client.evaluateGate('premium-features', userContext); // Track events await client.track({ name: 'button_clicked', userId: 'user-123', properties: { button_name: 'checkout', page: 'product-detail' } }); ``` ## Configuration ```typescript interface VariablyConfig { /** API key for authentication */ apiKey: string; /** Base URL for the GraphQL API (default: https://graphql.variably.tech) */ baseUrl?: string; /** Environment (development, staging, production) */ environment?: string; /** Request timeout in milliseconds (default: 5000) */ timeout?: number; /** Number of retry attempts (default: 3) */ retryAttempts?: number; /** Enable analytics tracking (default: true) */ enableAnalytics?: boolean; /** Cache configuration */ cache?: { /** Cache TTL in milliseconds (default: 300000 = 5 minutes) */ ttl?: number; /** Maximum cache size (default: 1000) */ maxSize?: number; /** Enable cache (default: true) */ enabled?: boolean; }; /** Real-time Feature Gate updates via WebSocket */ realTimeUpdates?: { /** Enable real-time updates (default: false) */ enabled?: boolean; /** Project ID to subscribe to (required if enabled) */ projectId?: string; /** Auto-invalidate cache on updates (default: true) */ autoInvalidateCache?: boolean; }; } ``` ## Real-Time Feature Gate Updates **NEW**: The SDK now supports real-time Feature Gate updates via WebSocket subscriptions. When enabled, your application receives instant notifications when Feature Gates are updated in the Variably UI, and the SDK automatically invalidates its cache to ensure fresh evaluations. ### How It Works ``` UI Update → Backend → Valkey PubSub → GraphQL Subscription → WebSocket → SDK → Cache Invalidation ``` When you update a Feature Gate in the Variably UI: 1. Backend publishes the update to Valkey 2. GraphQL server receives the update and broadcasts to subscribed clients 3. SDK receives the WebSocket message 4. Cache is automatically invalidated for the updated gate 5. Next `evaluateGate()` call fetches fresh data ### Usage ```typescript import { VariablyClient } from '@varia-bly/variably-sdk'; const client = new VariablyClient({ apiKey: 'your-api-key', baseUrl: 'http://localhost:4000', // or your GraphQL endpoint // Enable real-time updates realTimeUpdates: { enabled: true, projectId: 'your-project-id', // Required: The project to subscribe to autoInvalidateCache: true // Optional: Auto-clear cache on updates (default: true) } }); // Now when you evaluate a gate, it will automatically receive real-time updates const hasAccess = await client.evaluateGate('premium-features', userContext); // When the gate is updated in the UI, the cache is automatically invalidated // and the next evaluation will fetch fresh data ``` ### Benefits - **Instant Updates**: Changes in the Variably UI are reflected immediately in your application - **Zero Configuration**: Just enable and provide your project ID - **Automatic Cache Invalidation**: No manual cache management required - **Graceful Degradation**: If WebSocket connection fails, SDK falls back to normal HTTP with caching - **Auto-Reconnection**: Built-in reconnection logic with exponential backoff - **Production Ready**: Comprehensive error handling and logging ### Advanced Usage ```typescript import { VariablyClient } from '@varia-bly/variably-sdk'; const client = new VariablyClient({ apiKey: 'your-api-key', baseUrl: 'http://localhost:4000', realTimeUpdates: { enabled: true, projectId: 'your-project-id', autoInvalidateCache: true } }); // Check if real-time sync is active if (client.realTimeSync?.isActive()) { console.log('Real-time updates are active'); console.log('Connection state:', client.realTimeSync.getConnectionState()); } // Stop real-time updates (e.g., during cleanup) client.realTimeSync?.stop(); ``` ### Requirements - GraphQL endpoint must support WebSocket subscriptions - Project ID must be valid and have Feature Gates configured - Network must allow WebSocket connections (ws:// or wss://) ## LLM Experiments **NEW in v2.2.0**: The SDK now includes comprehensive support for LLM experiments with A/B testing, prompt optimization, and React hooks. ```typescript import { VariablyLLMClient, useLLMPrompt } from '@varia-bly/variably-sdk'; // Basic LLM experiment const llmClient = new VariablyLLMClient({ apiKey: 'your-api-key' }); const response = await llmClient.executePrompt({ experimentId: 'exp_abc123', userContext: { userId: 'user_123' }, variables: { topic: 'AI' } }); console.log('Response:', response.content); console.log('Variant:', response.variant.name); // React Hook for LLM experiments function MyComponent() { const { execute, response, loading } = useLLMPrompt({ experimentId: 'exp_abc123', apiKey: 'your-api-key' }); return ( <div> <button onClick={() => execute({ userContext: { userId: 'user_123' } })}> Generate </button> {loading && <p>Loading...</p>} {response && <p>{response.content}</p>} </div> ); } ``` ### Context-Aware Evaluation (Better RAG Quality) — v2.5.0+ For RAG chatbots, passing conversation history and retrieved chunks enables **groundedness scoring, hallucination detection, and conversational coherence** — dimensions impossible to evaluate in isolation. The `evaluationContext` is **not sent to the LLM** — only used by Variably's evaluator. #### REST Client (Managed Execution) ```typescript import { VariablyRESTClient, EvaluationContext } from '@varia-bly/variably-sdk'; const client = new VariablyRESTClient({ apiKey: 'your-api-key' }); const evaluationContext: EvaluationContext = { // Retrieved RAG chunks — enables groundedness & hallucination scoring referenceMaterials: [ { id: 'chunk-001', content: 'Unhealthy diets high in refined sugars, fats...', source: 'Kenya National Clinical Guidelines', type: 'chunk', relevanceScore: 0.89, }, { id: 'chunk-002', content: 'Modifiable risk factors include obesity...', source: 'Kenya National Clinical Guidelines', type: 'chunk', relevanceScore: 0.82, }, ], // Conversation history — enables coherence & consistency scoring workflowHistory: [ { role: 'user', content: 'What causes diabetes?' }, { role: 'assistant', content: 'Key factors include genetics, diet...' }, { role: 'user', content: 'What about potatoes?' }, ], // The query sent to the retriever (optional) retrievalQuery: 'potato consumption glycemic index diabetes risk', }; // Non-streaming const response = await client.evaluatePrompt( 'rag-prompt-experiment', { topic: 'potatoes', context: contextText }, { userId: 'user_123' }, { evaluationContext } ); // Streaming (SSE) const controller = client.evaluatePromptStream( 'rag-prompt-experiment', { topic: 'potatoes', context: contextText }, { userId: 'user_123' }, { onToken: (t) => process.stdout.write(t.content) }, { evaluationContext } ); ``` #### LLM Client ```typescript const response = await llmClient.executePrompt({ experimentId: 'rag-prompt-experiment', userContext: { userId: 'user_123' }, variables: { query: 'What about potatoes?', context: contextText }, evaluationContext: { referenceMaterials: [ { id: 'chunk-001', content: 'Unhealthy diets...', source: 'Guidelines', relevanceScore: 0.89 }, ], workflowHistory: [ { role: 'user', content: 'What causes diabetes?' }, { role: 'assistant', content: 'Key factors include genetics, diet...' }, ], retrievalQuery: 'potato consumption glycemic index diabetes risk', }, }); ``` #### Async Scoring (BYOR) ```typescript await client.submitScoringRequest({ experimentId: 'exp-123', variantId: 'variant-a', projectId: 'proj-456', prompt: executedPrompt, response: llmResponse, evaluationContext: { referenceMaterials: retrievedChunks, workflowHistory: conversationHistory, }, }); ``` **What this enables:** | Dimension | Description | Requires | |-----------|-------------|----------| | `faithfulness` | % of claims grounded in retrieved chunks | `referenceMaterials` | | `hallucinationRate` | % of claims with no source in context | `referenceMaterials` | | `contextUtilization` | % of relevant chunks actually used | `referenceMaterials` | | `conversationConsistency` | No contradictions with prior turns | `workflowHistory` | | `contextRetention` | Maintains topic awareness across turns | `workflowHistory` | | `transparency` | Discloses when going beyond source material | `referenceMaterials` | #### Type Reference ```typescript interface EvaluationContext { referenceMaterials?: ReferenceMaterial[]; // RAG chunks / source documents workflowHistory?: WorkflowStep[]; // Conversation turns retrievalQuery?: string; // Query sent to retriever } interface ReferenceMaterial { id: string; // Chunk ID content: string; // Chunk text source?: string; // Document source URL/name type?: string; // e.g. "chunk", "document" relevanceScore?: number; // Retriever similarity score } interface WorkflowStep { role?: string; // "user" | "assistant" step?: string; // Step name in workflow input?: string; // Step input output?: string; // Step output content?: string; // Message content } ``` See [Context-Aware RAG Evaluation](../../docs/concepts/context-aware-rag-evaluation.md) for the full concept doc with architecture diagrams and integration examples. **📖 Full Documentation**: See [LLM_EXPERIMENTS.md](./LLM_EXPERIMENTS.md) for comprehensive documentation including: - React Hooks (`useLLMPrompt`, `useLLMStreaming`, `useConversation`, `useLLMEvaluation`) - Streaming responses - Response evaluation and quality scoring - Business metrics tracking - Batch execution - Advanced features and best practices ## Advanced Usage ### Environment Variables You can create a client using environment variables: ```typescript import { createClientFromEnv } from '@varia-bly/variably-sdk'; // Uses these environment variables: // VARIABLY_API_KEY // VARIABLY_BASE_URL // VARIABLY_ENVIRONMENT // VARIABLY_TIMEOUT // VARIABLY_ENABLE_ANALYTICS const client = createClientFromEnv(); ``` ### Different Flag Types ```typescript // Boolean flags const boolValue = await client.evaluateFlagBool('feature-enabled', false, userContext); // String flags const stringValue = await client.evaluateFlagString('theme', 'light', userContext); // Number flags const numberValue = await client.evaluateFlagNumber('max-items', 10, userContext); // JSON flags const jsonValue = await client.evaluateFlagJSON('config', { timeout: 5000 }, userContext); // Get full evaluation details const result = await client.evaluateFlag('feature-flag', 'default', userContext); console.log(result); // { key, value, reason, cacheHit, evaluatedAt, error? } ``` ### Batch Evaluation ```typescript const flags = await client.evaluateFlags([ 'feature-a', 'feature-b', 'feature-c' ], userContext); console.log(flags['feature-a'].value); ``` ### Event Tracking ```typescript // Single event await client.track({ name: 'purchase_completed', userId: 'user-123', properties: { amount: 99.99, currency: 'USD', items: ['item-1', 'item-2'] } }); // Batch events await client.trackBatch([ { name: 'page_view', userId: 'user-123', properties: { page: '/home' } }, { name: 'button_click', userId: 'user-123', properties: { button: 'cta' } } ]); ``` ### Cache Management ```typescript // Clear cache client.clearCache(); // Get cache stats const stats = client.cache.getStats(); console.log(stats); // { size, maxSize, hitRate, enabled } ``` ### Metrics ```typescript // Get SDK metrics const metrics = client.getMetrics(); console.log(metrics); // { // apiCalls: number, // cacheHits: number, // cacheMisses: number, // errors: number, // averageLatency: number, // cacheHitRate: number, // errorRate: number, // flagsEvaluated: number, // gatesEvaluated: number, // eventsTracked: number, // startTime: Date // } ``` ### Custom Logger ```typescript import { VariablyClient, createLogger } from '@varia-bly/variably-sdk'; const logger = createLogger({ level: 'debug', type: 'structured' // 'console', 'silent', 'structured' }); // Or use a custom logging function const customLogger = createLogger({ level: 'info', custom: (level, message, meta) => { // Send to your logging service console.log(`${level}: ${message}`, meta); } }); ``` ## Browser Usage The SDK works in both Node.js and browser environments: ```html <script type="module"> import { VariablyClient } from 'https://unpkg.com/@varia-bly/variably-sdk@latest/dist/index.esm.js'; const client = new VariablyClient({ apiKey: 'your-api-key', baseUrl: 'https://api.variably.com' }); // Use the client... </script> ``` ## Error Handling ```typescript import { VariablyError, NetworkError, AuthenticationError, ValidationError } from '@varia-bly/variably-sdk'; try { const result = await client.evaluateFlag('my-flag', false, userContext); } catch (error) { if (error instanceof AuthenticationError) { console.error('Invalid API key'); } else if (error instanceof NetworkError) { console.error('Network error:', error.statusCode); } else if (error instanceof ValidationError) { console.error('Validation error:', error.field); } else { console.error('Unknown error:', error.message); } } ``` ## TypeScript Support The SDK is written in TypeScript and includes full type definitions: ```typescript import { VariablyClient, UserContext, FlagResult } from '@varia-bly/variably-sdk'; const userContext: UserContext = { userId: 'user-123', email: 'user@example.com', attributes: { plan: 'premium', signupDate: '2023-01-01' } }; const result: FlagResult = await client.evaluateFlag('feature', false, userContext); ``` ## Development ### Building ```bash npm run build ``` ### Testing ```bash npm test ``` ### Linting ```bash npm run lint ``` ## License MIT License - see LICENSE file for details.