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