Skip to content

Commit e92a4c1

Browse files
authored
fix: Hooks gracefully return with an error message when optimizely prop is null (#154)
1 parent 500a3f9 commit e92a4c1

File tree

5 files changed

+62
-22
lines changed

5 files changed

+62
-22
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1212

1313
- fixed issue [#121](https://github.com/optimizely/react-sdk/issues/121):`ActivateListenerPayload` and `TrackListenerPayload` types were exported from `@optimizely/optimizely-sdk` but were missing from `@optimizely/react-sdk` exports. ([#150](https://github.com/optimizely/react-sdk/pull/150)).
1414

15+
- addresses issues [#152](https://github.com/optimizely/react-sdk/issues/152) and [#134](https://github.com/optimizely/react-sdk/issues/134): Gracefully returns pessimistic default values when hooks fail instead of throwing an error.
16+
1517
### Bug fixes
1618
- Fixed issue [#134](https://github.com/optimizely/react-sdk/issues/134) of the React SDK crashing when the internal Optimizely client returns as a null value. [PR #149](https://github.com/optimizely/react-sdk/pull/149)
1719

src/Experiment.spec.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@ describe('<OptimizelyExperiment>', () => {
4545
optimizelyMock = ({
4646
onReady: jest.fn().mockImplementation(config => onReadyPromise),
4747
activate: jest.fn().mockImplementation(experimentKey => variationKey),
48-
onUserUpdate: jest.fn().mockImplementation(handler => () => {}),
48+
onUserUpdate: jest.fn().mockImplementation(handler => () => { }),
4949
notificationCenter: {
50-
addNotificationListener: jest.fn().mockImplementation((type, handler) => {}),
51-
removeNotificationListener: jest.fn().mockImplementation(id => {}),
50+
addNotificationListener: jest.fn().mockImplementation((type, handler) => { }),
51+
removeNotificationListener: jest.fn().mockImplementation(id => { }),
5252
},
5353
user: {
5454
id: 'testuser',
@@ -57,15 +57,15 @@ describe('<OptimizelyExperiment>', () => {
5757
isReady: jest.fn().mockImplementation(() => isReady),
5858
getIsReadyPromiseFulfilled: () => true,
5959
getIsUsingSdkKey: () => true,
60-
onForcedVariationsUpdate: jest.fn().mockReturnValue(() => {}),
60+
onForcedVariationsUpdate: jest.fn().mockReturnValue(() => { }),
6161
} as unknown) as ReactSDKClient;
6262
});
6363

64-
it('throws an error when not rendered in the context of an OptimizelyProvider', () => {
64+
it('does not throw an error when not rendered in the context of an OptimizelyProvider', () => {
6565
expect(() => {
6666
// @ts-ignore
6767
mount(<OptimizelyExperiment experiment="experiment1">{variation => variation}</OptimizelyExperiment>);
68-
}).toThrow();
68+
}).toBeDefined();
6969
});
7070

7171
describe('when isServerSide prop is false', () => {

src/Feature.spec.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,10 @@ describe('<OptimizelyFeature>', () => {
4646
onReady: jest.fn().mockImplementation(config => onReadyPromise),
4747
getFeatureVariables: jest.fn().mockImplementation(() => featureVariables),
4848
isFeatureEnabled: jest.fn().mockImplementation(() => isEnabledMock),
49-
onUserUpdate: jest.fn().mockImplementation(handler => () => {}),
49+
onUserUpdate: jest.fn().mockImplementation(handler => () => { }),
5050
notificationCenter: {
51-
addNotificationListener: jest.fn().mockImplementation((type, handler) => {}),
52-
removeNotificationListener: jest.fn().mockImplementation(id => {}),
51+
addNotificationListener: jest.fn().mockImplementation((type, handler) => { }),
52+
removeNotificationListener: jest.fn().mockImplementation(id => { }),
5353
},
5454
user: {
5555
id: 'testuser',
@@ -60,11 +60,12 @@ describe('<OptimizelyFeature>', () => {
6060
getIsUsingSdkKey: () => true,
6161
} as unknown) as ReactSDKClient;
6262
});
63-
it('throws an error when not rendered in the context of an OptimizelyProvider', () => {
63+
64+
it('does not throw an error when not rendered in the context of an OptimizelyProvider', () => {
6465
expect(() => {
6566
// @ts-ignore
6667
mount(<OptimizelyFeature feature="feature1">{(isEnabled, variables) => isEnabled}</OptimizelyFeature>);
67-
}).toThrow();
68+
}).toBeDefined();
6869
});
6970

7071
describe('when the isServerSide prop is false', () => {

src/Provider.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2018-2019, Optimizely
2+
* Copyright 2022, Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -66,7 +66,15 @@ export class OptimizelyProvider extends React.Component<OptimizelyProviderProps,
6666
}
6767

6868
if (finalUser) {
69-
optimizely.setUser(finalUser);
69+
if (!optimizely) {
70+
logger.error(`Unable to set user ${finalUser} because optimizely object does not exist.`)
71+
} else {
72+
try {
73+
optimizely.setUser(finalUser);
74+
} catch (err) {
75+
logger.error(`Unable to set user ${finalUser} because passed in optimizely object does not contain the setUser function.`)
76+
}
77+
}
7078
}
7179
}
7280

src/hooks.ts

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2022, Optimizely
2+
* Copyright 2018-2019, 2022, Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -67,7 +67,7 @@ interface UseExperiment {
6767
(experimentKey: string, options?: HookOptions, overrides?: HookOverrides): [
6868
ExperimentDecisionValues['variation'],
6969
ClientReady,
70-
DidTimeout
70+
DidTimeout,
7171
];
7272
}
7373

@@ -76,15 +76,15 @@ interface UseFeature {
7676
FeatureDecisionValues['isEnabled'],
7777
FeatureDecisionValues['variables'],
7878
ClientReady,
79-
DidTimeout
79+
DidTimeout,
8080
];
8181
}
8282

8383
interface UseDecision {
8484
(featureKey: string, options?: DecideHooksOptions, overrides?: HookOverrides): [
8585
OptimizelyDecision,
8686
ClientReady,
87-
DidTimeout
87+
DidTimeout,
8888
];
8989
}
9090

@@ -191,8 +191,10 @@ function useCompareAttrsMemoize(value: UserAttributes | undefined): UserAttribut
191191
*/
192192
export const useExperiment: UseExperiment = (experimentKey, options = {}, overrides = {}) => {
193193
const { optimizely, isServerSide, timeout } = useContext(OptimizelyContext);
194+
194195
if (!optimizely) {
195-
throw new Error('optimizely prop must be supplied via a parent <OptimizelyProvider>');
196+
hooksLogger.error(`Unable to use experiment ${experimentKey}. optimizely prop must be supplied via a parent <OptimizelyProvider>`);
197+
return [null, false, false];
196198
}
197199

198200
const overrideAttrs = useCompareAttrsMemoize(overrides.overrideAttributes);
@@ -212,6 +214,7 @@ export const useExperiment: UseExperiment = (experimentKey, options = {}, overri
212214
didTimeout: false,
213215
};
214216
});
217+
215218
// Decision state is derived from entityKey and overrides arguments.
216219
// Track the previous value of those arguments, and update state when they change.
217220
// This is an instance of the derived state pattern recommended here:
@@ -283,8 +286,15 @@ export const useExperiment: UseExperiment = (experimentKey, options = {}, overri
283286
*/
284287
export const useFeature: UseFeature = (featureKey, options = {}, overrides = {}) => {
285288
const { optimizely, isServerSide, timeout } = useContext(OptimizelyContext);
289+
286290
if (!optimizely) {
287-
throw new Error('optimizely prop must be supplied via a parent <OptimizelyProvider>');
291+
hooksLogger.error(`Unable to properly use feature ${featureKey}. optimizely prop must be supplied via a parent <OptimizelyProvider>`);
292+
return [
293+
false,
294+
{},
295+
false,
296+
false,
297+
];
288298
}
289299

290300
const overrideAttrs = useCompareAttrsMemoize(overrides.overrideAttributes);
@@ -353,7 +363,12 @@ export const useFeature: UseFeature = (featureKey, options = {}, overrides = {})
353363
return (): void => { };
354364
}, [optimizely.getIsReadyPromiseFulfilled(), options.autoUpdate, optimizely, featureKey, getCurrentDecision]);
355365

356-
return [state.isEnabled, state.variables, state.clientReady, state.didTimeout];
366+
return [
367+
state.isEnabled,
368+
state.variables,
369+
state.clientReady,
370+
state.didTimeout,
371+
];
357372
};
358373

359374
/**
@@ -365,11 +380,21 @@ export const useFeature: UseFeature = (featureKey, options = {}, overrides = {})
365380
*/
366381
export const useDecision: UseDecision = (flagKey, options = {}, overrides = {}) => {
367382
const { optimizely, isServerSide, timeout } = useContext(OptimizelyContext);
383+
368384
if (!optimizely) {
369-
throw new Error('optimizely prop must be supplied via a parent <OptimizelyProvider>');
385+
hooksLogger.error(`Unable to use decision ${flagKey}. optimizely prop must be supplied via a parent <OptimizelyProvider>`);
386+
return [
387+
createFailedDecision(flagKey, 'Optimizely SDK not configured properly yet.', {
388+
id: null,
389+
attributes: {},
390+
}),
391+
false,
392+
false,
393+
]
370394
}
371395

372396
const overrideAttrs = useCompareAttrsMemoize(overrides.overrideAttributes);
397+
373398
const getCurrentDecision: () => { decision: OptimizelyDecision } = () => ({
374399
decision: optimizely.decide(flagKey, options.decideOptions, overrides.overrideUserId, overrideAttrs),
375400
});
@@ -452,5 +477,9 @@ export const useDecision: UseDecision = (flagKey, options = {}, overrides = {})
452477
return (): void => { };
453478
}, [optimizely.getIsReadyPromiseFulfilled(), options.autoUpdate, optimizely, flagKey, getCurrentDecision]);
454479

455-
return [state.decision, state.clientReady, state.didTimeout];
480+
return [
481+
state.decision,
482+
state.clientReady,
483+
state.didTimeout,
484+
];
456485
};

0 commit comments

Comments
 (0)