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

944 lines 40.6 kB
/** * Variably JavaScript/TypeScript SDK Client */ import { GraphQLClient } from './graphql-client'; import { CacheManager } from './cache'; import { MetricsCollector } from './metrics'; import { ConsoleLogger } from './logger'; import { AutoTracker } from './auto-tracker'; import { StatisticalAnalysis } from './statistical-analysis'; import { RealTimeAlerting } from './alerting'; import { RealTimeGateSync } from './realtime-gate-sync'; import { ConfigurationError } from './errors'; export class VariablyClient { constructor(config) { this.currentUserContext = null; this.lastExperimentContext = null; this.experimentSuccessMetrics = new Set(); this.config = this.validateAndNormalizeConfig(config); this.logger = new ConsoleLogger('info'); this.metrics = new MetricsCollector(); this.cache = new CacheManager(this.config.cache, this.logger); this.graphqlClient = new GraphQLClient(this.config, this.logger, this.metrics); // Initialize auto-tracker if enabled if (this.config.enableAutoTracking) { this.autoTracker = new AutoTracker(this.config.autoTrackEvents, this.track.bind(this), this.trackMetric.bind(this), () => this.getCurrentUserContext()); // Auto-tracker will be started when user context is set via setUserContext() } // Initialize real-time alerting system this.alerting = new RealTimeAlerting(); // Initialize real-time gate synchronization if (this.config.realTimeUpdates?.enabled) { this.logger.info('Initializing real-time gate synchronization', { projectId: this.config.realTimeUpdates.projectId, baseUrl: this.config.baseUrl }); this.realTimeSync = new RealTimeGateSync(this.config, this.cache, this.logger); // Start real-time subscriptions automatically this.realTimeSync.start().catch(error => { this.logger.error('Failed to start real-time subscriptions', { error: error instanceof Error ? error.message : String(error), stack: error instanceof Error ? error.stack : undefined }); }); } this.logger.info('Variably client initialized', { environment: this.config.environment, baseUrl: this.config.baseUrl, autoTracking: this.config.enableAutoTracking, realTimeUpdates: this.config.realTimeUpdates?.enabled || false }); } /** * 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 ConfigurationError('Flag key must be a non-empty string', 'flagKey'); } if (!userContext?.userId) { throw new 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 }); // Track cached flag evaluation if (this.config.enableAnalytics) { try { await this.track({ name: 'feature_flag_evaluated', userId: userContext.userId, properties: { flag_key: flagKey, flag_value: cached.value, evaluation_reason: cached.reason, rule_id: cached.ruleId, cache_hit: true, timestamp: new Date().toISOString() } }); } catch (trackError) { this.logger.warn('Failed to track cached flag evaluation', { error: trackError instanceof Error ? trackError.message : String(trackError) }); } } 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 }); // Automatically track flag evaluation as an event if (this.config.enableAnalytics) { try { await this.track({ name: 'feature_flag_evaluated', userId: userContext.userId, properties: { flag_key: flagKey, flag_value: response.value, evaluation_reason: response.reason, rule_id: response.rule_id, cache_hit: false, timestamp: new Date().toISOString() } }); } catch (trackError) { // Don't fail the flag evaluation if tracking fails this.logger.warn('Failed to track flag evaluation event', { error: trackError instanceof Error ? trackError.message : String(trackError) }); } } 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 ConfigurationError('Gate key must be a non-empty string', 'gateKey'); } if (!userContext?.userId) { throw new 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 }); // Update auto-tracker with experiment context if (this.autoTracker && cached.experimentId) { this.autoTracker.updateExperimentContext(gateKey, cached.experimentId); } // Track cached gate evaluation if (this.config.enableAnalytics) { try { await this.track({ name: 'feature_gate_evaluated', userId: userContext.userId, properties: { gate_key: gateKey, gate_value: cached.value, evaluation_reason: cached.reason, rule_id: cached.ruleId, experiment_id: cached.experimentId, cache_hit: true, timestamp: new Date().toISOString() } }); } catch (trackError) { this.logger.warn('Failed to track cached gate evaluation', { error: trackError instanceof Error ? trackError.message : String(trackError) }); } } 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, experimentId: response.experiment_id, cacheHit: false, evaluatedAt: new Date() }; this.cache.set(cacheKey, result); this.logger.debug('Gate evaluation successful', { gateKey, enabled: response.value, userId: userContext.userId }); // Update auto-tracker with experiment context if (this.autoTracker && response.experiment_id) { this.autoTracker.updateExperimentContext(gateKey, response.experiment_id); } // Update success metrics if provided by the backend (independent of experiment context) if (response.success_metrics && Array.isArray(response.success_metrics)) { this.experimentSuccessMetrics.clear(); response.success_metrics.forEach(metric => this.experimentSuccessMetrics.add(metric)); this.logger.debug('Updated experiment success metrics from gate response', { gateKey, count: this.experimentSuccessMetrics.size, metrics: Array.from(this.experimentSuccessMetrics) }); } // Store experiment context for manual event tracking if (response.experiment_id && response.variant_id) { this.lastExperimentContext = { experimentId: response.experiment_id, variantId: response.variant_id }; } // Automatically track gate evaluation as an event if (this.config.enableAnalytics) { try { await this.track({ name: 'feature_gate_evaluated', userId: userContext.userId, properties: { gate_key: gateKey, gate_value: response.value, evaluation_reason: response.reason, rule_id: response.rule_id, cache_hit: false, timestamp: new Date().toISOString() } }); } catch (trackError) { // Don't fail the gate evaluation if tracking fails this.logger.warn('Failed to track gate evaluation event', { error: trackError instanceof Error ? trackError.message : String(trackError) }); } } 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 } } /** * Evaluate a feature gate and return full context including experiment information */ async evaluateGateWithContext(gateKey, userContext) { try { this.metrics.recordGateEvaluation(); // Validate inputs if (!gateKey || typeof gateKey !== 'string') { throw new ConfigurationError('Gate key must be a non-empty string', 'gateKey'); } if (!userContext?.userId) { throw new 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 }); // Update auto-tracker with experiment context if (this.autoTracker && cached.experimentId) { this.autoTracker.updateExperimentContext(gateKey, cached.experimentId); } // Track cached gate evaluation if (this.config.enableAnalytics) { try { await this.track({ name: 'feature_gate_evaluated', userId: userContext.userId, properties: { gate_key: gateKey, gate_value: cached.value, evaluation_reason: cached.reason, rule_id: cached.ruleId, experiment_id: cached.experimentId, variant_id: cached.variantId, cache_hit: true, timestamp: new Date().toISOString() } }); } catch (trackError) { this.logger.warn('Failed to track cached gate evaluation', { error: trackError instanceof Error ? trackError.message : String(trackError) }); } } // Return full context return { gate_key: gateKey, value: cached.value, reason: cached.reason, rule_id: cached.ruleId, experiment_id: cached.experimentId, variant_id: cached.variantId, environment: cached.environment || 'production' }; } this.metrics.recordCacheMiss(); // Evaluate via GraphQL API const response = await this.graphqlClient.evaluateGate(gateKey, userContext); // Cache the result with full context const result = { key: gateKey, value: response.value, reason: response.reason || 'api_evaluation', ruleId: response.rule_id, experimentId: response.experiment_id, variantId: response.variant_id, environment: response.environment, cacheHit: false, evaluatedAt: new Date() }; this.cache.set(cacheKey, result); this.logger.debug('Gate evaluation successful', { gateKey, enabled: response.value, experimentId: response.experiment_id, variantId: response.variant_id, userId: userContext.userId }); // Update auto-tracker with experiment context if (this.autoTracker && response.experiment_id) { this.autoTracker.updateExperimentContext(gateKey, response.experiment_id); } // Update success metrics if provided by the backend (independent of experiment context) if (response.success_metrics && Array.isArray(response.success_metrics)) { this.experimentSuccessMetrics.clear(); response.success_metrics.forEach(metric => this.experimentSuccessMetrics.add(metric)); this.logger.debug('Updated experiment success metrics from gate response', { gateKey, count: this.experimentSuccessMetrics.size, metrics: Array.from(this.experimentSuccessMetrics) }); } // Store experiment context for manual event tracking if (response.experiment_id && response.variant_id) { this.lastExperimentContext = { experimentId: response.experiment_id, variantId: response.variant_id }; } // Automatically track gate evaluation as an event if (this.config.enableAnalytics) { try { await this.track({ name: 'feature_gate_evaluated', userId: userContext.userId, properties: { gate_key: gateKey, gate_value: response.value, evaluation_reason: response.reason, rule_id: response.rule_id, cache_hit: false, timestamp: new Date().toISOString() } }); } catch (trackError) { // Don't fail the gate evaluation if tracking fails this.logger.warn('Failed to track gate evaluation event', { error: trackError instanceof Error ? trackError.message : String(trackError) }); } } // Return response with both snake_case and camelCase for compatibility return { gate_key: response.gate_key, value: response.value, reason: response.reason, rule_id: response.rule_id, environment: response.environment, // Snake_case fields (from API) experiment_id: response.experiment_id, variant_id: response.variant_id, is_control: response.is_control, success_metrics: response.success_metrics, // CamelCase fields (for frontend compatibility) experimentId: response.experiment_id, variantId: response.variant_id, isControl: response.is_control, successMetrics: response.success_metrics, successMetricsSource: 'graphql_api' }; } catch (error) { this.logger.error('Gate evaluation failed', { gateKey, error: error instanceof Error ? error.message : String(error), userId: userContext.userId }); // Throw the error instead of returning fallback throw error; } } /** * 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 ConfigurationError('Event name must be a non-empty string', 'event.name'); } if (!event.userId || typeof event.userId !== 'string') { throw new ConfigurationError('Event userId must be a non-empty string', 'event.userId'); } // Ensure event has timestamp if (!event.timestamp) { event.timestamp = new Date(); } // Intelligently add experiment context only for experiment success metrics if (this.shouldEnrichWithExperimentContext(event.name)) { event.properties = { ...event.properties, experiment_id: this.lastExperimentContext.experimentId, variant_id: this.lastExperimentContext.variantId }; this.logger.debug('Event enriched with experiment context', { eventName: event.name, experimentId: this.lastExperimentContext.experimentId, variantId: this.lastExperimentContext.variantId }); } 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 ConfigurationError('Event name must be a non-empty string', 'event.name'); } if (!event.userId || typeof event.userId !== 'string') { throw new ConfigurationError('Event userId must be a non-empty string', 'event.userId'); } // Intelligently add experiment context only for experiment success metrics let enrichedProperties = event.properties; if (this.shouldEnrichWithExperimentContext(event.name)) { enrichedProperties = { ...event.properties, experiment_id: this.lastExperimentContext.experimentId, variant_id: this.lastExperimentContext.variantId }; } return { ...event, properties: enrichedProperties, 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; } } /** * Track an experiment-specific metric */ async trackMetric(experimentId, metric) { try { if (!this.config.enableAnalytics) { this.logger.debug('Analytics disabled, skipping metric tracking'); return; } // Validate inputs if (!experimentId || typeof experimentId !== 'string') { throw new ConfigurationError('Experiment ID must be a non-empty string', 'experimentId'); } if (!metric.userId || typeof metric.userId !== 'string') { throw new ConfigurationError('Metric userId must be a non-empty string', 'metric.userId'); } if (!metric.metricKey || typeof metric.metricKey !== 'string') { throw new ConfigurationError('Metric key must be a non-empty string', 'metric.metricKey'); } if (typeof metric.value !== 'number') { throw new ConfigurationError('Metric value must be a number', 'metric.value'); } // Ensure metric has timestamp if (!metric.timestamp) { metric.timestamp = new Date(); } this.metrics.recordEventTracked(); // Convert experiment metric to regular event for consistent tracking const eventData = { name: metric.metricKey, userId: metric.userId, sessionId: metric.sessionId, properties: { ...metric.metadata, experiment_id: experimentId, metric_value: metric.value, metric_type: 'experiment_metric' }, timestamp: metric.timestamp }; // Use regular event tracking which handles experiment context properly await this.graphqlClient.trackEvent(eventData); this.logger.debug('Experiment metric tracked successfully', { experimentId, metricKey: metric.metricKey, userId: metric.userId, value: metric.value }); } catch (error) { this.logger.error('Experiment metric tracking failed', { experimentId, metricKey: metric.metricKey, userId: metric.userId, 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(); } /** * Calculate A/B test statistical significance */ calculateABTestSignificance(treatmentConversions, treatmentTotal, controlConversions, controlTotal, confidenceLevel = 0.95) { return StatisticalAnalysis.calculateABTestSignificance(treatmentConversions, treatmentTotal, controlConversions, controlTotal, confidenceLevel); } /** * Calculate required sample size for A/B test */ calculateRequiredSampleSize(baselineConversion, minimumDetectableEffect, power = 0.8, confidenceLevel = 0.95) { return StatisticalAnalysis.calculateRequiredSampleSize(baselineConversion, minimumDetectableEffect, power, confidenceLevel); } /** * Perform cohort analysis for user retention */ analyzeCohort(cohortUsers, userEvents, retentionEvent = 'session_start', periods = ['day_1', 'day_7', 'day_30', 'day_90']) { return StatisticalAnalysis.analyzeCohort(cohortUsers, userEvents, retentionEvent, periods); } /** * Analyze conversion funnel */ analyzeFunnel(funnelSteps, userEvents, timeWindow = 24 * 60 * 60 * 1000) { return StatisticalAnalysis.analyzeFunnel(funnelSteps, userEvents, timeWindow); } /** * Add metric threshold for real-time alerting */ addMetricThreshold(threshold) { if (this.alerting) { this.alerting.addThreshold(threshold); } } /** * Remove metric threshold */ removeMetricThreshold(thresholdId) { if (this.alerting) { this.alerting.removeThreshold(thresholdId); } } /** * Add alert channel for notifications */ addAlertChannel(channel) { if (this.alerting) { this.alerting.addChannel(channel); } } /** * Record a metric data point for real-time monitoring */ recordMetric(dataPoint) { if (this.alerting) { this.alerting.recordMetric(dataPoint); } } /** * Get alert history */ getAlertHistory(hours = 24) { return this.alerting?.getAlertHistory(hours) || []; } /** * Get current metric value */ getCurrentMetricValue(metricName) { return this.alerting?.getCurrentMetricValue(metricName); } /** * Get metric statistics over time window */ getMetricStats(metricName, timeWindow) { return this.alerting?.getMetricStats(metricName, timeWindow); } /** * Logout user and clear tokens */ async logout() { try { this.logger.debug('Starting logout process'); // Call backend logout endpoint to blacklist token await this.graphqlClient.logout(); // Clear local cache this.clearCache(); // Clear any stored tokens in local storage/session storage this.clearStoredTokens(); this.logger.info('Logout completed successfully'); } catch (error) { this.logger.error('Logout failed', { error: error instanceof Error ? error.message : String(error) }); // Even if backend logout fails, clear local tokens for security this.clearStoredTokens(); this.clearCache(); throw error; } } /** * Clear stored authentication tokens from local storage */ clearStoredTokens() { if (typeof window !== 'undefined') { // Clear common token storage locations localStorage.removeItem('variably_token'); localStorage.removeItem('variably_refresh_token'); localStorage.removeItem('auth_token'); localStorage.removeItem('access_token'); sessionStorage.removeItem('variably_token'); sessionStorage.removeItem('variably_refresh_token'); sessionStorage.removeItem('auth_token'); sessionStorage.removeItem('access_token'); this.logger.debug('Local tokens cleared'); } } /** * Initialize experiment success metrics - metrics will be populated dynamically from gate evaluation responses */ async loadExperimentSuccessMetrics() { try { // Initialize empty set - metrics will be populated from backend gate evaluation responses this.experimentSuccessMetrics.clear(); this.logger.debug('Initialized experiment success metrics set - will be populated from gate evaluations', { count: this.experimentSuccessMetrics.size }); } catch (error) { this.logger.error('Failed to initialize experiment success metrics', { error }); } } /** * Check if an event should be enriched with experiment context */ shouldEnrichWithExperimentContext(eventName) { return this.experimentSuccessMetrics.has(eventName) && this.lastExperimentContext !== null; } /** * Set current user context for auto-tracking */ setUserContext(userContext) { this.currentUserContext = userContext; // Load experiment success metrics when user context is set if (userContext) { this.loadExperimentSuccessMetrics(); } // If auto-tracker exists but isn't tracking yet, start it now that we have user context if (this.autoTracker && userContext?.userId && typeof window !== 'undefined') { if (!this.autoTracker.isActive) { this.autoTracker.start(); } } } /** * Get current user context */ getCurrentUserContext() { return this.currentUserContext; } /** * Destroy the client and clean up resources */ destroy() { if (this.autoTracker) { this.autoTracker.stop(); this.autoTracker = undefined; } if (this.alerting) { this.alerting.destroy(); this.alerting = undefined; } this.cache.destroy(); this.logger.debug('Variably client destroyed'); } /** * Validate and normalize configuration */ validateAndNormalizeConfig(config) { if (!config.apiKey || typeof config.apiKey !== 'string') { throw new 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, enableAutoTracking: config.enableAutoTracking || false, autoTrackEvents: { pageViews: config.autoTrackEvents?.pageViews !== false, clicks: config.autoTrackEvents?.clicks !== false, scrollDepth: config.autoTrackEvents?.scrollDepth !== false, sessionDuration: config.autoTrackEvents?.sessionDuration !== false, formSubmissions: config.autoTrackEvents?.formSubmissions || false, clickSelectors: config.autoTrackEvents?.clickSelectors || ['button', 'a', '[data-track]'], excludeSelectors: config.autoTrackEvents?.excludeSelectors || ['.no-track', '[data-no-track]'], minSessionDuration: config.autoTrackEvents?.minSessionDuration || 30000, scrollThresholds: config.autoTrackEvents?.scrollThresholds || [25, 50, 75, 90], conversionTracking: { enabled: config.autoTrackEvents?.conversionTracking?.enabled || false, conversionEvents: config.autoTrackEvents?.conversionTracking?.conversionEvents || [], dataExtraction: { trackPosition: config.autoTrackEvents?.conversionTracking?.dataExtraction?.trackPosition || false, trackLayoutType: config.autoTrackEvents?.conversionTracking?.dataExtraction?.trackLayoutType || false, layoutSelectors: config.autoTrackEvents?.conversionTracking?.dataExtraction?.layoutSelectors || {} } } }, cache: { ttl: config.cache?.ttl || 300000, // 5 minutes maxSize: config.cache?.maxSize || 1000, enabled: config.cache?.enabled !== false }, realTimeUpdates: { enabled: config.realTimeUpdates?.enabled || false, projectId: config.realTimeUpdates?.projectId || '', autoInvalidateCache: config.realTimeUpdates?.autoInvalidateCache !== 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); } } // Factory functions for convenience export function createClient(config) { return new VariablyClient(config); } export 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