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

488 lines 15.7 kB
/** * GraphQL client for Variably API */ import { NetworkError, AuthenticationError, RateLimitError, TimeoutError, ValidationError } from './errors'; export class GraphQLClient { constructor(config, logger, metrics) { this.baseUrl = config.baseUrl.replace(/\/$/, ''); // Remove trailing slash this.apiKey = config.apiKey; this.timeout = config.timeout; this.retryAttempts = config.retryAttempts; this.logger = logger; this.metrics = metrics; } /** * Evaluate a single feature flag */ async evaluateFlag(flagKey, context) { const query = ` query SdkEvaluateFlag($input: SDKFlagEvaluationInput!) { sdkEvaluateFlag(input: $input) { flagKey value reason ruleId cacheHit evaluatedAt } } `; const variables = { input: { flagKey, context: this.convertUserContext(context) } }; const response = await this.executeQuery({ query, variables }); const result = response.sdkEvaluateFlag; return { flag_key: result.flagKey, value: result.value, reason: result.reason, rule_id: result.ruleId }; } /** * Evaluate multiple feature flags in batch */ async evaluateFlags(flagKeys, context) { const query = ` query SdkEvaluateFlags($input: SDKBatchFlagEvaluationInput!) { sdkEvaluateFlags(input: $input) { results { flagKey value reason ruleId cacheHit evaluatedAt } timestamp } } `; const variables = { input: { flagKeys, context: this.convertUserContext(context) } }; const response = await this.executeQuery({ query, variables }); const batchResult = response.sdkEvaluateFlags; const results = {}; for (const result of batchResult.results) { results[result.flagKey] = { flag_key: result.flagKey, value: result.value, reason: result.reason, rule_id: result.ruleId }; } return { results }; } /** * Evaluate a feature gate */ async evaluateGate(gateKey, context) { const query = ` query SdkEvaluateGate($input: SDKGateEvaluationInput!) { sdkEvaluateGate(input: $input) { gateKey value reason ruleId environment experimentId variantId isControl successMetrics cacheHit evaluatedAt } } `; const variables = { input: { gateKey, context: this.convertUserContext(context) } }; const response = await this.executeQuery({ query, variables }); const result = response.sdkEvaluateGate; return { gate_key: result.gateKey, value: result.value, reason: result.reason, rule_id: result.ruleId, environment: result.environment, experiment_id: result.experimentId, variant_id: result.variantId, is_control: result.isControl, success_metrics: result.successMetrics || [] }; } /** * Track a single event */ async trackEvent(event) { const mutation = ` mutation SdkTrackEvent($input: SDKEventInput!) { sdkTrackEvent(input: $input) } `; const variables = { input: { name: event.name, userId: event.userId, sessionId: event.sessionId, properties: event.properties, timestamp: event.timestamp || new Date() } }; await this.executeMutation({ query: mutation, variables }); } /** * Track multiple events in batch */ async trackEvents(events) { const mutation = ` mutation SdkTrackEvents($inputs: [SDKEventInput!]!) { sdkTrackEvents(inputs: $inputs) } `; const variables = { inputs: events.map(event => ({ name: event.name, userId: event.userId, sessionId: event.sessionId, properties: event.properties, timestamp: event.timestamp || new Date() })) }; await this.executeMutation({ query: mutation, variables }); } /** * Track experiment metric */ async trackExperimentMetric(experimentId, metric) { const mutation = ` mutation SdkTrackExperimentMetric($input: SDKExperimentMetricInput!) { sdkTrackExperimentMetric(input: $input) } `; const variables = { input: { experimentId, userId: metric.userId, metricKey: metric.metricKey, value: metric.value, metadata: metric.metadata, sessionId: metric.sessionId, timestamp: metric.timestamp || new Date() } }; await this.executeMutation({ query: mutation, variables }); } /** * Logout and blacklist current token */ async logout() { const mutation = ` mutation SdkLogout { sdkLogout { success message } } `; await this.executeMutation({ query: mutation }); } /** * Execute LLM prompt */ async executeLLMPrompt(messages, provider, model, temperature, maxTokens) { const mutation = ` mutation SdkExecuteLLMPrompt($input: SDKLLMExecutionInput!) { sdkExecuteLLMPrompt(input: $input) { content model provider tokenUsage { promptTokens completionTokens totalTokens estimatedCost } finishReason } } `; const variables = { input: { messages, provider, model, temperature: temperature || 0.7, maxTokens: maxTokens || 2000 } }; const response = await this.executeMutation({ query: mutation, variables }); return response.sdkExecuteLLMPrompt; } /** * Evaluate a prompt experiment - selects variant, executes LLM, and saves evaluation * This is the recommended method for prompt experiments as it saves evaluation data */ async evaluatePromptExperiment(experimentKey, context, inputVariables, metadata) { const mutation = ` mutation SdkEvaluatePromptExperiment($input: SDKPromptEvaluationInput!) { sdkEvaluatePromptExperiment(input: $input) { executionId experimentId variantUsed content model provider tokenUsage { promptTokens completionTokens totalTokens estimatedCost } latencyMs qualityScore } } `; const variables = { input: { experimentKey, context: this.convertUserContext(context), inputVariables: inputVariables || {}, metadata: metadata || {}, } }; const response = await this.executeMutation({ query: mutation, variables }); return response.sdkEvaluatePromptExperiment; } /** * Execute a GraphQL query */ async executeQuery(request) { return this.executeGraphQL(request); } /** * Execute a GraphQL mutation */ async executeMutation(request) { return this.executeGraphQL(request); } /** * Execute a GraphQL request with retry logic */ async executeGraphQL(request) { const url = `${this.baseUrl}/graphql`; let lastError; for (let attempt = 0; attempt <= this.retryAttempts; attempt++) { try { const startTime = Date.now(); const response = await this.makeRequest(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-API-Key': this.apiKey, 'User-Agent': 'Variably-JS-SDK/2.0.0' }, body: JSON.stringify(request) }); const duration = Date.now() - startTime; this.metrics.recordLatency(duration); this.metrics.recordApiCall(); if (response == null) { throw new Error('No response received from server'); } if (!response.ok) { const errorText = await response.text(); this.handleHttpError(response.status, url, errorText); } const result = await response.json(); // Handle GraphQL errors if (result.errors && result.errors.length > 0) { const error = result.errors[0]; throw new Error(`GraphQL Error: ${error.message}`); } if (!result.data) { throw new Error('GraphQL response missing data'); } this.logger.debug('GraphQL request successful', { operation: request.operationName || 'unknown', duration, attempt: attempt + 1 }); return result.data; } catch (error) { lastError = error; this.metrics.recordError(); this.logger.warn('GraphQL request failed', { operation: request.operationName || 'unknown', error: error instanceof Error ? error.message : String(error), attempt: attempt + 1 }); // Don't retry on certain errors if (!this.shouldRetry(error instanceof Error ? error : new Error(String(error)), attempt)) { break; } // Wait before retry with exponential backoff if (attempt < this.retryAttempts) { const delay = Math.min(1000 * Math.pow(2, attempt), 10000); await this.sleep(delay); } } } throw lastError || new Error('Unknown error occurred'); } /** * Execute the actual HTTP request */ async makeRequest(url, options) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = await fetch(url, { ...options, signal: controller.signal }); return response; } catch (error) { if (error instanceof Error && error.name === 'AbortError') { throw new TimeoutError(`Request timeout after ${this.timeout}ms`, 'graphql_request'); } throw error; } finally { clearTimeout(timeoutId); } } /** * Handle HTTP error responses */ handleHttpError(status, url, errorText) { let errorMessage; try { const errorData = JSON.parse(errorText); errorMessage = errorData.message || errorData.error || errorText; } catch { errorMessage = errorText || `HTTP ${status}`; } switch (status) { case 400: throw new ValidationError(`Bad Request: ${errorMessage}`, 'request'); case 401: throw new AuthenticationError(`Unauthorized: ${errorMessage}`); case 403: throw new AuthenticationError(`Forbidden: ${errorMessage}`); case 404: throw new NetworkError(`Not Found: ${errorMessage}`, status, url); case 429: const retryAfter = 60; // Default retry after 60 seconds throw new RateLimitError(`Rate limited: ${errorMessage}`, retryAfter); case 422: throw new ValidationError(`Validation Error: ${errorMessage}`, 'validation'); default: throw new NetworkError(`HTTP ${status}: ${errorMessage}`, status, url); } } /** * Determine if an error should trigger a retry */ shouldRetry(error, attempt) { if (attempt >= this.retryAttempts) { return false; } // Don't retry authentication errors if (error instanceof AuthenticationError) { return false; } // Don't retry validation errors if (error instanceof ValidationError) { return false; } // Retry network errors with retryable status codes if (error instanceof NetworkError) { return NetworkError.isRetryable(error.statusCode); } // Retry rate limit errors if (error instanceof RateLimitError) { return true; } // Retry timeout errors if (error instanceof TimeoutError) { return true; } // Retry other network-related errors if (error.message.includes('fetch') || error.message.includes('network')) { return true; } return false; } /** * Convert SDK UserContext to GraphQL input format */ convertUserContext(context) { return { userId: context.userId, email: context.email, country: context.country, language: context.language, platform: context.platform, version: context.version, ipAddress: context.ipAddress, userAgent: context.userAgent, sessionId: context.sessionId, attributes: context.attributes }; } /** * Track a success metric for an LLM prompt experiment * Uses the sdkTrackExperimentMetric GraphQL mutation */ async trackSuccessMetric(experimentId, metricName, metricKey, userId, sessionId, variantId, variantKey, value, metadata) { const mutation = ` mutation SdkTrackExperimentMetric($input: SDKExperimentMetricInput!) { sdkTrackExperimentMetric(input: $input) } `; const variables = { input: { experimentId, metricName, metricKey, userId, sessionId, variantId, variantKey, value: value !== undefined ? value : 1.0, metadata: metadata || {} } }; this.logger.debug('Tracking success metric via GraphQL', { experimentId, metricKey, userId, hasSessionId: !!sessionId, hasVariantId: !!variantId, }); await this.executeMutation({ query: mutation, variables }); this.logger.debug('Success metric tracked successfully'); } /** * Sleep for a given number of milliseconds */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } //# sourceMappingURL=graphql-client.js.map