UNPKG

@varia-bly/variably-sdk

Version:

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

347 lines 11.5 kB
"use strict"; /** * GraphQL client for Variably API */ Object.defineProperty(exports, "__esModule", { value: true }); exports.GraphQLClient = void 0; const errors_1 = require("./errors"); 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 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 }; } /** * 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 }); } /** * 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', 'Authorization': `Bearer ${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 errors_1.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 errors_1.ValidationError(`Bad Request: ${errorMessage}`, 'request'); case 401: throw new errors_1.AuthenticationError(`Unauthorized: ${errorMessage}`); case 403: throw new errors_1.AuthenticationError(`Forbidden: ${errorMessage}`); case 404: throw new errors_1.NetworkError(`Not Found: ${errorMessage}`, status, url); case 429: const retryAfter = 60; // Default retry after 60 seconds throw new errors_1.RateLimitError(`Rate limited: ${errorMessage}`, retryAfter); case 422: throw new errors_1.ValidationError(`Validation Error: ${errorMessage}`, 'validation'); default: throw new errors_1.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 errors_1.AuthenticationError) { return false; } // Don't retry validation errors if (error instanceof errors_1.ValidationError) { return false; } // Retry network errors with retryable status codes if (error instanceof errors_1.NetworkError) { return errors_1.NetworkError.isRetryable(error.statusCode); } // Retry rate limit errors if (error instanceof errors_1.RateLimitError) { return true; } // Retry timeout errors if (error instanceof errors_1.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 }; } /** * Sleep for a given number of milliseconds */ sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } } exports.GraphQLClient = GraphQLClient; //# sourceMappingURL=graphql-client.js.map