Skip to content
Merged
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
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -491,7 +491,7 @@ The following type definitions are used in the `ReactSDKClient` interface:
- `isFeatureEnabled(featureKey: string, overrideUserId?: string, overrideAttributes?: UserAttributes): boolean` Return the enabled status for the given feature and user
- `getEnabledFeatures(overrideUserId?: string, overrideAttributes?: UserAttributes): Array<string>`: Return the keys of all features enabled for the given user
- `track(eventKey: string, overrideUserId?: string | EventTags, overrideAttributes?: UserAttributes, eventTags?: EventTags): void` Track an event to the Optimizely results backend
- `setForcedVariation(experiment: string, overrideUserIdOrVariationKey: string, variationKey?: string | null): boolean` Set a forced variation for the given experiment, variation, and user
- `setForcedVariation(experiment: string, overrideUserIdOrVariationKey: string, variationKey?: string | null): boolean` Set a forced variation for the given experiment, variation, and user. **Note**: calling `setForcedVariation` on a given client will trigger a re-render of all `useExperiment` hooks and `OptimizelyExperiment` components that are using that client.
- `getForcedVariation(experiment: string, overrideUserId?: string): string | null` Get the forced variation for the given experiment, variation, and user

## Rollout or experiment a feature user-by-user
Expand Down Expand Up @@ -586,60 +586,60 @@ First-party code subject to copyrights held by Optimizely, Inc. and its contribu

This repository includes the following third party open source code:

[**hoist-non-react-statics**](https://github.com/mridgway/hoist-non-react-statics)
[**hoist-non-react-statics**](https://github.com/mridgway/hoist-non-react-statics)
Copyright &copy; 2015 Yahoo!, Inc.
License: [BSD](https://github.com/mridgway/hoist-non-react-statics/blob/master/LICENSE.md)

[**js-tokens**](https://github.com/lydell/js-tokens)
[**js-tokens**](https://github.com/lydell/js-tokens)
Copyright &copy; 2014, 2015, 2016, 2017, 2018, 2019 Simon Lydell
License: [MIT](https://github.com/lydell/js-tokens/blob/master/LICENSE)

[**json-schema**](https://github.com/kriszyp/json-schema)
[**json-schema**](https://github.com/kriszyp/json-schema)
Copyright &copy; 2005-2015, The Dojo Foundation
License: [BSD](https://github.com/kriszyp/json-schema/blob/master/LICENSE)

[**lodash**](https://github.com/lodash/lodash/)
[**lodash**](https://github.com/lodash/lodash/)
Copyright &copy; JS Foundation and other contributors
License: [MIT](https://github.com/lodash/lodash/blob/master/LICENSE)

[**loose-envify**](https://github.com/zertosh/loose-envify)
[**loose-envify**](https://github.com/zertosh/loose-envify)
Copyright &copy; 2015 Andres Suarez <zertosh@gmail.com>
License: [MIT](https://github.com/zertosh/loose-envify/blob/master/LICENSE)

[**node-murmurhash**](https://github.com/perezd/node-murmurhash)
[**node-murmurhash**](https://github.com/perezd/node-murmurhash)
Copyright &copy; 2012 Gary Court, Derek Perez
License: [MIT](https://github.com/perezd/node-murmurhash/blob/master/README.md)

[**object-assign**](https://github.com/sindresorhus/object-assign)
[**object-assign**](https://github.com/sindresorhus/object-assign)
Copyright &copy; Sindre Sorhus (sindresorhus.com)
License: [MIT](https://github.com/sindresorhus/object-assign/blob/master/license)

[**promise-polyfill**](https://github.com/taylorhakes/promise-polyfill)
[**promise-polyfill**](https://github.com/taylorhakes/promise-polyfill)
Copyright &copy; 2014 Taylor Hakes
Copyright &copy; 2014 Forbes Lindesay
License: [MIT](https://github.com/taylorhakes/promise-polyfill/blob/master/LICENSE)

[**prop-types**](https://github.com/facebook/prop-types)
[**prop-types**](https://github.com/facebook/prop-types)
Copyright &copy; 2013-present, Facebook, Inc.
License: [MIT](https://github.com/facebook/prop-types/blob/master/LICENSE)

[**react-is**](https://github.com/facebook/react)
[**react-is**](https://github.com/facebook/react)
Copyright &copy; Facebook, Inc. and its affiliates.
License: [MIT](https://github.com/facebook/react/blob/master/LICENSE)

[**react**](https://github.com/facebook/react)
[**react**](https://github.com/facebook/react)
Copyright &copy; Facebook, Inc. and its affiliates.
License: [MIT](https://github.com/facebook/react/blob/master/LICENSE)

[**scheduler**](https://github.com/facebook/react)
[**scheduler**](https://github.com/facebook/react)
Copyright &copy; Facebook, Inc. and its affiliates.
License: [MIT](https://github.com/facebook/react/blob/master/LICENSE)

[**utility-types**](https://github.com/piotrwitek/utility-types)
[**utility-types**](https://github.com/piotrwitek/utility-types)
Copyright &copy; 2016 Piotr Witek <piotrek.witek@gmail.com>
License: [MIT](https://github.com/piotrwitek/utility-types/blob/master/LICENSE)

[**node-uuid**](https://github.com/kelektiv/node-uuid)
[**node-uuid**](https://github.com/kelektiv/node-uuid)
Copyright &copy; 2010-2016 Robert Kieffer and other contributors
License: [MIT](https://github.com/kelektiv/node-uuid/blob/master/LICENSE.md)

Expand Down
1 change: 1 addition & 0 deletions src/Experiment.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ describe('<OptimizelyExperiment>', () => {
attributes: {},
},
isReady: jest.fn().mockReturnValue(false),
onForcedVariationsUpdate: jest.fn().mockReturnValue(() => {}),
} as unknown) as ReactSDKClient;
});

Expand Down
28 changes: 28 additions & 0 deletions src/client.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,4 +635,32 @@ describe('ReactSDKClient', () => {
});
});
});

describe('onForcedVariationsUpdate', () => {
let instance: ReactSDKClient;
beforeEach(() => {
instance = createInstance(config);
instance.setUser({
id: 'xxfueaojfe8&86',
attributes: {
foo: 'bar',
},
});
});

it('calls the handler function when setForcedVariation is called', () => {
const handler = jest.fn();
instance.onForcedVariationsUpdate(handler);
instance.setForcedVariation('my_exp', 'xxfueaojfe8&86', 'variation_a');
expect(handler).toBeCalledTimes(1);
});

it('removes the handler when the cleanup fn is called', () => {
const handler = jest.fn();
const cleanup = instance.onForcedVariationsUpdate(handler);
cleanup();
instance.setForcedVariation('my_exp', 'xxfueaojfe8&86', 'variation_a');
expect(handler).not.toBeCalled();
});
});
});
26 changes: 25 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type DisposeFn = () => void;

type OnUserUpdateHandler = (userInfo: UserContext) => void;

type OnForcedVariationsUpdateHandler = () => void;

export type OnReadyResult = {
success: boolean;
reason?: string;
Expand Down Expand Up @@ -131,6 +133,8 @@ export interface ReactSDKClient extends optimizely.Client {
setForcedVariation(experiment: string, overrideUserIdOrVariationKey: string, variationKey?: string | null): boolean;

getForcedVariation(experiment: string, overrideUserId?: string): string | null;

onForcedVariationsUpdate(handler: OnForcedVariationsUpdateHandler): DisposeFn;
}

type UserContext = {
Expand All @@ -150,6 +154,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient {
private userPromise: Promise<OnReadyResult>;
private isUserPromiseResolved = false;
private onUserUpdateHandlers: OnUserUpdateHandler[] = [];
private onForcedVariationsUpdateHandlers: OnForcedVariationsUpdateHandler[] = [];

private readonly _client: optimizely.Client;

Expand Down Expand Up @@ -237,6 +242,23 @@ class OptimizelyReactSDKClient implements ReactSDKClient {
};
}

/**
* Register a handler to be called whenever setForcedVariation is called on
* this client. Returns a function that un-registers the handler when called.
* @param {OnForcedVariationsUpdateHandler} handler
* @returns {DisposeFn}
*/
onForcedVariationsUpdate(handler: OnForcedVariationsUpdateHandler): DisposeFn {
this.onForcedVariationsUpdateHandlers.push(handler);

return (): void => {
const ind = this.onForcedVariationsUpdateHandlers.indexOf(handler);
if (ind > -1) {
this.onForcedVariationsUpdateHandlers.splice(ind, 1);
}
};
}

isReady(): boolean {
return this.dataReadyPromiseFulfilled;
}
Expand Down Expand Up @@ -594,7 +616,9 @@ class OptimizelyReactSDKClient implements ReactSDKClient {
if (finalUserId === null) {
return false;
}
return this._client.setForcedVariation(experiment, finalUserId, finalVariationKey);
const result = this._client.setForcedVariation(experiment, finalUserId, finalVariationKey);
this.onForcedVariationsUpdateHandlers.forEach(handler => handler());
return result;
}

/**
Expand Down
25 changes: 25 additions & 0 deletions src/hooks.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ describe('hooks', () => {
let UseExperimentLoggingComponent: React.FunctionComponent<any>;
let UseFeatureLoggingComponent: React.FunctionComponent<any>;
let mockLog: jest.Mock;
let forcedVariationUpdateCallbacks: Array<() => void>;

beforeEach(() => {
getOnReadyPromise = ({ timeout = 0 }: any): Promise<OnReadyResult> =>
Expand All @@ -76,6 +77,7 @@ describe('hooks', () => {
mockDelay = 10;
readySuccess = true;
notificationListenerCallbacks = [];
forcedVariationUpdateCallbacks = [];

optimizelyMock = ({
activate: activateMock,
Expand All @@ -97,6 +99,11 @@ describe('hooks', () => {
attributes: {},
},
isReady: () => readySuccess,
onForcedVariationsUpdate: jest.fn().mockImplementation(handler => {
forcedVariationUpdateCallbacks.push(handler);
return () => {};
}),
getForcedVariations: jest.fn().mockReturnValue({}),
} as unknown) as ReactSDKClient;

mockLog = jest.fn();
Expand Down Expand Up @@ -359,6 +366,24 @@ describe('hooks', () => {
component.update();
expect(activateMock).not.toHaveBeenCalled();
});

it('should re-render after setForcedVariation is called on the client', async () => {
activateMock.mockReturnValue(null);
const component = Enzyme.mount(
<OptimizelyProvider optimizely={optimizelyMock}>
<MyExperimentComponent options={{ autoUpdate: true }} />
</OptimizelyProvider>
);

component.update();
expect(component.text()).toBe('null|true|false');

activateMock.mockReturnValue('12345');
forcedVariationUpdateCallbacks[0]();

component.update();
expect(component.text()).toBe('12345|true|false');
});
});

describe('useFeature', () => {
Expand Down
11 changes: 11 additions & 0 deletions src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,17 @@ export const useExperiment: UseExperiment = (experimentKey, options = {}, overri
return (): void => {};
}, [isClientReady, options.autoUpdate, optimizely, experimentKey, getCurrentDecision]);

useEffect(
() =>
optimizely.onForcedVariationsUpdate(() => {
setState(prevState => ({
...prevState,
...getCurrentDecision(),
}));
}),
[getCurrentDecision, optimizely]
);

return [state.variation, state.clientReady, state.didTimeout];
};

Expand Down