From f6a97923e9b450b57b31330bc74f45c80977f0be Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 27 Sep 2023 15:10:09 -0700 Subject: [PATCH 1/4] feat: Add typed variation methods. --- contract-tests/index.js | 3 +- contract-tests/sdkClientEntity.js | 30 ++- .../common/src/api/data/LDEvaluationDetail.ts | 19 ++ .../__tests__/LDClient.evaluation.test.ts | 93 +++++++++ .../__tests__/LDClient.migrations.test.ts | 6 +- .../shared/sdk-server/src/LDClientImpl.ts | 166 ++++++++++++++-- packages/shared/sdk-server/src/Migration.ts | 4 +- .../shared/sdk-server/src/api/LDClient.ts | 184 +++++++++++++++++- .../src/api/data/LDMigrationVariation.ts | 2 +- .../sdk-server/src/evaluation/ErrorKinds.ts | 1 + 10 files changed, 475 insertions(+), 33 deletions(-) diff --git a/contract-tests/index.js b/contract-tests/index.js index 6a5b72809..1588b4654 100644 --- a/contract-tests/index.js +++ b/contract-tests/index.js @@ -31,7 +31,8 @@ app.get('/', (req, res) => { 'migrations', 'event-sampling', 'config-override-kind', - 'metric-kind' + 'metric-kind', + 'strongly-typed', ], }); }); diff --git a/contract-tests/sdkClientEntity.js b/contract-tests/sdkClientEntity.js index bd9ad2d4f..459e28eb7 100644 --- a/contract-tests/sdkClientEntity.js +++ b/contract-tests/sdkClientEntity.js @@ -124,10 +124,32 @@ export async function newSdkClientEntity(options) { case 'evaluate': { const pe = params.evaluate; if (pe.detail) { - return await client.variationDetail(pe.flagKey, pe.context || pe.user, pe.defaultValue); + switch(pe.valueType) { + case "bool": + return await client.boolVariationDetail(pe.flagKey, pe.context || pe.user, pe.defaultValue); + case "int": + return await client.numberVariationDetail(pe.flagKey, pe.context || pe.user, pe.defaultValue); + case "double": + return await client.numberVariationDetail(pe.flagKey, pe.context || pe.user, pe.defaultValue); + case "string": + return await client.stringVariationDetail(pe.flagKey, pe.context || pe.user, pe.defaultValue); + default: + return await client.variationDetail(pe.flagKey, pe.context || pe.user, pe.defaultValue); + } + } else { - const value = await client.variation(pe.flagKey, pe.context || pe.user, pe.defaultValue); - return { value }; + switch(pe.valueType) { + case "bool": + return {value: await client.boolVariation(pe.flagKey, pe.context || pe.user, pe.defaultValue)}; + case "int": + return {value: await client.numberVariation(pe.flagKey, pe.context || pe.user, pe.defaultValue)}; + case "double": + return {value: await client.numberVariation(pe.flagKey, pe.context || pe.user, pe.defaultValue)}; + case "string": + return {value: await client.stringVariation(pe.flagKey, pe.context || pe.user, pe.defaultValue)}; + default: + return {value: await client.variation(pe.flagKey, pe.context || pe.user, pe.defaultValue)}; + } } } @@ -160,7 +182,7 @@ export async function newSdkClientEntity(options) { case 'migrationVariation': const migrationVariation = params.migrationVariation; - const res = await client.variationMigration( + const res = await client.migrationVariation( migrationVariation.key, migrationVariation.context, migrationVariation.defaultStage, diff --git a/packages/shared/common/src/api/data/LDEvaluationDetail.ts b/packages/shared/common/src/api/data/LDEvaluationDetail.ts index 37c959ce1..0a4f11dd7 100644 --- a/packages/shared/common/src/api/data/LDEvaluationDetail.ts +++ b/packages/shared/common/src/api/data/LDEvaluationDetail.ts @@ -27,3 +27,22 @@ export interface LDEvaluationDetail { */ reason: LDEvaluationReason; } + +export interface LDEvaluationDetailTyped { + /** + * The result of the flag evaluation. This will be either one of the flag's variations or + * the default value that was passed to `LDClient.variationDetail`. + */ + value: TFlag; + + /** + * The index of the returned value within the flag's list of variations, e.g. 0 for the + * first variation-- or `null` if the default value was returned. + */ + variationIndex?: number | null; + + /** + * An object describing the main factor that influenced the flag evaluation value. + */ + reason: LDEvaluationReason; +} diff --git a/packages/shared/sdk-server/__tests__/LDClient.evaluation.test.ts b/packages/shared/sdk-server/__tests__/LDClient.evaluation.test.ts index a5665de72..301f2c2d2 100644 --- a/packages/shared/sdk-server/__tests__/LDClient.evaluation.test.ts +++ b/packages/shared/sdk-server/__tests__/LDClient.evaluation.test.ts @@ -160,6 +160,99 @@ describe('given an LDClient with test data', () => { const valueB = await client.variation('my-feature-flag-1', userContextObject, 'default'); expect(valueB).toEqual(true); }); + + it('evaluates with jsonVariation', async () => { + td.update(td.flag('flagkey').booleanFlag().on(true)); + const boolRes: boolean = (await client.jsonVariation('flagkey', defaultUser, false)) as boolean; + expect(boolRes).toBe(true); + + td.update(td.flag('flagkey').valueForAll(62)); + const numericRes: number = (await client.jsonVariation( + 'flagkey', + defaultUser, + false, + )) as number; + expect(numericRes).toBe(62); + + td.update(td.flag('flagkey').valueForAll('potato')); + const stringRes: string = (await client.jsonVariation('flagkey', defaultUser, false)) as string; + expect(stringRes).toBe('potato'); + }); + + it('evaluates an existing boolean flag', async () => { + td.update(td.flag('flagkey').booleanFlag().on(true)); + expect(await client.boolVariation('flagkey', defaultUser, false)).toEqual(true); + }); + + it('it uses the default value when a boolean variation is for a flag of the wrong type', async () => { + td.update(td.flag('flagkey').valueForAll('potato')); + expect(await client.boolVariation('flagkey', defaultUser, false)).toEqual(false); + }); + + it('evaluates an existing numeric flag', async () => { + td.update(td.flag('flagkey').booleanFlag().valueForAll(18)); + expect(await client.numberVariation('flagkey', defaultUser, 36)).toEqual(18); + }); + + it('it uses the default value when a numeric variation is for a flag of the wrong type', async () => { + td.update(td.flag('flagkey').valueForAll('potato')); + expect(await client.numberVariation('flagkey', defaultUser, 36)).toEqual(36); + }); + + it('evaluates an existing string flag', async () => { + td.update(td.flag('flagkey').booleanFlag().valueForAll('potato')); + expect(await client.stringVariation('flagkey', defaultUser, 'default')).toEqual('potato'); + }); + + it('it uses the default value when a string variation is for a flag of the wrong type', async () => { + td.update(td.flag('flagkey').valueForAll(8)); + expect(await client.stringVariation('flagkey', defaultUser, 'default')).toEqual('default'); + }); + + it('evaluates an existing boolean flag with detail', async () => { + td.update(td.flag('flagkey').booleanFlag().on(true)); + const res = await client.boolVariationDetail('flagkey', defaultUser, false); + expect(res.value).toEqual(true); + expect(res.reason.kind).toBe('FALLTHROUGH'); + }); + + it('it uses the default value when a boolean variation is for a flag of the wrong type with detail', async () => { + td.update(td.flag('flagkey').valueForAll('potato')); + const res = await client.boolVariationDetail('flagkey', defaultUser, false); + expect(res.value).toEqual(false); + expect(res.reason.kind).toEqual('ERROR'); + expect(res.reason.errorKind).toEqual('WRONG_TYPE'); + }); + + it('evaluates an existing numeric flag with detail', async () => { + td.update(td.flag('flagkey').booleanFlag().valueForAll(18)); + const res = await client.numberVariationDetail('flagkey', defaultUser, 36); + expect(res.value).toEqual(18); + expect(res.reason.kind).toBe('FALLTHROUGH'); + }); + + it('it uses the default value when a numeric variation is for a flag of the wrong type with detail', async () => { + td.update(td.flag('flagkey').valueForAll('potato')); + const res = await client.numberVariationDetail('flagkey', defaultUser, 36); + expect(res.value).toEqual(36); + expect(res.reason.kind).toEqual('ERROR'); + expect(res.reason.errorKind).toEqual('WRONG_TYPE'); + }); + + it('evaluates an existing string flag with detail', async () => { + td.update(td.flag('flagkey').booleanFlag().valueForAll('potato')); + const res = await client.stringVariationDetail('flagkey', defaultUser, 'default'); + expect(res.value).toEqual('potato'); + expect(res.reason.kind).toBe('FALLTHROUGH'); + }); + + it('it uses the default value when a string variation is for a flag of the wrong type with detail', async () => { + td.update(td.flag('flagkey').valueForAll(8)); + const res = await client.stringVariationDetail('flagkey', defaultUser, 'default'); + expect(res.value).toEqual('default'); + expect(res.reason.kind).toEqual('ERROR'); + expect(res.reason.errorKind).toEqual('WRONG_TYPE'); + }); }); describe('given an offline client', () => { diff --git a/packages/shared/sdk-server/__tests__/LDClient.migrations.test.ts b/packages/shared/sdk-server/__tests__/LDClient.migrations.test.ts index 460c3c333..b3ee43612 100644 --- a/packages/shared/sdk-server/__tests__/LDClient.migrations.test.ts +++ b/packages/shared/sdk-server/__tests__/LDClient.migrations.test.ts @@ -57,7 +57,7 @@ describe('given an LDClient with test data', () => { const defaultValue = Object.values(LDMigrationStage).find((item) => item !== value); // Verify the pre-condition that the default value is not the value under test. expect(defaultValue).not.toEqual(value); - const res = await client.variationMigration( + const res = await client.migrationVariation( flagKey, { key: 'test-key' }, defaultValue as LDMigrationStage, @@ -74,7 +74,7 @@ describe('given an LDClient with test data', () => { LDMigrationStage.RampDown, LDMigrationStage.Complete, ])('returns the default value if the flag does not exist: default = %p', async (stage) => { - const res = await client.variationMigration('no-flag', { key: 'test-key' }, stage); + const res = await client.migrationVariation('no-flag', { key: 'test-key' }, stage); expect(res.value).toEqual(stage); }); @@ -82,7 +82,7 @@ describe('given an LDClient with test data', () => { it('produces an error event for a migration flag with an incorrect value', async () => { const flagKey = 'bad-migration'; td.update(td.flag(flagKey).valueForAll('potato')); - const res = await client.variationMigration(flagKey, { key: 'test-key' }, LDMigrationStage.Off); + const res = await client.migrationVariation(flagKey, { key: 'test-key' }, LDMigrationStage.Off); expect(res.value).toEqual(LDMigrationStage.Off); expect(errors.length).toEqual(1); expect(errors[0].message).toEqual( diff --git a/packages/shared/sdk-server/src/LDClientImpl.ts b/packages/shared/sdk-server/src/LDClientImpl.ts index 80c94f904..53d0c117e 100644 --- a/packages/shared/sdk-server/src/LDClientImpl.ts +++ b/packages/shared/sdk-server/src/LDClientImpl.ts @@ -7,9 +7,11 @@ import { internal, LDContext, LDEvaluationDetail, + LDEvaluationDetailTyped, LDLogger, Platform, subsystem, + TypeValidators, } from '@launchdarkly/js-sdk-common'; import { @@ -301,7 +303,105 @@ export default class LDClientImpl implements LDClient { }); } - async variationMigration( + private typedEval( + key: string, + context: LDContext, + defaultValue: TResult, + eventFactory: EventFactory, + typeChecker: (value: unknown) => [boolean, string], + ): Promise { + return new Promise>((resolve) => { + this.evaluateIfPossible( + key, + context, + defaultValue, + eventFactory, + (res) => { + const typedRes: LDEvaluationDetailTyped = { + value: res.detail.value as TResult, + reason: res.detail.reason, + variationIndex: res.detail.variationIndex, + }; + resolve(typedRes); + }, + typeChecker, + ); + }); + } + + async boolVariation(key: string, context: LDContext, defaultValue: boolean): Promise { + return ( + await this.typedEval(key, context, defaultValue, this.eventFactoryDefault, (value) => [ + TypeValidators.Boolean.is(value), + TypeValidators.Boolean.getType(), + ]) + ).value; + } + + async numberVariation(key: string, context: LDContext, defaultValue: number): Promise { + return ( + await this.typedEval(key, context, defaultValue, this.eventFactoryDefault, (value) => [ + TypeValidators.Number.is(value), + TypeValidators.Number.getType(), + ]) + ).value; + } + + async stringVariation(key: string, context: LDContext, defaultValue: string): Promise { + return ( + await this.typedEval(key, context, defaultValue, this.eventFactoryDefault, (value) => [ + TypeValidators.String.is(value), + TypeValidators.String.getType(), + ]) + ).value; + } + + jsonVariation(key: string, context: LDContext, defaultValue: unknown): Promise { + return this.variation(key, context, defaultValue); + } + + boolVariationDetail( + key: string, + context: LDContext, + defaultValue: boolean, + ): Promise> { + return this.typedEval(key, context, defaultValue, this.eventFactoryWithReasons, (value) => [ + TypeValidators.Boolean.is(value), + TypeValidators.Boolean.getType(), + ]); + } + + numberVariationDetail( + key: string, + context: LDContext, + defaultValue: number, + ): Promise> { + return this.typedEval(key, context, defaultValue, this.eventFactoryWithReasons, (value) => [ + TypeValidators.Number.is(value), + TypeValidators.Number.getType(), + ]); + } + + stringVariationDetail( + key: string, + context: LDContext, + defaultValue: string, + ): Promise> { + return this.typedEval(key, context, defaultValue, this.eventFactoryWithReasons, (value) => [ + TypeValidators.String.is(value), + TypeValidators.String.getType(), + ]); + } + + jsonVariationDetail( + key: string, + context: LDContext, + defaultValue: unknown, + ): Promise> { + return this.variationDetail(key, context, defaultValue); + } + + async migrationVariation( key: string, context: LDContext, defaultValue: LDMigrationStage, @@ -523,6 +623,7 @@ export default class LDClientImpl implements LDClient { defaultValue: any, eventFactory: EventFactory, cb: (res: EvalResult, flag?: Flag) => void, + typeChecker?: (value: any) => [boolean, string], ): void { if (this.config.offline) { this.logger?.info('Variation called in offline mode. Returning default value.'); @@ -574,24 +675,25 @@ export default class LDClientImpl implements LDClient { evalRes.setDefault(defaultValue); } - // Immediately invoked function expression to get the event out of the callback - // path and allow access to async methods. - (async () => { - const indexSamplingRatio = await this.eventConfig.indexEventSamplingRatio(); - evalRes.events?.forEach((event) => { - this.eventProcessor.sendEvent({ ...event, indexSamplingRatio }); - }); - this.eventProcessor.sendEvent( - eventFactory.evalEvent( - flag, - evalContext, - evalRes.detail, + if (typeChecker) { + const [matched, type] = typeChecker(evalRes.detail.value); + if (!matched) { + // TODO: change the detail. + // TODO: Use the default. + const errorRes = EvalResult.forError( + ErrorKinds.WrongType, + `Did not receive expected type (${type}) evaluating feature flag "${flagKey}"`, defaultValue, - undefined, - indexSamplingRatio, - ), - ); - })(); + ); + // Method intentionally not awaited. + this.sendEvalEvent(evalRes, eventFactory, flag, evalContext, defaultValue); + cb(errorRes, flag); + return; + } + } + + // Method intentionally not awaited. + this.sendEvalEvent(evalRes, eventFactory, flag, evalContext, defaultValue); cb(evalRes, flag); }, eventFactory, @@ -599,12 +701,36 @@ export default class LDClientImpl implements LDClient { }); } + private async sendEvalEvent( + evalRes: EvalResult, + eventFactory: EventFactory, + flag: Flag, + evalContext: Context, + defaultValue: any, + ) { + const indexSamplingRatio = await this.eventConfig.indexEventSamplingRatio(); + evalRes.events?.forEach((event) => { + this.eventProcessor.sendEvent({ ...event, indexSamplingRatio }); + }); + this.eventProcessor.sendEvent( + eventFactory.evalEvent( + flag, + evalContext, + evalRes.detail, + defaultValue, + undefined, + indexSamplingRatio, + ), + ); + } + private evaluateIfPossible( flagKey: string, context: LDContext, defaultValue: any, eventFactory: EventFactory, cb: (res: EvalResult, flag?: Flag) => void, + typeChecker?: (value: any) => [boolean, string], ): void { if (!this.initialized()) { this.featureStore.initialized((storeInitialized) => { @@ -613,7 +739,7 @@ export default class LDClientImpl implements LDClient { 'Variation called before LaunchDarkly client initialization completed' + " (did you wait for the 'ready' event?) - using last known values from feature store", ); - this.variationInternal(flagKey, context, defaultValue, eventFactory, cb); + this.variationInternal(flagKey, context, defaultValue, eventFactory, cb, typeChecker); return; } this.logger?.warn( @@ -624,6 +750,6 @@ export default class LDClientImpl implements LDClient { }); return; } - this.variationInternal(flagKey, context, defaultValue, eventFactory, cb); + this.variationInternal(flagKey, context, defaultValue, eventFactory, cb, typeChecker); } } diff --git a/packages/shared/sdk-server/src/Migration.ts b/packages/shared/sdk-server/src/Migration.ts index ebcebf195..3eebf76d9 100644 --- a/packages/shared/sdk-server/src/Migration.ts +++ b/packages/shared/sdk-server/src/Migration.ts @@ -232,7 +232,7 @@ class Migration< defaultStage: LDMigrationStage, payload?: TMigrationReadInput, ): Promise> { - const stage = await this.client.variationMigration(key, context, defaultStage); + const stage = await this.client.migrationVariation(key, context, defaultStage); const res = await this.readTable[stage.value]({ payload, tracker: stage.tracker, @@ -248,7 +248,7 @@ class Migration< defaultStage: LDMigrationStage, payload?: TMigrationWriteInput, ): Promise> { - const stage = await this.client.variationMigration(key, context, defaultStage); + const stage = await this.client.migrationVariation(key, context, defaultStage); const res = await this.writeTable[stage.value]({ payload, tracker: stage.tracker, diff --git a/packages/shared/sdk-server/src/api/LDClient.ts b/packages/shared/sdk-server/src/api/LDClient.ts index cb4534947..177727c44 100644 --- a/packages/shared/sdk-server/src/api/LDClient.ts +++ b/packages/shared/sdk-server/src/api/LDClient.ts @@ -1,4 +1,9 @@ -import { LDContext, LDEvaluationDetail, LDFlagValue } from '@launchdarkly/js-sdk-common'; +import { + LDContext, + LDEvaluationDetail, + LDEvaluationDetailTyped, + LDFlagValue, +} from '@launchdarkly/js-sdk-common'; import { LDMigrationOpEvent, LDMigrationVariation } from './data'; import { LDFlagsState } from './data/LDFlagsState'; @@ -137,12 +142,187 @@ export interface LDClient { * @returns * A Promise which will be resolved with the result (as an{@link LDMigrationVariation}). */ - variationMigration( + migrationVariation( key: string, context: LDContext, defaultValue: LDMigrationStage, ): Promise; + /** + * Determines the boolean variation of a feature flag for a context. + * + * If the flag variation does not have a boolean value, defaultValue is returned. + * + * @param key The unique key of the feature flag. + * @param context The context requesting the flag. The client will generate an analytics event to + * register this context with LaunchDarkly if the context does not already exist. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @returns + * A Promise which will be resolved with the result value. + */ + boolVariation(key: string, context: LDContext, defaultValue: boolean): Promise; + + /** + * Determines the numeric variation of a feature flag for a context. + * + * If the flag variation does not have a numeric value, defaultValue is returned. + * + * @param key The unique key of the feature flag. + * @param context The context requesting the flag. The client will generate an analytics event to + * register this context with LaunchDarkly if the context does not already exist. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @returns + * A Promise which will be resolved with the result value. + */ + numberVariation(key: string, context: LDContext, defaultValue: number): Promise; + + /** + * Determines the string variation of a feature flag for a context. + * + * If the flag variation does not have a string value, defaultValue is returned. + * + * @param key The unique key of the feature flag. + * @param context The context requesting the flag. The client will generate an analytics event to + * register this context with LaunchDarkly if the context does not already exist. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @returns + * A Promise which will be resolved with the result value. + */ + stringVariation(key: string, context: LDContext, defaultValue: string): Promise; + + /** + * Determines the variation of a feature flag for a context. + * + * This version may be favored in TypeScript versus `variation` because it returns + * an `unknown` type instead of `any`. `unknown` will require a cast before usage. + * + * @param key The unique key of the feature flag. + * @param context The context requesting the flag. The client will generate an analytics event to + * register this context with LaunchDarkly if the context does not already exist. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @returns + * A Promise which will be resolved with the result value. + */ + jsonVariation(key: string, context: LDContext, defaultValue: unknown): Promise; + + /** + * Determines the boolean variation of a feature flag for a context, along with information about + * how it was calculated. + * + * The `reason` property of the result will also be included in analytics events, if you are + * capturing detailed event data for this flag. + * + * If the flag variation does not have a boolean value, defaultValue is returned. The reason will + * indicate an error of the type `WRONG_KIND` in this case. + * + * For more information, see the [SDK reference + * guide](https://docs.launchdarkly.com/sdk/features/evaluation-reasons#nodejs-server-side). + * + * @param key The unique key of the feature flag. + * @param context The context requesting the flag. The client will generate an analytics event to + * register this context with LaunchDarkly if the context does not already exist. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @returns + * A Promise which will be resolved with the result + * (as an {@link LDEvaluationDetailTyped}). + */ + boolVariationDetail( + key: string, + context: LDContext, + defaultValue: boolean, + ): Promise>; + + /** + * Determines the numeric variation of a feature flag for a context, along with information about + * how it was calculated. + * + * The `reason` property of the result will also be included in analytics events, if you are + * capturing detailed event data for this flag. + * + * If the flag variation does not have a numeric value, defaultValue is returned. The reason will + * indicate an error of the type `WRONG_KIND` in this case. + * + * For more information, see the [SDK reference + * guide](https://docs.launchdarkly.com/sdk/features/evaluation-reasons#nodejs-server-side). + * + * @param key The unique key of the feature flag. + * @param context The context requesting the flag. The client will generate an analytics event to + * register this context with LaunchDarkly if the context does not already exist. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @returns + * A Promise which will be resolved with the result + * (as an {@link LDEvaluationDetailTyped}). + */ + numberVariationDetail( + key: string, + context: LDContext, + defaultValue: number, + ): Promise>; + + /** + * Determines the string variation of a feature flag for a context, along with information about + * how it was calculated. + * + * The `reason` property of the result will also be included in analytics events, if you are + * capturing detailed event data for this flag. + * + * If the flag variation does not have a string value, defaultValue is returned. The reason will + * indicate an error of the type `WRONG_KIND` in this case. + * + * For more information, see the [SDK reference + * guide](https://docs.launchdarkly.com/sdk/features/evaluation-reasons#nodejs-server-side). + * + * @param key The unique key of the feature flag. + * @param context The context requesting the flag. The client will generate an analytics event to + * register this context with LaunchDarkly if the context does not already exist. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @returns + * A Promise which will be resolved with the result + * (as an {@link LDEvaluationDetailTyped}). + */ + stringVariationDetail( + key: string, + context: LDContext, + defaultValue: string, + ): Promise>; + + /** + * Determines the variation of a feature flag for a context, along with information about how it + * was calculated. + * + * The `reason` property of the result will also be included in analytics events, if you are + * capturing detailed event data for this flag. + * + * This version may be favored in TypeScript versus `variation` because it returns + * an `unknown` type instead of `any`. `unknown` will require a cast before usage. + * + * For more information, see the [SDK reference + * guide](https://docs.launchdarkly.com/sdk/features/evaluation-reasons#nodejs-server-side). + * + * @param key The unique key of the feature flag. + * @param context The context requesting the flag. The client will generate an analytics event to + * register this context with LaunchDarkly if the context does not already exist. + * @param defaultValue The default value of the flag, to be used if the value is not available + * from LaunchDarkly. + * @param callback A Node-style callback to receive the result (as an {@link LDEvaluationDetail}). + * If omitted, you will receive a Promise instead. + * @returns + * If you provided a callback, then nothing. Otherwise, a Promise which will be resolved with + * the result (as an{@link LDEvaluationDetailTyped}). + */ + jsonVariationDetail( + key: string, + context: LDContext, + defaultValue: unknown, + ): Promise>; + /** * Builds an object that encapsulates the state of all feature flags for a given context. * This includes the flag values and also metadata that can be used on the front end. This diff --git a/packages/shared/sdk-server/src/api/data/LDMigrationVariation.ts b/packages/shared/sdk-server/src/api/data/LDMigrationVariation.ts index 396c0012b..fe4f81efb 100644 --- a/packages/shared/sdk-server/src/api/data/LDMigrationVariation.ts +++ b/packages/shared/sdk-server/src/api/data/LDMigrationVariation.ts @@ -80,7 +80,7 @@ export interface LDMigrationTracker { export interface LDMigrationVariation { /** * The result of the flag evaluation. This will be either one of the flag's variations or - * the default value that was passed to `LDClient.variationMigration`. + * the default value that was passed to `LDClient.migrationVariation`. */ value: LDMigrationStage; diff --git a/packages/shared/sdk-server/src/evaluation/ErrorKinds.ts b/packages/shared/sdk-server/src/evaluation/ErrorKinds.ts index e2bb87af2..33ba8667a 100644 --- a/packages/shared/sdk-server/src/evaluation/ErrorKinds.ts +++ b/packages/shared/sdk-server/src/evaluation/ErrorKinds.ts @@ -8,6 +8,7 @@ enum ErrorKinds { UserNotSpecified = 'USER_NOT_SPECIFIED', FlagNotFound = 'FLAG_NOT_FOUND', ClientNotReady = 'CLIENT_NOT_READY', + WrongType = 'WRONG_TYPE', } /** From 0fa64ebaa452c87e7ef49fc7eb2cc2bd2825e2ab Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 27 Sep 2023 15:18:18 -0700 Subject: [PATCH 2/4] Better comment. Remove TODOs. --- packages/shared/sdk-server/src/LDClientImpl.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/shared/sdk-server/src/LDClientImpl.ts b/packages/shared/sdk-server/src/LDClientImpl.ts index 53d0c117e..98fc32fed 100644 --- a/packages/shared/sdk-server/src/LDClientImpl.ts +++ b/packages/shared/sdk-server/src/LDClientImpl.ts @@ -678,21 +678,19 @@ export default class LDClientImpl implements LDClient { if (typeChecker) { const [matched, type] = typeChecker(evalRes.detail.value); if (!matched) { - // TODO: change the detail. - // TODO: Use the default. const errorRes = EvalResult.forError( ErrorKinds.WrongType, `Did not receive expected type (${type}) evaluating feature flag "${flagKey}"`, defaultValue, ); - // Method intentionally not awaited. + // Method intentionally not awaited, puts event processing outside hot path. this.sendEvalEvent(evalRes, eventFactory, flag, evalContext, defaultValue); cb(errorRes, flag); return; } } - // Method intentionally not awaited. + // Method intentionally not awaited, puts event processing outside hot path. this.sendEvalEvent(evalRes, eventFactory, flag, evalContext, defaultValue); cb(evalRes, flag); }, From d62fe1bc7a87e8f641d0a761a808886bbb92b0b3 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 27 Sep 2023 15:25:34 -0700 Subject: [PATCH 3/4] Update contract-tests/sdkClientEntity.js Co-authored-by: Yusinto Ngadiman --- contract-tests/sdkClientEntity.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contract-tests/sdkClientEntity.js b/contract-tests/sdkClientEntity.js index 459e28eb7..6756bcd28 100644 --- a/contract-tests/sdkClientEntity.js +++ b/contract-tests/sdkClientEntity.js @@ -127,8 +127,7 @@ export async function newSdkClientEntity(options) { switch(pe.valueType) { case "bool": return await client.boolVariationDetail(pe.flagKey, pe.context || pe.user, pe.defaultValue); - case "int": - return await client.numberVariationDetail(pe.flagKey, pe.context || pe.user, pe.defaultValue); + case "int": // Intentional fallthrough. case "double": return await client.numberVariationDetail(pe.flagKey, pe.context || pe.user, pe.defaultValue); case "string": From 24abe4cc346d15051038cc6f78327d4cfe2d1649 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Wed, 27 Sep 2023 15:26:17 -0700 Subject: [PATCH 4/4] intentional fallthrough --- contract-tests/sdkClientEntity.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contract-tests/sdkClientEntity.js b/contract-tests/sdkClientEntity.js index 6756bcd28..080c97813 100644 --- a/contract-tests/sdkClientEntity.js +++ b/contract-tests/sdkClientEntity.js @@ -140,8 +140,7 @@ export async function newSdkClientEntity(options) { switch(pe.valueType) { case "bool": return {value: await client.boolVariation(pe.flagKey, pe.context || pe.user, pe.defaultValue)}; - case "int": - return {value: await client.numberVariation(pe.flagKey, pe.context || pe.user, pe.defaultValue)}; + case "int": // Intentional fallthrough. case "double": return {value: await client.numberVariation(pe.flagKey, pe.context || pe.user, pe.defaultValue)}; case "string":