diff --git a/packages/sui-pde/src/adapters/default.js b/packages/sui-pde/src/adapters/default.js index c89c0f185..dd41c1c03 100644 --- a/packages/sui-pde/src/adapters/default.js +++ b/packages/sui-pde/src/adapters/default.js @@ -15,6 +15,14 @@ export default class DefaultAdapter { return null } + addDecideListener() { + return null + } + + removeNotificationListener() { + return null + } + decide() { return null } diff --git a/packages/sui-pde/src/adapters/optimizely/index.js b/packages/sui-pde/src/adapters/optimizely/index.js index 3ba274c50..3bee49244 100644 --- a/packages/sui-pde/src/adapters/optimizely/index.js +++ b/packages/sui-pde/src/adapters/optimizely/index.js @@ -14,9 +14,9 @@ const DEFAULT_EVENTS_OPTIONS = { const DEFAULT_TIMEOUT = 500 -const {enums: LOG_LEVEL} = optimizelySDK +const {enums} = optimizelySDK -const LOGGER_LEVEL = process.env.NODE_ENV === 'production' ? LOG_LEVEL.error : LOG_LEVEL.info +const LOGGER_LEVEL = process.env.NODE_ENV === 'production' ? enums.error : enums.info export default class OptimizelyAdapter { /** @@ -123,10 +123,12 @@ export default class OptimizelyAdapter { * @param {Object} params * @param {string} params.name * @param {object} [params.attributes] - * @returns {string=} variation name + * @returns {object} decision */ decide({name, attributes}) { - if (!this._hasUserConsents) return null + if (!this._hasUserConsents) { + return {enabled: false, flagKey: name} + } const user = this._optimizely.createUserContext(this._userId, { ...this._applicationAttributes, @@ -136,6 +138,23 @@ export default class OptimizelyAdapter { return user.decide(name) } + /** + * @param {Object} params + * @param {function} params.onDecide + * @returns {number} notificationId + */ + addDecideListener({onDecide}) { + return this._optimizely.notificationCenter.addNotificationListener(enums.NOTIFICATION_TYPES.DECISION, onDecide) + } + + /** + * @param {Object} params + * @param {number} params.notificationId + */ + removeNotificationListener({notificationId}) { + this._optimizely.notificationCenter.removeNotificationListener(notificationId) + } + /** * Gets the variation without tracking the impression * @param {Object} params diff --git a/packages/sui-pde/src/adapters/optimizely/multiple.js b/packages/sui-pde/src/adapters/optimizely/multiple.js index 741afcb08..04991c3d4 100644 --- a/packages/sui-pde/src/adapters/optimizely/multiple.js +++ b/packages/sui-pde/src/adapters/optimizely/multiple.js @@ -64,6 +64,14 @@ class MultipleOptimizelyAdapter { return this.#adapters[adapterId].decide(props) } + addDecideListener({adapterId = defaultAdapterId, ...props}) { + return this.#adapters[adapterId].addDecideListener(props) + } + + removeNotificationListener({adapterId = defaultAdapterId, ...props}) { + this.#adapters[adapterId].removeNotificationListener(props) + } + getVariation({adapterId = defaultAdapterId, ...props}) { return this.#adapters[adapterId].getVariation(props) } diff --git a/packages/sui-pde/src/hooks/useDecision.js b/packages/sui-pde/src/hooks/useDecision.js index 41bce419f..cd2e63df3 100644 --- a/packages/sui-pde/src/hooks/useDecision.js +++ b/packages/sui-pde/src/hooks/useDecision.js @@ -39,6 +39,16 @@ export default function useDecision(name, {attributes, trackExperimentViewed, qu return {enabled: true, flagKey: name, variationKey: forced} } + const notificationId = pde.addDecideListener({ + onDecide: ({type, decisionInfo: decision}) => { + const {ruleKey, variationKey, decisionEventDispatched} = decision + + if (type === 'flag' && decisionEventDispatched) { + strategy.trackExperiment({variationName: variationKey, experimentName: ruleKey}) + } + } + }) + const data = strategy.decide({ pde, name, @@ -46,13 +56,7 @@ export default function useDecision(name, {attributes, trackExperimentViewed, qu adapterId }) - const {ruleKey, variationKey} = data || {} - - const isExperiment = !!ruleKey - - if (isExperiment) { - strategy.trackExperiment({variationName: variationKey, experimentName: ruleKey}) - } + pde.removeNotificationListener({notificationId}) return data } catch (error) { diff --git a/packages/sui-pde/src/pde.js b/packages/sui-pde/src/pde.js index 9d4298d85..fb9075d95 100644 --- a/packages/sui-pde/src/pde.js +++ b/packages/sui-pde/src/pde.js @@ -37,6 +37,23 @@ export default class PDE { return this._adapter.decide({name, attributes, adapterId}) } + /** + * @param {Object} params + * @param {function} params.onDecide + * @returns {string} notificationId + */ + addDecideListener({onDecide}) { + return this._adapter.addDecideListener({onDecide}) + } + + /** + * @param {Object} params + * @param {number} params.notificationId + */ + removeNotificationListener({notificationId}) { + this._adapter.removeNotificationListener({notificationId}) + } + getInitialData() { return this._adapter.getInitialData() } diff --git a/packages/sui-pde/test/common/useDecisionSpec.js b/packages/sui-pde/test/common/useDecisionSpec.js index 7ab093891..15649fad1 100644 --- a/packages/sui-pde/test/common/useDecisionSpec.js +++ b/packages/sui-pde/test/common/useDecisionSpec.js @@ -30,7 +30,7 @@ describe('useDecision hook', () => { let decide before(() => { - decide = sinon.stub().returns({ + const decision = { variationKey: 'variation', enabled: true, variables: {}, @@ -38,10 +38,18 @@ describe('useDecision hook', () => { flagKey: 'flag', userContext: {}, reasons: [] - }) + } + decide = sinon.stub().returns(decision) + + const addDecideListener = ({onDecide}) => + onDecide({type: 'flag', decisionInfo: {...decision, decisionEventDispatched: true}}) + const removeNotificationListener = sinon.stub() + // eslint-disable-next-line react/prop-types wrapper = ({children}) => ( - {children} + + {children} + ) }) @@ -226,9 +234,14 @@ describe('useDecision hook', () => { let wrapper beforeEach(() => { decide = sinon.stub().throws(new Error('fake activation error')) + const addDecideListener = sinon.stub() + const removeNotificationListener = sinon.stub() + // eslint-disable-next-line react/prop-types wrapper = ({children}) => ( - {children} + + {children} + ) }) @@ -247,11 +260,14 @@ describe('useDecision hook', () => { ready: cb => cb(), track: sinon.spy() } + const removeNotificationListener = sinon.stub() - stubFactory = decide => { + stubFactory = ({decide, addDecideListener}) => { // eslint-disable-next-line react/prop-types wrapper = ({children}) => ( - {children} + + {children} + ) } }) @@ -263,8 +279,8 @@ describe('useDecision hook', () => { describe('when the second time returns the same value as the first time', () => { beforeEach(() => { const decide = sinon.stub() - - decide.onCall(0).returns({ + const addDecideListener = sinon.stub() + const decision = { variationKey: 'variation', enabled: true, variables: {}, @@ -272,7 +288,19 @@ describe('useDecision hook', () => { flagKey: 'flag', userContext: {}, reasons: [] - }) + } + + decide.onCall(0).returns(decision) + addDecideListener.onCall(0).callsFake(({onDecide}) => + onDecide({ + type: 'flag', + decisionInfo: { + ...decision, + decisionEventDispatched: true + } + }) + ) + decide.onCall(1).returns({ variationKey: 'variation', enabled: true, @@ -282,8 +310,17 @@ describe('useDecision hook', () => { userContext: {}, reasons: [] }) + addDecideListener.onCall(1).callsFake(({onDecide}) => + onDecide({ + type: 'flag', + decisionInfo: { + ...decision, + decisionEventDispatched: true + } + }) + ) - stubFactory(decide) + stubFactory({decide, addDecideListener}) }) it('should send only one experiment viewed event', () => { @@ -300,8 +337,8 @@ describe('useDecision hook', () => { describe('when the second time returns a different value as the first time', () => { beforeEach(() => { const decide = sinon.stub() - - decide.onCall(0).returns({ + const addDecideListener = sinon.stub() + const decision = { variationKey: 'variation_a', enabled: true, variables: {}, @@ -309,18 +346,35 @@ describe('useDecision hook', () => { flagKey: 'flag', userContext: {}, reasons: [] - }) + } + + decide.onCall(0).returns(decision) + addDecideListener.onCall(0).callsFake(({onDecide}) => + onDecide({ + type: 'flag', + decisionInfo: { + ...decision, + decisionEventDispatched: true + } + }) + ) + decide.onCall(1).returns({ - variationKey: 'variation_b', - enabled: true, - variables: {}, - ruleKey: 'rule', - flagKey: 'flag', - userContext: {}, - reasons: [] + ...decision, + variationKey: 'variation_b' }) + addDecideListener.onCall(1).callsFake(({onDecide}) => + onDecide({ + type: 'flag', + decisionInfo: { + ...decision, + variationKey: 'variation_b', + decisionEventDispatched: true + } + }) + ) - stubFactory(decide) + stubFactory({decide, addDecideListener}) }) it('should send two experiment viewed events', () => {