diff --git a/libs/hooks/open-telemetry/src/index.ts b/libs/hooks/open-telemetry/src/index.ts index 2732dd18..17ba3415 100644 --- a/libs/hooks/open-telemetry/src/index.ts +++ b/libs/hooks/open-telemetry/src/index.ts @@ -1,2 +1,2 @@ export * from './lib/traces'; -export * from './lib/metrics'; \ No newline at end of file +export * from './lib/metrics'; diff --git a/libs/hooks/open-telemetry/src/lib/conventions.ts b/libs/hooks/open-telemetry/src/lib/conventions.ts index caa83db8..43636d5b 100644 --- a/libs/hooks/open-telemetry/src/lib/conventions.ts +++ b/libs/hooks/open-telemetry/src/lib/conventions.ts @@ -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`; \ No newline at end of file +export const REASON_ATTR: keyof EvaluationAttributes = `${FEATURE_FLAG}.reason`; diff --git a/libs/hooks/open-telemetry/src/lib/metrics/index.ts b/libs/hooks/open-telemetry/src/lib/metrics/index.ts index a37cf2bd..744f7dc0 100644 --- a/libs/hooks/open-telemetry/src/lib/metrics/index.ts +++ b/libs/hooks/open-telemetry/src/lib/metrics/index.ts @@ -1 +1 @@ -export * from './metrics-hook'; \ No newline at end of file +export * from './metrics-hook'; diff --git a/libs/hooks/open-telemetry/src/lib/metrics/metrics-hook.ts b/libs/hooks/open-telemetry/src/lib/metrics/metrics-hook.ts index ebd23d90..32990114 100644 --- a/libs/hooks/open-telemetry/src/lib/metrics/metrics-hook.ts +++ b/libs/hooks/open-telemetry/src/lib/metrics/metrics-hook.ts @@ -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; private readonly evaluationErrorCounter: Counter; - 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, { diff --git a/libs/hooks/open-telemetry/src/lib/traces/index.ts b/libs/hooks/open-telemetry/src/lib/traces/index.ts index e36960e5..d3b978b8 100644 --- a/libs/hooks/open-telemetry/src/lib/traces/index.ts +++ b/libs/hooks/open-telemetry/src/lib/traces/index.ts @@ -1 +1 @@ -export * from './tracing-hook'; \ No newline at end of file +export * from './tracing-hook'; diff --git a/libs/hooks/open-telemetry/src/lib/traces/tracing-hook.spec.ts b/libs/hooks/open-telemetry/src/lib/traces/tracing-hook.spec.ts index c526c6c3..3f623616 100644 --- a/libs/hooks/open-telemetry/src/lib/traces/tracing-hook.spec.ts +++ b/libs/hooks/open-telemetry/src/lib/traces/tracing-hook.spec.ts @@ -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 = { 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 = { 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 = { @@ -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', diff --git a/libs/hooks/open-telemetry/src/lib/traces/tracing-hook.ts b/libs/hooks/open-telemetry/src/lib/traces/tracing-hook.ts index 21d0ece9..10834fce 100644 --- a/libs/hooks/open-telemetry/src/lib/traces/tracing-hook.ts +++ b/libs/hooks/open-telemetry/src/lib/traces/tracing-hook.ts @@ -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 { diff --git a/libs/providers/config-cat/src/lib/config-cat-provider.spec.ts b/libs/providers/config-cat/src/lib/config-cat-provider.spec.ts index 4ff07b77..c8760d75 100644 --- a/libs/providers/config-cat/src/lib/config-cat-provider.spec.ts +++ b/libs/providers/config-cat/src/lib/config-cat-provider.spec.ts @@ -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, ); }); }); diff --git a/libs/providers/config-cat/src/lib/config-cat-provider.ts b/libs/providers/config-cat/src/lib/config-cat-provider.ts index 996b559c..c1a4dfb5 100644 --- a/libs/providers/config-cat/src/lib/config-cat-provider.ts +++ b/libs/providers/config-cat/src/lib/config-cat-provider.ts @@ -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> { 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( 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> { 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( 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> { 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( flagKey, undefined, - transformContext(context) + transformContext(context), ); const validatedValue = validateFlagType('number', value); @@ -156,7 +156,7 @@ export class ConfigCatProvider implements Provider { public async resolveObjectEvaluation( flagKey: string, defaultValue: U, - context: EvaluationContext + context: EvaluationContext, ): Promise> { 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( value: U, data: Omit, - reason?: ResolutionReason + reason?: ResolutionReason, ): ResolutionDetails { const matchedRule = Boolean(data.matchedEvaluationRule || data.matchedEvaluationPercentageRule); const evaluatedReason = matchedRule ? StandardResolutionReasons.TARGETING_MATCH : StandardResolutionReasons.STATIC; diff --git a/libs/providers/config-cat/src/lib/context-transformer.spec.ts b/libs/providers/config-cat/src/lib/context-transformer.spec.ts index a52e02cb..1b0b8820 100644 --- a/libs/providers/config-cat/src/lib/context-transformer.spec.ts +++ b/libs/providers/config-cat/src/lib/context-transformer.spec.ts @@ -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', diff --git a/libs/providers/env-var/src/lib/env-var-provider.ts b/libs/providers/env-var/src/lib/env-var-provider.ts index 5665abcf..39169c83 100644 --- a/libs/providers/env-var/src/lib/env-var-provider.ts +++ b/libs/providers/env-var/src/lib/env-var-provider.ts @@ -75,7 +75,7 @@ export class EnvVarProvider implements Provider { private evaluateEnvironmentVariable( key: string, - parse: (value: string) => T + parse: (value: string) => T, ): ResolutionDetails { const envVarKey = this.options.disableConstantCase ? key : constantCase(key); const value = process.env[envVarKey]; diff --git a/libs/providers/flagd-web/src/e2e/jest.config.ts b/libs/providers/flagd-web/src/e2e/jest.config.ts index 0503f957..c2e36e76 100644 --- a/libs/providers/flagd-web/src/e2e/jest.config.ts +++ b/libs/providers/flagd-web/src/e2e/jest.config.ts @@ -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, -}; \ No newline at end of file +}; diff --git a/libs/providers/flagd-web/src/e2e/setup.ts b/libs/providers/flagd-web/src/e2e/setup.ts index c2c01d2c..c99452b6 100644 --- a/libs/providers/flagd-web/src/e2e/setup.ts +++ b/libs/providers/flagd-web/src/e2e/setup.ts @@ -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!'); diff --git a/libs/providers/flagd-web/src/e2e/step-definitions/evaluation.spec.ts b/libs/providers/flagd-web/src/e2e/step-definitions/evaluation.spec.ts index c488d507..9f0c2f30 100644 --- a/libs/providers/flagd-web/src/e2e/step-definitions/evaluation.spec.ts +++ b/libs/providers/flagd-web/src/e2e/step-definitions/evaluation.spec.ts @@ -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); - } + }, ); }); }); diff --git a/libs/providers/flagd-web/src/lib/flagd-web-provider.ts b/libs/providers/flagd-web/src/lib/flagd-web-provider.ts index b30ff2ca..b1bd6354 100644 --- a/libs/providers/flagd-web/src/lib/flagd-web-provider.ts +++ b/libs/providers/flagd-web/src/lib/flagd-web-provider.ts @@ -50,7 +50,7 @@ export class FlagdWebProvider implements Provider { options: FlagdProviderOptions, logger?: Logger, promiseClient?: PromiseClient, - callbackClient?: CallbackClient + callbackClient?: CallbackClient, ) { 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); } - } + }, ); }); } diff --git a/libs/providers/flagd-web/src/lib/options.ts b/libs/providers/flagd-web/src/lib/options.ts index a7d13026..2e3c39d5 100644 --- a/libs/providers/flagd-web/src/lib/options.ts +++ b/libs/providers/flagd-web/src/lib/options.ts @@ -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, }; diff --git a/libs/providers/go-feature-flag-web/src/lib/context-transfomer.spec.ts b/libs/providers/go-feature-flag-web/src/lib/context-transfomer.spec.ts index 2f490211..d81fe561 100644 --- a/libs/providers/go-feature-flag-web/src/lib/context-transfomer.spec.ts +++ b/libs/providers/go-feature-flag-web/src/lib/context-transfomer.spec.ts @@ -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', () => { diff --git a/libs/providers/go-feature-flag-web/src/lib/context-transformer.ts b/libs/providers/go-feature-flag-web/src/lib/context-transformer.ts index a640016e..5c76223e 100644 --- a/libs/providers/go-feature-flag-web/src/lib/context-transformer.ts +++ b/libs/providers/go-feature-flag-web/src/lib/context-transformer.ts @@ -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(); } diff --git a/libs/providers/go-feature-flag-web/src/lib/fetch-error.ts b/libs/providers/go-feature-flag-web/src/lib/fetch-error.ts index fdfb9a52..164c137f 100644 --- a/libs/providers/go-feature-flag-web/src/lib/fetch-error.ts +++ b/libs/providers/go-feature-flag-web/src/lib/fetch-error.ts @@ -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; } diff --git a/libs/providers/go-feature-flag-web/src/lib/go-feature-flag-web-provider.spec.ts b/libs/providers/go-feature-flag-web/src/lib/go-feature-flag-web-provider.spec.ts index 364b5bfa..7ccbc4c7 100644 --- a/libs/providers/go-feature-flag-web/src/lib/go-feature-flag-web-provider.spec.ts +++ b/libs/providers/go-feature-flag-web/src/lib/go-feature-flag-web-provider.spec.ts @@ -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 = { 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 = { 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 = { 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 = { 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 = { 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 = { 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(); }); - }) + }); }); diff --git a/libs/providers/go-feature-flag-web/src/lib/go-feature-flag-web-provider.ts b/libs/providers/go-feature-flag-web/src/lib/go-feature-flag-web-provider.ts index 3b57b037..a72f4a23 100644 --- a/libs/providers/go-feature-flag-web/src/lib/go-feature-flag-web-provider.ts +++ b/libs/providers/go-feature-flag-web/src/lib/go-feature-flag-web-provider.ts @@ -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 { 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 { - 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 { 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 { - return this.evaluate(flagKey, 'number') + return this.evaluate(flagKey, 'number'); } resolveObjectEvaluation(flagKey: string): ResolutionDetails { - return this.evaluate(flagKey, 'object') + return this.evaluate(flagKey, 'object'); } resolveStringEvaluation(flagKey: string): ResolutionDetails { - return this.evaluate(flagKey, 'string') + return this.evaluate(flagKey, 'string'); } resolveBooleanEvaluation(flagKey: string): ResolutionDetails { - return this.evaluate(flagKey, 'boolean') + return this.evaluate(flagKey, 'boolean'); } private evaluate(flagKey: string, type: string): ResolutionDetails { @@ -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 = data.flags[currentValue]; const resolutionDetails: ResolutionDetails = { 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 { } } } - diff --git a/libs/providers/go-feature-flag-web/src/lib/model.ts b/libs/providers/go-feature-flag-web/src/lib/model.ts index 353ab459..c1dcaf72 100644 --- a/libs/providers/go-feature-flag-web/src/lib/model.ts +++ b/libs/providers/go-feature-flag-web/src/lib/model.ts @@ -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 { * by GO Feature Flag. */ export interface GOFeatureFlagAllFlagsResponse { - valid: boolean - flags: Record> + valid: boolean; + flags: Record>; } /** * 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 }; } diff --git a/libs/providers/go-feature-flag/src/lib/context-transformer.ts b/libs/providers/go-feature-flag/src/lib/context-transformer.ts index 7a6997b3..3fdd21e4 100644 --- a/libs/providers/go-feature-flag/src/lib/context-transformer.ts +++ b/libs/providers/go-feature-flag/src/lib/context-transformer.ts @@ -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']; } diff --git a/libs/providers/go-feature-flag/src/lib/data-collector-hook.ts b/libs/providers/go-feature-flag/src/lib/data-collector-hook.ts index 988c4be3..f5ffe79c 100644 --- a/libs/providers/go-feature-flag/src/lib/data-collector-hook.ts +++ b/libs/providers/go-feature-flag/src/lib/data-collector-hook.ts @@ -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 = {events: dataToSend, meta: this.dataCollectorMetadata,} + const request: DataCollectorRequest = { 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, - hookHints?: HookHints - ) { - if (!this.collectUnCachedEvaluation && evaluationDetails.reason !== StandardResolutionReasons.CACHED){ + after(hookContext: HookContext, evaluationDetails: EvaluationDetails, hookHints?: HookHints) { + if (!this.collectUnCachedEvaluation && evaluationDetails.reason !== StandardResolutionReasons.CACHED) { return; } diff --git a/libs/providers/go-feature-flag/src/lib/errors/proxyNotReady.ts b/libs/providers/go-feature-flag/src/lib/errors/proxyNotReady.ts index a8de1095..ef814697 100644 --- a/libs/providers/go-feature-flag/src/lib/errors/proxyNotReady.ts +++ b/libs/providers/go-feature-flag/src/lib/errors/proxyNotReady.ts @@ -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; } } diff --git a/libs/providers/go-feature-flag/src/lib/errors/proxyTimeout.ts b/libs/providers/go-feature-flag/src/lib/errors/proxyTimeout.ts index f7ccc538..cebf1f27 100644 --- a/libs/providers/go-feature-flag/src/lib/errors/proxyTimeout.ts +++ b/libs/providers/go-feature-flag/src/lib/errors/proxyTimeout.ts @@ -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; } } diff --git a/libs/providers/go-feature-flag/src/lib/errors/unauthorized.ts b/libs/providers/go-feature-flag/src/lib/errors/unauthorized.ts index 9dbac900..41af5d18 100644 --- a/libs/providers/go-feature-flag/src/lib/errors/unauthorized.ts +++ b/libs/providers/go-feature-flag/src/lib/errors/unauthorized.ts @@ -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; } } diff --git a/libs/providers/go-feature-flag/src/lib/errors/unknownError.ts b/libs/providers/go-feature-flag/src/lib/errors/unknownError.ts index 8283d8cc..1fcb5c29 100644 --- a/libs/providers/go-feature-flag/src/lib/errors/unknownError.ts +++ b/libs/providers/go-feature-flag/src/lib/errors/unknownError.ts @@ -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; } } diff --git a/libs/providers/go-feature-flag/src/lib/go-feature-flag-provider.spec.ts b/libs/providers/go-feature-flag/src/lib/go-feature-flag-provider.spec.ts index 39f44229..9fb20310 100644 --- a/libs/providers/go-feature-flag/src/lib/go-feature-flag-provider.spec.ts +++ b/libs/providers/go-feature-flag/src/lib/go-feature-flag-provider.spec.ts @@ -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); - 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); - 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); - 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); - 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); - 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); - 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); - 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); - 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); - 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); - 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); - 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); - 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); - 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); - 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); - 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); - 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); - 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); - 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); - 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); - 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); - 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); - 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); - - 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); - 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); - 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', + ); }); }); }); diff --git a/libs/providers/go-feature-flag/src/lib/go-feature-flag-provider.ts b/libs/providers/go-feature-flag/src/lib/go-feature-flag-provider.ts index 97373211..9cf368dc 100644 --- a/libs/providers/go-feature-flag/src/lib/go-feature-flag-provider.ts +++ b/libs/providers/go-feature-flag/src/lib/go-feature-flag-provider.ts @@ -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> { return this.resolveEvaluationGoFeatureFlagProxy( 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> { - return this.resolveEvaluationGoFeatureFlagProxy( - flagKey, - defaultValue, - transformContext(context), - 'string' - ); + return this.resolveEvaluationGoFeatureFlagProxy(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> { - return this.resolveEvaluationGoFeatureFlagProxy( - flagKey, - defaultValue, - transformContext(context), - 'number' - ); + return this.resolveEvaluationGoFeatureFlagProxy(flagKey, defaultValue, transformContext(context), 'number'); } /** @@ -201,14 +195,9 @@ export class GoFeatureFlagProvider implements Provider { async resolveObjectEvaluation( flagKey: string, defaultValue: U, - context: EvaluationContext + context: EvaluationContext, ): Promise> { - return this.resolveEvaluationGoFeatureFlagProxy( - flagKey, - defaultValue, - transformContext(context), - 'object' - ); + return this.resolveEvaluationGoFeatureFlagProxy(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> { // 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 = {user, defaultValue}; + const request: GoFeatureFlagProxyRequest = { 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 = { @@ -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; diff --git a/libs/providers/go-feature-flag/src/lib/model.ts b/libs/providers/go-feature-flag/src/lib/model.ts index 04c1b167..d4319f58 100644 --- a/libs/providers/go-feature-flag/src/lib/model.ts +++ b/libs/providers/go-feature-flag/src/lib/model.ts @@ -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 { events: FeatureEvent[]; meta: Record; diff --git a/libs/providers/launchdarkly-client/src/lib/launchdarkly-client-provider.spec.ts b/libs/providers/launchdarkly-client/src/lib/launchdarkly-client-provider.spec.ts index 7d87ca64..c337d8ef 100644 --- a/libs/providers/launchdarkly-client/src/lib/launchdarkly-client-provider.spec.ts +++ b/libs/providers/launchdarkly-client/src/lib/launchdarkly-client-provider.spec.ts @@ -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 = { - variationDetail: jest.fn(), - identify: jest.fn(), - waitForInitialization: jest.fn(), - on: jest.fn(), - close: jest.fn(), - } as unknown as jest.Mocked; + variationDetail: jest.fn(), + identify: jest.fn(), + waitForInitialization: jest.fn(), + on: jest.fn(), + close: jest.fn(), + } as unknown as jest.Mocked; 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.", + ); }); }); }); diff --git a/libs/providers/launchdarkly-client/src/lib/launchdarkly-client-provider.ts b/libs/providers/launchdarkly-client/src/lib/launchdarkly-client-provider.ts index 15885ed3..ee5a0d1d 100644 --- a/libs/providers/launchdarkly-client/src/lib/launchdarkly-client-provider.ts +++ b/libs/providers/launchdarkly-client/src/lib/launchdarkly-client-provider.ts @@ -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 { diff --git a/libs/providers/launchdarkly-client/src/lib/translate-context.spec.ts b/libs/providers/launchdarkly-client/src/lib/translate-context.spec.ts index d005907f..9af98e26 100644 --- a/libs/providers/launchdarkly-client/src/lib/translate-context.spec.ts +++ b/libs/providers/launchdarkly-client/src/lib/translate-context.spec.ts @@ -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(), diff --git a/libs/providers/launchdarkly-client/src/lib/translate-context.ts b/libs/providers/launchdarkly-client/src/lib/translate-context.ts index 80324428..3aa49bf5 100644 --- a/libs/providers/launchdarkly-client/src/lib/translate-context.ts +++ b/libs/providers/launchdarkly-client/src/lib/translate-context.ts @@ -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; - 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; + 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') { diff --git a/libs/providers/launchdarkly-client/src/lib/translate-result.spec.ts b/libs/providers/launchdarkly-client/src/lib/translate-result.spec.ts index 365f7d36..fca48b72 100644 --- a/libs/providers/launchdarkly-client/src/lib/translate-result.spec.ts +++ b/libs/providers/launchdarkly-client/src/lib/translate-result.spec.ts @@ -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({ - value, - reason: { - kind: 'OFF', - }, - }).value).toEqual(value); + it.each([true, 'potato', 42, { yes: 'no' }])('puts the value into the result.', (value) => { + expect( + translateResult({ + value, + reason: { + kind: 'OFF', + }, + }).value, + ).toEqual(value); }); it('converts the variationIndex into a string variant', () => { - expect(translateResult({ - value: true, - variationIndex: 9, - reason: { - kind: 'OFF', - }, - }).variant).toEqual('9'); + expect( + translateResult({ + 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({ - 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({ + 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({ diff --git a/libs/shared/flagd-core/src/lib/feature-flag.spec.ts b/libs/shared/flagd-core/src/lib/feature-flag.spec.ts index 1fd4ebd7..db3d6095 100644 --- a/libs/shared/flagd-core/src/lib/feature-flag.spec.ts +++ b/libs/shared/flagd-core/src/lib/feature-flag.spec.ts @@ -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 }); }); }); diff --git a/libs/shared/flagd-core/src/lib/feature-flag.ts b/libs/shared/flagd-core/src/lib/feature-flag.ts index 46849bb8..94bd0259 100644 --- a/libs/shared/flagd-core/src/lib/feature-flag.ts +++ b/libs/shared/flagd-core/src/lib/feature-flag.ts @@ -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; diff --git a/libs/shared/flagd-core/src/lib/flagd-core.spec.ts b/libs/shared/flagd-core/src/lib/flagd-core.spec.ts index 2969a530..5d56f206 100644 --- a/libs/shared/flagd-core/src/lib/flagd-core.spec.ts +++ b/libs/shared/flagd-core/src/lib/flagd-core.spec.ts @@ -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); }); }); diff --git a/libs/shared/flagd-core/src/lib/flagd-core.ts b/libs/shared/flagd-core/src/lib/flagd-core.ts index 7ac63522..4d1e7ca1 100644 --- a/libs/shared/flagd-core/src/lib/flagd-core.ts +++ b/libs/shared/flagd-core/src/lib/flagd-core.ts @@ -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 { - // 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 { diff --git a/libs/shared/flagd-core/src/lib/parser.spec.ts b/libs/shared/flagd-core/src/lib/parser.spec.ts index b76d00f7..04b1c8fb 100644 --- a/libs/shared/flagd-core/src/lib/parser.spec.ts +++ b/libs/shared/flagd-core/src/lib/parser.spec.ts @@ -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] }); }); }); diff --git a/libs/shared/flagd-core/src/lib/parser.ts b/libs/shared/flagd-core/src/lib/parser.ts index ddbf4e7d..26851818 100644 --- a/libs/shared/flagd-core/src/lib/parser.ts +++ b/libs/shared/flagd-core/src/lib/parser.ts @@ -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(); diff --git a/libs/shared/flagd-core/src/lib/storage.ts b/libs/shared/flagd-core/src/lib/storage.ts index c4edb2ea..4ac6c47d 100644 --- a/libs/shared/flagd-core/src/lib/storage.ts +++ b/libs/shared/flagd-core/src/lib/storage.ts @@ -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); } } } diff --git a/libs/shared/flagd-core/src/lib/targeting/common.ts b/libs/shared/flagd-core/src/lib/targeting/common.ts index 0791b92e..93191cfb 100644 --- a/libs/shared/flagd-core/src/lib/targeting/common.ts +++ b/libs/shared/flagd-core/src/lib/targeting/common.ts @@ -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'; diff --git a/libs/shared/flagd-core/src/lib/targeting/fractional.ts b/libs/shared/flagd-core/src/lib/targeting/fractional.ts index 382f01ec..abe47432 100644 --- a/libs/shared/flagd-core/src/lib/targeting/fractional.ts +++ b/libs/shared/flagd-core/src/lib/targeting/fractional.ts @@ -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): string | null { @@ -11,7 +11,7 @@ export function fractional(data: unknown, context: Record): 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): 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): 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; diff --git a/libs/shared/flagd-core/src/lib/targeting/sem-ver.ts b/libs/shared/flagd-core/src/lib/targeting/sem-ver.ts index c47f431f..d5e14742 100644 --- a/libs/shared/flagd-core/src/lib/targeting/sem-ver.ts +++ b/libs/shared/flagd-core/src/lib/targeting/sem-ver.ts @@ -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; } diff --git a/libs/shared/flagd-core/src/lib/targeting/string-comp.ts b/libs/shared/flagd-core/src/lib/targeting/string-comp.ts index 3e79509f..caeca90c 100644 --- a/libs/shared/flagd-core/src/lib/targeting/string-comp.ts +++ b/libs/shared/flagd-core/src/lib/targeting/string-comp.ts @@ -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; } } diff --git a/libs/shared/flagd-core/src/lib/targeting/targeting.spec.ts b/libs/shared/flagd-core/src/lib/targeting/targeting.spec.ts index c5e39379..e593c005 100644 --- a/libs/shared/flagd-core/src/lib/targeting/targeting.spec.ts +++ b/libs/shared/flagd-core/src/lib/targeting/targeting.spec.ts @@ -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); + }); +}); diff --git a/libs/shared/flagd-core/src/lib/targeting/targeting.ts b/libs/shared/flagd-core/src/lib/targeting/targeting.ts index fae7c0df..9f37f25b 100644 --- a/libs/shared/flagd-core/src/lib/targeting/targeting.ts +++ b/libs/shared/flagd-core/src/lib/targeting/targeting.ts @@ -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); } diff --git a/tools/workspace-plugin/src/generators/open-feature/index.ts b/tools/workspace-plugin/src/generators/open-feature/index.ts index 9c08d99f..a469cbae 100644 --- a/tools/workspace-plugin/src/generators/open-feature/index.ts +++ b/tools/workspace-plugin/src/generators/open-feature/index.ts @@ -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; });