From a0900908b3e31643574899663fd9931774526a84 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 24 Jul 2023 13:29:11 -0700 Subject: [PATCH 1/2] feat: Add migrationVariation method. --- .../__tests__/LDClient.migrations.test.ts | 105 ++++++++++++++++++ packages/shared/sdk-server/package.json | 2 +- .../shared/sdk-server/src/LDClientImpl.ts | 27 ++++- .../shared/sdk-server/src/api/LDClient.ts | 23 ++++ .../src/api/data/LDMigrationStage.ts | 12 ++ .../shared/sdk-server/src/api/data/index.ts | 1 + 6 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 packages/shared/sdk-server/__tests__/LDClient.migrations.test.ts create mode 100644 packages/shared/sdk-server/src/api/data/LDMigrationStage.ts diff --git a/packages/shared/sdk-server/__tests__/LDClient.migrations.test.ts b/packages/shared/sdk-server/__tests__/LDClient.migrations.test.ts new file mode 100644 index 000000000..a24a0214e --- /dev/null +++ b/packages/shared/sdk-server/__tests__/LDClient.migrations.test.ts @@ -0,0 +1,105 @@ +import { LDClientImpl, LDMigrationStage } from '../src'; +import { LDClientCallbacks } from '../src/LDClientImpl'; +import TestData from '../src/integrations/test_data/TestData'; +import basicPlatform from './evaluation/mocks/platform'; + +/** + * Basic callback handler that records errors for tests. + */ +export default function makeCallbacks(): [Error[], LDClientCallbacks] { + const errors: Error[] = []; + return [ + errors, + { + onError: (error) => { + errors.push(error); + }, + onFailed: () => {}, + onReady: () => {}, + onUpdate: () => {}, + hasEventListeners: () => true, + }, + ]; +} + +describe('given an LDClient with test data', () => { + let client: LDClientImpl; + let td: TestData; + let callbacks: LDClientCallbacks; + let errors: Error[]; + + beforeEach(async () => { + td = new TestData(); + [errors, callbacks] = makeCallbacks(); + client = new LDClientImpl( + 'sdk-key', + basicPlatform, + { + updateProcessor: td.getFactory(), + sendEvents: false, + }, + callbacks + ); + + await client.waitForInitialization(); + }); + + afterEach(() => { + client.close(); + }); + + it.each(['off', 'dualwrite', 'shadow', 'live', 'rampdown', 'complete'])( + 'handles valid migration stages: %p', + async (value) => { + const flagKey = 'migration'; + td.update(td.flag(flagKey).valueForAll(value)); + // Get a default value that is not the value under test. + 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( + flagKey, + { key: 'test-key' }, + defaultValue as LDMigrationStage + ); + expect(res).toEqual(value); + } + ); + + it.each([ + LDMigrationStage.Off, + LDMigrationStage.DualWrite, + LDMigrationStage.Shadow, + LDMigrationStage.Live, + 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); + + expect(res).toEqual(stage); + }); + + 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); + expect(res).toEqual(LDMigrationStage.Off); + expect(errors.length).toEqual(1); + expect(errors[0].message).toEqual( + 'Unrecognized MigrationState for "bad-migration"; returning default value.' + ); + }); + + it('includes an error in the node callback', (done) => { + const flagKey = 'bad-migration'; + td.update(td.flag(flagKey).valueForAll('potato')); + client.variationMigration(flagKey, { key: 'test-key' }, LDMigrationStage.Off, (err, value) => { + const error = err as Error; + expect(error.message).toEqual( + 'Unrecognized MigrationState for "bad-migration"; returning default value.' + ); + expect(value).toEqual(LDMigrationStage.Off); + done(); + }); + }); +}); diff --git a/packages/shared/sdk-server/package.json b/packages/shared/sdk-server/package.json index 0e44d1d6c..2080bca76 100644 --- a/packages/shared/sdk-server/package.json +++ b/packages/shared/sdk-server/package.json @@ -24,7 +24,7 @@ "build": "npx tsc", "clean": "npx tsc --build --clean", "lint": "npx eslint . --ext .ts", - "lint:fix": "yarn run lint -- --fix" + "lint:fix": "yarn run lint --fix" }, "license": "Apache-2.0", "dependencies": { diff --git a/packages/shared/sdk-server/src/LDClientImpl.ts b/packages/shared/sdk-server/src/LDClientImpl.ts index 2367dc0e3..af2574692 100644 --- a/packages/shared/sdk-server/src/LDClientImpl.ts +++ b/packages/shared/sdk-server/src/LDClientImpl.ts @@ -10,7 +10,15 @@ import { subsystem, internal, } from '@launchdarkly/js-sdk-common'; -import { LDClient, LDFlagsStateOptions, LDOptions, LDStreamProcessor, LDFlagsState } from './api'; +import { + LDClient, + LDFlagsStateOptions, + LDOptions, + LDStreamProcessor, + LDFlagsState, + LDMigrationStage, + IsMigrationStage, +} from './api'; import { BigSegmentStoreMembership } from './api/interfaces'; import BigSegmentsManager from './BigSegmentsManager'; import BigSegmentStoreStatusProvider from './BigSegmentStatusProviderImpl'; @@ -260,6 +268,23 @@ export default class LDClientImpl implements LDClient { return res.detail; } + async variationMigration( + key: string, + context: LDContext, + defaultValue: LDMigrationStage, + callback?: (err: any, res: LDMigrationStage) => void + ): Promise { + const stringValue = await this.variation(key, context, defaultValue as string); + if (stringValue && !IsMigrationStage(stringValue)) { + const error = new Error(`Unrecognized MigrationState for "${key}"; returning default value.`); + this.onError(error); + callback?.(error, defaultValue); + return defaultValue; + } + callback?.(null, stringValue as LDMigrationStage); + return stringValue as LDMigrationStage; + } + async allFlagsState( context: LDContext, options?: LDFlagsStateOptions, diff --git a/packages/shared/sdk-server/src/api/LDClient.ts b/packages/shared/sdk-server/src/api/LDClient.ts index e36e5843f..76fab37f5 100644 --- a/packages/shared/sdk-server/src/api/LDClient.ts +++ b/packages/shared/sdk-server/src/api/LDClient.ts @@ -1,6 +1,7 @@ import { LDContext, LDEvaluationDetail, LDFlagValue } from '@launchdarkly/js-sdk-common'; import { LDFlagsStateOptions } from './data/LDFlagsStateOptions'; import { LDFlagsState } from './data/LDFlagsState'; +import { LDMigrationStage } from './data/LDMigrationStage'; /** * The LaunchDarkly SDK client object. @@ -119,6 +120,28 @@ export interface LDClient { callback?: (err: any, res: LDEvaluationDetail) => void ): Promise; + /** + * TKTK: Should use a common description. + * + * If the evaluated value of the flag cannot be converted to an LDMigrationStage, then an error + * event will be raised. + * + * @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 LDMigrationStage}). + * @returns + * A Promise which will be resolved with the result (as an{@link LDMigrationStage}). + */ + variationMigration( + key: string, + context: LDContext, + defaultValue: LDMigrationStage, + callback?: (err: any, res: LDMigrationStage) => void + ): 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/LDMigrationStage.ts b/packages/shared/sdk-server/src/api/data/LDMigrationStage.ts new file mode 100644 index 000000000..c582a4f7c --- /dev/null +++ b/packages/shared/sdk-server/src/api/data/LDMigrationStage.ts @@ -0,0 +1,12 @@ +export enum LDMigrationStage { + Off = 'off', + DualWrite = 'dualwrite', + Shadow = 'shadow', + Live = 'live', + Rampdown = 'rampdown', + Complete = 'complete', +} + +export function IsMigrationStage(value: string): boolean { + return Object.values(LDMigrationStage).includes(value as LDMigrationStage); +} diff --git a/packages/shared/sdk-server/src/api/data/index.ts b/packages/shared/sdk-server/src/api/data/index.ts index c0bd06c1e..cb75a1bc7 100644 --- a/packages/shared/sdk-server/src/api/data/index.ts +++ b/packages/shared/sdk-server/src/api/data/index.ts @@ -1,2 +1,3 @@ export * from './LDFlagsStateOptions'; export * from './LDFlagsState'; +export * from './LDMigrationStage'; From 5f2b983ae738de1d1cf226befa527dd987a39f84 Mon Sep 17 00:00:00 2001 From: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com> Date: Mon, 24 Jul 2023 13:37:21 -0700 Subject: [PATCH 2/2] Removing redundant check. --- packages/shared/sdk-server/src/LDClientImpl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared/sdk-server/src/LDClientImpl.ts b/packages/shared/sdk-server/src/LDClientImpl.ts index af2574692..51a045235 100644 --- a/packages/shared/sdk-server/src/LDClientImpl.ts +++ b/packages/shared/sdk-server/src/LDClientImpl.ts @@ -275,7 +275,7 @@ export default class LDClientImpl implements LDClient { callback?: (err: any, res: LDMigrationStage) => void ): Promise { const stringValue = await this.variation(key, context, defaultValue as string); - if (stringValue && !IsMigrationStage(stringValue)) { + if (!IsMigrationStage(stringValue)) { const error = new Error(`Unrecognized MigrationState for "${key}"; returning default value.`); this.onError(error); callback?.(error, defaultValue);