From 381f54d03ea0d6f161f07e14bb97cb3452f0fc3a Mon Sep 17 00:00:00 2001 From: fayyazarshad <42437293+fayyazarshad@users.noreply.github.com> Date: Thu, 9 Jul 2020 02:39:00 +0500 Subject: [PATCH] feat: Added getFeatureVariableJson, getFeatureVariable and getAllFeatureVariables (#53) ## Summary - Update `@optimizely/optimizely-sdk` dependency to 4.1.0, adding support for JSON feature variables - Implemented getFeatureVariableJSON, getFeatureVariable and getAllFeatureVariables in client interface - Added deprecation note about `getFeatureVariables` client method ## Test plan Added unit tests for getFeatureVariableJSON, getFeatureVariable and getAllFeatureVariables Co-authored-by: Zeeshan Ashraf --- README.md | 2 +- package.json | 2 +- src/client.spec.ts | 171 +++++++++++++++++++++++++++++++++++++++++++++ src/client.ts | 113 +++++++++++++++++++++++++++++- yarn.lock | 19 +++-- 5 files changed, 294 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 141cb77b..78c5cc01 100644 --- a/README.md +++ b/README.md @@ -484,7 +484,7 @@ The following type definitions are used in the `ReactSDKClient` interface: - `onUserUpdate(handler: (userInfo: User) => void): () => void` Subscribe a callback to be called when this instance's current user changes. Returns a function that will unsubscribe the callback. - `activate(experimentKey: string, overrideUserId?: string, overrideAttributes?: UserAttributes): string | null` Activate an experiment, and return the variation for the given user. - `getVariation(experimentKey: string, overrideUserId?: string, overrideAttributes?: UserAttributes): string | null` Return the variation for the given experiment and user. -- `getFeatureVariables(featureKey: string, overrideUserId?: string, overrideAttributes?: UserAttributes): VariableValuesObject`: Decide and return variable values for the given feature and user +- `getFeatureVariables(featureKey: string, overrideUserId?: string, overrideAttributes?: UserAttributes): VariableValuesObject`: Decide and return variable values for the given feature and user
Warning: Deprecated since 2.1.0
`getAllFeatureVariables` is added in JavaScript SDK which is similarly returning all the feature variables, but it sends only single notification of type `all-feature-variables` instead of sending for each variable. As `getFeatureVariables` was added when this functionality wasn't provided by `JavaScript SDK`, so there is no need of it now and it would be removed in next major release - `getFeatureVariableString(featureKey: string, variableKey: string, overrideUserId?: string, overrideAttributes?: optimizely.UserAttributes): string | null`: Decide and return the variable value for the given feature, variable, and user - `getFeatureVariableInteger(featureKey: string, variableKey: string, overrideUserId?: string, overrideAttributes?: UserAttributes): number | null` Decide and return the variable value for the given feature, variable, and user - `getFeatureVariableBoolean(featureKey: string, variableKey: string, overrideUserId?: string, overrideAttributes?: UserAttributes): boolean | null` Decide and return the variable value for the given feature, variable, and user diff --git a/package.json b/package.json index b3c5c257..663f8bd3 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ }, "dependencies": { "@optimizely/js-sdk-logging": "^0.1.0", - "@optimizely/optimizely-sdk": "4.0.0", + "@optimizely/optimizely-sdk": "4.1.0", "hoist-non-react-statics": "^3.3.0", "prop-types": "^15.6.2", "utility-types": "^2.1.0 || ^3.0.0" diff --git a/src/client.spec.ts b/src/client.spec.ts index 209f7d7c..e9e45a81 100644 --- a/src/client.spec.ts +++ b/src/client.spec.ts @@ -38,6 +38,9 @@ describe('ReactSDKClient', () => { getForcedVariation: jest.fn(() => null), getFeatureVariableBoolean: jest.fn(() => null), getFeatureVariableDouble: jest.fn(() => null), + getFeatureVariableJSON: jest.fn(() => null), + getAllFeatureVariables: jest.fn(() => { return {} }), + getFeatureVariable: jest.fn(() => null), getFeatureVariableInteger: jest.fn(() => null), getFeatureVariableString: jest.fn(() => null), getOptimizelyConfig: jest.fn(() => null), @@ -358,6 +361,68 @@ describe('ReactSDKClient', () => { expect(mockInnerClient.getFeatureVariableDouble).toBeCalledWith('feat1', 'dvar1', 'user2', { bar: 'baz' }); }); + it('can use pre-set and override user for getFeatureVariableJSON', () => { + const mockFn = mockInnerClient.getFeatureVariableJSON as jest.Mock; + mockFn.mockReturnValue({ + num_buttons: 0, + text: 'default value', + }); + let result = instance.getFeatureVariableJSON('feat1', 'dvar1'); + expect(result).toEqual({ + num_buttons: 0, + text: 'default value', + }); + expect(mockFn).toBeCalledTimes(1); + expect(mockFn).toBeCalledWith('feat1', 'dvar1', 'user1', { + foo: 'bar', + }); + mockFn.mockReset(); + mockFn.mockReturnValue({ + num_buttons: 0, + text: 'variable value', + }); + result = instance.getFeatureVariableJSON('feat1', 'dvar1', 'user2', { + bar: 'baz', + }); + expect(result).toEqual({ + num_buttons: 0, + text: 'variable value', + }); + expect(mockInnerClient.getFeatureVariableJSON).toBeCalledTimes(1); + expect(mockInnerClient.getFeatureVariableJSON).toBeCalledWith('feat1', 'dvar1', 'user2', { bar: 'baz' }); + }); + + it('can use pre-set and override user for getFeatureVariable', () => { + const mockFn = mockInnerClient.getFeatureVariable as jest.Mock; + mockFn.mockReturnValue({ + num_buttons: 0, + text: 'default value', + }); + let result = instance.getFeatureVariable('feat1', 'dvar1', 'user1'); + expect(result).toEqual({ + num_buttons: 0, + text: 'default value', + }); + expect(mockFn).toBeCalledTimes(1); + expect(mockFn).toBeCalledWith('feat1', 'dvar1', 'user1', { + foo: 'bar', + }); + mockFn.mockReset(); + mockFn.mockReturnValue({ + num_buttons: 0, + text: 'variable value', + }); + result = instance.getFeatureVariable('feat1', 'dvar1', 'user2', { + bar: 'baz', + }); + expect(result).toEqual({ + num_buttons: 0, + text: 'variable value', + }); + expect(mockInnerClient.getFeatureVariable).toBeCalledTimes(1); + expect(mockInnerClient.getFeatureVariable).toBeCalledWith('feat1', 'dvar1', 'user2', { bar: 'baz' }); + }); + it('can use pre-set and override user for setForcedVariation', () => { const mockFn = mockInnerClient.setForcedVariation as jest.Mock; mockFn.mockReturnValue(true); @@ -398,6 +463,7 @@ describe('ReactSDKClient', () => { anyClient.getFeatureVariableString.mockReturnValue(null); anyClient.getFeatureVariableInteger.mockReturnValue(null); anyClient.getFeatureVariableDouble.mockReturnValue(null); + anyClient.getFeatureVariableJSON.mockReturnValue(null); const instance = createInstance(config); const result = instance.getFeatureVariables('feat1'); expect(result).toEqual({}); @@ -427,6 +493,10 @@ describe('ReactSDKClient', () => { type: 'double', key: 'dvar', }, + { + type: 'json', + key: 'jvar', + }, ], }, }, @@ -437,6 +507,9 @@ describe('ReactSDKClient', () => { anyClient.getFeatureVariableString.mockReturnValue('whatsup'); anyClient.getFeatureVariableInteger.mockReturnValue(10); anyClient.getFeatureVariableDouble.mockReturnValue(-10.5); + anyClient.getFeatureVariableJSON.mockReturnValue({ + value: 'json value' + }); const instance = createInstance(config); instance.setUser({ id: 'user1123', @@ -447,7 +520,105 @@ describe('ReactSDKClient', () => { svar: 'whatsup', ivar: 10, dvar: -10.5, + jvar: { + value: 'json value' + } + }); + }); + }); + + describe('getAllFeatureVariables', () => { + it('returns an empty object when the inner SDK returns no variables', () => { + const anyClient = mockInnerClient.getAllFeatureVariables as jest.Mock; + anyClient.mockReturnValue({}); + const instance = createInstance(config); + const result = instance.getAllFeatureVariables('feat1', 'user1'); + expect(result).toEqual({}); + }); + + it('returns an object with variables of all types returned from the inner sdk ', () => { + const anyClient = mockInnerClient.getAllFeatureVariables as jest.Mock; + anyClient.mockReturnValue({ + bvar: true, + svar: 'whatsup', + ivar: 10, + dvar: -10.5, + jvar: { + value: 'json value' + } + }); + const instance = createInstance(config); + instance.setUser({ + id: 'user1123', + }); + const result = instance.getAllFeatureVariables('feat1', 'user1'); + expect(result).toEqual({ + bvar: true, + svar: 'whatsup', + ivar: 10, + dvar: -10.5, + jvar: { + value: 'json value' + } + }); + }); + + it('can use pre-set and override user for getAllFeatureVariables', () => { + const mockFn = mockInnerClient.getAllFeatureVariables as jest.Mock; + mockFn.mockReturnValue({ + bvar: true, + svar: 'whatsup', + ivar: 10, + dvar: -10.5, + jvar: { + value: 'json value' + } + }); + const instance = createInstance(config); + instance.setUser({ + id: 'user1', + attributes: { + foo: 'bar', + }, + }); + let result = instance.getAllFeatureVariables('feat1', 'user1'); + expect(result).toEqual({ + bvar: true, + svar: 'whatsup', + ivar: 10, + dvar: -10.5, + jvar: { + value: 'json value' + } + }); + expect(mockFn).toBeCalledTimes(1); + expect(mockFn).toBeCalledWith('feat1', 'user1', { + foo: 'bar', + }); + mockFn.mockReset(); + mockFn.mockReturnValue({ + bvar: false, + svar: 'another var', + ivar: 11, + dvar: -11.5, + jvar: { + value: 'json another value' + } + }); + result = instance.getAllFeatureVariables('feat1', 'user2', { + bar: 'baz', + }); + expect(result).toEqual({ + bvar: false, + svar: 'another var', + ivar: 11, + dvar: -11.5, + jvar: { + value: 'json another value' + } }); + expect(mockInnerClient.getAllFeatureVariables).toBeCalledTimes(1); + expect(mockInnerClient.getAllFeatureVariables).toBeCalledWith('feat1', 'user2', { bar: 'baz' }); }); }); }); diff --git a/src/client.ts b/src/client.ts index 55a5b782..01c69e6d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -16,6 +16,7 @@ import * as optimizely from '@optimizely/optimizely-sdk'; import * as logging from '@optimizely/js-sdk-logging'; +import { UserAttributes } from "@optimizely/optimizely-sdk"; const logger = logging.getLogger('ReactSDK'); @@ -89,6 +90,26 @@ export interface ReactSDKClient extends optimizely.Client { overrideAttributes?: optimizely.UserAttributes ): number | null; + getFeatureVariableJSON( + featureKey: string, + variableKey: string, + overrideUserId?: string, + overrideAttributes?: optimizely.UserAttributes, + ): unknown + + getFeatureVariable( + featureKey: string, + variableKey: string, + overrideUserId: string, + overrideAttributes?: optimizely.UserAttributes + ): unknown + + getAllFeatureVariables( + featureKey: string, + overrideUserId: string, + overrideAttributes?: optimizely.UserAttributes + ): { [variableKey: string]: unknown } + isFeatureEnabled( featureKey: string, overrideUserId?: string, @@ -305,8 +326,14 @@ class OptimizelyReactSDKClient implements ReactSDKClient { } /** + * @deprecated since 2.1.0 + * getAllFeatureVariables is added in JavaScript SDK which is similarly returning all the feature variables, but + * it sends only single notification of type "all-feature-variables" instead of sending for each variable. + * As getFeatureVariables was added when this functionality wasn't provided by JavaScript SDK, so there is no + * need of it now and it would be removed in next major release + * * Get all variables for a feature, regardless of the feature being enabled/disabled - * @param {string} feature + * @param {string} featureKey * @param {string} [overrideUserId] * @param {optimizely.UserAttributes} [overrideAttributes] * @returns {VariableValuesObject} @@ -354,6 +381,10 @@ class OptimizelyReactSDKClient implements ReactSDKClient { case 'double': variableObj[key] = this._client.getFeatureVariableDouble(featureKey, key, userId, userAttributes); break; + + case 'json': + variableObj[key] = this._client.getFeatureVariableJSON(featureKey, key, userId, userAttributes); + break; } }); @@ -452,6 +483,86 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return this._client.getFeatureVariableDouble(feature, variable, user.id, user.attributes); } + /** + * Returns value for the given json variable attached to the given feature + * flag + * @param {string} feature + * @param {string} variable + * @param {string} [overrideUserId] + * @param {optimizely.UserAttributes} [overrideAttributes] + * @returns {(unknown | null)} + * @memberof OptimizelyReactSDKClient + */ + public getFeatureVariableJSON( + feature: string, + variable: string, + overrideUserId?: string, + overrideAttributes?: optimizely.UserAttributes + ): unknown { + const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + if (user.id === null) { + return null + } + return this._client.getFeatureVariableJSON( + feature, + variable, + user.id, + user.attributes, + ); + } + + /** + * Returns dynamically-typed value of the variable attached to the given + * feature flag. Returns null if the feature key or variable key is invalid. + * @param {string} featureKey + * @param {string} variableKey + * @param {string} [overrideUserId] + * @param {optimizely.UserAttributes} [overrideAttributes] + * @returns {(unknown | null)} + * @memberof OptimizelyReactSDKClient + */ + getFeatureVariable( + featureKey: string, + variableKey: string, + overrideUserId: string, + overrideAttributes?: optimizely.UserAttributes + ): unknown { + const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + if (user.id === null) { + return null + } + return this._client.getFeatureVariable( + featureKey, + variableKey, + user.id, + user.attributes, + ); + } + + /** + * Returns values for all the variables attached to the given feature flag + * @param {string} featureKey + * @param {string} overrideUserId + * @param {optimizely.UserAttributes} [overrideAttributes] + * @returns {({ [variableKey: string]: unknown } | null)} + * @memberof OptimizelyReactSDKClient + */ + getAllFeatureVariables( + featureKey: string, + overrideUserId: string, + overrideAttributes?: optimizely.UserAttributes + ): { [variableKey: string]: unknown } { + const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + if (user.id === null) { + return {}; + } + return this._client.getAllFeatureVariables( + featureKey, + user.id, + user.attributes, + ); + } + /** * Get an array of all enabled features * @param {string} [overrideUserId] diff --git a/yarn.lock b/yarn.lock index 643f6b33..9c2f7f6d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26,10 +26,10 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@optimizely/js-sdk-datafile-manager@^0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@optimizely/js-sdk-datafile-manager/-/js-sdk-datafile-manager-0.5.0.tgz#d6283ca3f8d2a60f30a6c300aaadf6a99cd9255e" - integrity sha512-ZDov8jphA+Ez+fA0anioA8ooJrraCFbeGm7GejzPTCZPMisNGtchrpdHr9TMd+hld+TOMBZ5NbqfMvvYhbB34A== +"@optimizely/js-sdk-datafile-manager@^0.6.0": + version "0.6.0" + resolved "https://registry.yarnpkg.com/@optimizely/js-sdk-datafile-manager/-/js-sdk-datafile-manager-0.6.0.tgz#00751f6887544a0da6bfa5f6e1b2c577ab034e6e" + integrity sha512-eqUZZZ1M6MYF2vttdjhV2GEd+C8/A94hS3fnGm1ScTMzWeoCcvlrdg3gO0WoXOVh2t7JfkvY9nNdkRJTan3uKg== dependencies: "@optimizely/js-sdk-logging" "^0.1.0" "@optimizely/js-sdk-utils" "^0.2.0" @@ -64,18 +64,17 @@ dependencies: uuid "^3.3.2" -"@optimizely/optimizely-sdk@4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@optimizely/optimizely-sdk/-/optimizely-sdk-4.0.0.tgz#0e63fb3fdd70e95481029025b2c633e9bd93f88d" - integrity sha512-ufwndTjg6wPXnJmbW/3SK2F3Dt7E1S1VQZ5oCoYrsLZ2oFrhES/urbWWTzC1t83gAokbqzSEZDuc/OBdZ6c9SA== +"@optimizely/optimizely-sdk@4.1.0": + version "4.1.0" + resolved "https://registry.yarnpkg.com/@optimizely/optimizely-sdk/-/optimizely-sdk-4.1.0.tgz#61c09dcb0df79e26ce5f5960204a171a86a38761" + integrity sha512-FIKAnPFYvAvtLJR66T0nvu+cKx52QDC4TMsQFojQo/LuJfkj3gRb5xlpv71LBkUCJIDQb9lg+bU0XndVOFEKBg== dependencies: - "@optimizely/js-sdk-datafile-manager" "^0.5.0" + "@optimizely/js-sdk-datafile-manager" "^0.6.0" "@optimizely/js-sdk-event-processor" "^0.4.0" "@optimizely/js-sdk-logging" "^0.1.0" "@optimizely/js-sdk-utils" "^0.2.0" json-schema "^0.2.3" murmurhash "0.0.2" - uuid "^3.3.2" "@types/cheerio@*": version "0.22.11"