UNPKG

@varia-bly/variably-sdk

Version:

Official JavaScript/TypeScript SDK for Variably feature flags, experimentation, and real-time dynamic configurations

373 lines 14.6 kB
"use strict"; /** * Variably JavaScript/TypeScript SDK Client */ Object.defineProperty(exports, "__esModule", { value: true }); exports.VariablyClient = void 0; exports.createClient = createClient; exports.createClientFromEnv = createClientFromEnv; const graphql_client_1 = require("./graphql-client"); const cache_1 = require("./cache"); const metrics_1 = require("./metrics"); const logger_1 = require("./logger"); const errors_1 = require("./errors"); class VariablyClient { constructor(config) { this.config = this.validateAndNormalizeConfig(config); this.logger = new logger_1.ConsoleLogger('info'); this.metrics = new metrics_1.MetricsCollector(); this.cache = new cache_1.CacheManager(this.config.cache, this.logger); this.graphqlClient = new graphql_client_1.GraphQLClient(this.config, this.logger, this.metrics); this.logger.info('Variably client initialized', { environment: this.config.environment, baseUrl: this.config.baseUrl }); } /** * Evaluate a boolean feature flag */ async evaluateFlagBool(flagKey, defaultValue, userContext) { const result = await this.evaluateFlag(flagKey, defaultValue, userContext); return typeof result.value === 'boolean' ? result.value : defaultValue; } /** * Evaluate a string feature flag */ async evaluateFlagString(flagKey, defaultValue, userContext) { const result = await this.evaluateFlag(flagKey, defaultValue, userContext); return typeof result.value === 'string' ? result.value : defaultValue; } /** * Evaluate a number feature flag */ async evaluateFlagNumber(flagKey, defaultValue, userContext) { const result = await this.evaluateFlag(flagKey, defaultValue, userContext); return typeof result.value === 'number' ? result.value : defaultValue; } /** * Evaluate a JSON feature flag */ async evaluateFlagJSON(flagKey, defaultValue, userContext) { const result = await this.evaluateFlag(flagKey, defaultValue, userContext); return result.error ? defaultValue : result.value; } /** * Evaluate a feature flag with full result details */ async evaluateFlag(flagKey, defaultValue, userContext) { try { this.metrics.recordFlagEvaluation(); // Validate inputs if (!flagKey || typeof flagKey !== 'string') { throw new errors_1.ConfigurationError('Flag key must be a non-empty string', 'flagKey'); } if (!userContext?.userId) { throw new errors_1.ConfigurationError('User context must include userId', 'userContext.userId'); } // Check cache first const cacheKey = this.generateCacheKey(flagKey, userContext); const cached = this.cache.get(cacheKey); if (cached) { this.metrics.recordCacheHit(); this.logger.debug('Flag evaluation cache hit', { flagKey, userId: userContext.userId }); return { ...cached, cacheHit: true }; } this.metrics.recordCacheMiss(); // Evaluate via GraphQL API const response = await this.graphqlClient.evaluateFlag(flagKey, userContext); const result = { key: flagKey, value: response.value, reason: response.reason || 'api_evaluation', ruleId: response.rule_id, cacheHit: false, evaluatedAt: new Date() }; // Cache the result this.cache.set(cacheKey, result); this.logger.debug('Flag evaluation successful', { flagKey, value: response.value, userId: userContext.userId }); return result; } catch (error) { this.logger.error('Flag evaluation failed', { flagKey, error: error instanceof Error ? error.message : String(error), userId: userContext.userId }); // Return default value with error return { key: flagKey, value: defaultValue, reason: 'error_fallback', cacheHit: false, evaluatedAt: new Date(), error: error instanceof Error ? error : new Error(String(error)) }; } } /** * Evaluate multiple feature flags in batch */ async evaluateFlags(flagKeys, userContext) { const results = {}; // Check cache for each flag const uncachedFlags = []; for (const flagKey of flagKeys) { this.metrics.recordFlagEvaluation(); const cacheKey = this.generateCacheKey(flagKey, userContext); const cached = this.cache.get(cacheKey); if (cached) { this.metrics.recordCacheHit(); results[flagKey] = { ...cached, cacheHit: true }; } else { this.metrics.recordCacheMiss(); uncachedFlags.push(flagKey); } } // Evaluate uncached flags via batch API if (uncachedFlags.length > 0) { try { const batchResponse = await this.graphqlClient.evaluateFlags(uncachedFlags, userContext); for (const [flagKey, response] of Object.entries(batchResponse.results)) { const result = { key: flagKey, value: response.value, reason: response.reason || 'api_evaluation', ruleId: response.rule_id, cacheHit: false, evaluatedAt: new Date() }; results[flagKey] = result; // Cache the result const cacheKey = this.generateCacheKey(flagKey, userContext); this.cache.set(cacheKey, result); } } catch (error) { this.logger.error('Batch flag evaluation failed', { error: error instanceof Error ? error.message : String(error) }); // Set error results for uncached flags for (const flagKey of uncachedFlags) { results[flagKey] = { key: flagKey, value: false, reason: 'error_fallback', cacheHit: false, evaluatedAt: new Date(), error: error instanceof Error ? error : new Error(String(error)) }; } } } return results; } /** * Evaluate a feature gate */ async evaluateGate(gateKey, userContext) { try { this.metrics.recordGateEvaluation(); // Validate inputs if (!gateKey || typeof gateKey !== 'string') { throw new errors_1.ConfigurationError('Gate key must be a non-empty string', 'gateKey'); } if (!userContext?.userId) { throw new errors_1.ConfigurationError('User context must include userId', 'userContext.userId'); } // Check cache first const cacheKey = this.generateGateCacheKey(gateKey, userContext); const cached = this.cache.get(cacheKey); if (cached) { this.metrics.recordCacheHit(); this.logger.debug('Gate evaluation cache hit', { gateKey, userId: userContext.userId }); return cached.value; } this.metrics.recordCacheMiss(); // Evaluate via GraphQL API const response = await this.graphqlClient.evaluateGate(gateKey, userContext); // Cache the result const result = { key: gateKey, value: response.value, reason: response.reason || 'api_evaluation', ruleId: response.rule_id, cacheHit: false, evaluatedAt: new Date() }; this.cache.set(cacheKey, result); this.logger.debug('Gate evaluation successful', { gateKey, enabled: response.value, userId: userContext.userId }); return response.value; } catch (error) { this.logger.error('Gate evaluation failed', { gateKey, error: error instanceof Error ? error.message : String(error), userId: userContext.userId }); return false; // Default to false for gates } } /** * Track an analytics event */ async track(event) { try { if (!this.config.enableAnalytics) { this.logger.debug('Analytics disabled, skipping event tracking'); return; } // Validate event if (!event.name || typeof event.name !== 'string') { throw new errors_1.ConfigurationError('Event name must be a non-empty string', 'event.name'); } if (!event.userId || typeof event.userId !== 'string') { throw new errors_1.ConfigurationError('Event userId must be a non-empty string', 'event.userId'); } // Ensure event has timestamp if (!event.timestamp) { event.timestamp = new Date(); } this.metrics.recordEventTracked(); await this.graphqlClient.trackEvent(event); this.logger.debug('Event tracked successfully', { eventName: event.name, userId: event.userId }); } catch (error) { this.logger.error('Event tracking failed', { eventName: event.name, userId: event.userId, error: error instanceof Error ? error.message : String(error) }); throw error; } } /** * Track multiple events in batch */ async trackBatch(events) { try { if (!this.config.enableAnalytics) { this.logger.debug('Analytics disabled, skipping batch event tracking'); return; } // Validate and prepare events const processedEvents = events.map(event => { if (!event.name || typeof event.name !== 'string') { throw new errors_1.ConfigurationError('Event name must be a non-empty string', 'event.name'); } if (!event.userId || typeof event.userId !== 'string') { throw new errors_1.ConfigurationError('Event userId must be a non-empty string', 'event.userId'); } return { ...event, timestamp: event.timestamp || new Date() }; }); events.forEach(() => this.metrics.recordEventTracked()); await this.graphqlClient.trackEvents(processedEvents); this.logger.debug('Batch events tracked successfully', { count: events.length }); } catch (error) { this.logger.error('Batch event tracking failed', { count: events.length, error: error instanceof Error ? error.message : String(error) }); throw error; } } /** * Clear the cache */ clearCache() { this.cache.clear(); this.logger.debug('Cache cleared'); } /** * Get SDK metrics */ getMetrics() { return this.metrics.getMetrics(); } /** * Destroy the client and clean up resources */ destroy() { this.cache.destroy(); this.logger.debug('Variably client destroyed'); } /** * Validate and normalize configuration */ validateAndNormalizeConfig(config) { if (!config.apiKey || typeof config.apiKey !== 'string') { throw new errors_1.ConfigurationError('API key is required', 'apiKey'); } return { apiKey: config.apiKey, baseUrl: config.baseUrl || 'https://graphql.variably.tech', environment: config.environment || 'development', timeout: config.timeout || 5000, retryAttempts: config.retryAttempts || 3, enableAnalytics: config.enableAnalytics !== false, cache: { ttl: config.cache?.ttl || 300000, // 5 minutes maxSize: config.cache?.maxSize || 1000, enabled: config.cache?.enabled !== false } }; } /** * Generate cache key for flag evaluation */ generateCacheKey(flagKey, userContext) { const contextHash = this.hashUserContext(userContext); return `flag:${flagKey}:${contextHash}`; } /** * Generate cache key for gate evaluation */ generateGateCacheKey(gateKey, userContext) { const contextHash = this.hashUserContext(userContext); return `gate:${gateKey}:${contextHash}`; } /** * Generate a simple hash of user context for caching */ hashUserContext(userContext) { const key = `${userContext.userId}:${JSON.stringify(userContext.attributes || {})}`; // Simple hash function - in production you might want something more robust let hash = 0; for (let i = 0; i < key.length; i++) { const char = key.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return hash.toString(36); } } exports.VariablyClient = VariablyClient; // Factory functions for convenience function createClient(config) { return new VariablyClient(config); } function createClientFromEnv() { const config = { apiKey: process.env.VARIABLY_API_KEY || '', baseUrl: process.env.VARIABLY_BASE_URL, environment: process.env.VARIABLY_ENVIRONMENT, timeout: process.env.VARIABLY_TIMEOUT ? parseInt(process.env.VARIABLY_TIMEOUT, 10) : undefined, enableAnalytics: process.env.VARIABLY_ENABLE_ANALYTICS !== 'false' }; return new VariablyClient(config); } //# sourceMappingURL=client.js.map