@varia-bly/variably-sdk
Version:
Official JavaScript/TypeScript SDK for Variably feature flags, experimentation, and real-time dynamic configurations
373 lines • 14.6 kB
JavaScript
"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