-
Notifications
You must be signed in to change notification settings - Fork 254
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement the 'rollout' config option for percentage rollouts of sub-…
…features (#2880) * Initial remote config component wrapper * Initial rollout implementation and start porting Android tests * Remaining rollout tests * Properly mock extension version * Use post-processed config in static methods * Comment how rollout works * Lint * Add privacy-configuration as a dev dependency * Fix bad merge * Move config ResourceLoader into it's own component * Move config process to class method * Rename config to remoteConfig for clarity
- Loading branch information
1 parent
fb4268d
commit 7860833
Showing
11 changed files
with
869 additions
and
41 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
/** | ||
* @typedef {import('../settings.js')} Settings | ||
* @typedef {import('./tds').default} TDSStorage | ||
* @typedef {import('@duckduckgo/privacy-configuration/schema/config.js').GenericV4Config} Config | ||
*/ | ||
|
||
import { getFeatureSettings, isFeatureEnabled, satisfiesMinVersion } from '../utils'; | ||
import { getExtensionVersion, getFromSessionStorage } from '../wrapper'; | ||
import ResourceLoader from './resource-loader'; | ||
import constants from '../../../data/constants'; | ||
|
||
/** | ||
* @returns {Promise<string>} | ||
*/ | ||
async function getConfigUrl() { | ||
const override = await getFromSessionStorage('configURLOverride'); | ||
if (override) { | ||
return override; | ||
} | ||
return constants.tdsLists[2].url; | ||
} | ||
|
||
export default class RemoteConfig extends ResourceLoader { | ||
/** | ||
* @param {{ | ||
* settings: Settings | ||
* }} opts | ||
*/ | ||
constructor({ settings }) { | ||
super( | ||
{ | ||
name: 'config', | ||
remoteUrl: getConfigUrl, | ||
localUrl: '/data/bundled/extension-config.json', | ||
updateIntervalMinutes: 15, | ||
}, | ||
{ settings }, | ||
); | ||
/** @type {Config?} */ | ||
this.config = null; | ||
this.settings = settings; | ||
this.onUpdate(async (_, etag, v) => { | ||
this.updateConfig(v); | ||
}); | ||
} | ||
|
||
/** | ||
* | ||
* @param {Config} configValue | ||
*/ | ||
updateConfig(configValue) { | ||
// copy config value before modification | ||
const configCopy = structuredClone(configValue); | ||
this.config = this._processRawConfig(configCopy); | ||
} | ||
|
||
/** | ||
* | ||
* @param {string} featureName | ||
* @returns {boolean} | ||
*/ | ||
isFeatureEnabled(featureName) { | ||
return isFeatureEnabled(featureName, this.config || undefined); | ||
} | ||
|
||
/** | ||
* | ||
* @param {string} featureName | ||
* @returns {object} | ||
*/ | ||
getFeatureSettings(featureName) { | ||
return getFeatureSettings(featureName, this.config || undefined); | ||
} | ||
|
||
isSubFeatureEnabled(featureName, subFeatureName) { | ||
if (this.config) { | ||
return isSubFeatureEnabled(featureName, subFeatureName, this.config); | ||
} else { | ||
return false; | ||
} | ||
} | ||
|
||
/** | ||
* Process config to apply rollout, targets and cohorts options to derive sub-feature enabled state. | ||
* @param {Config} configValue | ||
* @returns {Config} | ||
*/ | ||
_processRawConfig(configValue) { | ||
Object.entries(configValue.features).forEach(([featureName, feature]) => { | ||
Object.entries(feature.features || {}).forEach(([name, subfeature]) => { | ||
if (subfeature.rollout && subfeature.state === 'enabled') { | ||
/* Handle a rollout: Dice roll is stored in settings and used that to decide | ||
* whether the feature is set as 'enabled' or not. | ||
*/ | ||
const rolloutSettingsKey = `rollouts.${featureName}.${name}.roll`; | ||
const validSteps = subfeature.rollout.steps.filter((v) => v.percent > 0 && v.percent <= 100); | ||
const rolloutPercent = validSteps.length > 0 ? validSteps.reverse()[0].percent : 0.0; | ||
if (!this.settings.getSetting(rolloutSettingsKey)) { | ||
this.settings.updateSetting(rolloutSettingsKey, Math.random() * 100); | ||
} | ||
const dieRoll = this.settings.getSetting(rolloutSettingsKey); | ||
subfeature.state = rolloutPercent >= dieRoll ? 'enabled' : 'disabled'; | ||
} | ||
}); | ||
}); | ||
return configValue; | ||
} | ||
} | ||
|
||
/** | ||
* | ||
* @param {string} featureName | ||
* @param {string} subFeatureName | ||
* @param {Config} config | ||
* @returns {boolean} | ||
*/ | ||
export function isSubFeatureEnabled(featureName, subFeatureName, config) { | ||
const feature = config.features[featureName]; | ||
const subFeature = (feature?.features || {})[subFeatureName]; | ||
if (!feature || !subFeature) { | ||
return false; | ||
} | ||
if (subFeature.minSupportedVersion) { | ||
const extensionVersionString = getExtensionVersion(); | ||
if (!satisfiesMinVersion(subFeature.minSupportedVersion, extensionVersionString)) { | ||
return false; | ||
} | ||
} | ||
return subFeature.state === 'enabled'; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.