Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 175 additions & 0 deletions src/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,18 @@ describe('ReactSDKClient', () => {
};

let mockInnerClient: optimizely.Client;
let mockOptimizelyUserContext: optimizely.OptimizelyUserContext;
let createInstanceSpy: jest.Mock<optimizely.Client, [optimizely.Config]>;

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),
Expand All @@ -55,6 +63,7 @@ describe('ReactSDKClient', () => {
clearAllNotificationListeners: jest.fn(),
},
};

const anyOptly = optimizely as any;
anyOptly.createInstance.mockReturnValue(mockInnerClient);
createInstanceSpy = optimizely.createInstance as jest.Mock<optimizely.Client, [optimizely.Config]>;
Expand Down Expand Up @@ -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', () => {
Expand Down
76 changes: 74 additions & 2 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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<optimizely.Client, 'createUserContext'> {
user: UserContext;

onReady(opts?: { timeout?: number }): Promise<any>;
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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
Expand Down
11 changes: 10 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down