@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
JavaScript
/**
* 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