Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(packages/sui-pde): add decide listener #1837

Merged
merged 3 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
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) {
Copy link
Member Author

@andresz1 andresz1 Oct 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will make sure that ExperimentViewed is only sent when the feature test has a linked experiment

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
28 changes: 23 additions & 5 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 @@ -248,10 +261,15 @@ describe('useDecision hook', () => {
track: sinon.spy()
}

const addDecideListener = sinon.stub()
const removeNotificationListener = sinon.stub()

stubFactory = decide => {
// 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
Loading