diff --git a/src/client.spec.ts b/src/client.spec.ts index 7f786573..dd0e4dc0 100644 --- a/src/client.spec.ts +++ b/src/client.spec.ts @@ -25,10 +25,18 @@ describe('ReactSDKClient', () => { }; let mockInnerClient: optimizely.Client; + let mockOptimizelyUserContext: optimizely.OptimizelyUserContext; let createInstanceSpy: jest.Mock; beforeEach(() => { + mockOptimizelyUserContext = { + decide: jest.fn(), + decideAll: jest.fn(), + decideForKeys: jest.fn(), + } as any; + mockInnerClient = { + createUserContext: jest.fn(() => mockOptimizelyUserContext), activate: jest.fn(() => null), track: jest.fn(), isFeatureEnabled: jest.fn(() => false), @@ -55,6 +63,7 @@ describe('ReactSDKClient', () => { clearAllNotificationListeners: jest.fn(), }, }; + const anyOptly = optimizely as any; anyOptly.createInstance.mockReturnValue(mockInnerClient); createInstanceSpy = optimizely.createInstance as jest.Mock; @@ -456,6 +465,172 @@ describe('ReactSDKClient', () => { expect(mockFn).toBeCalledTimes(1); expect(mockFn).toBeCalledWith('exp1', 'user2'); }); + + it('can use pre-set and override user for decide', () => { + const mockFn = mockOptimizelyUserContext.decide as jest.Mock; + const mockCreateUserContext = mockInnerClient.createUserContext as jest.Mock; + mockFn.mockReturnValue({ + enabled: true, + flagKey: 'theFlag1', + reasons: [], + ruleKey: '', + userContext: mockOptimizelyUserContext, + variables: {}, + variationKey: 'varition1', + }); + let result = instance.decide('exp1'); + expect(result).toEqual({ + enabled: true, + flagKey: 'theFlag1', + reasons: [], + ruleKey: '', + userContext: mockOptimizelyUserContext, + variables: {}, + variationKey: 'varition1', + }); + expect(mockFn).toBeCalledTimes(1); + expect(mockFn).toBeCalledWith('exp1', []); + expect(mockCreateUserContext).toBeCalledWith('user1', { foo: 'bar' }); + mockFn.mockReset(); + mockFn.mockReturnValue({ + enabled: true, + flagKey: 'theFlag2', + reasons: [], + ruleKey: '', + userContext: mockOptimizelyUserContext, + variables: {}, + variationKey: 'varition2', + }); + result = instance.decide('exp1', [optimizely.OptimizelyDecideOption.INCLUDE_REASONS], 'user2', { bar: 'baz' }); + expect(result).toEqual({ + enabled: true, + flagKey: 'theFlag2', + reasons: [], + ruleKey: '', + userContext: mockOptimizelyUserContext, + variables: {}, + variationKey: 'varition2', + }); + expect(mockFn).toBeCalledTimes(1); + expect(mockFn).toBeCalledWith('exp1', [optimizely.OptimizelyDecideOption.INCLUDE_REASONS]); + expect(mockCreateUserContext).toBeCalledWith('user2', { bar: 'baz' }); + }); + + it('can use pre-set and override user for decideAll', () => { + const mockFn = mockOptimizelyUserContext.decideAll as jest.Mock; + const mockCreateUserContext = mockInnerClient.createUserContext as jest.Mock; + mockFn.mockReturnValue({ + 'theFlag1': { + enabled: true, + flagKey: 'theFlag1', + reasons: [], + ruleKey: '', + userContext: mockOptimizelyUserContext, + variables: {}, + variationKey: 'varition1', + } + }); + let result = instance.decideAll(); + expect(result).toEqual({ + 'theFlag1': { + enabled: true, + flagKey: 'theFlag1', + reasons: [], + ruleKey: '', + userContext: mockOptimizelyUserContext, + variables: {}, + variationKey: 'varition1', + } + }); + expect(mockFn).toBeCalledTimes(1); + expect(mockFn).toBeCalledWith([]); + expect(mockCreateUserContext).toBeCalledWith('user1', { foo: 'bar' }); + mockFn.mockReset(); + mockFn.mockReturnValue({ + 'theFlag2': { + enabled: true, + flagKey: 'theFlag2', + reasons: [], + ruleKey: '', + userContext: mockOptimizelyUserContext, + variables: {}, + variationKey: 'varition2', + } + }); + result = instance.decideAll([optimizely.OptimizelyDecideOption.INCLUDE_REASONS], 'user2', { bar: 'baz' }); + expect(result).toEqual({ + 'theFlag2': { + enabled: true, + flagKey: 'theFlag2', + reasons: [], + ruleKey: '', + userContext: mockOptimizelyUserContext, + variables: {}, + variationKey: 'varition2', + } + }); + expect(mockFn).toBeCalledTimes(1); + expect(mockFn).toBeCalledWith([optimizely.OptimizelyDecideOption.INCLUDE_REASONS]); + expect(mockCreateUserContext).toBeCalledWith('user2', { bar: 'baz' }); + }); + + it('can use pre-set and override user for decideForKeys', () => { + const mockFn = mockOptimizelyUserContext.decideForKeys as jest.Mock; + const mockCreateUserContext = mockInnerClient.createUserContext as jest.Mock; + mockFn.mockReturnValue({ + 'theFlag1': { + enabled: true, + flagKey: 'theFlag1', + reasons: [], + ruleKey: '', + userContext: mockOptimizelyUserContext, + variables: {}, + variationKey: 'varition1', + } + }); + let result = instance.decideForKeys(['theFlag1']); + expect(result).toEqual({ + 'theFlag1': { + enabled: true, + flagKey: 'theFlag1', + reasons: [], + ruleKey: '', + userContext: mockOptimizelyUserContext, + variables: {}, + variationKey: 'varition1', + } + }); + expect(mockFn).toBeCalledTimes(1); + expect(mockFn).toBeCalledWith(['theFlag1'], []); + expect(mockCreateUserContext).toBeCalledWith('user1', { foo: 'bar' }); + mockFn.mockReset(); + mockFn.mockReturnValue({ + 'theFlag2': { + enabled: true, + flagKey: 'theFlag2', + reasons: [], + ruleKey: '', + userContext: mockOptimizelyUserContext, + variables: {}, + variationKey: 'varition2', + } + }); + result = instance.decideForKeys(['theFlag1'], [optimizely.OptimizelyDecideOption.INCLUDE_REASONS], 'user2', { bar: 'baz' }); + expect(result).toEqual({ + 'theFlag2': { + enabled: true, + flagKey: 'theFlag2', + reasons: [], + ruleKey: '', + userContext: mockOptimizelyUserContext, + variables: {}, + variationKey: 'varition2', + } + }); + expect(mockFn).toBeCalledTimes(1); + expect(mockFn).toBeCalledWith(['theFlag1'], [optimizely.OptimizelyDecideOption.INCLUDE_REASONS]); + expect(mockCreateUserContext).toBeCalledWith('user2', { bar: 'baz' }); + }); }); describe('getFeatureVariables', () => { diff --git a/src/client.ts b/src/client.ts index b9fc1b5a..c86d59ec 100644 --- a/src/client.ts +++ b/src/client.ts @@ -16,7 +16,6 @@ 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'); @@ -37,7 +36,7 @@ export type OnReadyResult = { const REACT_SDK_CLIENT_ENGINE = 'react-sdk'; const REACT_SDK_CLIENT_VERSION = '2.4.2'; -export interface ReactSDKClient extends optimizely.Client { +export interface ReactSDKClient extends Omit { user: UserContext; onReady(opts?: { timeout?: number }): Promise; @@ -131,6 +130,26 @@ export interface ReactSDKClient extends optimizely.Client { setForcedVariation(experiment: string, overrideUserIdOrVariationKey: string, variationKey?: string | null): boolean; getForcedVariation(experiment: string, overrideUserId?: string): string | null; + + decide( + key: string, + options?: optimizely.OptimizelyDecideOption[], + overrideUserId?: string, + overrideAttributes?: optimizely.UserAttributes + ): optimizely.OptimizelyDecision | null + + decideAll( + options?: optimizely.OptimizelyDecideOption[], + overrideUserId?: string, + overrideAttributes?: optimizely.UserAttributes + ): { [key: string]: optimizely.OptimizelyDecision } | null + + decideForKeys( + keys: string[], + options?: optimizely.OptimizelyDecideOption[], + overrideUserId?: string, + overrideAttributes?: optimizely.UserAttributes + ): { [key: string]: optimizely.OptimizelyDecision } | null } type UserContext = { @@ -262,6 +281,59 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return this._client.activate(experimentKey, user.id, user.attributes); } + public decide( + key: string, + options: optimizely.OptimizelyDecideOption[] = [], + overrideUserId?: string, + overrideAttributes?: optimizely.UserAttributes + ): optimizely.OptimizelyDecision | null { + const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + if (user.id === null) { + logger.info('Not Evaluating feature "%s" because userId is not set', key); + return null; + } + const optlyUserContext: optimizely.OptimizelyUserContext | null = this._client.createUserContext(user.id, user.attributes); + if (optlyUserContext) { + return optlyUserContext.decide(key, options); + } + return null; + } + + public decideForKeys( + keys: string[], + options: optimizely.OptimizelyDecideOption[] = [], + overrideUserId?: string, + overrideAttributes?: optimizely.UserAttributes + ): { [key: string]: optimizely.OptimizelyDecision } | null { + const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + if (user.id === null) { + logger.info('Not Evaluating features because userId is not set'); + return null; + } + const optlyUserContext: optimizely.OptimizelyUserContext | null = this._client.createUserContext(user.id, user.attributes); + if (optlyUserContext) { + return optlyUserContext.decideForKeys(keys, options); + } + return null; + } + + public decideAll( + options: optimizely.OptimizelyDecideOption[] = [], + overrideUserId?: string, + overrideAttributes?: optimizely.UserAttributes + ): { [key: string]: optimizely.OptimizelyDecision } | null { + const user = this.getUserContextWithOverrides(overrideUserId, overrideAttributes); + if (user.id === null) { + logger.info('Not Evaluating features because userId is not set'); + return null; + } + const optlyUserContext: optimizely.OptimizelyUserContext | null = this._client.createUserContext(user.id, user.attributes); + if (optlyUserContext) { + return optlyUserContext.decideAll(options); + } + return null; + } + /** * Gets variation where visitor will be bucketed * @param {string} experimentKey diff --git a/src/index.ts b/src/index.ts index 0034e3a5..8cfa8043 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,7 +21,16 @@ export { withOptimizely, WithOptimizelyProps, WithoutOptimizelyProps } from './w export { OptimizelyExperiment } from './Experiment'; export { OptimizelyVariation } from './Variation'; -export { logging, errorHandler, setLogger, setLogLevel, enums, eventDispatcher } from '@optimizely/optimizely-sdk'; +export { + logging, + errorHandler, + setLogger, + setLogLevel, + enums, + eventDispatcher, + OptimizelyDecision, + OptimizelyDecideOption, +} from '@optimizely/optimizely-sdk'; export { createInstance, ReactSDKClient } from './client';