Implement new typed interface

This commit is contained in:
Todd Baert 2022-03-28 12:30:06 -04:00
parent df0904d3eb
commit be8a5de12e
No known key found for this signature in database
GPG Key ID: 6832CDB677D5E06D
31 changed files with 11554 additions and 230 deletions

View File

@ -5,10 +5,10 @@
###############################################
# Options: true, false
new-welcome-message=false
NEW_WELCOME_MESSAGE=false
# Options: recursive, memo, loop, binet, default
fib-algo=default
FIB_ALGO=default
###############################################
##

11089
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,7 @@
"@opentelemetry/exporter-zipkin": "~1.0.0",
"@opentelemetry/sdk-node": "^0.27.0",
"@opentelemetry/sdk-trace-node": "~1.0.0",
"change-case": "^4.1.2",
"express": "4.17.2",
"express-validator": "^6.14.0",
"launchdarkly-node-server-sdk": "^6.4.0",

View File

@ -12,7 +12,7 @@ const app = express();
const oFeatClient = openfeature.getClient('api');
app.get('/api', async (req, res) => {
const message = (await oFeatClient.getVariation('new-welcome-message')).boolValue
const message = await oFeatClient.getBooleanValue('new-welcome-message', false)
? 'Welcome to the next gen api!'
: 'Welcome to the api!';
res.send({ message });

View File

@ -3,13 +3,19 @@
"sourceRoot": "packages/fibonacci/src",
"targets": {
"build": {
"executor": "@nrwl/js:tsc",
"executor": "@nrwl/node:webpack",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/packages/fibonacci",
"main": "packages/fibonacci/src/index.ts",
"tsConfig": "packages/fibonacci/tsconfig.lib.json",
"assets": ["packages/fibonacci/*.md"]
"tsConfig": "packages/fibonacci/tsconfig.lib.json"
},
"configurations": {
"production": {
"optimization": true,
"extractLicenses": true,
"inspect": false
}
}
},
"lint": {

View File

@ -3,13 +3,13 @@ import { openfeature } from '@openfeature/openfeature-js';
const oFeatClient = openfeature.getClient('fibonacci');
export async function fibonacci(num: number): Promise<number> {
const variation = await oFeatClient.getVariation('fib-algo');
const value = await oFeatClient.getStringValue('fib-algo', 'recursive');
/**
* TODO: See if variations should return OTel methods that allow developers to
* define logs, metrics, and events related to the flag. It could be useful
* here to determine the impact the algorithm has on performance.
*/
switch (variation.stringValue) {
switch (value) {
case 'recursive':
console.log('Running the recursive fibonacci function');
return getNthFibRecursive(num);

View File

@ -2,8 +2,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": []
"declaration": true
},
"include": ["**/*.ts"],
"exclude": ["**/*.spec.ts", "**/*.test.ts"]

View File

@ -3,13 +3,19 @@
"sourceRoot": "packages/js-cloudbees-provider/src",
"targets": {
"build": {
"executor": "@nrwl/js:tsc",
"executor": "@nrwl/node:webpack",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/packages/js-cloudbees-provider",
"main": "packages/js-cloudbees-provider/src/index.ts",
"tsConfig": "packages/js-cloudbees-provider/tsconfig.lib.json",
"assets": ["packages/js-cloudbees-provider/*.md"]
"tsConfig": "packages/js-cloudbees-provider/tsconfig.lib.json"
},
"configurations": {
"production": {
"optimization": true,
"extractLicenses": true,
"inspect": false
}
}
},
"lint": {

View File

@ -1,47 +1,58 @@
import {
FeatureProvider,
FlagEvaluationRequest,
FlagEvaluationVariationResponse,
Context,
FeatureProvider, parseValidJsonObject
} from '@openfeature/openfeature-js';
import * as Rox from 'rox-node';
export class CloudbeesProvider implements FeatureProvider {
name = 'cloudbees';
private initialized: Promise<void>;
constructor(private readonly appKey: string) {
Rox.setup(appKey, {}).then(() => {
console.log(`CloudBees Provider initialised: appKey ${appKey}`)
constructor(readonly appKey: string) {
// we don't expose any init events at the moment (we might later) so for now, lets create a private
// promise to await into before we evaluate any flags.
this.initialized = new Promise((resolve) => {
Rox.setup(appKey, {}).then(() => {
console.log(`CloudBees Provider initialized: appKey ${appKey}`);
resolve();
});
});
}
async evaluateFlag(
request: FlagEvaluationRequest
): Promise<FlagEvaluationVariationResponse> {
/**
* CloudBees Feature management uses different methods to distinguish between different types of flag.
* See https://docs.cloudbees.com/docs/cloudbees-feature-management/latest/feature-flags/dynamic-api:
* * Boolean flag value: Rox.dynamicApi.isEnabled
* * String flag value: Rox.dynamicApi.value
* * Number flag value: Rox.dynamicApi.getNumber
**/
/**
* CloudBees Feature Management also defines a default value for a flag in code.
* This default value is returned by the SDK if the flag is not enabled ('targeting' is off)
* See https://docs.cloudbees.com/docs/cloudbees-feature-management/latest/feature-flags/flag-default-values
**/
// This assumes a boolean flag. Should we include the flag type in the request?
const value = Rox.dynamicApi.isEnabled(request.flagId, false, request.context)
console.log(`${this.name} flag '${request.flagId}' has a value of '${value}'`);
return {
enabled: true, // Cloudbees will return default values if the flag is disabled, so from a caller's perspective it is always enabled
// Callers should not care if a flag is enabled or not
boolValue: value,
// stringValue: value.toString(),
// numberValue: value,
}
/**
* CloudBees Feature Management also defines a default value for a flag in code.
* This default value is returned by the SDK if the flag is not enabled ('targeting' is off)
* See https://docs.cloudbees.com/docs/cloudbees-feature-management/latest/feature-flags/flag-default-values
**/
async isEnabled(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
// for CloudBees Feature Management, isEnabled is functionally equal to getBooleanValue.
return this.getBooleanValue(flagId, defaultValue, context);
}
}
async getBooleanValue(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
await this.initialized;
return Rox.dynamicApi.isEnabled(flagId, defaultValue, context);
}
async getStringValue(flagId: string, defaultValue: string, context?: Context): Promise<string> {
await this.initialized;
return Rox.dynamicApi.value(flagId, defaultValue, context);
}
async getNumberValue(flagId: string, defaultValue: number, context?: Context): Promise<number> {
await this.initialized;
return Rox.dynamicApi.getNumber(flagId, defaultValue, context);
}
async getObjectValue<T extends object>(flagId: string, defaultValue: T, context?: Context): Promise<T> {
await this.initialized;
/**
* NOTE: objects are not supported in Cloudbees Feature Management, for demo purposes, we use the string API,
* and stringify the default.
* This may not be performant, and other, more elegant solutions should be considered.
*/
const value = Rox.dynamicApi.value(flagId, JSON.stringify(defaultValue), context);
return parseValidJsonObject(value);
}
}

View File

@ -2,8 +2,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": []
"declaration": true
},
"include": ["**/*.ts"],
"exclude": ["**/*.spec.ts", "**/*.test.ts"]

View File

@ -3,13 +3,19 @@
"sourceRoot": "packages/js-env-provider/src",
"targets": {
"build": {
"executor": "@nrwl/js:tsc",
"executor": "@nrwl/node:webpack",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/packages/js-env-provider",
"main": "packages/js-env-provider/src/index.ts",
"tsConfig": "packages/js-env-provider/tsconfig.lib.json",
"assets": ["packages/js-env-provider/*.md"]
"tsConfig": "packages/js-env-provider/tsconfig.lib.json"
},
"configurations": {
"production": {
"optimization": true,
"extractLicenses": true,
"inspect": false
}
}
},
"lint": {

View File

@ -1,23 +1,64 @@
import {
FeatureProvider,
FlagEvaluationRequest,
FlagEvaluationVariationResponse,
Context,
FeatureProvider, FlagTypeError,
FlagValueParseError,
parseValidBoolean,
parseValidNumber
} from '@openfeature/openfeature-js';
import { constantCase } from 'change-case';
export class OpenFeatureEnvProvider implements FeatureProvider {
name = 'environment variable';
async evaluateFlag(
request: FlagEvaluationRequest
): Promise<FlagEvaluationVariationResponse> {
console.log(`${this.name}: evaluation flag`);
const flagValue = process.env[request.flagId];
name =' environment variable';
isEnabled(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
return this.getBooleanValue(flagId, defaultValue, context);
}
console.log(`Flag '${request.flagId}' has a value of '${flagValue}'`);
return {
enabled: !!flagValue,
boolValue:
typeof flagValue === 'string' && flagValue.toLowerCase() === 'true',
};
getBooleanValue(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
const stringValue = this.getVarValue(flagId);
if (stringValue) {
return Promise.resolve(parseValidBoolean(stringValue));
} else {
throw new FlagTypeError(`Error resolving ${flagId} from environment`);
}
}
getStringValue(flagId: string, defaultValue: string, context?: Context): Promise<string> {
const stringValue = this.getVarValue(flagId);
if (stringValue) {
return Promise.resolve(stringValue);
} else {
throw new FlagTypeError(`Error resolving ${flagId} from environment`);
}
}
getNumberValue(flagId: string, defaultValue: number, context?: Context): Promise<number> {
const stringValue = this.getVarValue(flagId);
if (stringValue) {
return Promise.resolve(parseValidNumber(stringValue));
} else {
throw new FlagTypeError(`Error resolving ${flagId} from environment`);
}
}
getObjectValue<T extends object>(flagId: string, defaultValue: T, context?: Context): Promise<T> {
const stringValue = this.getVarValue(flagId);
if (stringValue) {
try {
const parsed = JSON.parse(stringValue);
return Promise.resolve(parsed);
} catch (err) {
throw new FlagValueParseError(`Error parsing ${flagId}`);
}
} else {
throw new FlagTypeError(`Error resolving ${flagId} from environment`);
}
}
getVarValue(key: string): string | undefined {
// convert key to ENV_VAR style casing
const envVarCaseKey = constantCase(key);
return process.env[envVarCaseKey];
}
}

View File

@ -3,13 +3,19 @@
"sourceRoot": "packages/js-launchdarkly-provider/src",
"targets": {
"build": {
"executor": "@nrwl/js:tsc",
"executor": "@nrwl/node:webpack",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/packages/js-launchdarkly-provider",
"main": "packages/js-launchdarkly-provider/src/index.ts",
"tsConfig": "packages/js-launchdarkly-provider/tsconfig.lib.json",
"assets": ["packages/js-launchdarkly-provider/*.md"]
"tsConfig": "packages/js-launchdarkly-provider/tsconfig.lib.json"
},
"configurations": {
"production": {
"optimization": true,
"extractLicenses": true,
"inspect": false
}
}
},
"lint": {

View File

@ -1,9 +1,10 @@
import {
Context,
FeatureProvider,
FlagEvaluationRequest,
FlagEvaluationVariationResponse,
FlagTypeError,
FlagValueParseError
} from '@openfeature/openfeature-js';
import { LDClient, init } from 'launchdarkly-node-server-sdk';
import { init, LDClient } from 'launchdarkly-node-server-sdk';
/**
* A comically primitive LaunchDarkly provider demo
@ -11,7 +12,7 @@ import { LDClient, init } from 'launchdarkly-node-server-sdk';
export class OpenFeatureLaunchDarklyProvider implements FeatureProvider {
name = 'LaunchDarkly';
private client: LDClient;
private initialized: Promise<boolean>;
private initialized: Promise<void>;
constructor(sdkKey: string) {
@ -22,28 +23,76 @@ export class OpenFeatureLaunchDarklyProvider implements FeatureProvider {
this.initialized = new Promise((resolve) => {
this.client.once('ready', () => {
console.log(`${this.name}: initialization complete.`);
resolve(true);
resolve();
});
});
}
isEnabled(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
return this.getBooleanValue(flagId, defaultValue, context);
}
async evaluateFlag(
request: FlagEvaluationRequest
): Promise<FlagEvaluationVariationResponse> {
console.log(`${this.name}: evaluation flag`);
async getBooleanValue(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
const value = await this.evaluateFlag(flagId, defaultValue, context);
if (typeof value === 'boolean') {
return value;
} else {
throw new FlagTypeError(this.getFlagTypeErrorMessage(flagId, value, 'boolean'));
}
}
async getStringValue(flagId: string, defaultValue: string, context?: Context): Promise<string> {
const value = await this.evaluateFlag(flagId, defaultValue, context);
if (typeof value === 'string') {
return value;
} else {
throw new FlagTypeError(this.getFlagTypeErrorMessage(flagId, value, 'string'));
}
}
async getNumberValue(flagId: string, defaultValue: number, context?: Context): Promise<number> {
const value = await this.evaluateFlag(flagId, defaultValue, context);
if (typeof value === 'number') {
return value;
} else {
throw new FlagTypeError(this.getFlagTypeErrorMessage(flagId, value, 'number'));
}
}
/**
* NOTE: objects are not supported in Launch Darkly, for demo purposes, we use the string API,
* and stringify the default.
* This may not be performant, and other, more elegant solutions should be considered.
*/
async getObjectValue<T extends object>(flagId: string, defaultValue: T, context?: Context, ): Promise<T> {
const value = await this.evaluateFlag(flagId, JSON.stringify(defaultValue), context);
if (typeof value === 'string') {
// we may want to allow the parsing to be customized.
try {
return JSON.parse(value);
} catch (err) {
throw new FlagValueParseError(`Error parsing flag value for ${flagId}`);
}
} else {
throw new FlagTypeError(this.getFlagTypeErrorMessage(flagId, value, 'object'));
}
}
private getFlagTypeErrorMessage(flagId: string, value: unknown, expectedType: string) {
return `Flag value ${flagId} had unexpected type ${typeof value}, expected ${expectedType}.`;
}
// LD values can be boolean, number, or string: https://docs.launchdarkly.com/sdk/client-side/node-js#getting-started
private async evaluateFlag(flagId: string, defaultValue: boolean | string | number | object, context?: Context): Promise<boolean | number | string> {
// await the initialization before actually calling for a flag.
await this.initialized;
const userKey = request.context.userId ?? 'anonymous';
const flagValue = await this.client.variation(request.flagId, { key: userKey}, false);
// eventually we'll want a well-defined SDK context object, whose properties will be mapped appropriately to each provider.
const userKey = context?.userId ?? 'anonymous';
const flagValue = await this.client.variation(flagId, { key: userKey}, defaultValue);
console.log(`Flag '${request.flagId}' has a value of '${flagValue}'`);
return {
enabled: !!flagValue,
boolValue: !!flagValue,
stringValue: flagValue.toString()
};
console.log(`Flag '${flagId}' has a value of '${flagValue}'`);
return flagValue;
}
}

View File

@ -3,13 +3,19 @@
"sourceRoot": "packages/js-split-provider/src",
"targets": {
"build": {
"executor": "@nrwl/js:tsc",
"executor": "@nrwl/node:webpack",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/packages/js-split-provider",
"main": "packages/js-split-provider/src/index.ts",
"tsConfig": "packages/js-split-provider/tsconfig.lib.json",
"assets": ["packages/js-split-provider/*.md"]
"tsConfig": "packages/js-split-provider/tsconfig.lib.json"
},
"configurations": {
"production": {
"optimization": true,
"extractLicenses": true,
"inspect": false
}
}
},
"lint": {

View File

@ -1,37 +1,71 @@
import {
FeatureProvider,
FlagEvaluationRequest,
FlagEvaluationVariationResponse,
Context,
FeatureProvider, FlagTypeError,
parseValidJsonObject,
parseValidNumber
} from '@openfeature/openfeature-js';
import type { IClient } from '@splitsoftware/splitio/types/splitio';
export class OpenFeatureSplitProvider implements FeatureProvider {
name = 'split';
private initialized: Promise<void>;
constructor(private readonly client: IClient) {}
constructor(private readonly client: IClient) {
// we don't expose any init events at the moment (we might later) so for now, lets create a private
// promise to await into before we evaluate any flags.
this.initialized = new Promise((resolve) => {
client.on(client.Event.SDK_READY, () => {
console.log(`Split Provider initialized`);
resolve();
});
});
}
async isEnabled(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
return this.getBooleanValue(flagId, defaultValue, context);
}
async evaluateFlag(
request: FlagEvaluationRequest
): Promise<FlagEvaluationVariationResponse> {
console.log(`${this.name}: evaluation flag`);
/**
* Split doesn't directly handle booleans as treatment values.
* It will be up to the provider author and it's users to come up with conventions for converting strings to booleans.
*/
async getBooleanValue(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
await this.initialized;
const stringValue = this.client.getTreatment(context?.userId ?? 'anonymous', flagId);
const asUnknown = stringValue as unknown;
const flagValue = this.client.getTreatment(
request.context.userId ?? 'anonymous',
request.flagId
);
switch (asUnknown) {
case 'on':
return true;
case 'off':
return false;
case 'true':
return true;
case 'false':
return false;
case true:
return true;
case false:
return false;
default:
throw new FlagTypeError(`Invalid boolean value for ${asUnknown}`)
}
}
console.log(`Flag '${request.flagId}' has a value of '${flagValue}'`);
/**
* Split uses strings for treatment values. On and off are default but the
* values can be changed. "control" is a reserved treatment value and means
* something went wrong.
*/
return {
enabled: !!flagValue,
boolValue:
typeof flagValue === 'string' &&
!['off', 'control'].includes(flagValue.toLowerCase()),
stringValue: flagValue,
};
async getStringValue(flagId: string, defaultValue: string, context?: Context): Promise<string> {
await this.initialized;
return this.client.getTreatment(context?.userId ?? 'anonymous', flagId);
}
async getNumberValue(flagId: string, defaultValue: number, context?: Context): Promise<number> {
await this.initialized;
const value = this.client.getTreatment(context?.userId ?? 'anonymous', flagId);
return parseValidNumber(value);
}
async getObjectValue<T extends object>(flagId: string, defaultValue: T, context?: Context): Promise<T> {
await this.initialized;
const value = this.client.getTreatment(context?.userId ?? 'anonymous', flagId);
return parseValidJsonObject(value);
}
}

View File

@ -2,8 +2,7 @@
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": []
"declaration": true
},
"include": ["**/*.ts"],
"exclude": ["**/*.spec.ts", "**/*.test.ts"]

View File

@ -3,13 +3,19 @@
"sourceRoot": "packages/openfeature-js/src",
"targets": {
"build": {
"executor": "@nrwl/js:tsc",
"executor": "@nrwl/node:webpack",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/packages/openfeature-js",
"main": "packages/openfeature-js/src/index.ts",
"tsConfig": "packages/openfeature-js/tsconfig.lib.json",
"assets": ["packages/openfeature-js/*.md"]
"tsConfig": "packages/openfeature-js/tsconfig.lib.json"
},
"configurations": {
"production": {
"optimization": true,
"extractLicenses": true,
"inspect": false
}
}
},
"lint": {

View File

@ -1,5 +1,7 @@
import { OpenFeatureAPI } from './lib/api';
export type { OpenFeatureAPI } from './lib/api';
export * from "./lib/types";
export * from './lib/types';
export * from './lib/errors';
export * from './lib/utils';
export const openfeature = OpenFeatureAPI.getInstance();

View File

@ -1,6 +1,6 @@
import { OpenFeatureClient } from './client';
import { getGlobal, registerGlobal } from './global';
import { Feature, FeatureProvider } from './types';
import { Features, FeatureProvider } from './types';
export class OpenFeatureAPI {
private provider?: FeatureProvider;
@ -16,7 +16,7 @@ export class OpenFeatureAPI {
return instance;
}
public getClient(name: string, version?: string): Feature {
public getClient(name?: string, version?: string): Features {
return new OpenFeatureClient(this, { name, version });
}

View File

@ -1,21 +1,18 @@
import { OpenFeatureAPI } from './api';
import {
FeatureProvider,
Context,
FlagEvaluationVariationResponse,
Feature,
} from './types';
import { NOOP_FEATURE_PROVIDER } from './noop-provider';
import { Span, trace, Tracer } from '@opentelemetry/api';
import { OpenFeatureAPI } from './api';
import { NOOP_FEATURE_PROVIDER } from './noop-provider';
import { SpanProperties } from './span-properties';
import {
Context, FeatureProvider, Features, FlagType
} from './types';
type OpenFeatureClientOptions = {
name: string;
name?: string;
version?: string;
};
export class OpenFeatureClient implements Feature {
private _name: string;
export class OpenFeatureClient implements Features {
private _name: string | undefined;
private _version?: string;
private _trace: Tracer;
@ -29,49 +26,64 @@ export class OpenFeatureClient implements Feature {
this._trace = trace.getTracer(OpenFeatureClient.name);
}
// TODO: see if a default callback makes sense here
async isEnabled(id: string, context?: Context): Promise<boolean> {
return (await this.evaluateFlag('is_enabled', id, context)).enabled;
isEnabled(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
return this.getBooleanValue(flagId, defaultValue, context);
}
async getVariation(
id: string,
context?: Context
): Promise<FlagEvaluationVariationResponse> {
return this.evaluateFlag('variation', id, context);
getBooleanValue(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
return this.evaluateFlag('boolean', flagId, defaultValue, context);
}
private async evaluateFlag(
type: 'is_enabled' | 'variation',
getStringValue(flagId: string, defaultValue: string, context?: Context): Promise<string> {
return this.evaluateFlag('string', flagId, defaultValue, context);
}
getNumberValue(flagId: string, defaultValue: number, context?: Context): Promise<number> {
return this.evaluateFlag('number', flagId, defaultValue, context);
}
getObjectValue<T extends object>(flagId: string, defaultValue: T, context?: Context): Promise<T> {
return this.evaluateFlag('json', flagId, defaultValue, context);
}
private async evaluateFlag<T extends boolean | string | number | object>(
type: FlagType,
id: string,
defaultValue: T,
context?: Context
) {
): Promise<T> {
const span = this.startSpan(`feature flag - ${type}`);
try {
span.setAttribute(SpanProperties.FEATURE_FLAG_ID, id);
const response = await this.getProvider().evaluateFlag({
flagId: id,
context: context ?? {},
clientName: this._name,
clientVersion: this._version,
});
span.setAttribute(SpanProperties.FEATURE_FLAG_ENABLED, response.enabled);
if (response.stringValue) {
span.setAttribute(
SpanProperties.FEATURE_FLAG_VARIATION_STRING,
response.stringValue
);
const provider = this.getProvider();
let valuePromise: boolean | string | number | object;
switch(type) {
case 'boolean': {
valuePromise = provider.getBooleanValue(id, defaultValue as boolean, context);
break;
}
case 'string': {
valuePromise = provider.getStringValue(id, defaultValue as string, context);
break;
}
case 'number': {
valuePromise = provider.getNumberValue(id, defaultValue as number, context);
break;
}
case 'json': {
valuePromise = provider.getObjectValue(id, defaultValue as object, context);
break;
}
}
return response;
const value = await valuePromise;
span.setAttribute(SpanProperties.FEATURE_FLAG_VALUE, value.toString());
return value as T;
} catch (err) {
console.error(err);
const enabled = false;
span.setAttribute(SpanProperties.FEATURE_FLAG_ENABLED, enabled);
return { enabled };
span.setAttribute(SpanProperties.FEATURE_FLAG_VALUE, defaultValue.toString());
return defaultValue;
} finally {
span.end();
}

View File

@ -0,0 +1,27 @@
export enum ErrorCodes {
GeneralError = 'GENERAL_ERROR',
FlagTypeError = 'FLAG_TYPE_ERROR',
FlagValueParseError = 'FLAG_VALUE_PARSE_ERROR'
};;
export abstract class OpenFeatureError extends Error {
abstract code: ErrorCodes;
}
export class FlagTypeError extends OpenFeatureError {
code: ErrorCodes;
constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, FlagTypeError.prototype);
this.code = ErrorCodes.FlagTypeError;
}
}
export class FlagValueParseError extends OpenFeatureError {
code: ErrorCodes;
constructor(message?: string) {
super(message);
Object.setPrototypeOf(this, FlagTypeError.prototype);
this.code = ErrorCodes.FlagValueParseError;
}
}

View File

@ -1,10 +1,27 @@
import { FeatureProvider, FlagEvaluationResponse } from './types';
import { Context, FeatureProvider } from './types';
class NoopFeatureProvider implements FeatureProvider {
name = 'No-op Provider';
async evaluateFlag(): Promise<FlagEvaluationResponse> {
return { enabled: false };
constructor() {
console.warn(`No provider configured. Falling back to ${this.name}.`);
}
readonly name = 'No-op Provider';
isEnabled(id: string, defaultValue: boolean, context?: Context): Promise<boolean> {
return Promise.resolve(defaultValue);
}
getBooleanValue(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean> {
return Promise.resolve(defaultValue);
}
getStringValue(flagId: string, defaultValue: string, context?: Context): Promise<string> {
return Promise.resolve(defaultValue);
}
getNumberValue(flagId: string, defaultValue: number, context?: Context): Promise<number> {
return Promise.resolve(defaultValue);
}
getObjectValue<T extends object>(flagId: string, defaultValue: T, context?: Context): Promise<T> {
return Promise.resolve(defaultValue);
}
}

View File

@ -3,6 +3,5 @@ export const SpanProperties = {
FEATURE_FLAG_CLIENT_VERSION: 'feature_flag_client_version',
FEATURE_FLAG_SERVICE: 'feature_flag_service',
FEATURE_FLAG_ID: 'feature_flag_id',
FEATURE_FLAG_ENABLED: 'feature_flag_enabled',
FEATURE_FLAG_VARIATION_STRING: 'feature_flag_variation_string',
FEATURE_FLAG_VALUE: 'feature_flag_variation_string',
};

View File

@ -2,34 +2,52 @@ export type Context = {
userId?: string;
};
export interface Feature {
isEnabled(id: string, context?: Context): Promise<boolean>;
getVariation(
id: string,
context?: Context
): Promise<FlagEvaluationVariationResponse>;
export type FlagType = 'boolean' | 'string' | 'number' | 'json';
/**
* This interface is common to both Providers and the SDK presented to the Application Author.
* This is incidental, and may not continue to be the case, especially as additional configuration is provided.
*/
export interface Features {
/**
* Get a boolean flag value.
*
* NOTE: In some providers this has distinct behavior from getBooleanValue
*/
isEnabled(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean>;
/**
* Get a boolean flag value.
*/
getBooleanValue(flagId: string, defaultValue: boolean, context?: Context): Promise<boolean>;
/**
* Get a string flag value.
*/
getStringValue(flagId: string, defaultValue: string, context?: Context): Promise<string>;
/**
* Get a number flag value.
*/
getNumberValue(flagId: string, defaultValue: number, context?: Context): Promise<number>;
/**
* Get a object (JSON) flag value.
*/
getObjectValue<T extends object>(flagId: string, defaultValue: T, context?: Context): Promise<T>;
}
export interface FeatureProvider extends Features {
name: string;
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Client extends Features {}
export type FlagEvaluationRequest = {
clientName: string;
clientVersion?: string;
flagId: string;
context: Context;
};
export type FlagEvaluationResponse = {
enabled: boolean;
};
export type FlagEvaluationVariationResponse = FlagEvaluationResponse & {
stringValue?: string;
boolValue?: boolean;
numberValue?: number;
};
export interface FeatureProvider {
name: string;
evaluateFlag(
request: FlagEvaluationRequest
): Promise<FlagEvaluationVariationResponse>;
}

View File

@ -0,0 +1,41 @@
import { FlagTypeError, FlagValueParseError } from './errors';
export const parseValidNumber = (stringValue: string) => {
const result = Number.parseFloat(stringValue);
if (Number.isNaN(result)) {
throw new FlagTypeError(`Invalid numeric value ${stringValue}`);
}
return result;
};
export const parseValidBoolean = (stringValue: string) => {
const asUnknown = stringValue as unknown;
switch (asUnknown) {
case 'true':
return true;
case 'false':
return false;
case true:
return true;
case false:
return false;
default:
throw new FlagTypeError(`Invalid boolean value for ${asUnknown}`)
}
};
export const parseValidJsonObject = <T extends object>(stringValue: string): T => {
// we may want to allow the parsing to be customized.
try {
const value = JSON.parse(stringValue);
if (typeof value === 'object') {
throw new FlagTypeError(`Flag value ${stringValue} had unexpected type ${typeof value}, expected "object"`);
}
return value;
} catch (err) {
throw new FlagValueParseError(`Error parsing ${stringValue} as JSON`);
}
}

View File

@ -1,7 +1,7 @@
'use strict';
const { openfeature } = require('../dist/packages/openfeature-js/src');
const { CloudbeesProvider } = require("../dist/packages/js-cloudbees-provider/src");
const { openfeature } = require('../dist/packages/openfeature-js/main');
const { CloudbeesProvider } = require("../dist/packages/js-cloudbees-provider/main");
/**
* Registers the environment variable provider to the globally scoped

View File

@ -1,9 +1,9 @@
'use strict';
const { openfeature } = require('../dist/packages/openfeature-js/src');
const { openfeature } = require('../dist/packages/openfeature-js/main');
const {
OpenFeatureEnvProvider,
} = require('../dist/packages/js-env-provider/src');
} = require('../dist/packages/js-env-provider/main');
/**
* Registers the environment variable provider to the globally scoped

View File

@ -1,9 +1,9 @@
'use strict';
const { openfeature } = require('../dist/packages/openfeature-js/src');
const { openfeature } = require('../dist/packages/openfeature-js/main');
const {
OpenFeatureLaunchDarklyProvider,
} = require('../dist/packages/js-launchdarkly-provider/src');
} = require('../dist/packages/js-launchdarkly-provider/main');
/**
* Registers the LaunchDarkly provider to the globally scoped

View File

@ -1,9 +1,9 @@
'use strict';
const { openfeature } = require('../dist/packages/openfeature-js/src');
const { openfeature } = require('../dist/packages/openfeature-js/main');
const {
OpenFeatureSplitProvider,
} = require('../dist/packages/js-split-provider/src');
} = require('../dist/packages/js-split-provider/main');
const { SplitFactory } = require('@splitsoftware/splitio');
/**

View File

@ -15,6 +15,7 @@
"skipLibCheck": true,
"skipDefaultLibCheck": true,
"baseUrl": ".",
"outDir": "lib",
"paths": {
"@openfeature/fibonacci": ["packages/fibonacci/src/index.ts"],
"@openfeature/js-cloudbees-provider": ["packages/js-cloudbees-provider/src/index.ts"],
@ -22,7 +23,8 @@
"@openfeature/js-split-provider": ["packages/js-split-provider/src/index.ts"],
"@openfeature/js-launchdarkly-provider": ["packages/js-launchdarkly-provider/src/index.ts"],
"@openfeature/openfeature-js": ["packages/openfeature-js/src/index.ts"]
}
},
"types": ["node"]
},
"exclude": ["node_modules", "tmp"]
}