chore: address lint issues (#642)
Signed-off-by: Michael Beemer <beeme1mr@users.noreply.github.com>
This commit is contained in:
parent
2213946d9a
commit
bbd9aee896
|
|
@ -1,2 +1,2 @@
|
|||
export * from './lib/traces';
|
||||
export * from './lib/metrics';
|
||||
export * from './lib/metrics';
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
// see: https://opentelemetry.io/docs/specs/otel/logs/semantic_conventions/feature-flags/
|
||||
export const FEATURE_FLAG = 'feature_flag';
|
||||
export const EXCEPTION_ATTR = 'exception'
|
||||
export const EXCEPTION_ATTR = 'exception';
|
||||
|
||||
export const ACTIVE_COUNT_NAME = `${FEATURE_FLAG}.evaluation_active_count`;
|
||||
export const REQUESTS_TOTAL_NAME = `${FEATURE_FLAG}.evaluation_requests_total`;
|
||||
export const SUCCESS_TOTAL_NAME = `${FEATURE_FLAG}.evaluation_success_total`;
|
||||
export const ERROR_TOTAL_NAME = `${FEATURE_FLAG}.evaluation_error_total`;
|
||||
|
||||
export type EvaluationAttributes = {[key: `${typeof FEATURE_FLAG}.${string}`]: string | undefined };
|
||||
export type EvaluationAttributes = { [key: `${typeof FEATURE_FLAG}.${string}`]: string | undefined };
|
||||
export type ExceptionAttributes = { [EXCEPTION_ATTR]: string };
|
||||
|
||||
export const KEY_ATTR: keyof EvaluationAttributes = `${FEATURE_FLAG}.key`;
|
||||
export const PROVIDER_NAME_ATTR: keyof EvaluationAttributes = `${FEATURE_FLAG}.provider_name`;
|
||||
export const VARIANT_ATTR: keyof EvaluationAttributes = `${FEATURE_FLAG}.variant`;
|
||||
export const REASON_ATTR: keyof EvaluationAttributes = `${FEATURE_FLAG}.reason`;
|
||||
export const REASON_ATTR: keyof EvaluationAttributes = `${FEATURE_FLAG}.reason`;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export * from './metrics-hook';
|
||||
export * from './metrics-hook';
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import {
|
|||
type EvaluationDetails,
|
||||
type FlagValue,
|
||||
type Hook,
|
||||
type HookContext
|
||||
type HookContext,
|
||||
} from '@openfeature/server-sdk';
|
||||
import { Attributes, Counter, UpDownCounter, ValueType, metrics } from '@opentelemetry/api';
|
||||
import {
|
||||
|
|
@ -19,7 +19,7 @@ import {
|
|||
REASON_ATTR,
|
||||
REQUESTS_TOTAL_NAME,
|
||||
SUCCESS_TOTAL_NAME,
|
||||
VARIANT_ATTR
|
||||
VARIANT_ATTR,
|
||||
} from '../conventions';
|
||||
import { OpenTelemetryHook, OpenTelemetryHookOptions } from '../otel-hook';
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ const ERROR_DESCRIPTION = 'feature flag evaluation error counter';
|
|||
|
||||
/**
|
||||
* A hook that adds conventionally-compliant metrics to feature flag evaluations.
|
||||
*
|
||||
*
|
||||
* See {@link https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/feature-flags/}
|
||||
*/
|
||||
export class MetricsHook extends OpenTelemetryHook implements Hook {
|
||||
|
|
@ -46,7 +46,10 @@ export class MetricsHook extends OpenTelemetryHook implements Hook {
|
|||
private readonly evaluationSuccessCounter: Counter<EvaluationAttributes | Attributes>;
|
||||
private readonly evaluationErrorCounter: Counter<ErrorEvaluationAttributes>;
|
||||
|
||||
constructor(options?: MetricsHookOptions, private readonly logger?: Logger) {
|
||||
constructor(
|
||||
options?: MetricsHookOptions,
|
||||
private readonly logger?: Logger,
|
||||
) {
|
||||
super(options, logger);
|
||||
const meter = metrics.getMeter(METER_NAME);
|
||||
this.evaluationActiveUpDownCounter = meter.createUpDownCounter(ACTIVE_COUNT_NAME, {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export * from './tracing-hook';
|
||||
export * from './tracing-hook';
|
||||
|
|
|
|||
|
|
@ -51,32 +51,32 @@ describe('OpenTelemetry Hooks', () => {
|
|||
variant: 'enabled',
|
||||
flagMetadata: {},
|
||||
};
|
||||
|
||||
|
||||
tracingHook.after(hookContext, evaluationDetails);
|
||||
|
||||
|
||||
expect(addEvent).toBeCalledWith('feature_flag', {
|
||||
'feature_flag.key': 'testFlagKey',
|
||||
'feature_flag.provider_name': 'testProvider',
|
||||
'feature_flag.variant': 'enabled',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should use a stringified value as the variant value on the span event', () => {
|
||||
const evaluationDetails: EvaluationDetails<boolean> = {
|
||||
flagKey: hookContext.flagKey,
|
||||
value: true,
|
||||
flagMetadata: {},
|
||||
};
|
||||
|
||||
|
||||
tracingHook.after(hookContext, evaluationDetails);
|
||||
|
||||
|
||||
expect(addEvent).toBeCalledWith('feature_flag', {
|
||||
'feature_flag.key': 'testFlagKey',
|
||||
'feature_flag.provider_name': 'testProvider',
|
||||
'feature_flag.variant': 'true',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should set the value without extra quotes if value is already a string', () => {
|
||||
const evaluationDetails: EvaluationDetails<string> = {
|
||||
flagKey: hookContext.flagKey,
|
||||
|
|
@ -84,14 +84,14 @@ describe('OpenTelemetry Hooks', () => {
|
|||
flagMetadata: {},
|
||||
};
|
||||
tracingHook.after(hookContext, evaluationDetails);
|
||||
|
||||
|
||||
expect(addEvent).toBeCalledWith('feature_flag', {
|
||||
'feature_flag.key': 'testFlagKey',
|
||||
'feature_flag.provider_name': 'testProvider',
|
||||
'feature_flag.variant': 'already-string',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should not call addEvent because there is no active span', () => {
|
||||
getActiveSpan.mockReturnValueOnce(undefined);
|
||||
const evaluationDetails: EvaluationDetails<boolean> = {
|
||||
|
|
@ -100,7 +100,7 @@ describe('OpenTelemetry Hooks', () => {
|
|||
variant: 'enabled',
|
||||
flagMetadata: {},
|
||||
};
|
||||
|
||||
|
||||
tracingHook.after(hookContext, evaluationDetails);
|
||||
expect(addEvent).not.toBeCalled();
|
||||
});
|
||||
|
|
@ -108,7 +108,6 @@ describe('OpenTelemetry Hooks', () => {
|
|||
|
||||
describe('attribute mapper configured', () => {
|
||||
describe('no error in mapper', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
tracingHook = new TracingHook({
|
||||
attributeMapper: (flagMetadata) => {
|
||||
|
|
@ -132,9 +131,9 @@ describe('OpenTelemetry Hooks', () => {
|
|||
metadata3: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
tracingHook.after(hookContext, evaluationDetails);
|
||||
|
||||
|
||||
expect(addEvent).toBeCalledWith('feature_flag', {
|
||||
'feature_flag.key': 'testFlagKey',
|
||||
'feature_flag.provider_name': 'testProvider',
|
||||
|
|
@ -147,7 +146,6 @@ describe('OpenTelemetry Hooks', () => {
|
|||
});
|
||||
|
||||
describe('error in mapper', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
tracingHook = new TracingHook({
|
||||
attributeMapper: (_) => {
|
||||
|
|
@ -167,9 +165,9 @@ describe('OpenTelemetry Hooks', () => {
|
|||
metadata3: true,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
tracingHook.after(hookContext, evaluationDetails);
|
||||
|
||||
|
||||
expect(addEvent).toBeCalledWith('feature_flag', {
|
||||
'feature_flag.key': 'testFlagKey',
|
||||
'feature_flag.provider_name': 'testProvider',
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ export type TracingHookOptions = OpenTelemetryHookOptions;
|
|||
|
||||
/**
|
||||
* A hook that adds conventionally-compliant span events to feature flag evaluations.
|
||||
*
|
||||
*
|
||||
* See {@link https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/feature-flags/}
|
||||
*/
|
||||
export class TracingHook extends OpenTelemetryHook implements Hook {
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ describe('ConfigCatProvider', () => {
|
|||
|
||||
it('should throw TypeMismatchError if type is different than expected', async () => {
|
||||
await expect(provider.resolveBooleanEvaluation('number1', false, { targetingKey })).rejects.toThrow(
|
||||
TypeMismatchError
|
||||
TypeMismatchError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -183,7 +183,7 @@ describe('ConfigCatProvider', () => {
|
|||
|
||||
it('should throw TypeMismatchError if type is different than expected', async () => {
|
||||
await expect(provider.resolveStringEvaluation('number1', 'default', { targetingKey })).rejects.toThrow(
|
||||
TypeMismatchError
|
||||
TypeMismatchError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -201,7 +201,7 @@ describe('ConfigCatProvider', () => {
|
|||
|
||||
it('should throw TypeMismatchError if type is different than expected', async () => {
|
||||
await expect(provider.resolveNumberEvaluation('stringTest', 0, { targetingKey })).rejects.toThrow(
|
||||
TypeMismatchError
|
||||
TypeMismatchError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -223,7 +223,7 @@ describe('ConfigCatProvider', () => {
|
|||
|
||||
it('should throw TypeMismatchError if string is only a JSON primitive', async () => {
|
||||
await expect(provider.resolveObjectEvaluation('jsonPrimitive', {}, { targetingKey })).rejects.toThrow(
|
||||
TypeMismatchError
|
||||
TypeMismatchError,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ export class ConfigCatProvider implements Provider {
|
|||
hooks.on('configChanged', (projectConfig: IConfig | undefined) =>
|
||||
this.events.emit(ProviderEvents.ConfigurationChanged, {
|
||||
flagsChanged: projectConfig ? Object.keys(projectConfig.settings) : undefined,
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
hooks.on('clientError', (message: string, error) => {
|
||||
|
|
@ -90,7 +90,7 @@ export class ConfigCatProvider implements Provider {
|
|||
async resolveBooleanEvaluation(
|
||||
flagKey: string,
|
||||
defaultValue: boolean,
|
||||
context: EvaluationContext
|
||||
context: EvaluationContext,
|
||||
): Promise<ResolutionDetails<boolean>> {
|
||||
if (!this._client) {
|
||||
throw new GeneralError('Provider is not initialized');
|
||||
|
|
@ -99,7 +99,7 @@ export class ConfigCatProvider implements Provider {
|
|||
const { value, ...evaluationData } = await this._client.getValueDetailsAsync<SettingValue>(
|
||||
flagKey,
|
||||
undefined,
|
||||
transformContext(context)
|
||||
transformContext(context),
|
||||
);
|
||||
|
||||
const validatedValue = validateFlagType('boolean', value);
|
||||
|
|
@ -112,7 +112,7 @@ export class ConfigCatProvider implements Provider {
|
|||
public async resolveStringEvaluation(
|
||||
flagKey: string,
|
||||
defaultValue: string,
|
||||
context: EvaluationContext
|
||||
context: EvaluationContext,
|
||||
): Promise<ResolutionDetails<string>> {
|
||||
if (!this._client) {
|
||||
throw new GeneralError('Provider is not initialized');
|
||||
|
|
@ -121,7 +121,7 @@ export class ConfigCatProvider implements Provider {
|
|||
const { value, ...evaluationData } = await this._client.getValueDetailsAsync<SettingValue>(
|
||||
flagKey,
|
||||
undefined,
|
||||
transformContext(context)
|
||||
transformContext(context),
|
||||
);
|
||||
|
||||
const validatedValue = validateFlagType('string', value);
|
||||
|
|
@ -134,7 +134,7 @@ export class ConfigCatProvider implements Provider {
|
|||
public async resolveNumberEvaluation(
|
||||
flagKey: string,
|
||||
defaultValue: number,
|
||||
context: EvaluationContext
|
||||
context: EvaluationContext,
|
||||
): Promise<ResolutionDetails<number>> {
|
||||
if (!this._client) {
|
||||
throw new GeneralError('Provider is not initialized');
|
||||
|
|
@ -143,7 +143,7 @@ export class ConfigCatProvider implements Provider {
|
|||
const { value, ...evaluationData } = await this._client.getValueDetailsAsync<SettingValue>(
|
||||
flagKey,
|
||||
undefined,
|
||||
transformContext(context)
|
||||
transformContext(context),
|
||||
);
|
||||
|
||||
const validatedValue = validateFlagType('number', value);
|
||||
|
|
@ -156,7 +156,7 @@ export class ConfigCatProvider implements Provider {
|
|||
public async resolveObjectEvaluation<U extends JsonValue>(
|
||||
flagKey: string,
|
||||
defaultValue: U,
|
||||
context: EvaluationContext
|
||||
context: EvaluationContext,
|
||||
): Promise<ResolutionDetails<U>> {
|
||||
if (!this._client) {
|
||||
throw new GeneralError('Provider is not initialized');
|
||||
|
|
@ -165,7 +165,7 @@ export class ConfigCatProvider implements Provider {
|
|||
const { value, ...evaluationData } = await this._client.getValueDetailsAsync(
|
||||
flagKey,
|
||||
undefined,
|
||||
transformContext(context)
|
||||
transformContext(context),
|
||||
);
|
||||
|
||||
if (typeof value === 'undefined') {
|
||||
|
|
@ -197,7 +197,7 @@ export class ConfigCatProvider implements Provider {
|
|||
function toResolutionDetails<U extends JsonValue>(
|
||||
value: U,
|
||||
data: Omit<IEvaluationDetails, 'value'>,
|
||||
reason?: ResolutionReason
|
||||
reason?: ResolutionReason,
|
||||
): ResolutionDetails<U> {
|
||||
const matchedRule = Boolean(data.matchedEvaluationRule || data.matchedEvaluationPercentageRule);
|
||||
const evaluatedReason = matchedRule ? StandardResolutionReasons.TARGETING_MATCH : StandardResolutionReasons.STATIC;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ describe('context-transformer', () => {
|
|||
expect(() => transformContext(context)).toThrow(TargetingKeyMissingError);
|
||||
});
|
||||
|
||||
|
||||
it('map targeting key to identifier', () => {
|
||||
const context: EvaluationContext = {
|
||||
targetingKey: 'test',
|
||||
|
|
@ -126,8 +125,8 @@ describe('context-transformer', () => {
|
|||
it('map several custom properties correctly', () => {
|
||||
const context: EvaluationContext = {
|
||||
targetingKey: 'test',
|
||||
email: "email",
|
||||
country: "country",
|
||||
email: 'email',
|
||||
country: 'country',
|
||||
customString: 'customString',
|
||||
customNumber: 1,
|
||||
customBoolean: true,
|
||||
|
|
@ -140,8 +139,8 @@ describe('context-transformer', () => {
|
|||
|
||||
const user = {
|
||||
identifier: 'test',
|
||||
email: "email",
|
||||
country: "country",
|
||||
email: 'email',
|
||||
country: 'country',
|
||||
custom: {
|
||||
customString: 'customString',
|
||||
customBoolean: 'true',
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export class EnvVarProvider implements Provider {
|
|||
|
||||
private evaluateEnvironmentVariable<T extends JsonValue>(
|
||||
key: string,
|
||||
parse: (value: string) => T
|
||||
parse: (value: string) => T,
|
||||
): ResolutionDetails<T> {
|
||||
const envVarKey = this.options.disableConstantCase ? key : constantCase(key);
|
||||
const value = process.env[envVarKey];
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export default {
|
||||
displayName: 'providers-flagd-web-e2e',
|
||||
transform: {
|
||||
'^.+\\.[tj]s$': ['ts-jest', { tsConfig: './tsconfig.lib.json'}],
|
||||
'^.+\\.[tj]s$': ['ts-jest', { tsConfig: './tsconfig.lib.json' }],
|
||||
},
|
||||
testEnvironment: 'node',
|
||||
preset: 'ts-jest',
|
||||
|
|
@ -9,4 +9,4 @@ export default {
|
|||
setupFiles: ['./setup.ts'],
|
||||
verbose: true,
|
||||
silent: false,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,14 +6,16 @@ const FLAGD_WEB_NAME = 'flagd-web';
|
|||
|
||||
// register the flagd provider before the tests.
|
||||
console.log('Setting flagd web provider...');
|
||||
OpenFeature.setProvider(new FlagdWebProvider({
|
||||
host: 'localhost',
|
||||
port: 8013,
|
||||
tls: false,
|
||||
maxRetries: -1,
|
||||
}));
|
||||
OpenFeature.setProvider(
|
||||
new FlagdWebProvider({
|
||||
host: 'localhost',
|
||||
port: 8013,
|
||||
tls: false,
|
||||
maxRetries: -1,
|
||||
}),
|
||||
);
|
||||
assert(
|
||||
OpenFeature.providerMetadata.name === FLAGD_WEB_NAME,
|
||||
new Error(`Expected ${FLAGD_WEB_NAME} provider to be configured, instead got: ${OpenFeature.providerMetadata.name}`)
|
||||
new Error(`Expected ${FLAGD_WEB_NAME} provider to be configured, instead got: ${OpenFeature.providerMetadata.name}`),
|
||||
);
|
||||
console.log('flagd web provider configured!');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,13 @@
|
|||
import { EvaluationContext, EvaluationDetails, JsonObject, JsonValue, OpenFeature, ProviderEvents, ResolutionDetails, StandardResolutionReasons } from '@openfeature/web-sdk';
|
||||
import {
|
||||
EvaluationContext,
|
||||
EvaluationDetails,
|
||||
JsonObject,
|
||||
JsonValue,
|
||||
OpenFeature,
|
||||
ProviderEvents,
|
||||
ResolutionDetails,
|
||||
StandardResolutionReasons,
|
||||
} from '@openfeature/web-sdk';
|
||||
import { defineFeature, loadFeature } from 'jest-cucumber';
|
||||
|
||||
// load the feature file.
|
||||
|
|
@ -8,7 +17,7 @@ const feature = loadFeature('features/evaluation.feature');
|
|||
const client = OpenFeature.getClient();
|
||||
|
||||
const givenAnOpenfeatureClientIsRegistered = (
|
||||
given: (stepMatcher: string, stepDefinitionCallback: () => void) => void
|
||||
given: (stepMatcher: string, stepDefinitionCallback: () => void) => void,
|
||||
) => {
|
||||
given('a provider is registered', () => undefined);
|
||||
};
|
||||
|
|
@ -33,7 +42,7 @@ defineFeature(feature, (test) => {
|
|||
/^a boolean flag with key "(.*)" is evaluated with default value "(.*)"$/,
|
||||
(key: string, defaultValue: string) => {
|
||||
value = client.getBooleanValue(key, defaultValue === 'true');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
then(/^the resolved boolean value should be "(.*)"$/, (expectedValue: string) => {
|
||||
|
|
@ -50,7 +59,7 @@ defineFeature(feature, (test) => {
|
|||
/^a string flag with key "(.*)" is evaluated with default value "(.*)"$/,
|
||||
(key: string, defaultValue: string) => {
|
||||
value = client.getStringValue(key, defaultValue);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
then(/^the resolved string value should be "(.*)"$/, (expectedValue: string) => {
|
||||
|
|
@ -67,7 +76,7 @@ defineFeature(feature, (test) => {
|
|||
/^an integer flag with key "(.*)" is evaluated with default value (\d+)$/,
|
||||
(key: string, defaultValue: number) => {
|
||||
value = client.getNumberValue(key, defaultValue);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
then(/^the resolved integer value should be (\d+)$/, (expectedStringValue: string) => {
|
||||
|
|
@ -85,7 +94,7 @@ defineFeature(feature, (test) => {
|
|||
/^a float flag with key "(.*)" is evaluated with default value (\d+\.?\d*)$/,
|
||||
(key: string, defaultValue: string) => {
|
||||
value = client.getNumberValue(key, Number.parseFloat(defaultValue));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
then(/^the resolved float value should be (\d+\.?\d*)$/, (expectedValue: string) => {
|
||||
|
|
@ -108,7 +117,7 @@ defineFeature(feature, (test) => {
|
|||
expect(jsonObject[field1]).toEqual(boolVal === 'true');
|
||||
expect(jsonObject[field2]).toEqual(strVal);
|
||||
expect(jsonObject[field3]).toEqual(Number.parseInt(intVal));
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -120,7 +129,7 @@ defineFeature(feature, (test) => {
|
|||
/^a boolean flag with key "(.*)" is evaluated with details and default value "(.*)"$/,
|
||||
(key: string, defaultValue: string) => {
|
||||
details = client.getBooleanDetails(key, defaultValue === 'true');
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
then(
|
||||
|
|
@ -129,7 +138,7 @@ defineFeature(feature, (test) => {
|
|||
expect(details.value).toEqual(expectedValue === 'true');
|
||||
expect(details.variant).toEqual(expectedVariant);
|
||||
expect(details.reason).toEqual(expectedReason);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -142,7 +151,7 @@ defineFeature(feature, (test) => {
|
|||
/^a string flag with key "(.*)" is evaluated with details and default value "(.*)"$/,
|
||||
(key: string, defaultValue: string) => {
|
||||
details = client.getStringDetails(key, defaultValue);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
then(
|
||||
|
|
@ -151,7 +160,7 @@ defineFeature(feature, (test) => {
|
|||
expect(details.value).toEqual(expectedValue);
|
||||
expect(details.variant).toEqual(expectedVariant);
|
||||
expect(details.reason).toEqual(expectedReason);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -164,7 +173,7 @@ defineFeature(feature, (test) => {
|
|||
/^an integer flag with key "(.*)" is evaluated with details and default value (\d+)$/,
|
||||
(key: string, defaultValue: string) => {
|
||||
details = client.getNumberDetails(key, Number.parseInt(defaultValue));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
then(
|
||||
|
|
@ -173,7 +182,7 @@ defineFeature(feature, (test) => {
|
|||
expect(details.value).toEqual(Number.parseInt(expectedValue));
|
||||
expect(details.variant).toEqual(expectedVariant);
|
||||
expect(details.reason).toEqual(expectedReason);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -186,7 +195,7 @@ defineFeature(feature, (test) => {
|
|||
/^a float flag with key "(.*)" is evaluated with details and default value (\d+\.?\d*)$/,
|
||||
(key: string, defaultValue: string) => {
|
||||
details = client.getNumberDetails(key, Number.parseFloat(defaultValue));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
then(
|
||||
|
|
@ -195,7 +204,7 @@ defineFeature(feature, (test) => {
|
|||
expect(details.value).toEqual(Number.parseFloat(expectedValue));
|
||||
expect(details.variant).toEqual(expectedVariant);
|
||||
expect(details.reason).toEqual(expectedReason);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -216,7 +225,7 @@ defineFeature(feature, (test) => {
|
|||
expect(jsonObject[field1]).toEqual(boolValue === 'true');
|
||||
expect(jsonObject[field2]).toEqual(stringValue);
|
||||
expect(jsonObject[field3]).toEqual(Number.parseInt(intValue));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
and(
|
||||
|
|
@ -224,7 +233,7 @@ defineFeature(feature, (test) => {
|
|||
(expectedVariant: string, expectedReason: string) => {
|
||||
expect(details.variant).toEqual(expectedVariant);
|
||||
expect(details.reason).toEqual(expectedReason);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -245,7 +254,7 @@ defineFeature(feature, (test) => {
|
|||
stringValue1: string,
|
||||
stringValue2: string,
|
||||
intValue: string,
|
||||
boolValue: string
|
||||
boolValue: string,
|
||||
) => {
|
||||
context[stringField1] = stringValue1;
|
||||
context[stringField2] = stringValue2;
|
||||
|
|
@ -253,7 +262,7 @@ defineFeature(feature, (test) => {
|
|||
context[boolField] = boolValue === 'true';
|
||||
|
||||
await OpenFeature.setContext(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
and(/^a flag with key "(.*)" is evaluated with default value "(.*)"$/, (key: string, defaultValue: string) => {
|
||||
|
|
@ -285,7 +294,7 @@ defineFeature(feature, (test) => {
|
|||
flagKey = key;
|
||||
fallbackValue = defaultValue;
|
||||
details = client.getStringDetails(flagKey, defaultValue);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
then(/^the default string value should be returned$/, () => {
|
||||
|
|
@ -297,7 +306,7 @@ defineFeature(feature, (test) => {
|
|||
(errorCode: string) => {
|
||||
expect(details.reason).toEqual(StandardResolutionReasons.ERROR);
|
||||
expect(details.errorCode).toEqual(errorCode);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -314,7 +323,7 @@ defineFeature(feature, (test) => {
|
|||
flagKey = key;
|
||||
fallbackValue = Number.parseInt(defaultValue);
|
||||
details = client.getNumberDetails(flagKey, Number.parseInt(defaultValue));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
then(/^the default integer value should be returned$/, () => {
|
||||
|
|
@ -326,7 +335,7 @@ defineFeature(feature, (test) => {
|
|||
(errorCode: string) => {
|
||||
expect(details.reason).toEqual(StandardResolutionReasons.ERROR);
|
||||
expect(details.errorCode).toEqual(errorCode);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export class FlagdWebProvider implements Provider {
|
|||
options: FlagdProviderOptions,
|
||||
logger?: Logger,
|
||||
promiseClient?: PromiseClient<typeof Service>,
|
||||
callbackClient?: CallbackClient<typeof Service>
|
||||
callbackClient?: CallbackClient<typeof Service>,
|
||||
) {
|
||||
const { host, port, tls, maxRetries, maxDelay, pathPrefix } = getOptions(options);
|
||||
const transport = createConnectTransport({
|
||||
|
|
@ -151,7 +151,7 @@ export class FlagdWebProvider implements Provider {
|
|||
this._logger?.warn(`${FlagdWebProvider.name}: max retries reached`);
|
||||
this.events.emit(ProviderEvents.Error);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export interface Options {
|
|||
|
||||
/**
|
||||
* The path at which the flagd gRPC service is available, for example: /flagd-api (optional).
|
||||
*
|
||||
*
|
||||
* @default ""
|
||||
*/
|
||||
pathPrefix: string;
|
||||
|
|
@ -52,7 +52,7 @@ export function getOptions(options: FlagdProviderOptions): Options {
|
|||
tls: true,
|
||||
maxRetries: 0,
|
||||
maxDelay: DEFAULT_MAX_DELAY,
|
||||
pathPrefix: ""
|
||||
pathPrefix: '',
|
||||
},
|
||||
...options,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import {GoFeatureFlagEvaluationContext} from './model';
|
||||
import {transformContext} from './context-transformer';
|
||||
import {TargetingKeyMissingError, EvaluationContext} from "@openfeature/web-sdk";
|
||||
import { GoFeatureFlagEvaluationContext } from './model';
|
||||
import { transformContext } from './context-transformer';
|
||||
import { TargetingKeyMissingError, EvaluationContext } from '@openfeature/web-sdk';
|
||||
|
||||
describe('contextTransformer', () => {
|
||||
it('should use the targetingKey as user key', () => {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
import {GoFeatureFlagEvaluationContext} from './model';
|
||||
import {TargetingKeyMissingError, EvaluationContext} from "@openfeature/web-sdk";
|
||||
import { GoFeatureFlagEvaluationContext } from './model';
|
||||
import { TargetingKeyMissingError, EvaluationContext } from '@openfeature/web-sdk';
|
||||
|
||||
/**
|
||||
* transformContext takes the raw OpenFeature context returns a GoFeatureFlagEvaluationContext.
|
||||
* @param context - the context used for flag evaluation.
|
||||
* @returns {GoFeatureFlagEvaluationContext} the user against who we will evaluate the flag.
|
||||
*/
|
||||
export function transformContext(
|
||||
context: EvaluationContext
|
||||
): GoFeatureFlagEvaluationContext {
|
||||
const {targetingKey, ...attributes} = context;
|
||||
export function transformContext(context: EvaluationContext): GoFeatureFlagEvaluationContext {
|
||||
const { targetingKey, ...attributes } = context;
|
||||
if (targetingKey === undefined || targetingKey === null || targetingKey === '') {
|
||||
throw new TargetingKeyMissingError();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
* the method fetch.
|
||||
* It allows to throw an error with the status code.
|
||||
*/
|
||||
export class FetchError extends Error{
|
||||
export class FetchError extends Error {
|
||||
status: number;
|
||||
constructor(status:number) {
|
||||
constructor(status: number) {
|
||||
super(`Request failed with status code ${status}`);
|
||||
this.status = status;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {GoFeatureFlagWebProvider} from './go-feature-flag-web-provider';
|
||||
import { GoFeatureFlagWebProvider } from './go-feature-flag-web-provider';
|
||||
import {
|
||||
EvaluationContext,
|
||||
OpenFeature,
|
||||
|
|
@ -6,12 +6,12 @@ import {
|
|||
StandardResolutionReasons,
|
||||
ErrorCode,
|
||||
EvaluationDetails,
|
||||
JsonValue}
|
||||
from "@openfeature/web-sdk";
|
||||
import WS from "jest-websocket-mock";
|
||||
import TestLogger from "./test-logger";
|
||||
import {GOFeatureFlagWebsocketResponse} from "./model";
|
||||
import fetchMock from "fetch-mock-jest";
|
||||
JsonValue,
|
||||
} from '@openfeature/web-sdk';
|
||||
import WS from 'jest-websocket-mock';
|
||||
import TestLogger from './test-logger';
|
||||
import { GOFeatureFlagWebsocketResponse } from './model';
|
||||
import fetchMock from 'fetch-mock-jest';
|
||||
|
||||
describe('GoFeatureFlagWebProvider', () => {
|
||||
let websocketMockServer: WS;
|
||||
|
|
@ -19,65 +19,65 @@ describe('GoFeatureFlagWebProvider', () => {
|
|||
const allFlagsEndpoint = `${endpoint}v1/allflags`;
|
||||
const websocketEndpoint = 'ws://localhost:1031/ws/v1/flag/change';
|
||||
const defaultAllFlagResponse = {
|
||||
"flags": {
|
||||
"bool_flag": {
|
||||
"value": true,
|
||||
"timestamp": 1689020159,
|
||||
"variationType": "True",
|
||||
"trackEvents": true,
|
||||
"reason": "DEFAULT",
|
||||
"metadata": {
|
||||
"description": "this is a test flag"
|
||||
}
|
||||
flags: {
|
||||
bool_flag: {
|
||||
value: true,
|
||||
timestamp: 1689020159,
|
||||
variationType: 'True',
|
||||
trackEvents: true,
|
||||
reason: 'DEFAULT',
|
||||
metadata: {
|
||||
description: 'this is a test flag',
|
||||
},
|
||||
},
|
||||
"number_flag": {
|
||||
"value": 123,
|
||||
"timestamp": 1689020159,
|
||||
"variationType": "True",
|
||||
"trackEvents": true,
|
||||
"reason": "DEFAULT",
|
||||
"metadata": {
|
||||
"description": "this is a test flag"
|
||||
}
|
||||
number_flag: {
|
||||
value: 123,
|
||||
timestamp: 1689020159,
|
||||
variationType: 'True',
|
||||
trackEvents: true,
|
||||
reason: 'DEFAULT',
|
||||
metadata: {
|
||||
description: 'this is a test flag',
|
||||
},
|
||||
},
|
||||
"string_flag": {
|
||||
"value": 'value-flag',
|
||||
"timestamp": 1689020159,
|
||||
"variationType": "True",
|
||||
"trackEvents": true,
|
||||
"reason": "DEFAULT",
|
||||
"metadata": {
|
||||
"description": "this is a test flag"
|
||||
}
|
||||
string_flag: {
|
||||
value: 'value-flag',
|
||||
timestamp: 1689020159,
|
||||
variationType: 'True',
|
||||
trackEvents: true,
|
||||
reason: 'DEFAULT',
|
||||
metadata: {
|
||||
description: 'this is a test flag',
|
||||
},
|
||||
},
|
||||
object_flag: {
|
||||
value: { id: '123' },
|
||||
timestamp: 1689020159,
|
||||
variationType: 'True',
|
||||
trackEvents: true,
|
||||
reason: 'DEFAULT',
|
||||
metadata: {
|
||||
description: 'this is a test flag',
|
||||
},
|
||||
},
|
||||
"object_flag": {
|
||||
"value": {id: '123'},
|
||||
"timestamp": 1689020159,
|
||||
"variationType": "True",
|
||||
"trackEvents": true,
|
||||
"reason": "DEFAULT",
|
||||
"metadata": {
|
||||
"description": "this is a test flag"
|
||||
}
|
||||
}
|
||||
},
|
||||
"valid": true
|
||||
valid: true,
|
||||
};
|
||||
const alternativeAllFlagResponse = {
|
||||
"flags": {
|
||||
"bool_flag": {
|
||||
"value": false,
|
||||
"timestamp": 1689020159,
|
||||
"variationType": "NEW_VARIATION",
|
||||
"trackEvents": false,
|
||||
"errorCode": "",
|
||||
"reason": "TARGETING_MATCH",
|
||||
"metadata": {
|
||||
"description": "this is a test flag"
|
||||
}
|
||||
}
|
||||
flags: {
|
||||
bool_flag: {
|
||||
value: false,
|
||||
timestamp: 1689020159,
|
||||
variationType: 'NEW_VARIATION',
|
||||
trackEvents: false,
|
||||
errorCode: '',
|
||||
reason: 'TARGETING_MATCH',
|
||||
metadata: {
|
||||
description: 'this is a test flag',
|
||||
},
|
||||
},
|
||||
},
|
||||
"valid": true
|
||||
valid: true,
|
||||
};
|
||||
let defaultProvider: GoFeatureFlagWebProvider;
|
||||
let defaultContext: EvaluationContext;
|
||||
|
|
@ -93,19 +93,22 @@ describe('GoFeatureFlagWebProvider', () => {
|
|||
fetchMock.mockClear();
|
||||
fetchMock.mockReset();
|
||||
await jest.resetAllMocks();
|
||||
websocketMockServer = new WS(websocketEndpoint, {jsonProtocol: true});
|
||||
fetchMock.post(allFlagsEndpoint,defaultAllFlagResponse);
|
||||
defaultProvider = new GoFeatureFlagWebProvider({
|
||||
endpoint: endpoint,
|
||||
apiTimeout: 1000,
|
||||
maxRetries: 1,
|
||||
}, logger);
|
||||
defaultContext = {targetingKey: 'user-key'};
|
||||
websocketMockServer = new WS(websocketEndpoint, { jsonProtocol: true });
|
||||
fetchMock.post(allFlagsEndpoint, defaultAllFlagResponse);
|
||||
defaultProvider = new GoFeatureFlagWebProvider(
|
||||
{
|
||||
endpoint: endpoint,
|
||||
apiTimeout: 1000,
|
||||
maxRetries: 1,
|
||||
},
|
||||
logger,
|
||||
);
|
||||
defaultContext = { targetingKey: 'user-key' };
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await WS.clean();
|
||||
websocketMockServer.close()
|
||||
websocketMockServer.close();
|
||||
await OpenFeature.close();
|
||||
await OpenFeature.clearHooks();
|
||||
fetchMock.mockClear();
|
||||
|
|
@ -120,11 +123,14 @@ describe('GoFeatureFlagWebProvider', () => {
|
|||
});
|
||||
|
||||
function newDefaultProvider(): GoFeatureFlagWebProvider {
|
||||
return new GoFeatureFlagWebProvider({
|
||||
endpoint: endpoint,
|
||||
apiTimeout: 1000,
|
||||
maxRetries: 1,
|
||||
}, logger);
|
||||
return new GoFeatureFlagWebProvider(
|
||||
{
|
||||
endpoint: endpoint,
|
||||
apiTimeout: 1000,
|
||||
maxRetries: 1,
|
||||
},
|
||||
logger,
|
||||
);
|
||||
}
|
||||
|
||||
describe('provider metadata', () => {
|
||||
|
|
@ -146,8 +152,8 @@ describe('GoFeatureFlagWebProvider', () => {
|
|||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
|
||||
const got1 = client.getBooleanDetails('bool_flag', false);
|
||||
fetchMock.post(allFlagsEndpoint, alternativeAllFlagResponse, {overwriteRoutes: true});
|
||||
await OpenFeature.setContext({targetingKey: "1234"});
|
||||
fetchMock.post(allFlagsEndpoint, alternativeAllFlagResponse, { overwriteRoutes: true });
|
||||
await OpenFeature.setContext({ targetingKey: '1234' });
|
||||
const got2 = client.getBooleanDetails('bool_flag', false);
|
||||
|
||||
expect(got1.value).toEqual(defaultAllFlagResponse.flags.bool_flag.value);
|
||||
|
|
@ -167,14 +173,14 @@ describe('GoFeatureFlagWebProvider', () => {
|
|||
await websocketMockServer.connected;
|
||||
// Need to wait before using the mock
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
await websocketMockServer.close()
|
||||
await websocketMockServer.close();
|
||||
|
||||
const got = client.getBooleanDetails('bool_flag', false);
|
||||
expect(got.reason).toEqual(StandardResolutionReasons.CACHED);
|
||||
});
|
||||
|
||||
it('should emit an error if we have the wrong credentials', async () => {
|
||||
fetchMock.post(allFlagsEndpoint,401, {overwriteRoutes: true});
|
||||
fetchMock.post(allFlagsEndpoint, 401, { overwriteRoutes: true });
|
||||
const providerName = expect.getState().currentTestName || 'test';
|
||||
await OpenFeature.setContext(defaultContext);
|
||||
OpenFeature.setProvider(providerName, newDefaultProvider());
|
||||
|
|
@ -182,13 +188,14 @@ describe('GoFeatureFlagWebProvider', () => {
|
|||
client.addHandler(ProviderEvents.Error, errorHandler);
|
||||
// wait the event to be triggered
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
expect(errorHandler).toBeCalled()
|
||||
expect(logger.inMemoryLogger['error'][0])
|
||||
.toEqual('GoFeatureFlagWebProvider: invalid token used to contact GO Feature Flag instance: Error: Request failed with status code 401');
|
||||
expect(errorHandler).toBeCalled();
|
||||
expect(logger.inMemoryLogger['error'][0]).toEqual(
|
||||
'GoFeatureFlagWebProvider: invalid token used to contact GO Feature Flag instance: Error: Request failed with status code 401',
|
||||
);
|
||||
});
|
||||
|
||||
it('should emit an error if we receive a 404 from GO Feature Flag', async () => {
|
||||
fetchMock.post(allFlagsEndpoint,404, {overwriteRoutes: true});
|
||||
fetchMock.post(allFlagsEndpoint, 404, { overwriteRoutes: true });
|
||||
await OpenFeature.setContext(defaultContext);
|
||||
OpenFeature.setProvider('test-provider', defaultProvider);
|
||||
const client = await OpenFeature.getClient('test-provider');
|
||||
|
|
@ -198,9 +205,10 @@ describe('GoFeatureFlagWebProvider', () => {
|
|||
client.addHandler(ProviderEvents.ConfigurationChanged, configurationChangedHandler);
|
||||
// wait the event to be triggered
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
expect(errorHandler).toBeCalled()
|
||||
expect(logger.inMemoryLogger['error'][0])
|
||||
.toEqual('GoFeatureFlagWebProvider: impossible to call go-feature-flag relay proxy Error: Request failed with status code 404');
|
||||
expect(errorHandler).toBeCalled();
|
||||
expect(logger.inMemoryLogger['error'][0]).toEqual(
|
||||
'GoFeatureFlagWebProvider: impossible to call go-feature-flag relay proxy Error: Request failed with status code 404',
|
||||
);
|
||||
});
|
||||
|
||||
it('should get a valid boolean flag evaluation', async () => {
|
||||
|
|
@ -208,14 +216,14 @@ describe('GoFeatureFlagWebProvider', () => {
|
|||
await OpenFeature.setContext(defaultContext);
|
||||
OpenFeature.setProvider('test-provider', defaultProvider);
|
||||
const client = await OpenFeature.getClient('test-provider');
|
||||
await websocketMockServer.connected
|
||||
await websocketMockServer.connected;
|
||||
const got = client.getBooleanDetails(flagKey, false);
|
||||
const want: EvaluationDetails<boolean> = {
|
||||
flagKey,
|
||||
value: true,
|
||||
variant: 'True',
|
||||
flagMetadata: {
|
||||
description: "this is a test flag"
|
||||
description: 'this is a test flag',
|
||||
},
|
||||
reason: StandardResolutionReasons.DEFAULT,
|
||||
};
|
||||
|
|
@ -227,14 +235,14 @@ describe('GoFeatureFlagWebProvider', () => {
|
|||
await OpenFeature.setContext(defaultContext);
|
||||
OpenFeature.setProvider('test-provider', defaultProvider);
|
||||
const client = await OpenFeature.getClient('test-provider');
|
||||
await websocketMockServer.connected
|
||||
await websocketMockServer.connected;
|
||||
const got = client.getStringDetails(flagKey, 'false');
|
||||
const want: EvaluationDetails<string> = {
|
||||
flagKey,
|
||||
value: 'value-flag',
|
||||
variant: 'True',
|
||||
flagMetadata: {
|
||||
description: "this is a test flag"
|
||||
description: 'this is a test flag',
|
||||
},
|
||||
reason: StandardResolutionReasons.DEFAULT,
|
||||
};
|
||||
|
|
@ -246,14 +254,14 @@ describe('GoFeatureFlagWebProvider', () => {
|
|||
await OpenFeature.setContext(defaultContext);
|
||||
OpenFeature.setProvider('test-provider', defaultProvider);
|
||||
const client = await OpenFeature.getClient('test-provider');
|
||||
await websocketMockServer.connected
|
||||
await websocketMockServer.connected;
|
||||
const got = client.getNumberDetails(flagKey, 456);
|
||||
const want: EvaluationDetails<number> = {
|
||||
flagKey,
|
||||
value: 123,
|
||||
variant: 'True',
|
||||
flagMetadata: {
|
||||
description: "this is a test flag"
|
||||
description: 'this is a test flag',
|
||||
},
|
||||
reason: StandardResolutionReasons.DEFAULT,
|
||||
};
|
||||
|
|
@ -265,14 +273,14 @@ describe('GoFeatureFlagWebProvider', () => {
|
|||
await OpenFeature.setContext(defaultContext);
|
||||
OpenFeature.setProvider('test-provider', defaultProvider);
|
||||
const client = await OpenFeature.getClient('test-provider');
|
||||
await websocketMockServer.connected
|
||||
const got = client.getObjectDetails(flagKey, {error: true});
|
||||
await websocketMockServer.connected;
|
||||
const got = client.getObjectDetails(flagKey, { error: true });
|
||||
const want: EvaluationDetails<JsonValue> = {
|
||||
flagKey,
|
||||
value: {id: "123"},
|
||||
value: { id: '123' },
|
||||
variant: 'True',
|
||||
flagMetadata: {
|
||||
description: "this is a test flag"
|
||||
description: 'this is a test flag',
|
||||
},
|
||||
reason: StandardResolutionReasons.DEFAULT,
|
||||
};
|
||||
|
|
@ -284,15 +292,15 @@ describe('GoFeatureFlagWebProvider', () => {
|
|||
await OpenFeature.setContext(defaultContext);
|
||||
OpenFeature.setProvider('test-provider', defaultProvider);
|
||||
const client = await OpenFeature.getClient('test-provider');
|
||||
await websocketMockServer.connected
|
||||
await websocketMockServer.connected;
|
||||
const got = client.getStringDetails(flagKey, 'false');
|
||||
const want: EvaluationDetails<string> = {
|
||||
flagKey,
|
||||
value: "false",
|
||||
value: 'false',
|
||||
reason: StandardResolutionReasons.ERROR,
|
||||
errorCode: ErrorCode.TYPE_MISMATCH,
|
||||
flagMetadata: {},
|
||||
errorMessage: "flag key bool_flag is not of type string",
|
||||
errorMessage: 'flag key bool_flag is not of type string',
|
||||
};
|
||||
expect(got).toEqual(want);
|
||||
});
|
||||
|
|
@ -302,7 +310,7 @@ describe('GoFeatureFlagWebProvider', () => {
|
|||
await OpenFeature.setContext(defaultContext);
|
||||
OpenFeature.setProvider('test-provider', defaultProvider);
|
||||
const client = await OpenFeature.getClient('test-provider');
|
||||
await websocketMockServer.connected
|
||||
await websocketMockServer.connected;
|
||||
const got = client.getBooleanDetails(flagKey, false);
|
||||
const want: EvaluationDetails<boolean> = {
|
||||
flagKey,
|
||||
|
|
@ -310,7 +318,7 @@ describe('GoFeatureFlagWebProvider', () => {
|
|||
reason: StandardResolutionReasons.ERROR,
|
||||
errorCode: ErrorCode.FLAG_NOT_FOUND,
|
||||
flagMetadata: {},
|
||||
errorMessage: "flag key not-exist not found in cache",
|
||||
errorMessage: 'flag key not-exist not found in cache',
|
||||
};
|
||||
expect(got).toEqual(want);
|
||||
});
|
||||
|
|
@ -353,17 +361,17 @@ describe('GoFeatureFlagWebProvider', () => {
|
|||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
websocketMockServer.send({
|
||||
added: {
|
||||
"added-flag-1": {},
|
||||
"added-flag-2": {}
|
||||
'added-flag-1': {},
|
||||
'added-flag-2': {},
|
||||
},
|
||||
updated: {
|
||||
"updated-flag-1": {},
|
||||
"updated-flag-2": {},
|
||||
'updated-flag-1': {},
|
||||
'updated-flag-2': {},
|
||||
},
|
||||
deleted: {
|
||||
"deleted-flag-1": {},
|
||||
"deleted-flag-2": {},
|
||||
}
|
||||
'deleted-flag-1': {},
|
||||
'deleted-flag-2': {},
|
||||
},
|
||||
} as GOFeatureFlagWebsocketResponse);
|
||||
// waiting the call to the API to be successful
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
|
|
@ -375,17 +383,27 @@ describe('GoFeatureFlagWebProvider', () => {
|
|||
expect(configurationChangedHandler.mock.calls[0][0]).toEqual({
|
||||
clientName: 'test-provider',
|
||||
message: 'flag configuration have changed',
|
||||
flagsChanged: ['deleted-flag-1', 'deleted-flag-2', 'updated-flag-1', 'updated-flag-2', 'added-flag-1', 'added-flag-2']
|
||||
})
|
||||
flagsChanged: [
|
||||
'deleted-flag-1',
|
||||
'deleted-flag-2',
|
||||
'updated-flag-1',
|
||||
'updated-flag-2',
|
||||
'added-flag-1',
|
||||
'added-flag-2',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should call client handler with ProviderEvents.Stale when websocket is unreachable', async () => {
|
||||
// await OpenFeature.setContext(defaultContext); // we deactivate this call because the context is already set, and we want to avoid calling contextChanged function
|
||||
const provider = new GoFeatureFlagWebProvider({
|
||||
endpoint,
|
||||
maxRetries: 1,
|
||||
retryInitialDelay: 10,
|
||||
}, logger);
|
||||
const provider = new GoFeatureFlagWebProvider(
|
||||
{
|
||||
endpoint,
|
||||
maxRetries: 1,
|
||||
retryInitialDelay: 10,
|
||||
},
|
||||
logger,
|
||||
);
|
||||
OpenFeature.setProvider('test-provider', provider);
|
||||
const client = await OpenFeature.getClient('test-provider');
|
||||
client.addHandler(ProviderEvents.Ready, readyHandler);
|
||||
|
|
@ -398,7 +416,7 @@ describe('GoFeatureFlagWebProvider', () => {
|
|||
|
||||
// Need to wait before using the mock
|
||||
await new Promise((resolve) => setTimeout(resolve, 5));
|
||||
await websocketMockServer.close()
|
||||
await websocketMockServer.close();
|
||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||
|
||||
expect(readyHandler).toBeCalled();
|
||||
|
|
@ -406,5 +424,5 @@ describe('GoFeatureFlagWebProvider', () => {
|
|||
expect(configurationChangedHandler).not.toBeCalled();
|
||||
expect(staleHandler).toBeCalled();
|
||||
});
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,19 +11,19 @@ import {
|
|||
ResolutionDetails,
|
||||
StandardResolutionReasons,
|
||||
TypeMismatchError,
|
||||
} from "@openfeature/web-sdk";
|
||||
} from '@openfeature/web-sdk';
|
||||
import {
|
||||
FlagState,
|
||||
GoFeatureFlagAllFlagRequest,
|
||||
GOFeatureFlagAllFlagsResponse,
|
||||
GoFeatureFlagWebProviderOptions,
|
||||
GOFeatureFlagWebsocketResponse,
|
||||
} from "./model";
|
||||
import {transformContext} from "./context-transformer";
|
||||
import {FetchError} from "./fetch-error";
|
||||
} from './model';
|
||||
import { transformContext } from './context-transformer';
|
||||
import { FetchError } from './fetch-error';
|
||||
|
||||
export class GoFeatureFlagWebProvider implements Provider {
|
||||
private readonly _websocketPath = "ws/v1/flag/change"
|
||||
private readonly _websocketPath = 'ws/v1/flag/change';
|
||||
|
||||
metadata = {
|
||||
name: GoFeatureFlagWebProvider.name,
|
||||
|
|
@ -73,7 +73,9 @@ export class GoFeatureFlagWebProvider implements Provider {
|
|||
this._logger?.debug(`${GoFeatureFlagWebProvider.name}: go-feature-flag provider initialized`);
|
||||
})
|
||||
.catch((error) => {
|
||||
this._logger?.error(`${GoFeatureFlagWebProvider.name}: initialization failed, provider is on error, we will try to reconnect: ${error}`);
|
||||
this._logger?.error(
|
||||
`${GoFeatureFlagWebProvider.name}: initialization failed, provider is on error, we will try to reconnect: ${error}`,
|
||||
);
|
||||
this._status = ProviderStatus.ERROR;
|
||||
this.handleFetchErrors(error);
|
||||
|
||||
|
|
@ -81,7 +83,7 @@ export class GoFeatureFlagWebProvider implements Provider {
|
|||
// and we launch the retry to fetch the data.
|
||||
this.retryFetchAll(context);
|
||||
this.reconnectWebsocket();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -90,16 +92,17 @@ export class GoFeatureFlagWebProvider implements Provider {
|
|||
*/
|
||||
async connectWebsocket(): Promise<void> {
|
||||
const wsURL = new URL(this._endpoint);
|
||||
wsURL.pathname =
|
||||
wsURL.pathname.endsWith('/') ? wsURL.pathname + this._websocketPath : wsURL.pathname + '/' + this._websocketPath;
|
||||
wsURL.pathname = wsURL.pathname.endsWith('/')
|
||||
? wsURL.pathname + this._websocketPath
|
||||
: wsURL.pathname + '/' + this._websocketPath;
|
||||
wsURL.protocol = wsURL.protocol === 'https:' ? 'wss' : 'ws';
|
||||
|
||||
// adding API Key if GO Feature Flag use api keys.
|
||||
if(this._apiKey){
|
||||
if (this._apiKey) {
|
||||
wsURL.searchParams.set('apiKey', this._apiKey);
|
||||
}
|
||||
|
||||
this._logger?.debug(`${GoFeatureFlagWebProvider.name}: Trying to connect the websocket at ${wsURL}`)
|
||||
this._logger?.debug(`${GoFeatureFlagWebProvider.name}: Trying to connect the websocket at ${wsURL}`);
|
||||
|
||||
this._websocket = new WebSocket(wsURL);
|
||||
await this.waitWebsocketFinalStatus(this._websocket);
|
||||
|
|
@ -107,12 +110,12 @@ export class GoFeatureFlagWebProvider implements Provider {
|
|||
this._websocket.onopen = (event) => {
|
||||
this._logger?.info(`${GoFeatureFlagWebProvider.name}: Websocket to go-feature-flag open: ${event}`);
|
||||
};
|
||||
this._websocket.onmessage = async ({data}) => {
|
||||
this._websocket.onmessage = async ({ data }) => {
|
||||
this._logger?.info(`${GoFeatureFlagWebProvider.name}: Change in your configuration flag`);
|
||||
const t: GOFeatureFlagWebsocketResponse = JSON.parse(data);
|
||||
const flagsChanged = this.extractFlagNamesFromWebsocket(t);
|
||||
await this.retryFetchAll(OpenFeature.getContext(), flagsChanged);
|
||||
}
|
||||
};
|
||||
this._websocket.onclose = async () => {
|
||||
this._logger?.warn(`${GoFeatureFlagWebProvider.name}: Websocket closed, trying to reconnect`);
|
||||
await this.reconnectWebsocket();
|
||||
|
|
@ -167,46 +170,48 @@ export class GoFeatureFlagWebProvider implements Provider {
|
|||
let attempt = 0;
|
||||
while (attempt < this._maxRetries) {
|
||||
attempt++;
|
||||
await this.connectWebsocket()
|
||||
await this.connectWebsocket();
|
||||
if (this._websocket !== undefined && this._websocket.readyState === WebSocket.OPEN) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
delay *= this._retryDelayMultiplier;
|
||||
this._logger?.info(`${GoFeatureFlagWebProvider.name}: error while reconnecting the websocket, next try in ${delay} ms (${attempt}/${this._maxRetries}).`)
|
||||
this._logger?.info(
|
||||
`${GoFeatureFlagWebProvider.name}: error while reconnecting the websocket, next try in ${delay} ms (${attempt}/${this._maxRetries}).`,
|
||||
);
|
||||
}
|
||||
this.events.emit(ProviderEvents.Stale, {
|
||||
message: 'impossible to get status from GO Feature Flag (websocket connection stopped)'
|
||||
message: 'impossible to get status from GO Feature Flag (websocket connection stopped)',
|
||||
});
|
||||
}
|
||||
|
||||
onClose(): Promise<void> {
|
||||
this._websocket?.close(1000, "Closing GO Feature Flag provider");
|
||||
this._websocket?.close(1000, 'Closing GO Feature Flag provider');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async onContextChange(_: EvaluationContext, newContext: EvaluationContext): Promise<void> {
|
||||
this._logger?.debug(`${GoFeatureFlagWebProvider.name}: new context provided: ${newContext}`);
|
||||
this.events.emit(ProviderEvents.Stale, {message: 'context has changed'});
|
||||
this.events.emit(ProviderEvents.Stale, { message: 'context has changed' });
|
||||
await this.retryFetchAll(newContext);
|
||||
this.events.emit(ProviderEvents.Ready, {message: ''});
|
||||
this.events.emit(ProviderEvents.Ready, { message: '' });
|
||||
}
|
||||
|
||||
resolveNumberEvaluation(flagKey: string): ResolutionDetails<number> {
|
||||
return this.evaluate(flagKey, 'number')
|
||||
return this.evaluate(flagKey, 'number');
|
||||
}
|
||||
|
||||
resolveObjectEvaluation<T extends FlagValue>(flagKey: string): ResolutionDetails<T> {
|
||||
return this.evaluate(flagKey, 'object')
|
||||
return this.evaluate(flagKey, 'object');
|
||||
}
|
||||
|
||||
resolveStringEvaluation(flagKey: string): ResolutionDetails<string> {
|
||||
return this.evaluate(flagKey, 'string')
|
||||
return this.evaluate(flagKey, 'string');
|
||||
}
|
||||
|
||||
resolveBooleanEvaluation(flagKey: string): ResolutionDetails<boolean> {
|
||||
return this.evaluate(flagKey, 'boolean')
|
||||
return this.evaluate(flagKey, 'boolean');
|
||||
}
|
||||
|
||||
private evaluate<T extends FlagValue>(flagKey: string, type: string): ResolutionDetails<T> {
|
||||
|
|
@ -236,13 +241,15 @@ export class GoFeatureFlagWebProvider implements Provider {
|
|||
try {
|
||||
await this.fetchAll(ctx, flagsChanged);
|
||||
this._status = ProviderStatus.READY;
|
||||
return
|
||||
return;
|
||||
} catch (err) {
|
||||
this._status = ProviderStatus.ERROR;
|
||||
this.handleFetchErrors(err)
|
||||
this.handleFetchErrors(err);
|
||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||
delay *= this._retryDelayMultiplier;
|
||||
this._logger?.info(`${GoFeatureFlagWebProvider.name}: Waiting ${delay} ms before trying to evaluate the flags (${attempt}/${this._maxRetries}).`)
|
||||
this._logger?.info(
|
||||
`${GoFeatureFlagWebProvider.name}: Waiting ${delay} ms before trying to evaluate the flags (${attempt}/${this._maxRetries}).`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -258,49 +265,51 @@ export class GoFeatureFlagWebProvider implements Provider {
|
|||
private async fetchAll(context: EvaluationContext, flagsChanged: string[] = []) {
|
||||
const endpointURL = new URL(this._endpoint);
|
||||
const path = 'v1/allflags';
|
||||
endpointURL.pathname = endpointURL.pathname.endsWith('/') ? endpointURL.pathname + path : endpointURL.pathname + '/' + path;
|
||||
endpointURL.pathname = endpointURL.pathname.endsWith('/')
|
||||
? endpointURL.pathname + path
|
||||
: endpointURL.pathname + '/' + path;
|
||||
|
||||
const request: GoFeatureFlagAllFlagRequest = {evaluationContext: transformContext(context)};
|
||||
const request: GoFeatureFlagAllFlagRequest = { evaluationContext: transformContext(context) };
|
||||
const headers = new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
});
|
||||
if(this._apiKey){
|
||||
if (this._apiKey) {
|
||||
headers.set('Authorization', `Bearer ${this._apiKey}`);
|
||||
}
|
||||
|
||||
const init: RequestInit = {
|
||||
method: "POST",
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: JSON.stringify(request)
|
||||
body: JSON.stringify(request),
|
||||
};
|
||||
const response = await fetch(endpointURL.toString(), init);
|
||||
|
||||
if(!response?.ok){
|
||||
if (!response?.ok) {
|
||||
throw new FetchError(response.status);
|
||||
}
|
||||
|
||||
const data = await response.json() as GOFeatureFlagAllFlagsResponse;
|
||||
const data = (await response.json()) as GOFeatureFlagAllFlagsResponse;
|
||||
// In case we are in success
|
||||
let flags = {};
|
||||
Object.keys(data.flags).forEach(currentValue => {
|
||||
Object.keys(data.flags).forEach((currentValue) => {
|
||||
const resolved: FlagState<FlagValue> = data.flags[currentValue];
|
||||
const resolutionDetails: ResolutionDetails<FlagValue> = {
|
||||
value: resolved.value,
|
||||
variant: resolved.variationType,
|
||||
errorCode: resolved.errorCode,
|
||||
flagMetadata: resolved.metadata,
|
||||
reason: resolved.reason
|
||||
reason: resolved.reason,
|
||||
};
|
||||
flags = {
|
||||
...flags,
|
||||
[currentValue]: resolutionDetails
|
||||
[currentValue]: resolutionDetails,
|
||||
};
|
||||
});
|
||||
const hasFlagsLoaded = this._flags !== undefined && Object.keys(this._flags).length !== 0
|
||||
const hasFlagsLoaded = this._flags !== undefined && Object.keys(this._flags).length !== 0;
|
||||
this._flags = flags;
|
||||
if (hasFlagsLoaded) {
|
||||
this.events.emit(ProviderEvents.ConfigurationChanged, {
|
||||
|
|
@ -323,9 +332,13 @@ export class GoFeatureFlagWebProvider implements Provider {
|
|||
message: error.message,
|
||||
});
|
||||
if (error.status == 401) {
|
||||
this._logger?.error(`${GoFeatureFlagWebProvider.name}: invalid token used to contact GO Feature Flag instance: ${error}`);
|
||||
this._logger?.error(
|
||||
`${GoFeatureFlagWebProvider.name}: invalid token used to contact GO Feature Flag instance: ${error}`,
|
||||
);
|
||||
} else if (error.status === 404) {
|
||||
this._logger?.error(`${GoFeatureFlagWebProvider.name}: impossible to call go-feature-flag relay proxy ${error}`);
|
||||
this._logger?.error(
|
||||
`${GoFeatureFlagWebProvider.name}: impossible to call go-feature-flag relay proxy ${error}`,
|
||||
);
|
||||
} else {
|
||||
this._logger?.error(`${GoFeatureFlagWebProvider.name}: unknown error while retrieving flags: ${error}`);
|
||||
}
|
||||
|
|
@ -337,4 +350,3 @@ export class GoFeatureFlagWebProvider implements Provider {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {FlagValue, ErrorCode, EvaluationContextValue} from "@openfeature/web-sdk";
|
||||
import { FlagValue, ErrorCode, EvaluationContextValue } from '@openfeature/web-sdk';
|
||||
|
||||
/**
|
||||
* GoFeatureFlagEvaluationContext is the representation of a user for GO Feature Flag
|
||||
|
|
@ -20,7 +20,6 @@ export interface GoFeatureFlagAllFlagRequest {
|
|||
evaluationContext: GoFeatureFlagEvaluationContext;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* GoFeatureFlagProviderOptions is the object containing all the provider options
|
||||
* when initializing the open-feature provider.
|
||||
|
|
@ -52,7 +51,6 @@ export interface GoFeatureFlagWebProviderOptions {
|
|||
maxRetries?: number;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* FlagState is the object used to get the value return by GO Feature Flag.
|
||||
*/
|
||||
|
|
@ -73,15 +71,15 @@ export interface FlagState<T extends FlagValue> {
|
|||
* by GO Feature Flag.
|
||||
*/
|
||||
export interface GOFeatureFlagAllFlagsResponse {
|
||||
valid: boolean
|
||||
flags: Record<string, FlagState<FlagValue>>
|
||||
valid: boolean;
|
||||
flags: Record<string, FlagState<FlagValue>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format of the websocket event we can receive.
|
||||
*/
|
||||
export interface GOFeatureFlagWebsocketResponse {
|
||||
deleted?: { [key: string]: any }
|
||||
added?: { [key: string]: any }
|
||||
updated?: { [key: string]: any }
|
||||
deleted?: { [key: string]: any };
|
||||
added?: { [key: string]: any };
|
||||
updated?: { [key: string]: any };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,7 @@ import { GoFeatureFlagUser } from './model';
|
|||
* @param context - the context used for flag evaluation.
|
||||
* @returns {GoFeatureFlagUser} the user against who we will evaluate the flag.
|
||||
*/
|
||||
export function transformContext(
|
||||
context: EvaluationContext
|
||||
): GoFeatureFlagUser {
|
||||
export function transformContext(context: EvaluationContext): GoFeatureFlagUser {
|
||||
const { targetingKey, ...attributes } = context;
|
||||
|
||||
// If we don't have a targetingKey we are using a hash of the object to build
|
||||
|
|
@ -18,11 +16,7 @@ export function transformContext(
|
|||
|
||||
// Handle the special case of the anonymous field
|
||||
let anonymous = false;
|
||||
if (
|
||||
attributes !== undefined &&
|
||||
attributes !== null &&
|
||||
'anonymous' in attributes
|
||||
) {
|
||||
if (attributes !== undefined && attributes !== null && 'anonymous' in attributes) {
|
||||
if (typeof attributes['anonymous'] === 'boolean') {
|
||||
anonymous = attributes['anonymous'];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,21 +3,16 @@ import {
|
|||
FlagValue,
|
||||
Hook,
|
||||
HookContext,
|
||||
HookHints, Logger, StandardResolutionReasons,
|
||||
HookHints,
|
||||
Logger,
|
||||
StandardResolutionReasons,
|
||||
} from '@openfeature/server-sdk';
|
||||
import {
|
||||
DataCollectorHookOptions,
|
||||
DataCollectorRequest,
|
||||
DataCollectorResponse,
|
||||
FeatureEvent,
|
||||
} from './model';
|
||||
import {copy} from 'copy-anything';
|
||||
import { DataCollectorHookOptions, DataCollectorRequest, DataCollectorResponse, FeatureEvent } from './model';
|
||||
import { copy } from 'copy-anything';
|
||||
import axios from 'axios';
|
||||
|
||||
|
||||
const defaultTargetingKey = 'undefined-targetingKey';
|
||||
export class GoFeatureFlagDataCollectorHook implements Hook {
|
||||
|
||||
// bgSchedulerId contains the id of the setInterval that is running.
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
|
|
@ -54,16 +49,15 @@ export class GoFeatureFlagDataCollectorHook implements Hook {
|
|||
this.collectUnCachedEvaluation = options.collectUnCachedEvaluation;
|
||||
}
|
||||
|
||||
|
||||
init(){
|
||||
this.bgScheduler = setInterval(async () => await this.callGoffDataCollection(), this.dataFlushInterval)
|
||||
this.dataCollectorBuffer = []
|
||||
init() {
|
||||
this.bgScheduler = setInterval(async () => await this.callGoffDataCollection(), this.dataFlushInterval);
|
||||
this.dataCollectorBuffer = [];
|
||||
}
|
||||
|
||||
async close() {
|
||||
clearInterval(this.bgScheduler);
|
||||
// We call the data collector with what is still in the buffer.
|
||||
await this.callGoffDataCollection()
|
||||
await this.callGoffDataCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -78,7 +72,7 @@ export class GoFeatureFlagDataCollectorHook implements Hook {
|
|||
const dataToSend = copy(this.dataCollectorBuffer) || [];
|
||||
this.dataCollectorBuffer = [];
|
||||
|
||||
const request: DataCollectorRequest<boolean> = {events: dataToSend, meta: this.dataCollectorMetadata,}
|
||||
const request: DataCollectorRequest<boolean> = { events: dataToSend, meta: this.dataCollectorMetadata };
|
||||
const endpointURL = new URL(this.endpoint);
|
||||
endpointURL.pathname = 'v1/data/collector';
|
||||
|
||||
|
|
@ -91,19 +85,14 @@ export class GoFeatureFlagDataCollectorHook implements Hook {
|
|||
timeout: this.timeout,
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger?.error(`impossible to send the data to the collector: ${e}`)
|
||||
this.logger?.error(`impossible to send the data to the collector: ${e}`);
|
||||
// if we have an issue calling the collector we put the data back in the buffer
|
||||
this.dataCollectorBuffer = [...this.dataCollectorBuffer, ...dataToSend];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
after(
|
||||
hookContext: HookContext,
|
||||
evaluationDetails: EvaluationDetails<FlagValue>,
|
||||
hookHints?: HookHints
|
||||
) {
|
||||
if (!this.collectUnCachedEvaluation && evaluationDetails.reason !== StandardResolutionReasons.CACHED){
|
||||
after(hookContext: HookContext, evaluationDetails: EvaluationDetails<FlagValue>, hookHints?: HookHints) {
|
||||
if (!this.collectUnCachedEvaluation && evaluationDetails.reason !== StandardResolutionReasons.CACHED) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { ErrorCode, OpenFeatureError } from '@openfeature/server-sdk'
|
||||
import { ErrorCode, OpenFeatureError } from '@openfeature/server-sdk';
|
||||
|
||||
// ProxyNotReady is an error send when we try to call the relay proxy and he is not ready
|
||||
// to return a valid response.
|
||||
export class ProxyNotReady extends OpenFeatureError {
|
||||
code: ErrorCode
|
||||
code: ErrorCode;
|
||||
|
||||
constructor(message: string, originalError: Error) {
|
||||
super(`${message}: ${originalError}`)
|
||||
Object.setPrototypeOf(this, ProxyNotReady.prototype)
|
||||
this.code = ErrorCode.PROVIDER_NOT_READY
|
||||
super(`${message}: ${originalError}`);
|
||||
Object.setPrototypeOf(this, ProxyNotReady.prototype);
|
||||
this.code = ErrorCode.PROVIDER_NOT_READY;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { ErrorCode, OpenFeatureError } from '@openfeature/server-sdk'
|
||||
import { ErrorCode, OpenFeatureError } from '@openfeature/server-sdk';
|
||||
|
||||
// ProxyTimeout is an error send when we try to call the relay proxy and he his not responding
|
||||
// in the appropriate time.
|
||||
export class ProxyTimeout extends OpenFeatureError {
|
||||
code: ErrorCode
|
||||
code: ErrorCode;
|
||||
|
||||
constructor(message: string, originalError: Error) {
|
||||
super(`${message}: ${originalError}`)
|
||||
Object.setPrototypeOf(this, ProxyTimeout.prototype)
|
||||
this.code = ErrorCode.GENERAL
|
||||
super(`${message}: ${originalError}`);
|
||||
Object.setPrototypeOf(this, ProxyTimeout.prototype);
|
||||
this.code = ErrorCode.GENERAL;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { ErrorCode, OpenFeatureError } from '@openfeature/server-sdk'
|
||||
import { ErrorCode, OpenFeatureError } from '@openfeature/server-sdk';
|
||||
|
||||
// Unauthorized is an error sent when the provider makes an unauthorized call to the relay proxy.
|
||||
export class Unauthorized extends OpenFeatureError {
|
||||
code: ErrorCode
|
||||
code: ErrorCode;
|
||||
|
||||
constructor(message: string) {
|
||||
super(message)
|
||||
Object.setPrototypeOf(this, Unauthorized.prototype)
|
||||
this.code = ErrorCode.GENERAL
|
||||
super(message);
|
||||
Object.setPrototypeOf(this, Unauthorized.prototype);
|
||||
this.code = ErrorCode.GENERAL;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
import { ErrorCode, OpenFeatureError } from '@openfeature/server-sdk'
|
||||
import { ErrorCode, OpenFeatureError } from '@openfeature/server-sdk';
|
||||
|
||||
// UnknownError is an error send when something unexpected happened.
|
||||
export class UnknownError extends OpenFeatureError {
|
||||
code: ErrorCode
|
||||
code: ErrorCode;
|
||||
|
||||
constructor(message: string, originalError: Error | unknown) {
|
||||
super(`${message}: ${originalError}`)
|
||||
Object.setPrototypeOf(this, UnknownError.prototype)
|
||||
this.code = ErrorCode.GENERAL
|
||||
super(`${message}: ${originalError}`);
|
||||
Object.setPrototypeOf(this, UnknownError.prototype);
|
||||
this.code = ErrorCode.GENERAL;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,11 @@
|
|||
/**
|
||||
* @jest-environment node
|
||||
*/
|
||||
import {
|
||||
Client,
|
||||
ErrorCode,
|
||||
OpenFeature,
|
||||
ProviderStatus,
|
||||
StandardResolutionReasons,
|
||||
} from '@openfeature/server-sdk';
|
||||
import { Client, ErrorCode, OpenFeature, ProviderStatus, StandardResolutionReasons } from '@openfeature/server-sdk';
|
||||
import axios from 'axios';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import {GoFeatureFlagProvider} from './go-feature-flag-provider';
|
||||
import {GoFeatureFlagProxyResponse} from './model';
|
||||
import { GoFeatureFlagProvider } from './go-feature-flag-provider';
|
||||
import { GoFeatureFlagProxyResponse } from './model';
|
||||
import TestLogger from './test-logger';
|
||||
|
||||
describe('GoFeatureFlagProvider', () => {
|
||||
|
|
@ -48,14 +42,14 @@ describe('GoFeatureFlagProvider', () => {
|
|||
await OpenFeature.close();
|
||||
axiosMock.reset();
|
||||
axiosMock.resetHistory();
|
||||
goff = new GoFeatureFlagProvider({endpoint});
|
||||
goff = new GoFeatureFlagProvider({ endpoint });
|
||||
OpenFeature.setProvider('test-provider', goff);
|
||||
cli = OpenFeature.getClient('test-provider');
|
||||
});
|
||||
|
||||
describe('common usecases and errors', () => {
|
||||
it('should be an instance of GoFeatureFlagProvider', () => {
|
||||
const goff = new GoFeatureFlagProvider({endpoint});
|
||||
const goff = new GoFeatureFlagProvider({ endpoint });
|
||||
expect(goff).toBeInstanceOf(GoFeatureFlagProvider);
|
||||
});
|
||||
it('should throw an error if proxy not ready', async () => {
|
||||
|
|
@ -63,31 +57,31 @@ describe('GoFeatureFlagProvider', () => {
|
|||
const targetingKey = 'user-key';
|
||||
const dns = `${endpoint}v1/feature/${flagName}/eval`;
|
||||
axiosMock.onPost(dns).reply(404);
|
||||
const res = await cli.getBooleanDetails(flagName, false, {targetingKey})
|
||||
const res = await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
const want = {
|
||||
errorCode: ErrorCode.PROVIDER_NOT_READY,
|
||||
errorMessage: 'impossible to call go-feature-flag relay proxy on http://go-feature-flag-relay-proxy.local:1031/v1/feature/random-flag/eval: Error: Request failed with status code 404',
|
||||
errorMessage:
|
||||
'impossible to call go-feature-flag relay proxy on http://go-feature-flag-relay-proxy.local:1031/v1/feature/random-flag/eval: Error: Request failed with status code 404',
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.ERROR,
|
||||
value: false,
|
||||
flagMetadata: {}
|
||||
flagMetadata: {},
|
||||
};
|
||||
expect(res).toEqual(want);
|
||||
});
|
||||
it('should throw an error if the call timeout', async () => {
|
||||
|
||||
const flagName = 'random-flag';
|
||||
const targetingKey = 'user-key';
|
||||
const dns = `${endpoint}v1/feature/${flagName}/eval`;
|
||||
axiosMock.onPost(dns).timeout();
|
||||
const res = await cli.getBooleanDetails(flagName, false, {targetingKey})
|
||||
const res = await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
const want = {
|
||||
errorCode: ErrorCode.GENERAL,
|
||||
errorMessage: 'impossible to retrieve the random-flag on time: Error: timeout of 0ms exceeded',
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.ERROR,
|
||||
value: false,
|
||||
flagMetadata: {}
|
||||
flagMetadata: {},
|
||||
};
|
||||
expect(res).toEqual(want);
|
||||
});
|
||||
|
|
@ -101,14 +95,14 @@ describe('GoFeatureFlagProvider', () => {
|
|||
variationType: 'trueVariation',
|
||||
errorCode: ErrorCode.PROVIDER_NOT_READY,
|
||||
} as GoFeatureFlagProxyResponse<boolean>);
|
||||
const res = await cli.getBooleanDetails(flagName, false, {targetingKey})
|
||||
const res = await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
const want = {
|
||||
errorCode: ErrorCode.PROVIDER_NOT_READY,
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.UNKNOWN,
|
||||
value: true,
|
||||
variant: 'trueVariation',
|
||||
flagMetadata: {}
|
||||
flagMetadata: {},
|
||||
};
|
||||
expect(res).toEqual(want);
|
||||
});
|
||||
|
|
@ -121,14 +115,14 @@ describe('GoFeatureFlagProvider', () => {
|
|||
variationType: 'trueVariation',
|
||||
errorCode: 'NOT-AN-SDK-ERROR',
|
||||
} as unknown as GoFeatureFlagProxyResponse<boolean>);
|
||||
const res = await cli.getBooleanDetails(flagName, false, {targetingKey})
|
||||
const res = await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
const want = {
|
||||
errorCode: ErrorCode.GENERAL,
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.UNKNOWN,
|
||||
value: true,
|
||||
variant: 'trueVariation',
|
||||
flagMetadata: {}
|
||||
flagMetadata: {},
|
||||
};
|
||||
expect(res).toEqual(want);
|
||||
});
|
||||
|
|
@ -138,14 +132,14 @@ describe('GoFeatureFlagProvider', () => {
|
|||
const targetingKey = 'user-key';
|
||||
const dns = `${endpoint}v1/feature/${flagName}/eval`;
|
||||
axiosMock.onPost(dns).networkError();
|
||||
const res = await cli.getBooleanDetails(flagName, false, {targetingKey})
|
||||
const res = await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
const want = {
|
||||
errorCode: ErrorCode.GENERAL,
|
||||
errorMessage: `unknown error while retrieving flag ${flagName} for user ${targetingKey}: Error: Network Error`,
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.ERROR,
|
||||
value: false,
|
||||
flagMetadata: {}
|
||||
flagMetadata: {},
|
||||
};
|
||||
expect(res).toEqual(want);
|
||||
});
|
||||
|
|
@ -159,14 +153,14 @@ describe('GoFeatureFlagProvider', () => {
|
|||
variationType: 'trueVariation',
|
||||
errorCode: ErrorCode.FLAG_NOT_FOUND,
|
||||
} as GoFeatureFlagProxyResponse<string>);
|
||||
const res = await cli.getStringDetails(flagName, 'sdk-default', {targetingKey})
|
||||
const res = await cli.getStringDetails(flagName, 'sdk-default', { targetingKey });
|
||||
const want = {
|
||||
errorCode: ErrorCode.FLAG_NOT_FOUND,
|
||||
errorMessage: `Flag ${flagName} was not found in your configuration`,
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.ERROR,
|
||||
value: 'sdk-default',
|
||||
flagMetadata: {}
|
||||
flagMetadata: {},
|
||||
};
|
||||
expect(res).toEqual(want);
|
||||
});
|
||||
|
|
@ -175,14 +169,14 @@ describe('GoFeatureFlagProvider', () => {
|
|||
const targetingKey = 'user-key';
|
||||
const dns = `${endpoint}v1/feature/${flagName}/eval`;
|
||||
axiosMock.onPost(dns).reply(401, {} as GoFeatureFlagProxyResponse<string>);
|
||||
const res = await cli.getStringDetails(flagName, 'sdk-default', {targetingKey})
|
||||
const res = await cli.getStringDetails(flagName, 'sdk-default', { targetingKey });
|
||||
const want = {
|
||||
errorCode: ErrorCode.GENERAL,
|
||||
errorMessage: 'invalid token used to contact GO Feature Flag relay proxy instance',
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.ERROR,
|
||||
value: 'sdk-default',
|
||||
flagMetadata: {}
|
||||
flagMetadata: {},
|
||||
};
|
||||
expect(res).toEqual(want);
|
||||
});
|
||||
|
|
@ -199,18 +193,18 @@ describe('GoFeatureFlagProvider', () => {
|
|||
trackEvents: true,
|
||||
version: '1.0.0',
|
||||
} as GoFeatureFlagProxyResponse<boolean>);
|
||||
const res = await cli.getBooleanDetails(flagName, false, {targetingKey})
|
||||
const res = await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
const want = {
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.TARGETING_MATCH,
|
||||
value: true,
|
||||
variant: 'trueVariation',
|
||||
flagMetadata: {}
|
||||
flagMetadata: {},
|
||||
};
|
||||
expect(res).toEqual(want);
|
||||
});
|
||||
it('provider should start not ready', async () => {
|
||||
const goff = new GoFeatureFlagProvider({endpoint});
|
||||
const goff = new GoFeatureFlagProvider({ endpoint });
|
||||
expect(goff.status).toEqual(ProviderStatus.NOT_READY);
|
||||
});
|
||||
it('provider should be ready after after setting the provider to Open Feature', async () => {
|
||||
|
|
@ -224,20 +218,23 @@ describe('GoFeatureFlagProvider', () => {
|
|||
axiosMock.onPost(dns).reply(200, validBoolResponse);
|
||||
axiosMock.onPost(dataCollectorEndpoint).reply(500, {});
|
||||
|
||||
const goff = new GoFeatureFlagProvider({
|
||||
endpoint,
|
||||
flagCacheTTL: 3000,
|
||||
flagCacheSize: 100,
|
||||
dataFlushInterval: 2000, // in milliseconds
|
||||
}, testLogger)
|
||||
const goff = new GoFeatureFlagProvider(
|
||||
{
|
||||
endpoint,
|
||||
flagCacheTTL: 3000,
|
||||
flagCacheSize: 100,
|
||||
dataFlushInterval: 2000, // in milliseconds
|
||||
},
|
||||
testLogger,
|
||||
);
|
||||
|
||||
expect(goff.status).toEqual(ProviderStatus.NOT_READY);
|
||||
const got = await goff.resolveBooleanEvaluation(flagName, false, {targetingKey});
|
||||
const got = await goff.resolveBooleanEvaluation(flagName, false, { targetingKey });
|
||||
const want = {
|
||||
errorCode: ErrorCode.PROVIDER_NOT_READY,
|
||||
errorMessage: 'Provider in a status that does not allow to serve flag: NOT_READY',
|
||||
reason: StandardResolutionReasons.ERROR,
|
||||
value: false
|
||||
value: false,
|
||||
};
|
||||
expect(got).toEqual(want);
|
||||
});
|
||||
|
|
@ -253,14 +250,14 @@ describe('GoFeatureFlagProvider', () => {
|
|||
value: 'true',
|
||||
variationType: 'trueVariation',
|
||||
} as GoFeatureFlagProxyResponse<string>);
|
||||
const res = await cli.getBooleanDetails(flagName, false, {targetingKey})
|
||||
const res = await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
const want = {
|
||||
errorCode: ErrorCode.TYPE_MISMATCH,
|
||||
errorMessage: 'Flag value random-flag had unexpected type string, expected boolean.',
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.ERROR,
|
||||
value: false,
|
||||
flagMetadata: {}
|
||||
flagMetadata: {},
|
||||
};
|
||||
expect(res).toEqual(want);
|
||||
});
|
||||
|
|
@ -279,13 +276,13 @@ describe('GoFeatureFlagProvider', () => {
|
|||
version: '1.0.0',
|
||||
} as GoFeatureFlagProxyResponse<boolean>);
|
||||
|
||||
const res = await cli.getBooleanDetails(flagName, false, {targetingKey})
|
||||
const res = await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
const want = {
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.TARGETING_MATCH,
|
||||
value: true,
|
||||
flagMetadata: {},
|
||||
variant: 'trueVariation'
|
||||
variant: 'trueVariation',
|
||||
};
|
||||
expect(res).toEqual(want);
|
||||
});
|
||||
|
|
@ -303,13 +300,13 @@ describe('GoFeatureFlagProvider', () => {
|
|||
version: '1.0.0',
|
||||
} as GoFeatureFlagProxyResponse<boolean>);
|
||||
|
||||
const res = await cli.getBooleanDetails(flagName, false, {targetingKey})
|
||||
const res = await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
const want = {
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.SPLIT,
|
||||
value: true,
|
||||
flagMetadata: {},
|
||||
variant: 'trueVariation'
|
||||
variant: 'trueVariation',
|
||||
};
|
||||
expect(res).toEqual(want);
|
||||
});
|
||||
|
|
@ -326,7 +323,7 @@ describe('GoFeatureFlagProvider', () => {
|
|||
trackEvents: true,
|
||||
version: '1.0.0',
|
||||
} as GoFeatureFlagProxyResponse<boolean>);
|
||||
const res = await cli.getBooleanDetails(flagName, false, {targetingKey})
|
||||
const res = await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
const want = {
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.DISABLED,
|
||||
|
|
@ -347,11 +344,11 @@ describe('GoFeatureFlagProvider', () => {
|
|||
variationType: 'trueVariation',
|
||||
} as GoFeatureFlagProxyResponse<boolean>);
|
||||
|
||||
const res = await cli.getStringDetails(flagName, 'false', {targetingKey})
|
||||
const res = await cli.getStringDetails(flagName, 'false', { targetingKey });
|
||||
const want = {
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.ERROR,
|
||||
errorMessage:`Flag value ${flagName} had unexpected type boolean, expected string.`,
|
||||
errorMessage: `Flag value ${flagName} had unexpected type boolean, expected string.`,
|
||||
errorCode: ErrorCode.TYPE_MISMATCH,
|
||||
value: 'false',
|
||||
flagMetadata: {},
|
||||
|
|
@ -372,7 +369,7 @@ describe('GoFeatureFlagProvider', () => {
|
|||
version: '1.0.0',
|
||||
} as GoFeatureFlagProxyResponse<string>);
|
||||
|
||||
const res = await cli.getStringDetails(flagName, 'default', {targetingKey})
|
||||
const res = await cli.getStringDetails(flagName, 'default', { targetingKey });
|
||||
const want = {
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.TARGETING_MATCH,
|
||||
|
|
@ -396,7 +393,7 @@ describe('GoFeatureFlagProvider', () => {
|
|||
version: '1.0.0',
|
||||
} as GoFeatureFlagProxyResponse<string>);
|
||||
|
||||
const res = await cli.getStringDetails(flagName, 'default', {targetingKey})
|
||||
const res = await cli.getStringDetails(flagName, 'default', { targetingKey });
|
||||
const want = {
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.SPLIT,
|
||||
|
|
@ -420,7 +417,7 @@ describe('GoFeatureFlagProvider', () => {
|
|||
version: '1.0.0',
|
||||
} as GoFeatureFlagProxyResponse<string>);
|
||||
|
||||
const res = await cli.getStringDetails(flagName, 'randomDefaultValue', {targetingKey})
|
||||
const res = await cli.getStringDetails(flagName, 'randomDefaultValue', { targetingKey });
|
||||
const want = {
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.DISABLED,
|
||||
|
|
@ -441,7 +438,7 @@ describe('GoFeatureFlagProvider', () => {
|
|||
variationType: 'trueVariation',
|
||||
} as GoFeatureFlagProxyResponse<boolean>);
|
||||
|
||||
const res = await cli.getNumberDetails(flagName, 14, {targetingKey})
|
||||
const res = await cli.getNumberDetails(flagName, 14, { targetingKey });
|
||||
const want = {
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.ERROR,
|
||||
|
|
@ -466,12 +463,12 @@ describe('GoFeatureFlagProvider', () => {
|
|||
version: '1.0.0',
|
||||
} as GoFeatureFlagProxyResponse<number>);
|
||||
|
||||
const res = await cli.getNumberDetails(flagName, 14, {targetingKey})
|
||||
const res = await cli.getNumberDetails(flagName, 14, { targetingKey });
|
||||
const want = {
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.TARGETING_MATCH,
|
||||
value: 14,
|
||||
variant:'trueVariation',
|
||||
variant: 'trueVariation',
|
||||
flagMetadata: {},
|
||||
};
|
||||
expect(res).toEqual(want);
|
||||
|
|
@ -490,12 +487,12 @@ describe('GoFeatureFlagProvider', () => {
|
|||
version: '1.0.0',
|
||||
} as GoFeatureFlagProxyResponse<number>);
|
||||
|
||||
const res = await cli.getNumberDetails(flagName, 14, {targetingKey})
|
||||
const res = await cli.getNumberDetails(flagName, 14, { targetingKey });
|
||||
const want = {
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.SPLIT,
|
||||
value: 14,
|
||||
variant:'trueVariation',
|
||||
variant: 'trueVariation',
|
||||
flagMetadata: {},
|
||||
};
|
||||
expect(res).toEqual(want);
|
||||
|
|
@ -514,7 +511,7 @@ describe('GoFeatureFlagProvider', () => {
|
|||
version: '1.0.0',
|
||||
} as GoFeatureFlagProxyResponse<number>);
|
||||
|
||||
const res = await cli.getNumberDetails(flagName, 14, {targetingKey})
|
||||
const res = await cli.getNumberDetails(flagName, 14, { targetingKey });
|
||||
const want = {
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.DISABLED,
|
||||
|
|
@ -535,7 +532,7 @@ describe('GoFeatureFlagProvider', () => {
|
|||
variationType: 'trueVariation',
|
||||
} as GoFeatureFlagProxyResponse<boolean>);
|
||||
|
||||
const res = await cli.getObjectDetails(flagName, {}, {targetingKey})
|
||||
const res = await cli.getObjectDetails(flagName, {}, { targetingKey });
|
||||
const want = {
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.ERROR,
|
||||
|
|
@ -552,7 +549,7 @@ describe('GoFeatureFlagProvider', () => {
|
|||
const dns = `${endpoint}v1/feature/${flagName}/eval`;
|
||||
|
||||
axiosMock.onPost(dns).reply(200, {
|
||||
value: {key: true},
|
||||
value: { key: true },
|
||||
variationType: 'trueVariation',
|
||||
reason: StandardResolutionReasons.TARGETING_MATCH,
|
||||
failed: false,
|
||||
|
|
@ -560,13 +557,13 @@ describe('GoFeatureFlagProvider', () => {
|
|||
version: '1.0.0',
|
||||
} as GoFeatureFlagProxyResponse<object>);
|
||||
|
||||
const res = await cli.getObjectDetails(flagName, {key: 'default'}, {targetingKey})
|
||||
const res = await cli.getObjectDetails(flagName, { key: 'default' }, { targetingKey });
|
||||
const want = {
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.TARGETING_MATCH,
|
||||
value: {key: true},
|
||||
value: { key: true },
|
||||
flagMetadata: {},
|
||||
variant: 'trueVariation'
|
||||
variant: 'trueVariation',
|
||||
};
|
||||
expect(res).toEqual(want);
|
||||
});
|
||||
|
|
@ -576,7 +573,7 @@ describe('GoFeatureFlagProvider', () => {
|
|||
const dns = `${endpoint}v1/feature/${flagName}/eval`;
|
||||
|
||||
axiosMock.onPost(dns).reply(200, {
|
||||
value: {key: true},
|
||||
value: { key: true },
|
||||
variationType: 'trueVariation',
|
||||
reason: StandardResolutionReasons.SPLIT,
|
||||
failed: false,
|
||||
|
|
@ -584,13 +581,13 @@ describe('GoFeatureFlagProvider', () => {
|
|||
version: '1.0.0',
|
||||
} as GoFeatureFlagProxyResponse<object>);
|
||||
|
||||
const res = await cli.getObjectDetails(flagName, {key: 'default'}, {targetingKey})
|
||||
const res = await cli.getObjectDetails(flagName, { key: 'default' }, { targetingKey });
|
||||
const want = {
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.SPLIT,
|
||||
value: {key: true},
|
||||
value: { key: true },
|
||||
flagMetadata: {},
|
||||
variant: 'trueVariation'
|
||||
variant: 'trueVariation',
|
||||
};
|
||||
expect(res).toEqual(want);
|
||||
});
|
||||
|
|
@ -600,7 +597,7 @@ describe('GoFeatureFlagProvider', () => {
|
|||
const dns = `${endpoint}v1/feature/${flagName}/eval`;
|
||||
|
||||
axiosMock.onPost(dns).reply(200, {
|
||||
value: {key: 123},
|
||||
value: { key: 123 },
|
||||
variationType: 'defaultSdk',
|
||||
reason: StandardResolutionReasons.DISABLED,
|
||||
failed: false,
|
||||
|
|
@ -608,11 +605,11 @@ describe('GoFeatureFlagProvider', () => {
|
|||
version: '1.0.0',
|
||||
} as GoFeatureFlagProxyResponse<object>);
|
||||
|
||||
const res = await cli.getObjectDetails(flagName, {key: 'default'}, {targetingKey})
|
||||
const res = await cli.getObjectDetails(flagName, { key: 'default' }, { targetingKey });
|
||||
const want = {
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.DISABLED,
|
||||
value: {key: 'default'},
|
||||
value: { key: 'default' },
|
||||
flagMetadata: {},
|
||||
};
|
||||
expect(res).toEqual(want);
|
||||
|
|
@ -631,13 +628,13 @@ describe('GoFeatureFlagProvider', () => {
|
|||
version: '1.0.0',
|
||||
} as GoFeatureFlagProxyResponse<object>);
|
||||
|
||||
const res = await cli.getObjectDetails(flagName, {key: 'default'}, {targetingKey})
|
||||
const res = await cli.getObjectDetails(flagName, { key: 'default' }, { targetingKey });
|
||||
const want = {
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.TARGETING_MATCH,
|
||||
value: ['1', '2'],
|
||||
flagMetadata: {},
|
||||
variant: 'trueVariation'
|
||||
variant: 'trueVariation',
|
||||
};
|
||||
expect(res).toEqual(want);
|
||||
});
|
||||
|
|
@ -655,14 +652,13 @@ describe('GoFeatureFlagProvider', () => {
|
|||
version: '1.0.0',
|
||||
} as GoFeatureFlagProxyResponse<object>);
|
||||
|
||||
|
||||
const res = await cli.getObjectDetails(flagName, {key: 'default'}, {targetingKey})
|
||||
const res = await cli.getObjectDetails(flagName, { key: 'default' }, { targetingKey });
|
||||
const want = {
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.SPLIT,
|
||||
value: ['1', '2'],
|
||||
flagMetadata: {},
|
||||
variant: 'trueVariation'
|
||||
variant: 'trueVariation',
|
||||
};
|
||||
expect(res).toEqual(want);
|
||||
});
|
||||
|
|
@ -680,7 +676,7 @@ describe('GoFeatureFlagProvider', () => {
|
|||
version: '1.0.0',
|
||||
} as GoFeatureFlagProxyResponse<object>);
|
||||
|
||||
const res = await cli.getObjectDetails(flagName, ['key', '124'], {targetingKey})
|
||||
const res = await cli.getObjectDetails(flagName, ['key', '124'], { targetingKey });
|
||||
const want = {
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.DISABLED,
|
||||
|
|
@ -708,7 +704,7 @@ describe('GoFeatureFlagProvider', () => {
|
|||
cacheable: true,
|
||||
} as GoFeatureFlagProxyResponse<boolean>);
|
||||
|
||||
const res = await cli.getBooleanDetails(flagName, false, {targetingKey})
|
||||
const res = await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
const want = {
|
||||
flagKey: flagName,
|
||||
reason: StandardResolutionReasons.TARGETING_MATCH,
|
||||
|
|
@ -734,11 +730,11 @@ describe('GoFeatureFlagProvider', () => {
|
|||
flagCacheTTL: 3000,
|
||||
flagCacheSize: 1,
|
||||
disableDataCollection: true,
|
||||
})
|
||||
});
|
||||
OpenFeature.setProvider('test-provider-cache', goff);
|
||||
const cli = OpenFeature.getClient('test-provider-cache');
|
||||
const got1 = await cli.getBooleanDetails(flagName, false, {targetingKey});
|
||||
const got2 = await cli.getBooleanDetails(flagName, false, {targetingKey});
|
||||
const got1 = await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
const got2 = await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
expect(got1.reason).toEqual(StandardResolutionReasons.TARGETING_MATCH);
|
||||
expect(got2.reason).toEqual(StandardResolutionReasons.CACHED);
|
||||
expect(axiosMock.history['post'].length).toBe(1);
|
||||
|
|
@ -754,11 +750,11 @@ describe('GoFeatureFlagProvider', () => {
|
|||
endpoint,
|
||||
disableCache: true,
|
||||
disableDataCollection: true,
|
||||
})
|
||||
});
|
||||
OpenFeature.setProvider('test-provider-cache', goff);
|
||||
const cli = OpenFeature.getClient('test-provider-cache');
|
||||
const got1 = await cli.getBooleanDetails(flagName, false, {targetingKey});
|
||||
const got2 = await cli.getBooleanDetails(flagName, false, {targetingKey});
|
||||
const got1 = await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
const got2 = await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
expect(got1).toEqual(got2);
|
||||
expect(axiosMock.history['post'].length).toBe(2);
|
||||
});
|
||||
|
|
@ -775,12 +771,12 @@ describe('GoFeatureFlagProvider', () => {
|
|||
endpoint,
|
||||
flagCacheSize: 1,
|
||||
disableDataCollection: true,
|
||||
})
|
||||
});
|
||||
OpenFeature.setProvider('test-provider-cache', goff);
|
||||
const cli = OpenFeature.getClient('test-provider-cache');
|
||||
await cli.getBooleanDetails(flagName1, false, {targetingKey});
|
||||
await cli.getBooleanDetails(flagName2, false, {targetingKey});
|
||||
await cli.getBooleanDetails(flagName1, false, {targetingKey});
|
||||
await cli.getBooleanDetails(flagName1, false, { targetingKey });
|
||||
await cli.getBooleanDetails(flagName2, false, { targetingKey });
|
||||
await cli.getBooleanDetails(flagName1, false, { targetingKey });
|
||||
expect(axiosMock.history['post'].length).toBe(3);
|
||||
});
|
||||
|
||||
|
|
@ -788,16 +784,16 @@ describe('GoFeatureFlagProvider', () => {
|
|||
const flagName = 'random-flag';
|
||||
const targetingKey = 'user-key';
|
||||
const dns1 = `${endpoint}v1/feature/${flagName}/eval`;
|
||||
axiosMock.onPost(dns1).reply(200, {...validBoolResponse, cacheable: false});
|
||||
axiosMock.onPost(dns1).reply(200, { ...validBoolResponse, cacheable: false });
|
||||
const goff = new GoFeatureFlagProvider({
|
||||
endpoint,
|
||||
flagCacheSize: 1,
|
||||
disableDataCollection: true,
|
||||
})
|
||||
});
|
||||
OpenFeature.setProvider('test-provider-cache', goff);
|
||||
const cli = OpenFeature.getClient('test-provider-cache');
|
||||
await cli.getBooleanDetails(flagName, false, {targetingKey});
|
||||
await cli.getBooleanDetails(flagName, false, {targetingKey});
|
||||
await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
expect(axiosMock.history['post'].length).toBe(2);
|
||||
});
|
||||
|
||||
|
|
@ -805,18 +801,18 @@ describe('GoFeatureFlagProvider', () => {
|
|||
const flagName = 'random-flag';
|
||||
const targetingKey = 'user-key';
|
||||
const dns1 = `${endpoint}v1/feature/${flagName}/eval`;
|
||||
axiosMock.onPost(dns1).reply(200, {...validBoolResponse});
|
||||
axiosMock.onPost(dns1).reply(200, { ...validBoolResponse });
|
||||
const goff = new GoFeatureFlagProvider({
|
||||
endpoint,
|
||||
flagCacheSize: 1,
|
||||
disableDataCollection: true,
|
||||
flagCacheTTL: 200,
|
||||
})
|
||||
OpenFeature.setProvider('test-provider-cache',goff);
|
||||
});
|
||||
OpenFeature.setProvider('test-provider-cache', goff);
|
||||
const cli = OpenFeature.getClient('test-provider-cache');
|
||||
await cli.getBooleanDetails(flagName, false, {targetingKey});
|
||||
await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
await cli.getBooleanDetails(flagName, false, {targetingKey});
|
||||
await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
expect(axiosMock.history['post'].length).toBe(2);
|
||||
});
|
||||
|
||||
|
|
@ -832,11 +828,11 @@ describe('GoFeatureFlagProvider', () => {
|
|||
endpoint,
|
||||
flagCacheSize: 1,
|
||||
disableDataCollection: true,
|
||||
})
|
||||
});
|
||||
OpenFeature.setProvider('test-provider-cache', goff);
|
||||
const cli = OpenFeature.getClient('test-provider-cache');
|
||||
await cli.getBooleanDetails(flagName1, false, {targetingKey});
|
||||
await cli.getBooleanDetails(flagName2, false, {targetingKey});
|
||||
await cli.getBooleanDetails(flagName1, false, { targetingKey });
|
||||
await cli.getBooleanDetails(flagName2, false, { targetingKey });
|
||||
expect(axiosMock.history['post'].length).toBe(2);
|
||||
});
|
||||
it('should not retrieve from the cache if context properties are different but same targeting key', async () => {
|
||||
|
|
@ -848,11 +844,11 @@ describe('GoFeatureFlagProvider', () => {
|
|||
endpoint,
|
||||
flagCacheSize: 1,
|
||||
disableDataCollection: true,
|
||||
})
|
||||
});
|
||||
OpenFeature.setProvider('test-provider-cache', goff);
|
||||
const cli = OpenFeature.getClient('test-provider-cache');
|
||||
await cli.getBooleanDetails(flagName1, false, {targetingKey, email: 'foo.bar@gofeatureflag.org'});
|
||||
await cli.getBooleanDetails(flagName1, false, {targetingKey, email: 'bar.foo@gofeatureflag.org'});
|
||||
await cli.getBooleanDetails(flagName1, false, { targetingKey, email: 'foo.bar@gofeatureflag.org' });
|
||||
await cli.getBooleanDetails(flagName1, false, { targetingKey, email: 'bar.foo@gofeatureflag.org' });
|
||||
expect(axiosMock.history['post'].length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
|
@ -868,28 +864,31 @@ describe('GoFeatureFlagProvider', () => {
|
|||
flagCacheTTL: 3000,
|
||||
flagCacheSize: 100,
|
||||
dataFlushInterval: 1000, // in milliseconds
|
||||
})
|
||||
});
|
||||
const providerName = expect.getState().currentTestName || 'test';
|
||||
OpenFeature.setProvider(providerName, goff);
|
||||
const cli = OpenFeature.getClient(providerName);
|
||||
await cli.getBooleanDetails(flagName, false, {targetingKey});
|
||||
await cli.getBooleanDetails(flagName, false, {targetingKey});
|
||||
await OpenFeature.close()
|
||||
const collectorCalls = axiosMock.history['post'].filter(i => i.url === dataCollectorEndpoint);
|
||||
await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
await OpenFeature.close();
|
||||
const collectorCalls = axiosMock.history['post'].filter((i) => i.url === dataCollectorEndpoint);
|
||||
expect(collectorCalls.length).toBe(1);
|
||||
const got = JSON.parse(collectorCalls[0].data);
|
||||
expect(isNaN(got.events[0].creationDate)).toBe(false);
|
||||
const want = {
|
||||
events: [{
|
||||
contextKind: 'user',
|
||||
kind: 'feature',
|
||||
creationDate: got.events[0].creationDate,
|
||||
default: false,
|
||||
key: 'random-flag',
|
||||
value: true,
|
||||
variation: 'trueVariation',
|
||||
userKey: 'user-key'
|
||||
}], meta: {provider: 'open-feature-js-sdk'}
|
||||
events: [
|
||||
{
|
||||
contextKind: 'user',
|
||||
kind: 'feature',
|
||||
creationDate: got.events[0].creationDate,
|
||||
default: false,
|
||||
key: 'random-flag',
|
||||
value: true,
|
||||
variation: 'trueVariation',
|
||||
userKey: 'user-key',
|
||||
},
|
||||
],
|
||||
meta: { provider: 'open-feature-js-sdk' },
|
||||
};
|
||||
expect(want).toEqual(got);
|
||||
});
|
||||
|
|
@ -905,14 +904,14 @@ describe('GoFeatureFlagProvider', () => {
|
|||
flagCacheTTL: 3000,
|
||||
flagCacheSize: 100,
|
||||
dataFlushInterval: 100, // in milliseconds
|
||||
})
|
||||
});
|
||||
const providerName = expect.getState().currentTestName || 'test';
|
||||
OpenFeature.setProvider(providerName, goff);
|
||||
const cli = OpenFeature.getClient(providerName);
|
||||
await cli.getBooleanDetails(flagName, false, {targetingKey});
|
||||
await cli.getBooleanDetails(flagName, false, {targetingKey});
|
||||
await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
await new Promise((r) => setTimeout(r, 130));
|
||||
const collectorCalls = axiosMock.history['post'].filter(i => i.url === dataCollectorEndpoint);
|
||||
const collectorCalls = axiosMock.history['post'].filter((i) => i.url === dataCollectorEndpoint);
|
||||
expect(collectorCalls.length).toBe(1);
|
||||
});
|
||||
|
||||
|
|
@ -927,20 +926,20 @@ describe('GoFeatureFlagProvider', () => {
|
|||
flagCacheTTL: 3000,
|
||||
flagCacheSize: 100,
|
||||
dataFlushInterval: 100, // in milliseconds
|
||||
})
|
||||
});
|
||||
const providerName = expect.getState().currentTestName || 'test';
|
||||
OpenFeature.setProvider(providerName, goff);
|
||||
const cli = OpenFeature.getClient(providerName);
|
||||
await cli.getBooleanDetails(flagName, false, {targetingKey});
|
||||
await cli.getBooleanDetails(flagName, false, {targetingKey});
|
||||
await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
await new Promise((r) => setTimeout(r, 130));
|
||||
const collectorCalls = axiosMock.history['post'].filter(i => i.url === dataCollectorEndpoint);
|
||||
const collectorCalls = axiosMock.history['post'].filter((i) => i.url === dataCollectorEndpoint);
|
||||
expect(collectorCalls.length).toBe(1);
|
||||
axiosMock.resetHistory();
|
||||
await cli.getBooleanDetails(flagName, false, {targetingKey});
|
||||
await cli.getBooleanDetails(flagName, false, {targetingKey});
|
||||
await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
await new Promise((r) => setTimeout(r, 130));
|
||||
const collectorCalls2 = axiosMock.history['post'].filter(i => i.url === dataCollectorEndpoint);
|
||||
const collectorCalls2 = axiosMock.history['post'].filter((i) => i.url === dataCollectorEndpoint);
|
||||
expect(collectorCalls2.length).toBe(1);
|
||||
});
|
||||
|
||||
|
|
@ -955,14 +954,14 @@ describe('GoFeatureFlagProvider', () => {
|
|||
flagCacheTTL: 3000,
|
||||
flagCacheSize: 100,
|
||||
dataFlushInterval: 200, // in milliseconds
|
||||
})
|
||||
});
|
||||
const providerName = expect.getState().currentTestName || 'test';
|
||||
OpenFeature.setProvider(providerName, goff);
|
||||
const cli = OpenFeature.getClient(providerName);
|
||||
await cli.getBooleanDetails(flagName, false, {targetingKey});
|
||||
await cli.getBooleanDetails(flagName, false, {targetingKey});
|
||||
await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
await new Promise((r) => setTimeout(r, 130));
|
||||
const collectorCalls = axiosMock.history['post'].filter(i => i.url === dataCollectorEndpoint);
|
||||
const collectorCalls = axiosMock.history['post'].filter((i) => i.url === dataCollectorEndpoint);
|
||||
|
||||
expect(collectorCalls.length).toBe(0);
|
||||
});
|
||||
|
|
@ -975,21 +974,26 @@ describe('GoFeatureFlagProvider', () => {
|
|||
axiosMock.onPost(dns).reply(200, validBoolResponse);
|
||||
axiosMock.onPost(dataCollectorEndpoint).reply(500, {});
|
||||
|
||||
const goff = new GoFeatureFlagProvider({
|
||||
endpoint,
|
||||
flagCacheTTL: 3000,
|
||||
flagCacheSize: 100,
|
||||
dataFlushInterval: 2000, // in milliseconds
|
||||
}, testLogger)
|
||||
const goff = new GoFeatureFlagProvider(
|
||||
{
|
||||
endpoint,
|
||||
flagCacheTTL: 3000,
|
||||
flagCacheSize: 100,
|
||||
dataFlushInterval: 2000, // in milliseconds
|
||||
},
|
||||
testLogger,
|
||||
);
|
||||
const providerName = expect.getState().currentTestName || 'test';
|
||||
OpenFeature.setProvider(providerName, goff);
|
||||
const cli = OpenFeature.getClient(providerName);
|
||||
await cli.getBooleanDetails(flagName, false, {targetingKey});
|
||||
await cli.getBooleanDetails(flagName, false, {targetingKey});
|
||||
await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
await cli.getBooleanDetails(flagName, false, { targetingKey });
|
||||
await OpenFeature.close();
|
||||
|
||||
expect(testLogger.inMemoryLogger['error'].length).toBe(1);
|
||||
expect(testLogger.inMemoryLogger['error']).toContain('impossible to send the data to the collector: Error: Request failed with status code 500')
|
||||
expect(testLogger.inMemoryLogger['error']).toContain(
|
||||
'impossible to send the data to the collector: Error: Request failed with status code 500',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,19 +12,19 @@ import {
|
|||
TypeMismatchError,
|
||||
} from '@openfeature/server-sdk';
|
||||
import axios from 'axios';
|
||||
import {transformContext} from './context-transformer';
|
||||
import {ProxyNotReady} from './errors/proxyNotReady';
|
||||
import {ProxyTimeout} from './errors/proxyTimeout';
|
||||
import {UnknownError} from './errors/unknownError';
|
||||
import {Unauthorized} from './errors/unauthorized';
|
||||
import {LRUCache} from 'lru-cache';
|
||||
import { transformContext } from './context-transformer';
|
||||
import { ProxyNotReady } from './errors/proxyNotReady';
|
||||
import { ProxyTimeout } from './errors/proxyTimeout';
|
||||
import { UnknownError } from './errors/unknownError';
|
||||
import { Unauthorized } from './errors/unauthorized';
|
||||
import { LRUCache } from 'lru-cache';
|
||||
import {
|
||||
GoFeatureFlagProviderOptions,
|
||||
GoFeatureFlagProxyRequest,
|
||||
GoFeatureFlagProxyResponse,
|
||||
GoFeatureFlagUser,
|
||||
} from './model';
|
||||
import {GoFeatureFlagDataCollectorHook} from './data-collector-hook';
|
||||
import { GoFeatureFlagDataCollectorHook } from './data-collector-hook';
|
||||
import hash from 'object-hash';
|
||||
|
||||
// GoFeatureFlagProvider is the official Open-feature provider for GO Feature Flag.
|
||||
|
|
@ -63,12 +63,15 @@ export class GoFeatureFlagProvider implements Provider {
|
|||
this.timeout = options.timeout || 0; // default is 0 = no timeout
|
||||
this.endpoint = options.endpoint;
|
||||
|
||||
if (!options.disableDataCollection){
|
||||
this.dataCollectorHook = new GoFeatureFlagDataCollectorHook({
|
||||
endpoint: this.endpoint,
|
||||
timeout: this.timeout,
|
||||
dataFlushInterval: options.dataFlushInterval
|
||||
}, logger);
|
||||
if (!options.disableDataCollection) {
|
||||
this.dataCollectorHook = new GoFeatureFlagDataCollectorHook(
|
||||
{
|
||||
endpoint: this.endpoint,
|
||||
timeout: this.timeout,
|
||||
dataFlushInterval: options.dataFlushInterval,
|
||||
},
|
||||
logger,
|
||||
);
|
||||
this.hooks = [this.dataCollectorHook];
|
||||
}
|
||||
|
||||
|
|
@ -83,8 +86,9 @@ export class GoFeatureFlagProvider implements Provider {
|
|||
}
|
||||
|
||||
if (!options.disableCache) {
|
||||
const cacheSize = options.flagCacheSize !== undefined && options.flagCacheSize !== 0 ? options.flagCacheSize : 10000;
|
||||
this.cache = new LRUCache({maxSize: cacheSize, sizeCalculation: () => 1});
|
||||
const cacheSize =
|
||||
options.flagCacheSize !== undefined && options.flagCacheSize !== 0 ? options.flagCacheSize : 10000;
|
||||
this.cache = new LRUCache({ maxSize: cacheSize, sizeCalculation: () => 1 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,12 +98,12 @@ export class GoFeatureFlagProvider implements Provider {
|
|||
*/
|
||||
async initialize() {
|
||||
if (!this.disableDataCollection) {
|
||||
this.dataCollectorHook?.init()
|
||||
this.dataCollectorHook?.init();
|
||||
}
|
||||
this._status = ProviderStatus.READY;
|
||||
}
|
||||
|
||||
get status(){
|
||||
get status() {
|
||||
return this._status;
|
||||
}
|
||||
|
||||
|
|
@ -126,13 +130,13 @@ export class GoFeatureFlagProvider implements Provider {
|
|||
async resolveBooleanEvaluation(
|
||||
flagKey: string,
|
||||
defaultValue: boolean,
|
||||
context: EvaluationContext
|
||||
context: EvaluationContext,
|
||||
): Promise<ResolutionDetails<boolean>> {
|
||||
return this.resolveEvaluationGoFeatureFlagProxy<boolean>(
|
||||
flagKey,
|
||||
defaultValue,
|
||||
transformContext(context),
|
||||
'boolean'
|
||||
'boolean',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -151,14 +155,9 @@ export class GoFeatureFlagProvider implements Provider {
|
|||
async resolveStringEvaluation(
|
||||
flagKey: string,
|
||||
defaultValue: string,
|
||||
context: EvaluationContext
|
||||
context: EvaluationContext,
|
||||
): Promise<ResolutionDetails<string>> {
|
||||
return this.resolveEvaluationGoFeatureFlagProxy<string>(
|
||||
flagKey,
|
||||
defaultValue,
|
||||
transformContext(context),
|
||||
'string'
|
||||
);
|
||||
return this.resolveEvaluationGoFeatureFlagProxy<string>(flagKey, defaultValue, transformContext(context), 'string');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -176,14 +175,9 @@ export class GoFeatureFlagProvider implements Provider {
|
|||
async resolveNumberEvaluation(
|
||||
flagKey: string,
|
||||
defaultValue: number,
|
||||
context: EvaluationContext
|
||||
context: EvaluationContext,
|
||||
): Promise<ResolutionDetails<number>> {
|
||||
return this.resolveEvaluationGoFeatureFlagProxy<number>(
|
||||
flagKey,
|
||||
defaultValue,
|
||||
transformContext(context),
|
||||
'number'
|
||||
);
|
||||
return this.resolveEvaluationGoFeatureFlagProxy<number>(flagKey, defaultValue, transformContext(context), 'number');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -201,14 +195,9 @@ export class GoFeatureFlagProvider implements Provider {
|
|||
async resolveObjectEvaluation<U extends JsonValue>(
|
||||
flagKey: string,
|
||||
defaultValue: U,
|
||||
context: EvaluationContext
|
||||
context: EvaluationContext,
|
||||
): Promise<ResolutionDetails<U>> {
|
||||
return this.resolveEvaluationGoFeatureFlagProxy<U>(
|
||||
flagKey,
|
||||
defaultValue,
|
||||
transformContext(context),
|
||||
'object'
|
||||
);
|
||||
return this.resolveEvaluationGoFeatureFlagProxy<U>(flagKey, defaultValue, transformContext(context), 'object');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -230,10 +219,10 @@ export class GoFeatureFlagProvider implements Provider {
|
|||
flagKey: string,
|
||||
defaultValue: T,
|
||||
user: GoFeatureFlagUser,
|
||||
expectedType: string
|
||||
expectedType: string,
|
||||
): Promise<ResolutionDetails<T>> {
|
||||
// Check if the provider is ready to serve
|
||||
if(this._status === ProviderStatus.NOT_READY){
|
||||
if (this._status === ProviderStatus.NOT_READY) {
|
||||
return {
|
||||
value: defaultValue,
|
||||
reason: StandardResolutionReasons.ERROR,
|
||||
|
|
@ -252,7 +241,7 @@ export class GoFeatureFlagProvider implements Provider {
|
|||
}
|
||||
}
|
||||
|
||||
const request: GoFeatureFlagProxyRequest<T> = {user, defaultValue};
|
||||
const request: GoFeatureFlagProxyRequest<T> = { user, defaultValue };
|
||||
// build URL to access to the endpoint
|
||||
const endpointURL = new URL(this.endpoint);
|
||||
endpointURL.pathname = `v1/feature/${flagKey}/eval`;
|
||||
|
|
@ -272,49 +261,35 @@ export class GoFeatureFlagProvider implements Provider {
|
|||
throw new Unauthorized('invalid token used to contact GO Feature Flag relay proxy instance');
|
||||
}
|
||||
// Impossible to contact the relay-proxy
|
||||
if (
|
||||
axios.isAxiosError(error) &&
|
||||
(error.code === 'ECONNREFUSED' || error.response?.status === 404)
|
||||
) {
|
||||
throw new ProxyNotReady(
|
||||
`impossible to call go-feature-flag relay proxy on ${endpointURL}`,
|
||||
error
|
||||
);
|
||||
if (axios.isAxiosError(error) && (error.code === 'ECONNREFUSED' || error.response?.status === 404)) {
|
||||
throw new ProxyNotReady(`impossible to call go-feature-flag relay proxy on ${endpointURL}`, error);
|
||||
}
|
||||
|
||||
// Timeout when calling the API
|
||||
if (axios.isAxiosError(error) && error.code === 'ECONNABORTED') {
|
||||
throw new ProxyTimeout(
|
||||
`impossible to retrieve the ${flagKey} on time`,
|
||||
error
|
||||
);
|
||||
throw new ProxyTimeout(`impossible to retrieve the ${flagKey} on time`, error);
|
||||
}
|
||||
|
||||
throw new UnknownError(
|
||||
`unknown error while retrieving flag ${flagKey} for user ${user.key}`,
|
||||
error
|
||||
);
|
||||
throw new UnknownError(`unknown error while retrieving flag ${flagKey} for user ${user.key}`, error);
|
||||
}
|
||||
|
||||
// Check that we received the expectedType
|
||||
if (typeof apiResponseData.value !== expectedType) {
|
||||
throw new TypeMismatchError(
|
||||
`Flag value ${flagKey} had unexpected type ${typeof apiResponseData.value}, expected ${expectedType}.`
|
||||
`Flag value ${flagKey} had unexpected type ${typeof apiResponseData.value}, expected ${expectedType}.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Case of the flag is not found
|
||||
if (apiResponseData.errorCode === ErrorCode.FLAG_NOT_FOUND) {
|
||||
throw new FlagNotFoundError(
|
||||
`Flag ${flagKey} was not found in your configuration`
|
||||
);
|
||||
throw new FlagNotFoundError(`Flag ${flagKey} was not found in your configuration`);
|
||||
}
|
||||
|
||||
// Case of the flag is disabled
|
||||
if (apiResponseData.reason === StandardResolutionReasons.DISABLED) {
|
||||
// we don't set a variant since we are using the default value, and we are not able to know
|
||||
// which variant it is.
|
||||
return {value: defaultValue, reason: apiResponseData.reason};
|
||||
return { value: defaultValue, reason: apiResponseData.reason };
|
||||
}
|
||||
|
||||
const sdkResponse: ResolutionDetails<T> = {
|
||||
|
|
@ -331,9 +306,9 @@ export class GoFeatureFlagProvider implements Provider {
|
|||
|
||||
if (this.cache !== undefined && apiResponseData.cacheable) {
|
||||
if (this.cacheTTL === -1) {
|
||||
this.cache.set(cacheKey, sdkResponse)
|
||||
this.cache.set(cacheKey, sdkResponse);
|
||||
} else {
|
||||
this.cache.set(cacheKey, sdkResponse, {ttl: this.cacheTTL})
|
||||
this.cache.set(cacheKey, sdkResponse, { ttl: this.cacheTTL });
|
||||
}
|
||||
}
|
||||
return sdkResponse;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,4 @@
|
|||
import {
|
||||
ErrorCode,
|
||||
EvaluationContextValue,
|
||||
} from '@openfeature/server-sdk';
|
||||
import { ErrorCode, EvaluationContextValue } from '@openfeature/server-sdk';
|
||||
|
||||
/**
|
||||
* GoFeatureFlagUser is the representation of a user for GO Feature Flag
|
||||
|
|
@ -57,25 +54,25 @@ export interface GoFeatureFlagProviderOptions {
|
|||
apiKey?: string;
|
||||
|
||||
// disableCache (optional) set to true if you would like that every flag evaluation goes to the GO Feature Flag directly.
|
||||
disableCache?: boolean
|
||||
disableCache?: boolean;
|
||||
|
||||
// flagCacheSize (optional) is the maximum number of flag events we keep in memory to cache your flags.
|
||||
// default: 10000
|
||||
flagCacheSize?: number
|
||||
flagCacheSize?: number;
|
||||
|
||||
// flagCacheTTL (optional) is the time we keep the evaluation in the cache before we consider it as obsolete.
|
||||
// If you want to keep the value forever you can set the FlagCacheTTL field to -1
|
||||
// default: 1 minute
|
||||
flagCacheTTL?: number
|
||||
flagCacheTTL?: number;
|
||||
|
||||
// dataFlushInterval (optional) interval time (in millisecond) we use to call the relay proxy to collect data.
|
||||
// The parameter is used only if the cache is enabled, otherwise the collection of the data is done directly
|
||||
// when calling the evaluation API.
|
||||
// default: 1 minute
|
||||
dataFlushInterval?: number
|
||||
dataFlushInterval?: number;
|
||||
|
||||
// disableDataCollection set to true if you don't want to collect the usage of flags retrieved in the cache.
|
||||
disableDataCollection?: boolean
|
||||
disableDataCollection?: boolean;
|
||||
}
|
||||
|
||||
// GOFeatureFlagResolutionReasons allows to extends resolution reasons
|
||||
|
|
@ -84,7 +81,6 @@ export declare enum GOFeatureFlagResolutionReasons {}
|
|||
// GOFeatureFlagErrorCode allows to extends error codes
|
||||
export declare enum GOFeatureFlagErrorCode {}
|
||||
|
||||
|
||||
export interface DataCollectorRequest<T> {
|
||||
events: FeatureEvent<T>[];
|
||||
meta: Record<string, string>;
|
||||
|
|
|
|||
|
|
@ -19,21 +19,21 @@ const logger: TestLogger = new TestLogger();
|
|||
const testFlagKey = 'a-key';
|
||||
|
||||
describe('LaunchDarklyClientProvider', () => {
|
||||
let ldProvider: LaunchDarklyClientProvider
|
||||
let ldProvider: LaunchDarklyClientProvider;
|
||||
let ofClient: Client;
|
||||
const ldClientMock: jest.Mocked<LDClient> = {
|
||||
variationDetail: jest.fn(),
|
||||
identify: jest.fn(),
|
||||
waitForInitialization: jest.fn(),
|
||||
on: jest.fn(),
|
||||
close: jest.fn(),
|
||||
} as unknown as jest.Mocked<LDClient>;
|
||||
variationDetail: jest.fn(),
|
||||
identify: jest.fn(),
|
||||
waitForInitialization: jest.fn(),
|
||||
on: jest.fn(),
|
||||
close: jest.fn(),
|
||||
} as unknown as jest.Mocked<LDClient>;
|
||||
|
||||
beforeAll(() => {
|
||||
ldProvider = new LaunchDarklyClientProvider('test-env-key', { logger });
|
||||
OpenFeature.setProvider(ldProvider);
|
||||
ofClient = OpenFeature.getClient();
|
||||
})
|
||||
});
|
||||
beforeEach(() => {
|
||||
logger.reset();
|
||||
jest.clearAllMocks();
|
||||
|
|
@ -85,7 +85,7 @@ describe('LaunchDarklyClientProvider', () => {
|
|||
it('should set the status to READY if initialization succeeds', async () => {
|
||||
ldClientMock.waitForInitialization.mockResolvedValue();
|
||||
await provider.initialize();
|
||||
expect( ldClientMock.waitForInitialization).toHaveBeenCalledTimes(1);
|
||||
expect(ldClientMock.waitForInitialization).toHaveBeenCalledTimes(1);
|
||||
expect(provider.status).toBe('READY');
|
||||
});
|
||||
|
||||
|
|
@ -94,7 +94,6 @@ describe('LaunchDarklyClientProvider', () => {
|
|||
await provider.initialize();
|
||||
expect(provider.status).toBe('ERROR');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('resolveBooleanEvaluation', () => {
|
||||
|
|
@ -106,12 +105,10 @@ describe('LaunchDarklyClientProvider', () => {
|
|||
},
|
||||
});
|
||||
ofClient.getBooleanDetails(testFlagKey, false);
|
||||
expect(ldClientMock.variationDetail)
|
||||
.toHaveBeenCalledWith(testFlagKey, false);
|
||||
expect(ldClientMock.variationDetail).toHaveBeenCalledWith(testFlagKey, false);
|
||||
jest.clearAllMocks();
|
||||
ofClient.getBooleanValue(testFlagKey, false);
|
||||
expect(ldClientMock.variationDetail)
|
||||
.toHaveBeenCalledWith(testFlagKey, false);
|
||||
expect(ldClientMock.variationDetail).toHaveBeenCalledWith(testFlagKey, false);
|
||||
});
|
||||
|
||||
it('handles correct return types for boolean variations', () => {
|
||||
|
|
@ -150,7 +147,6 @@ describe('LaunchDarklyClientProvider', () => {
|
|||
errorCode: 'TYPE_MISMATCH',
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('resolveNumberEvaluation', () => {
|
||||
|
|
@ -163,12 +159,10 @@ describe('LaunchDarklyClientProvider', () => {
|
|||
});
|
||||
|
||||
ofClient.getNumberDetails(testFlagKey, 0);
|
||||
expect(ldClientMock.variationDetail)
|
||||
.toHaveBeenCalledWith(testFlagKey, 0);
|
||||
expect(ldClientMock.variationDetail).toHaveBeenCalledWith(testFlagKey, 0);
|
||||
jest.clearAllMocks();
|
||||
ofClient.getNumberValue(testFlagKey, 0);
|
||||
expect(ldClientMock.variationDetail)
|
||||
.toHaveBeenCalledWith(testFlagKey, 0);
|
||||
expect(ldClientMock.variationDetail).toHaveBeenCalledWith(testFlagKey, 0);
|
||||
});
|
||||
|
||||
it('handles correct return types for numeric variations', () => {
|
||||
|
|
@ -219,12 +213,10 @@ describe('LaunchDarklyClientProvider', () => {
|
|||
});
|
||||
|
||||
ofClient.getObjectDetails(testFlagKey, {});
|
||||
expect(ldClientMock.variationDetail)
|
||||
.toHaveBeenCalledWith(testFlagKey, {});
|
||||
expect(ldClientMock.variationDetail).toHaveBeenCalledWith(testFlagKey, {});
|
||||
jest.clearAllMocks();
|
||||
ofClient.getObjectValue(testFlagKey, {});
|
||||
expect(ldClientMock.variationDetail)
|
||||
.toHaveBeenCalledWith(testFlagKey, {});
|
||||
expect(ldClientMock.variationDetail).toHaveBeenCalledWith(testFlagKey, {});
|
||||
});
|
||||
|
||||
it('handles correct return types for object variations', () => {
|
||||
|
|
@ -263,9 +255,9 @@ describe('LaunchDarklyClientProvider', () => {
|
|||
errorCode: 'TYPE_MISMATCH',
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe('resolveStringEvaluation', ( ) => {
|
||||
describe('resolveStringEvaluation', () => {
|
||||
it('calls the client correctly for string variations', () => {
|
||||
ldClientMock.variationDetail = jest.fn().mockReturnValue({
|
||||
value: 'test',
|
||||
|
|
@ -275,12 +267,10 @@ describe('LaunchDarklyClientProvider', () => {
|
|||
});
|
||||
|
||||
ofClient.getStringDetails(testFlagKey, 'default');
|
||||
expect(ldClientMock.variationDetail)
|
||||
.toHaveBeenCalledWith(testFlagKey, 'default');
|
||||
expect(ldClientMock.variationDetail).toHaveBeenCalledWith(testFlagKey, 'default');
|
||||
jest.clearAllMocks();
|
||||
ofClient.getStringValue(testFlagKey, 'default');
|
||||
expect(ldClientMock.variationDetail)
|
||||
.toHaveBeenCalledWith(testFlagKey, 'default');
|
||||
expect(ldClientMock.variationDetail).toHaveBeenCalledWith(testFlagKey, 'default');
|
||||
});
|
||||
|
||||
it('handles correct return types for string variations', () => {
|
||||
|
|
@ -310,7 +300,7 @@ describe('LaunchDarklyClientProvider', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const res = ofClient.getStringDetails(testFlagKey, 'default');
|
||||
const res = ofClient.getStringDetails(testFlagKey, 'default');
|
||||
expect(res).toEqual({
|
||||
flagKey: testFlagKey,
|
||||
flagMetadata: {},
|
||||
|
|
@ -371,17 +361,20 @@ describe('LaunchDarklyClientProvider', () => {
|
|||
it('logs information about missing keys', async () => {
|
||||
ldClientMock.identify = jest.fn().mockResolvedValue({});
|
||||
await OpenFeature.setContext({});
|
||||
expect(ldClientMock.identify).toHaveBeenCalledWith(translateContext(logger, {}))
|
||||
expect(logger.logs[0]).toEqual("The EvaluationContext must contain either a 'targetingKey' "
|
||||
+ "or a 'key' and the type must be a string.");
|
||||
expect(ldClientMock.identify).toHaveBeenCalledWith(translateContext(logger, {}));
|
||||
expect(logger.logs[0]).toEqual(
|
||||
"The EvaluationContext must contain either a 'targetingKey' " + "or a 'key' and the type must be a string.",
|
||||
);
|
||||
});
|
||||
|
||||
it('logs information about double keys', async () => {
|
||||
ldClientMock.identify = jest.fn().mockResolvedValue({});
|
||||
await OpenFeature.setContext({ targetingKey: '1', key: '2' });
|
||||
expect(ldClientMock.identify).toHaveBeenCalledWith(translateContext(logger, { targetingKey: '1', key: '2' }))
|
||||
expect(logger.logs[0]).toEqual("The EvaluationContext contained both a 'targetingKey' and a"
|
||||
+ " 'key' attribute. The 'key' attribute will be discarded.");
|
||||
expect(ldClientMock.identify).toHaveBeenCalledWith(translateContext(logger, { targetingKey: '1', key: '2' }));
|
||||
expect(logger.logs[0]).toEqual(
|
||||
"The EvaluationContext contained both a 'targetingKey' and a" +
|
||||
" 'key' attribute. The 'key' attribute will be discarded.",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
GeneralError,
|
||||
OpenFeatureEventEmitter,
|
||||
ProviderEvents,
|
||||
ProviderStatus
|
||||
ProviderStatus,
|
||||
} from '@openfeature/web-sdk';
|
||||
|
||||
import isEmpty from 'lodash.isempty';
|
||||
|
|
@ -21,7 +21,6 @@ import { LaunchDarklyProviderOptions } from './launchdarkly-provider-options';
|
|||
import translateContext from './translate-context';
|
||||
import translateResult from './translate-result';
|
||||
|
||||
|
||||
/**
|
||||
* Create a ResolutionDetails for an evaluation that produced a type different
|
||||
* from the expected type.
|
||||
|
|
@ -63,14 +62,14 @@ export class LaunchDarklyClientProvider implements Provider {
|
|||
|
||||
constructor(
|
||||
private readonly envKey: string,
|
||||
{ logger, ...ldOptions }: LaunchDarklyProviderOptions) {
|
||||
{ logger, ...ldOptions }: LaunchDarklyProviderOptions,
|
||||
) {
|
||||
if (logger) {
|
||||
this.logger = logger;
|
||||
} else {
|
||||
this.logger = basicLogger({ level: 'info' });
|
||||
}
|
||||
this.ldOptions = { ...ldOptions, logger: this.logger };
|
||||
|
||||
}
|
||||
|
||||
private get client(): LDClient {
|
||||
|
|
|
|||
|
|
@ -26,10 +26,7 @@ describe('translateContext', () => {
|
|||
|
||||
it('gives targetingKey precedence over key', () => {
|
||||
const logger = new TestLogger();
|
||||
expect(translateContext(
|
||||
logger,
|
||||
{ targetingKey: 'target-key', key: 'key-key' },
|
||||
)).toEqual({
|
||||
expect(translateContext(logger, { targetingKey: 'target-key', key: 'key-key' })).toEqual({
|
||||
key: 'target-key',
|
||||
kind: 'user',
|
||||
});
|
||||
|
|
@ -49,10 +46,7 @@ describe('translateContext', () => {
|
|||
])('given correct built-in attributes', (key, value) => {
|
||||
const logger = new TestLogger();
|
||||
it('translates the key correctly', () => {
|
||||
expect(translateContext(
|
||||
logger,
|
||||
{ targetingKey: 'the-key', [key]: value },
|
||||
)).toEqual({
|
||||
expect(translateContext(logger, { targetingKey: 'the-key', [key]: value })).toEqual({
|
||||
key: 'the-key',
|
||||
[key]: value,
|
||||
kind: 'user',
|
||||
|
|
@ -63,10 +57,7 @@ describe('translateContext', () => {
|
|||
|
||||
it.each(['key', 'targetingKey'])('handles key or targetingKey', (key) => {
|
||||
const logger = new TestLogger();
|
||||
expect(translateContext(
|
||||
logger,
|
||||
{ [key]: 'the-key' },
|
||||
)).toEqual({
|
||||
expect(translateContext(logger, { [key]: 'the-key' })).toEqual({
|
||||
key: 'the-key',
|
||||
kind: 'user',
|
||||
});
|
||||
|
|
@ -79,10 +70,7 @@ describe('translateContext', () => {
|
|||
])('given incorrect built-in attributes', (key, value) => {
|
||||
it('the bad key is omitted', () => {
|
||||
const logger = new TestLogger();
|
||||
expect(translateContext(
|
||||
logger,
|
||||
{ targetingKey: 'the-key', [key]: value },
|
||||
)).toEqual({
|
||||
expect(translateContext(logger, { targetingKey: 'the-key', [key]: value })).toEqual({
|
||||
key: 'the-key',
|
||||
kind: 'user',
|
||||
});
|
||||
|
|
@ -102,12 +90,14 @@ describe('translateContext', () => {
|
|||
|
||||
it('accepts string/boolean/number arrays', () => {
|
||||
const logger = new TestLogger();
|
||||
expect(translateContext(logger, {
|
||||
targetingKey: 'the-key',
|
||||
strings: ['a', 'b', 'c'],
|
||||
numbers: [1, 2, 3],
|
||||
booleans: [true, false],
|
||||
})).toEqual({
|
||||
expect(
|
||||
translateContext(logger, {
|
||||
targetingKey: 'the-key',
|
||||
strings: ['a', 'b', 'c'],
|
||||
numbers: [1, 2, 3],
|
||||
booleans: [true, false],
|
||||
}),
|
||||
).toEqual({
|
||||
key: 'the-key',
|
||||
kind: 'user',
|
||||
strings: ['a', 'b', 'c'],
|
||||
|
|
@ -120,10 +110,7 @@ describe('translateContext', () => {
|
|||
it('converts date to ISO strings', () => {
|
||||
const date = new Date();
|
||||
const logger = new TestLogger();
|
||||
expect(translateContext(
|
||||
logger,
|
||||
{ targetingKey: 'the-key', date },
|
||||
)).toEqual({
|
||||
expect(translateContext(logger, { targetingKey: 'the-key', date })).toEqual({
|
||||
key: 'the-key',
|
||||
kind: 'user',
|
||||
date: date.toISOString(),
|
||||
|
|
|
|||
|
|
@ -30,19 +30,15 @@ const LDContextBuiltIns = {
|
|||
* @param object Object to place the value in.
|
||||
* @param visited Carry visited keys of the object
|
||||
*/
|
||||
function convertAttributes(
|
||||
logger: LDLogger,
|
||||
key: string,
|
||||
value: any,
|
||||
object: any,
|
||||
visited: any[],
|
||||
): any {
|
||||
function convertAttributes(logger: LDLogger, key: string, value: any, object: any, visited: any[]): any {
|
||||
if (visited.includes(value)) {
|
||||
// Prevent cycles by not visiting the same object
|
||||
// with in the same branch. Different branches
|
||||
// may contain the same object.
|
||||
logger.error('Detected a cycle within the evaluation context. The '
|
||||
+ 'affected part of the context will not be included in evaluation.');
|
||||
logger.error(
|
||||
'Detected a cycle within the evaluation context. The ' +
|
||||
'affected part of the context will not be included in evaluation.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
// This method is recursively populating objects, so we are intentionally
|
||||
|
|
@ -81,13 +77,16 @@ function translateContextCommon(
|
|||
const finalKey = inTargetingKey ?? keyAttr;
|
||||
|
||||
if (keyAttr != null && inTargetingKey != null) {
|
||||
logger.warn("The EvaluationContext contained both a 'targetingKey' and a 'key' attribute. The"
|
||||
+ " 'key' attribute will be discarded.");
|
||||
logger.warn(
|
||||
"The EvaluationContext contained both a 'targetingKey' and a 'key' attribute. The" +
|
||||
" 'key' attribute will be discarded.",
|
||||
);
|
||||
}
|
||||
|
||||
if (finalKey == null) {
|
||||
logger.error("The EvaluationContext must contain either a 'targetingKey' or a 'key' and the "
|
||||
+ 'type must be a string.');
|
||||
logger.error(
|
||||
"The EvaluationContext must contain either a 'targetingKey' or a 'key' and the " + 'type must be a string.',
|
||||
);
|
||||
}
|
||||
|
||||
const convertedContext: LDContextCommon = { key: finalKey };
|
||||
|
|
@ -101,7 +100,7 @@ function translateContextCommon(
|
|||
privateAttributes: value as string[],
|
||||
};
|
||||
} else if (key in LDContextBuiltIns) {
|
||||
const typedKey = key as 'name'| 'anonymous';
|
||||
const typedKey = key as 'name' | 'anonymous';
|
||||
if (typeof value === LDContextBuiltIns[typedKey]) {
|
||||
convertedContext[key] = value;
|
||||
} else {
|
||||
|
|
@ -124,31 +123,24 @@ function translateContextCommon(
|
|||
*
|
||||
* @internal
|
||||
*/
|
||||
export default function translateContext(
|
||||
logger: LDLogger,
|
||||
evalContext: EvaluationContext,
|
||||
): LDContext {
|
||||
export default function translateContext(logger: LDLogger, evalContext: EvaluationContext): LDContext {
|
||||
let finalKind = 'user';
|
||||
|
||||
// A multi-context.
|
||||
if (evalContext['kind'] === 'multi') {
|
||||
return Object.entries(evalContext)
|
||||
.reduce((acc: any, [key, value]: [string, EvaluationContextValue]) => {
|
||||
if (key === 'kind') {
|
||||
acc.kind = value;
|
||||
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
const valueRecord = value as Record<string, EvaluationContextValue>;
|
||||
acc[key] = translateContextCommon(
|
||||
logger,
|
||||
valueRecord,
|
||||
valueRecord['targetingKey'] as string,
|
||||
);
|
||||
} else {
|
||||
logger.error('Top level attributes in a multi-kind context should be Structure types.');
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
} if (evalContext['kind'] !== undefined && typeof evalContext['kind'] === 'string') {
|
||||
return Object.entries(evalContext).reduce((acc: any, [key, value]: [string, EvaluationContextValue]) => {
|
||||
if (key === 'kind') {
|
||||
acc.kind = value;
|
||||
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
const valueRecord = value as Record<string, EvaluationContextValue>;
|
||||
acc[key] = translateContextCommon(logger, valueRecord, valueRecord['targetingKey'] as string);
|
||||
} else {
|
||||
logger.error('Top level attributes in a multi-kind context should be Structure types.');
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
if (evalContext['kind'] !== undefined && typeof evalContext['kind'] === 'string') {
|
||||
// Single context with specified kind.
|
||||
finalKind = evalContext['kind'];
|
||||
} else if (evalContext['kind'] !== undefined && typeof evalContext['kind'] !== 'string') {
|
||||
|
|
|
|||
|
|
@ -1,45 +1,43 @@
|
|||
import translateResult from './translate-result';
|
||||
|
||||
describe('translateResult', () => {
|
||||
it.each([
|
||||
true,
|
||||
'potato',
|
||||
42,
|
||||
{ yes: 'no' },
|
||||
])('puts the value into the result.', (value) => {
|
||||
expect(translateResult<typeof value>({
|
||||
value,
|
||||
reason: {
|
||||
kind: 'OFF',
|
||||
},
|
||||
}).value).toEqual(value);
|
||||
it.each([true, 'potato', 42, { yes: 'no' }])('puts the value into the result.', (value) => {
|
||||
expect(
|
||||
translateResult<typeof value>({
|
||||
value,
|
||||
reason: {
|
||||
kind: 'OFF',
|
||||
},
|
||||
}).value,
|
||||
).toEqual(value);
|
||||
});
|
||||
|
||||
it('converts the variationIndex into a string variant', () => {
|
||||
expect(translateResult<boolean>({
|
||||
value: true,
|
||||
variationIndex: 9,
|
||||
reason: {
|
||||
kind: 'OFF',
|
||||
},
|
||||
}).variant).toEqual('9');
|
||||
expect(
|
||||
translateResult<boolean>({
|
||||
value: true,
|
||||
variationIndex: 9,
|
||||
reason: {
|
||||
kind: 'OFF',
|
||||
},
|
||||
}).variant,
|
||||
).toEqual('9');
|
||||
});
|
||||
|
||||
it.each([
|
||||
'OFF',
|
||||
'FALLTHROUGH',
|
||||
'TARGET_MATCH',
|
||||
'PREREQUISITE_FAILED',
|
||||
'ERROR',
|
||||
])('populates the resolution reason', (reason) => {
|
||||
expect(translateResult<boolean>({
|
||||
value: true,
|
||||
variationIndex: 9,
|
||||
reason: {
|
||||
kind: reason,
|
||||
},
|
||||
}).reason).toEqual(reason);
|
||||
});
|
||||
it.each(['OFF', 'FALLTHROUGH', 'TARGET_MATCH', 'PREREQUISITE_FAILED', 'ERROR'])(
|
||||
'populates the resolution reason',
|
||||
(reason) => {
|
||||
expect(
|
||||
translateResult<boolean>({
|
||||
value: true,
|
||||
variationIndex: 9,
|
||||
reason: {
|
||||
kind: reason,
|
||||
},
|
||||
}).reason,
|
||||
).toEqual(reason);
|
||||
},
|
||||
);
|
||||
|
||||
it('does not populate the errorCode when there is not an error', () => {
|
||||
const translated = translateResult<boolean>({
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {FeatureFlag, Flag} from './feature-flag';
|
||||
import { FeatureFlag, Flag } from './feature-flag';
|
||||
|
||||
describe('Flagd flag structure', () => {
|
||||
it('should be constructed with valid input - boolean', () => {
|
||||
|
|
@ -66,7 +66,7 @@ describe('Flagd flag structure', () => {
|
|||
expect(ff.state).toBe('ENABLED');
|
||||
expect(ff.defaultVariant).toBe('pi2');
|
||||
expect(ff.targeting).toBe('');
|
||||
expect(ff.variants.get('pi2')).toStrictEqual({value: 3.14, accuracy: 2});
|
||||
expect(ff.variants.get('pi5')).toStrictEqual({value: 3.14159, accuracy: 5});
|
||||
expect(ff.variants.get('pi2')).toStrictEqual({ value: 3.14, accuracy: 2 });
|
||||
expect(ff.variants.get('pi5')).toStrictEqual({ value: 3.14159, accuracy: 5 });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import {FlagValue} from '@openfeature/core';
|
||||
import { FlagValue } from '@openfeature/core';
|
||||
|
||||
/**
|
||||
* Flagd flag configuration structure mapping to schema definition.
|
||||
*/
|
||||
export interface Flag {
|
||||
state: "ENABLED" | "DISABLED";
|
||||
state: 'ENABLED' | 'DISABLED';
|
||||
defaultVariant: string;
|
||||
variants: { [key: string]: FlagValue };
|
||||
targeting?: string;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import {FlagdCore} from './flagd-core';
|
||||
import {GeneralError, StandardResolutionReasons, TypeMismatchError} from '@openfeature/core';
|
||||
import { FlagdCore } from './flagd-core';
|
||||
import { GeneralError, StandardResolutionReasons, TypeMismatchError } from '@openfeature/core';
|
||||
|
||||
describe('flagd-core resolving', () => {
|
||||
const flagCfg = `{"flags":{"myBoolFlag":{"state":"ENABLED","variants":{"on":true,"off":false},"defaultVariant":"on"},"myStringFlag":{"state":"ENABLED","variants":{"key1":"val1","key2":"val2"},"defaultVariant":"key1"},"myFloatFlag":{"state":"ENABLED","variants":{"one":1.23,"two":2.34},"defaultVariant":"one"},"myIntFlag":{"state":"ENABLED","variants":{"one":1,"two":2},"defaultVariant":"one"},"myObjectFlag":{"state":"ENABLED","variants":{"object1":{"key":"val"},"object2":{"key":true}},"defaultVariant":"object1"},"fibAlgo":{"variants":{"recursive":"recursive","memo":"memo","loop":"loop","binet":"binet"},"defaultVariant":"recursive","state":"ENABLED","targeting":{"if":[{"$ref":"emailWithFaas"},"binet",null]}},"targetedFlag":{"variants":{"first":"AAA","second":"BBB","third":"CCC"},"defaultVariant":"first","state":"ENABLED","targeting":{"if":[{"in":["@openfeature.dev",{"var":"email"}]},"second",{"in":["Chrome",{"var":"userAgent"}]},"third",null]}}},"$evaluators":{"emailWithFaas":{"in":["@faas.com",{"var":["email"]}]}}}`;
|
||||
|
|
@ -14,63 +14,61 @@ describe('flagd-core resolving', () => {
|
|||
const resolved = core.resolveBooleanEvaluation('myBoolFlag', false, {}, console);
|
||||
expect(resolved.value).toBeTruthy();
|
||||
expect(resolved.reason).toBe(StandardResolutionReasons.STATIC);
|
||||
expect(resolved.variant).toBe("on")
|
||||
expect(resolved.variant).toBe('on');
|
||||
});
|
||||
|
||||
it('should resolve string flag', () => {
|
||||
const resolved = core.resolveStringEvaluation('myStringFlag', 'key2', {}, console);
|
||||
expect(resolved.value).toBe('val1');
|
||||
expect(resolved.reason).toBe(StandardResolutionReasons.STATIC);
|
||||
expect(resolved.variant).toBe("key1")
|
||||
expect(resolved.variant).toBe('key1');
|
||||
});
|
||||
|
||||
it('should resolve number flag', () => {
|
||||
const resolved = core.resolveNumberEvaluation('myFloatFlag', 2.34, {}, console);
|
||||
expect(resolved.value).toBe(1.23);
|
||||
expect(resolved.reason).toBe(StandardResolutionReasons.STATIC);
|
||||
expect(resolved.variant).toBe("one")
|
||||
expect(resolved.variant).toBe('one');
|
||||
});
|
||||
|
||||
it('should resolve object flag', () => {
|
||||
const resolved = core.resolveObjectEvaluation('myObjectFlag', {key: true}, {}, console);
|
||||
expect(resolved.value).toStrictEqual({key: 'val'});
|
||||
const resolved = core.resolveObjectEvaluation('myObjectFlag', { key: true }, {}, console);
|
||||
expect(resolved.value).toStrictEqual({ key: 'val' });
|
||||
expect(resolved.reason).toBe(StandardResolutionReasons.STATIC);
|
||||
expect(resolved.variant).toBe("object1")
|
||||
expect(resolved.variant).toBe('object1');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('flagd-core targeting evaluations', () => {
|
||||
|
||||
const targetingFlag = '{"flags":{"targetedFlag":{"variants":{"first":"AAA","second":"BBB","third":"CCC"},"defaultVariant":"first","state":"ENABLED","targeting":{"if":[{"in":["@openfeature.dev",{"var":"email"}]},"second",null]}},"shortCircuit":{"variants":{"true":true,"false":false},"defaultVariant":"false","state":"ENABLED","targeting":{"==":[{"var":"favoriteNumber"},1]}}}}';
|
||||
const targetingFlag =
|
||||
'{"flags":{"targetedFlag":{"variants":{"first":"AAA","second":"BBB","third":"CCC"},"defaultVariant":"first","state":"ENABLED","targeting":{"if":[{"in":["@openfeature.dev",{"var":"email"}]},"second",null]}},"shortCircuit":{"variants":{"true":true,"false":false},"defaultVariant":"false","state":"ENABLED","targeting":{"==":[{"var":"favoriteNumber"},1]}}}}';
|
||||
let core: FlagdCore;
|
||||
|
||||
beforeAll(() => {
|
||||
core = new FlagdCore();
|
||||
core.setConfigurations(targetingFlag)
|
||||
core.setConfigurations(targetingFlag);
|
||||
});
|
||||
|
||||
it('should resolve for correct inputs', () => {
|
||||
const resolved = core.resolveStringEvaluation("targetedFlag", "none", {email: "admin@openfeature.dev"}, console);
|
||||
expect(resolved.value).toBe("BBB")
|
||||
expect(resolved.reason).toBe(StandardResolutionReasons.TARGETING_MATCH)
|
||||
expect(resolved.variant).toBe("second")
|
||||
const resolved = core.resolveStringEvaluation('targetedFlag', 'none', { email: 'admin@openfeature.dev' }, console);
|
||||
expect(resolved.value).toBe('BBB');
|
||||
expect(resolved.reason).toBe(StandardResolutionReasons.TARGETING_MATCH);
|
||||
expect(resolved.variant).toBe('second');
|
||||
});
|
||||
|
||||
it('should fallback to default - missing targeting context data', () => {
|
||||
const resolved = core.resolveStringEvaluation("targetedFlag", "none", {}, console);
|
||||
expect(resolved.value).toBe("AAA")
|
||||
expect(resolved.reason).toBe(StandardResolutionReasons.DEFAULT)
|
||||
expect(resolved.variant).toBe("first")
|
||||
const resolved = core.resolveStringEvaluation('targetedFlag', 'none', {}, console);
|
||||
expect(resolved.value).toBe('AAA');
|
||||
expect(resolved.reason).toBe(StandardResolutionReasons.DEFAULT);
|
||||
expect(resolved.variant).toBe('first');
|
||||
});
|
||||
|
||||
it('should handle short circuit fallbacks', () => {
|
||||
const resolved = core.resolveBooleanEvaluation("shortCircuit", false, {favoriteNumber: 1}, console);
|
||||
expect(resolved.value).toBe(true)
|
||||
expect(resolved.reason).toBe(StandardResolutionReasons.TARGETING_MATCH)
|
||||
expect(resolved.variant).toBe("true")
|
||||
const resolved = core.resolveBooleanEvaluation('shortCircuit', false, { favoriteNumber: 1 }, console);
|
||||
expect(resolved.value).toBe(true);
|
||||
expect(resolved.reason).toBe(StandardResolutionReasons.TARGETING_MATCH);
|
||||
expect(resolved.variant).toBe('true');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('flagd-core validations', () => {
|
||||
|
|
@ -85,25 +83,22 @@ describe('flagd-core validations', () => {
|
|||
});
|
||||
|
||||
it('should validate flag type - eval int as boolean', () => {
|
||||
expect(() => core.resolveBooleanEvaluation('myIntFlag', true, {}, console))
|
||||
.toThrow(GeneralError)
|
||||
expect(() => core.resolveBooleanEvaluation('myIntFlag', true, {}, console)).toThrow(GeneralError);
|
||||
});
|
||||
|
||||
it('should validate flag status', () => {
|
||||
const evaluation = core.resolveBooleanEvaluation('myBoolFlag', false, {}, console);
|
||||
|
||||
expect(evaluation).toBeTruthy()
|
||||
expect(evaluation.value).toBe(false)
|
||||
expect(evaluation.reason).toBe(StandardResolutionReasons.DISABLED)
|
||||
expect(evaluation).toBeTruthy();
|
||||
expect(evaluation.value).toBe(false);
|
||||
expect(evaluation.reason).toBe(StandardResolutionReasons.DISABLED);
|
||||
});
|
||||
|
||||
it('should validate variant', () => {
|
||||
expect(() => core.resolveStringEvaluation('myStringFlag', 'hello', {}, console))
|
||||
.toThrow(TypeMismatchError)
|
||||
expect(() => core.resolveStringEvaluation('myStringFlag', 'hello', {}, console)).toThrow(TypeMismatchError);
|
||||
});
|
||||
|
||||
it('should validate variant existence', () => {
|
||||
expect(() => core.resolveNumberEvaluation('myIntFlag', 100, {}, console))
|
||||
.toThrow(GeneralError)
|
||||
expect(() => core.resolveNumberEvaluation('myIntFlag', 100, {}, console)).toThrow(GeneralError);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {MemoryStorage, Storage} from './storage';
|
||||
import { MemoryStorage, Storage } from './storage';
|
||||
import {
|
||||
EvaluationContext,
|
||||
FlagNotFoundError,
|
||||
|
|
@ -9,8 +9,8 @@ import {
|
|||
StandardResolutionReasons,
|
||||
TypeMismatchError,
|
||||
} from '@openfeature/core';
|
||||
import {Targeting} from "./targeting/targeting";
|
||||
import {Logger} from "@openfeature/server-sdk";
|
||||
import { Targeting } from './targeting/targeting';
|
||||
import { Logger } from '@openfeature/server-sdk';
|
||||
|
||||
/**
|
||||
* Expose flag configuration setter and flag resolving methods.
|
||||
|
|
@ -76,7 +76,6 @@ export class FlagdCore {
|
|||
logger: Logger,
|
||||
type: string,
|
||||
): ResolutionDetails<T> {
|
||||
|
||||
// flag exist check
|
||||
const flag = this._storage.getFlag(flagKey);
|
||||
if (!flag) {
|
||||
|
|
@ -88,7 +87,7 @@ export class FlagdCore {
|
|||
return {
|
||||
value: defaultValue,
|
||||
reason: StandardResolutionReasons.DISABLED,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let variant;
|
||||
|
|
@ -117,16 +116,18 @@ export class FlagdCore {
|
|||
}
|
||||
|
||||
if (typeof variant !== 'string') {
|
||||
throw new TypeMismatchError('Variant must be a string, but found ' + typeof variant)
|
||||
throw new TypeMismatchError('Variant must be a string, but found ' + typeof variant);
|
||||
}
|
||||
|
||||
const resolvedVariant = flag.variants.get(variant)
|
||||
const resolvedVariant = flag.variants.get(variant);
|
||||
if (!resolvedVariant) {
|
||||
throw new GeneralError(`Variant ${variant} not found in flag with key ${flagKey}`);
|
||||
}
|
||||
|
||||
if (typeof resolvedVariant !== type) {
|
||||
throw new TypeMismatchError(`Evaluated type of the flag ${flagKey} does not match. Expected ${type}, got ${typeof resolvedVariant}`);
|
||||
throw new TypeMismatchError(
|
||||
`Evaluated type of the flag ${flagKey} does not match. Expected ${type}, got ${typeof resolvedVariant}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -57,6 +57,6 @@ describe('Flag configurations', () => {
|
|||
|
||||
const fibAlgo = flags.get('fibAlgo');
|
||||
expect(fibAlgo).toBeTruthy();
|
||||
expect(fibAlgo?.targeting).toStrictEqual({"if": [{"in": ["@faas.com", {"var": ["email"]}]}, "binet", null]});
|
||||
expect(fibAlgo?.targeting).toStrictEqual({ if: [{ in: ['@faas.com', { var: ['email'] }] }, 'binet', null] });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import Ajv from 'ajv';
|
||||
import {FeatureFlag, Flag} from './feature-flag';
|
||||
import { FeatureFlag, Flag } from './feature-flag';
|
||||
import mydata from '../../flagd-schemas/json/flagd-definitions.json';
|
||||
|
||||
const ajv = new Ajv();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import {FeatureFlag} from './feature-flag';
|
||||
import {parse} from './parser';
|
||||
import { FeatureFlag } from './feature-flag';
|
||||
import { parse } from './parser';
|
||||
|
||||
/**
|
||||
* The simple contract of the storage layer.
|
||||
|
|
@ -28,7 +28,7 @@ export class MemoryStorage implements Storage {
|
|||
try {
|
||||
this._flags = parse(cfg);
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export const flagdPropertyKey = "$flagd";
|
||||
export const flagKeyPropertyKey = "flagKey";
|
||||
export const timestampPropertyKey = "timestamp";
|
||||
export const targetingPropertyKey = "targetingKey";
|
||||
export const flagdPropertyKey = '$flagd';
|
||||
export const flagKeyPropertyKey = 'flagKey';
|
||||
export const timestampPropertyKey = 'timestamp';
|
||||
export const targetingPropertyKey = 'targetingKey';
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import {flagdPropertyKey, flagKeyPropertyKey, targetingPropertyKey} from "./common";
|
||||
import MurmurHash3 from "imurmurhash";
|
||||
import { flagdPropertyKey, flagKeyPropertyKey, targetingPropertyKey } from './common';
|
||||
import MurmurHash3 from 'imurmurhash';
|
||||
|
||||
export const fractionalRule = "fractional";
|
||||
export const fractionalRule = 'fractional';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function fractional(data: unknown, context: Record<any, any>): string | null {
|
||||
|
|
@ -11,7 +11,7 @@ export function fractional(data: unknown, context: Record<any, any>): string | n
|
|||
|
||||
const args = Array.from(data);
|
||||
if (args.length < 2) {
|
||||
console.error('Invalid targeting rule. Require at least two buckets.')
|
||||
console.error('Invalid targeting rule. Require at least two buckets.');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -23,26 +23,26 @@ export function fractional(data: unknown, context: Record<any, any>): string | n
|
|||
let bucketBy: string;
|
||||
let buckets: unknown[];
|
||||
|
||||
if (typeof args[0] == "string") {
|
||||
if (typeof args[0] == 'string') {
|
||||
bucketBy = args[0];
|
||||
buckets = args.slice(1, args.length)
|
||||
buckets = args.slice(1, args.length);
|
||||
} else {
|
||||
bucketBy = context[targetingPropertyKey];
|
||||
if (!bucketBy) {
|
||||
console.error('Missing targetingKey property')
|
||||
console.error('Missing targetingKey property');
|
||||
return null;
|
||||
}
|
||||
|
||||
buckets = args;
|
||||
}
|
||||
|
||||
let bucketingList
|
||||
let bucketingList;
|
||||
|
||||
try {
|
||||
bucketingList = toBucketingList(buckets)
|
||||
bucketingList = toBucketingList(buckets);
|
||||
} catch (e) {
|
||||
console.error('Error parsing targeting rule', e)
|
||||
return null
|
||||
console.error('Error parsing targeting rule', e);
|
||||
return null;
|
||||
}
|
||||
|
||||
const hashKey = flagdProperties[flagKeyPropertyKey] + bucketBy;
|
||||
|
|
@ -57,42 +57,42 @@ export function fractional(data: unknown, context: Record<any, any>): string | n
|
|||
sum += bucketEntry.fraction;
|
||||
|
||||
if (sum >= bucket) {
|
||||
return bucketEntry.variant
|
||||
return bucketEntry.variant;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function toBucketingList(from: unknown[]): { variant: string, fraction: number }[] {
|
||||
function toBucketingList(from: unknown[]): { variant: string; fraction: number }[] {
|
||||
// extract bucketing options
|
||||
const bucketingArray: { variant: string, fraction: number }[] = [];
|
||||
const bucketingArray: { variant: string; fraction: number }[] = [];
|
||||
|
||||
let bucketSum = 0;
|
||||
for (let i = 0; i < from.length; i++) {
|
||||
const entry = from[i]
|
||||
const entry = from[i];
|
||||
if (!Array.isArray(entry)) {
|
||||
throw new Error("Invalid bucket entries");
|
||||
throw new Error('Invalid bucket entries');
|
||||
}
|
||||
|
||||
if (entry.length != 2) {
|
||||
throw new Error("Invalid bucketing entry. Require two values - variant and percentage");
|
||||
throw new Error('Invalid bucketing entry. Require two values - variant and percentage');
|
||||
}
|
||||
|
||||
if (typeof entry[0] !== 'string') {
|
||||
throw new Error("Bucketing require variant to be present in string format");
|
||||
throw new Error('Bucketing require variant to be present in string format');
|
||||
}
|
||||
|
||||
if (typeof entry[1] !== 'number') {
|
||||
throw new Error("Bucketing require bucketing percentage to be present");
|
||||
throw new Error('Bucketing require bucketing percentage to be present');
|
||||
}
|
||||
|
||||
bucketingArray.push({fraction: entry[1], variant: entry[0]});
|
||||
bucketingArray.push({ fraction: entry[1], variant: entry[0] });
|
||||
bucketSum += entry[1];
|
||||
}
|
||||
|
||||
if (bucketSum != 100) {
|
||||
throw new Error("Bucketing sum must add up to 100");
|
||||
throw new Error('Bucketing sum must add up to 100');
|
||||
}
|
||||
|
||||
return bucketingArray;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {compare, parse} from 'semver'
|
||||
import { compare, parse } from 'semver';
|
||||
|
||||
export const semVerRule = 'sem_ver';
|
||||
|
||||
|
|
@ -7,7 +7,7 @@ export function semVer(data: unknown): boolean {
|
|||
return false;
|
||||
}
|
||||
|
||||
const args = Array.from(data)
|
||||
const args = Array.from(data);
|
||||
|
||||
if (args.length != 3) {
|
||||
return false;
|
||||
|
|
@ -42,5 +42,5 @@ export function semVer(data: unknown): boolean {
|
|||
return semVer1.minor == semVer2.minor;
|
||||
}
|
||||
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
export const startsWithRule = 'starts_with'
|
||||
export const endsWithRule = 'ends_with'
|
||||
export const startsWithRule = 'starts_with';
|
||||
export const endsWithRule = 'ends_with';
|
||||
|
||||
export function startsWithHandler(data: unknown) {
|
||||
return compare(startsWithRule, data)
|
||||
return compare(startsWithRule, data);
|
||||
}
|
||||
|
||||
export function endsWithHandler(data: unknown) {
|
||||
return compare(endsWithRule, data)
|
||||
return compare(endsWithRule, data);
|
||||
}
|
||||
|
||||
function compare(method: string, data: unknown): boolean {
|
||||
|
|
@ -17,19 +17,19 @@ function compare(method: string, data: unknown): boolean {
|
|||
const params = Array.from(data);
|
||||
|
||||
if (params.length != 2) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof params[0] !== 'string' || typeof params[1] !== 'string') {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (method) {
|
||||
case startsWithRule:
|
||||
return params[0].startsWith(params[1])
|
||||
return params[0].startsWith(params[1]);
|
||||
case endsWithRule:
|
||||
return params[0].endsWith(params[1])
|
||||
return params[0].endsWith(params[1]);
|
||||
default:
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,215 +1,204 @@
|
|||
import {Targeting} from "./targeting";
|
||||
import { Targeting } from './targeting';
|
||||
|
||||
describe("Targeting rule evaluator", () => {
|
||||
describe('Targeting rule evaluator', () => {
|
||||
let targeting: Targeting;
|
||||
|
||||
beforeAll(() => {
|
||||
targeting = new Targeting();
|
||||
})
|
||||
});
|
||||
|
||||
it('should inject flag key as a property', () => {
|
||||
const flagKey = "flagA"
|
||||
const input = {'===': [{var: "$flagd.flagKey"}, flagKey]}
|
||||
const flagKey = 'flagA';
|
||||
const input = { '===': [{ var: '$flagd.flagKey' }, flagKey] };
|
||||
|
||||
expect(targeting.applyTargeting(flagKey, input, {})).toBeTruthy()
|
||||
expect(targeting.applyTargeting(flagKey, input, {})).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should inject current timestamp as a property', () => {
|
||||
const ts = Math.floor(Date.now() / 1000)
|
||||
const input = {'>=': [{var: "$flagd.timestamp"}, ts]}
|
||||
const ts = Math.floor(Date.now() / 1000);
|
||||
const input = { '>=': [{ var: '$flagd.timestamp' }, ts] };
|
||||
|
||||
expect(targeting.applyTargeting("flag", input, {})).toBeTruthy()
|
||||
expect(targeting.applyTargeting('flag', input, {})).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
it('should override injected properties if already present in context', () => {
|
||||
const flagKey = "flagA"
|
||||
const input = {'===': [{var: "$flagd.flagKey"}, flagKey]}
|
||||
const flagKey = 'flagA';
|
||||
const input = { '===': [{ var: '$flagd.flagKey' }, flagKey] };
|
||||
const ctx = {
|
||||
$flagd: {
|
||||
flagKey: "someOtherFlag"
|
||||
}
|
||||
}
|
||||
flagKey: 'someOtherFlag',
|
||||
},
|
||||
};
|
||||
|
||||
expect(targeting.applyTargeting(flagKey, input, ctx)).toBeTruthy()
|
||||
expect(targeting.applyTargeting(flagKey, input, ctx)).toBeTruthy();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("String comparison operator", () => {
|
||||
describe('String comparison operator', () => {
|
||||
let targeting: Targeting;
|
||||
|
||||
beforeAll(() => {
|
||||
targeting = new Targeting();
|
||||
})
|
||||
|
||||
it("should evaluate starts with calls", () => {
|
||||
const input = {"starts_with": [{"var": "email"}, "admin"]}
|
||||
expect(targeting.applyTargeting("flag", input, {email: "admin@abc.com"})).toBeTruthy()
|
||||
});
|
||||
|
||||
it("should evaluate ends with calls", () => {
|
||||
const input = {"ends_with": [{"var": "email"}, "abc.com"]}
|
||||
expect(targeting.applyTargeting("flag", input, {email: "admin@abc.com"})).toBeTruthy()
|
||||
it('should evaluate starts with calls', () => {
|
||||
const input = { starts_with: [{ var: 'email' }, 'admin'] };
|
||||
expect(targeting.applyTargeting('flag', input, { email: 'admin@abc.com' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should evaluate ends with calls', () => {
|
||||
const input = { ends_with: [{ var: 'email' }, 'abc.com'] };
|
||||
expect(targeting.applyTargeting('flag', input, { email: 'admin@abc.com' })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("String comparison operator should validate", () => {
|
||||
describe('String comparison operator should validate', () => {
|
||||
let targeting: Targeting;
|
||||
|
||||
beforeAll(() => {
|
||||
targeting = new Targeting();
|
||||
})
|
||||
|
||||
it("missing input", () => {
|
||||
const input = {"starts_with": [{"var": "email"}]}
|
||||
expect(targeting.applyTargeting("flag", input, {email: "admin@abc.com"})).toBeFalsy()
|
||||
});
|
||||
|
||||
it("non string variable", () => {
|
||||
const input = {"starts_with": [{"var": "someNumber"}, "abc.com"]}
|
||||
expect(targeting.applyTargeting("flag", input, {someNumber: 123456})).toBeFalsy()
|
||||
it('missing input', () => {
|
||||
const input = { starts_with: [{ var: 'email' }] };
|
||||
expect(targeting.applyTargeting('flag', input, { email: 'admin@abc.com' })).toBeFalsy();
|
||||
});
|
||||
|
||||
it("non string comparator", () => {
|
||||
const input = {"starts_with": [{"var": "email"}, 123456]}
|
||||
expect(targeting.applyTargeting("flag", input, {email: "admin@abc.com"})).toBeFalsy()
|
||||
it('non string variable', () => {
|
||||
const input = { starts_with: [{ var: 'someNumber' }, 'abc.com'] };
|
||||
expect(targeting.applyTargeting('flag', input, { someNumber: 123456 })).toBeFalsy();
|
||||
});
|
||||
|
||||
it('non string comparator', () => {
|
||||
const input = { starts_with: [{ var: 'email' }, 123456] };
|
||||
expect(targeting.applyTargeting('flag', input, { email: 'admin@abc.com' })).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("Sem ver operator", () => {
|
||||
describe('Sem ver operator', () => {
|
||||
let targeting: Targeting;
|
||||
|
||||
beforeAll(() => {
|
||||
targeting = new Targeting();
|
||||
})
|
||||
});
|
||||
|
||||
it('should support equal operator', () => {
|
||||
const input = {"sem_ver": ['v1.2.3', "=", "1.2.3"]}
|
||||
expect(targeting.applyTargeting("flag", input, {})).toBeTruthy()
|
||||
const input = { sem_ver: ['v1.2.3', '=', '1.2.3'] };
|
||||
expect(targeting.applyTargeting('flag', input, {})).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should support neq operator', () => {
|
||||
const input = {"sem_ver": ['v1.2.3', "!=", "1.2.4"]}
|
||||
expect(targeting.applyTargeting("flag", input, {})).toBeTruthy()
|
||||
const input = { sem_ver: ['v1.2.3', '!=', '1.2.4'] };
|
||||
expect(targeting.applyTargeting('flag', input, {})).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should support lt operator', () => {
|
||||
const input = {"sem_ver": ['v1.2.3', "<", "1.2.4"]}
|
||||
expect(targeting.applyTargeting("flag", input, {})).toBeTruthy()
|
||||
const input = { sem_ver: ['v1.2.3', '<', '1.2.4'] };
|
||||
expect(targeting.applyTargeting('flag', input, {})).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should support lte operator', () => {
|
||||
const input = {"sem_ver": ['v1.2.3', "<=", "1.2.3"]}
|
||||
expect(targeting.applyTargeting("flag", input, {})).toBeTruthy()
|
||||
const input = { sem_ver: ['v1.2.3', '<=', '1.2.3'] };
|
||||
expect(targeting.applyTargeting('flag', input, {})).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should support gte operator', () => {
|
||||
const input = {"sem_ver": ['v1.2.3', ">=", "1.2.3"]}
|
||||
expect(targeting.applyTargeting("flag", input, {})).toBeTruthy()
|
||||
const input = { sem_ver: ['v1.2.3', '>=', '1.2.3'] };
|
||||
expect(targeting.applyTargeting('flag', input, {})).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should support gt operator', () => {
|
||||
const input = {"sem_ver": ['v1.2.4', ">", "1.2.3"]}
|
||||
expect(targeting.applyTargeting("flag", input, {})).toBeTruthy()
|
||||
const input = { sem_ver: ['v1.2.4', '>', '1.2.3'] };
|
||||
expect(targeting.applyTargeting('flag', input, {})).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should support major comparison operator', () => {
|
||||
const input = {"sem_ver": ["v1.2.3", "^", "v1.0.0"]}
|
||||
expect(targeting.applyTargeting("flag", input, {})).toBeTruthy()
|
||||
const input = { sem_ver: ['v1.2.3', '^', 'v1.0.0'] };
|
||||
expect(targeting.applyTargeting('flag', input, {})).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should support minor comparison operator', () => {
|
||||
const input = {"sem_ver": ["v5.0.3", "~", "v5.0.8"]}
|
||||
expect(targeting.applyTargeting("flag", input, {})).toBeTruthy()
|
||||
const input = { sem_ver: ['v5.0.3', '~', 'v5.0.8'] };
|
||||
expect(targeting.applyTargeting('flag', input, {})).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should handle unknown operator', () => {
|
||||
const input = {"sem_ver": ["v1.0.0", "-", "v1.0.0"]}
|
||||
expect(targeting.applyTargeting("flag", input, {})).toBeFalsy()
|
||||
const input = { sem_ver: ['v1.0.0', '-', 'v1.0.0'] };
|
||||
expect(targeting.applyTargeting('flag', input, {})).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should handle invalid inputs', () => {
|
||||
const input = {"sem_ver": ["myVersion_1", "=", "myVersion_1"]}
|
||||
expect(targeting.applyTargeting("flag", input, {})).toBeFalsy()
|
||||
const input = { sem_ver: ['myVersion_1', '=', 'myVersion_1'] };
|
||||
expect(targeting.applyTargeting('flag', input, {})).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should validate inputs', () => {
|
||||
const input = {"sem_ver": ["myVersion_2", "+", "myVersion_1", "myVersion_1"]}
|
||||
expect(targeting.applyTargeting("flag", input, {})).toBeFalsy()
|
||||
const input = { sem_ver: ['myVersion_2', '+', 'myVersion_1', 'myVersion_1'] };
|
||||
expect(targeting.applyTargeting('flag', input, {})).toBeFalsy();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
describe("fractional operator", () => {
|
||||
describe('fractional operator', () => {
|
||||
let targeting: Targeting;
|
||||
|
||||
beforeAll(() => {
|
||||
targeting = new Targeting();
|
||||
})
|
||||
});
|
||||
|
||||
it("should evaluate valid rule", () => {
|
||||
it('should evaluate valid rule', () => {
|
||||
const input = {
|
||||
fractional: [{ var: 'key' }, ['red', 50], ['blue', 50]],
|
||||
};
|
||||
|
||||
expect(targeting.applyTargeting('flagA', input, { key: 'bucketKeyA' })).toBe('red');
|
||||
});
|
||||
|
||||
it('should evaluate valid rule', () => {
|
||||
const input = {
|
||||
fractional: [{ var: 'key' }, ['red', 50], ['blue', 50]],
|
||||
};
|
||||
|
||||
expect(targeting.applyTargeting('flagA', input, { key: 'bucketKeyB' })).toBe('blue');
|
||||
});
|
||||
|
||||
it('should evaluate valid rule with targeting key', () => {
|
||||
const input = {
|
||||
fractional: [
|
||||
{"var": "key"},
|
||||
["red", 50],
|
||||
["blue", 50]
|
||||
]
|
||||
}
|
||||
['red', 50],
|
||||
['blue', 50],
|
||||
],
|
||||
};
|
||||
|
||||
expect(targeting.applyTargeting("flagA", input, {key: "bucketKeyA"})).toBe("red")
|
||||
})
|
||||
expect(targeting.applyTargeting('flagA', input, { targetingKey: 'bucketKeyB' })).toBe('blue');
|
||||
});
|
||||
});
|
||||
|
||||
it("should evaluate valid rule", () => {
|
||||
const input = {
|
||||
fractional: [
|
||||
{"var": "key"},
|
||||
["red", 50],
|
||||
["blue", 50]
|
||||
]
|
||||
}
|
||||
|
||||
expect(targeting.applyTargeting("flagA", input, {key: "bucketKeyB"})).toBe("blue")
|
||||
})
|
||||
|
||||
it("should evaluate valid rule with targeting key", () => {
|
||||
const input = {
|
||||
fractional: [
|
||||
["red", 50],
|
||||
["blue", 50]
|
||||
]
|
||||
}
|
||||
|
||||
expect(targeting.applyTargeting("flagA", input, {targetingKey: "bucketKeyB"})).toBe("blue")
|
||||
})
|
||||
})
|
||||
|
||||
describe("fractional operator should validate", () => {
|
||||
describe('fractional operator should validate', () => {
|
||||
let targeting: Targeting;
|
||||
|
||||
beforeAll(() => {
|
||||
targeting = new Targeting();
|
||||
})
|
||||
});
|
||||
|
||||
it("bucket sum to be 100", () => {
|
||||
it('bucket sum to be 100', () => {
|
||||
const input = {
|
||||
fractional: [
|
||||
["red", 55],
|
||||
["blue", 55]
|
||||
]
|
||||
}
|
||||
['red', 55],
|
||||
['blue', 55],
|
||||
],
|
||||
};
|
||||
|
||||
expect(targeting.applyTargeting("flagA", input, {targetingKey: "key"})).toBe(null)
|
||||
})
|
||||
expect(targeting.applyTargeting('flagA', input, { targetingKey: 'key' })).toBe(null);
|
||||
});
|
||||
|
||||
it("buckets properties to have variant and fraction", () => {
|
||||
it('buckets properties to have variant and fraction', () => {
|
||||
const input = {
|
||||
fractional: [
|
||||
["red", 50],
|
||||
[100, 50]
|
||||
]
|
||||
}
|
||||
['red', 50],
|
||||
[100, 50],
|
||||
],
|
||||
};
|
||||
|
||||
expect(targeting.applyTargeting("flagA", input, {targetingKey: "key"})).toBe(null)
|
||||
})
|
||||
})
|
||||
expect(targeting.applyTargeting('flagA', input, { targetingKey: 'key' })).toBe(null);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import {LogicEngine,} from "json-logic-engine";
|
||||
import {endsWithHandler, endsWithRule, startsWithHandler, startsWithRule} from "./string-comp";
|
||||
import {semVer, semVerRule} from "./sem-ver";
|
||||
import {fractional, fractionalRule} from "./fractional";
|
||||
import {flagdPropertyKey, flagKeyPropertyKey, timestampPropertyKey} from "./common";
|
||||
import { LogicEngine } from 'json-logic-engine';
|
||||
import { endsWithHandler, endsWithRule, startsWithHandler, startsWithRule } from './string-comp';
|
||||
import { semVer, semVerRule } from './sem-ver';
|
||||
import { fractional, fractionalRule } from './fractional';
|
||||
import { flagdPropertyKey, flagKeyPropertyKey, timestampPropertyKey } from './common';
|
||||
|
||||
export class Targeting {
|
||||
private readonly _logicEngine: LogicEngine;
|
||||
|
|
@ -19,7 +19,7 @@ export class Targeting {
|
|||
|
||||
applyTargeting(flagKey: string, logic: unknown, data: object): unknown {
|
||||
if (Object.hasOwn(data, flagdPropertyKey)) {
|
||||
console.warn(`overwriting ${flagdPropertyKey} property in the context`)
|
||||
console.warn(`overwriting ${flagdPropertyKey} property in the context`);
|
||||
}
|
||||
|
||||
const ctxData = {
|
||||
|
|
@ -28,7 +28,7 @@ export class Targeting {
|
|||
[flagKeyPropertyKey]: flagKey,
|
||||
[timestampPropertyKey]: Math.floor(Date.now() / 1000),
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
return this._logicEngine.run(logic, ctxData);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export default async function (tree: Tree, schema: SchemaOptions) {
|
|||
['spec.ts', 'ts'].forEach((suffix) => {
|
||||
tree.rename(
|
||||
joinPathFragments(projectLibDir, `${directory}-${fileName}.${suffix}`),
|
||||
joinPathFragments(projectLibDir, `${libFileName}.${suffix}`)
|
||||
joinPathFragments(projectLibDir, `${libFileName}.${suffix}`),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -180,14 +180,17 @@ function updatePackage(tree: Tree, projectRoot: string, schema: SchemaOptions) {
|
|||
};
|
||||
|
||||
// use undefined or this defaults to "commonjs", which breaks things: https://github.com/open-feature/js-sdk-contrib/pull/596
|
||||
json.type = undefined
|
||||
json.type = undefined;
|
||||
|
||||
// client packages have a web-sdk dep, server js-sdk
|
||||
json.peerDependencies = schema.category === 'client' ? {
|
||||
'@openfeature/web-sdk': '>=0.4.0',
|
||||
} : {
|
||||
'@openfeature/server-sdk': '^1.6.0',
|
||||
}
|
||||
json.peerDependencies =
|
||||
schema.category === 'client'
|
||||
? {
|
||||
'@openfeature/web-sdk': '>=0.4.0',
|
||||
}
|
||||
: {
|
||||
'@openfeature/server-sdk': '^1.6.0',
|
||||
};
|
||||
|
||||
return json;
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue