Skip to content

Commit

Permalink
Merge pull request #1837 from SUI-Components/feat/decide-experiment-v…
Browse files Browse the repository at this point in the history
…iewed

feat(packages/sui-pde): add decide listener
  • Loading branch information
andresz1 authored Oct 9, 2024
2 parents c89f51a + 068cd7e commit 3627577
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 32 deletions.
8 changes: 8 additions & 0 deletions packages/sui-pde/src/adapters/default.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ export default class DefaultAdapter {
return null
}

addDecideListener() {
return null
}

removeNotificationListener() {
return null
}

decide() {
return null
}
Expand Down
27 changes: 23 additions & 4 deletions packages/sui-pde/src/adapters/optimizely/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down
8 changes: 8 additions & 0 deletions packages/sui-pde/src/adapters/optimizely/multiple.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
18 changes: 11 additions & 7 deletions packages/sui-pde/src/hooks/useDecision.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,24 @@ 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,
attributes,
adapterId
})

const {ruleKey, variationKey} = data || {}

const isExperiment = !!ruleKey

if (isExperiment) {
strategy.trackExperiment({variationName: variationKey, experimentName: ruleKey})
}
pde.removeNotificationListener({notificationId})

return data
} catch (error) {
Expand Down
17 changes: 17 additions & 0 deletions packages/sui-pde/src/pde.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
96 changes: 75 additions & 21 deletions packages/sui-pde/test/common/useDecisionSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,26 @@ describe('useDecision hook', () => {
let decide

before(() => {
decide = sinon.stub().returns({
const decision = {
variationKey: 'variation',
enabled: true,
variables: {},
ruleKey: 'rule',
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}) => (
<PdeContext.Provider value={{features: [], pde: {decide}}}>{children}</PdeContext.Provider>
<PdeContext.Provider value={{features: [], pde: {decide, addDecideListener, removeNotificationListener}}}>
{children}
</PdeContext.Provider>
)
})

Expand Down Expand Up @@ -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}) => (
<PdeContext.Provider value={{features: [], pde: {decide}}}>{children}</PdeContext.Provider>
<PdeContext.Provider value={{features: [], pde: {decide, addDecideListener, removeNotificationListener}}}>
{children}
</PdeContext.Provider>
)
})

Expand All @@ -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}) => (
<PdeContext.Provider value={{features: [], pde: {decide}}}>{children}</PdeContext.Provider>
<PdeContext.Provider value={{features: [], pde: {decide, addDecideListener, removeNotificationListener}}}>
{children}
</PdeContext.Provider>
)
}
})
Expand All @@ -263,16 +279,28 @@ 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: {},
ruleKey: 'rule',
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,
Expand All @@ -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', () => {
Expand All @@ -300,27 +337,44 @@ 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: {},
ruleKey: 'rule',
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', () => {
Expand Down

0 comments on commit 3627577

Please sign in to comment.