diff --git a/.circleci/config.yml b/.circleci/config.yml
index 6507cf06c..1f1f608d1 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -6,6 +6,7 @@ jobs:
- image: circleci/node:8
environment:
- DISPLAY=:99
+ - image: redis
steps:
- checkout
@@ -37,6 +38,7 @@ jobs:
working_directory: ~/fxa
docker:
- image: circleci/node:8-stretch-browsers
+ - image: redis
steps:
- attach_workspace:
at: ~/
@@ -67,6 +69,7 @@ jobs:
working_directory: ~/fxa
docker:
- image: circleci/node:8-stretch-browsers
+ - image: redis
steps:
- attach_workspace:
at: ~/
@@ -97,6 +100,7 @@ jobs:
working_directory: ~/fxa
docker:
- image: circleci/node:8-stretch-browsers
+ - image: redis
steps:
- attach_workspace:
at: ~/
@@ -127,6 +131,7 @@ jobs:
working_directory: ~/fxa
docker:
- image: circleci/node:8-stretch-browsers
+ - image: redis
steps:
- attach_workspace:
at: ~/
@@ -153,6 +158,7 @@ jobs:
working_directory: ~/fxa
docker:
- image: circleci/node:8-stretch-browsers
+ - image: redis
steps:
- attach_workspace:
at: ~/
@@ -185,6 +191,7 @@ jobs:
dockerpush:
docker:
- image: circleci/node:8-stretch-browsers
+ - image: redis
steps:
- checkout
diff --git a/app/scripts/lib/app-start.js b/app/scripts/lib/app-start.js
index ed57b1bdc..82c0fc39e 100644
--- a/app/scripts/lib/app-start.js
+++ b/app/scripts/lib/app-start.js
@@ -95,7 +95,8 @@ Start.prototype = {
initializeExperimentGroupingRules () {
this._experimentGroupingRules = new ExperimentGroupingRules({
- env: this._config.env
+ env: this._config.env,
+ featureFlags: this._config.featureFlags
});
},
diff --git a/app/scripts/lib/config-loader.js b/app/scripts/lib/config-loader.js
index dd83672f2..980391677 100644
--- a/app/scripts/lib/config-loader.js
+++ b/app/scripts/lib/config-loader.js
@@ -53,6 +53,11 @@ ConfigLoader.prototype = {
try {
const serializedJSONConfig = decodeURIComponent(configFromHTML);
config = JSON.parse(serializedJSONConfig);
+
+ const serializedFeatureFlags = decodeURIComponent(
+ $('meta[name="fxa-feature-flags"]').attr('content')
+ );
+ config.featureFlags = JSON.parse(serializedFeatureFlags);
} catch (e) {
return Promise.reject(ConfigLoader.Errors.toError('INVALID_CONFIG'));
}
diff --git a/app/scripts/lib/experiments/grouping-rules/communication-prefs.js b/app/scripts/lib/experiments/grouping-rules/communication-prefs.js
index a1a0788d6..f8d477b00 100644
--- a/app/scripts/lib/experiments/grouping-rules/communication-prefs.js
+++ b/app/scripts/lib/experiments/grouping-rules/communication-prefs.js
@@ -25,30 +25,39 @@ const AVAILABLE_LANGUAGES = [
'zh-tw'
];
-const availableLocalesRegExpStr = `^(${AVAILABLE_LANGUAGES.join('|')})$`;
-const availableLocalesRegExp = new RegExp(availableLocalesRegExpStr);
+const AVAILABLE_LANGUAGES_REGEX = arrayToRegex(AVAILABLE_LANGUAGES);
function normalizeLanguage(lang) {
return lang.toLowerCase().replace(/_/g, '-');
}
-function areCommunicationPrefsAvailable(lang) {
+function areCommunicationPrefsAvailable(lang, availableLanguages) {
const normalizedLanguage = normalizeLanguage(lang);
- return availableLocalesRegExp.test(normalizedLanguage);
+ return availableLanguages.test(normalizedLanguage);
+}
+
+function arrayToRegex (array) {
+ return new RegExp(`^(?:${array.join('|')})$`);
}
module.exports = class CommunicationPrefsGroupingRule extends BaseGroupingRule {
constructor () {
super();
this.name = 'communicationPrefsVisible';
- this.availableLanguages = AVAILABLE_LANGUAGES;
}
choose (subject = {}) {
- if (! subject.lang) {
+ const { featureFlags, lang } = subject;
+ let availableLanguages = AVAILABLE_LANGUAGES_REGEX;
+
+ if (featureFlags && Array.isArray(featureFlags.communicationPrefLanguages)) {
+ availableLanguages = arrayToRegex(featureFlags.communicationPrefLanguages);
+ }
+
+ if (! lang) {
return false;
}
- return areCommunicationPrefsAvailable(subject.lang);
+ return areCommunicationPrefsAvailable(lang, availableLanguages);
}
};
diff --git a/app/scripts/lib/experiments/grouping-rules/index.js b/app/scripts/lib/experiments/grouping-rules/index.js
index 09e8222f5..408d66c1e 100644
--- a/app/scripts/lib/experiments/grouping-rules/index.js
+++ b/app/scripts/lib/experiments/grouping-rules/index.js
@@ -24,6 +24,7 @@ class ExperimentChoiceIndex {
constructor (options = {}) {
this._env = options.env;
this._experimentGroupingRules = options.experimentGroupingRules || experimentGroupingRules;
+ this._featureFlags = options.featureFlags;
}
/**
@@ -51,6 +52,7 @@ class ExperimentChoiceIndex {
const subjectCopy = Object.create(subject);
subjectCopy.env = subject.env || this._env;
subjectCopy.experimentGroupingRules = this;
+ subjectCopy.featureFlags = subject.featureFlags || this._featureFlags;
return experiment.choose(subjectCopy);
}
}
diff --git a/app/scripts/lib/experiments/grouping-rules/is-sampled-user.js b/app/scripts/lib/experiments/grouping-rules/is-sampled-user.js
index f6e2cab5c..4115deb61 100644
--- a/app/scripts/lib/experiments/grouping-rules/is-sampled-user.js
+++ b/app/scripts/lib/experiments/grouping-rules/is-sampled-user.js
@@ -16,18 +16,24 @@ module.exports = class IsSampledUserGroupingRule extends BaseGroupingRule {
}
choose (subject = {}) {
- const sampleRate = IsSampledUserGroupingRule.sampleRate(subject.env);
+ const sampleRate = IsSampledUserGroupingRule.sampleRate(subject);
return !! (subject.env && subject.uniqueUserId && this.bernoulliTrial(sampleRate, subject.uniqueUserId));
}
/**
- * Return the sample rate for `env`
+ * Return the sample rate from `featureFlags` or `env`
*
* @static
- * @param {String} env
+ * @param {Object} options
+ * @param {String} [options.env]
+ * @param {Object} [options.featureFlags]
* @returns {Number}
*/
- static sampleRate (env) {
+ static sampleRate ({ env, featureFlags }) {
+ if (featureFlags && featureFlags.metricsSampleRate >= 0) {
+ return featureFlags.metricsSampleRate;
+ }
+
return env === 'development' ? 1.0 : 0.1;
}
};
diff --git a/app/scripts/lib/experiments/grouping-rules/send-sms-install-link.js b/app/scripts/lib/experiments/grouping-rules/send-sms-install-link.js
index 940f326fd..79fb5d86f 100644
--- a/app/scripts/lib/experiments/grouping-rules/send-sms-install-link.js
+++ b/app/scripts/lib/experiments/grouping-rules/send-sms-install-link.js
@@ -24,13 +24,23 @@ module.exports = class SmsGroupingRule extends BaseGroupingRule {
}
choose (subject = {}) {
- if (! subject.account || ! subject.uniqueUserId || ! subject.country || ! CountryTelephoneInfo[subject.country]) {
+ if (! subject.account || ! subject.uniqueUserId || ! subject.country) {
+ return false;
+ }
+
+ let telephoneInfo = CountryTelephoneInfo[subject.country];
+ const { featureFlags } = subject;
+ if (featureFlags && featureFlags.smsCountries) {
+ telephoneInfo = featureFlags.smsCountries[subject.country];
+ }
+
+ if (! telephoneInfo) {
return false;
}
let choice = false;
// If rolloutRate is not specified, assume 0.
- const { rolloutRate } = CountryTelephoneInfo[subject.country] || 0;
+ const rolloutRate = telephoneInfo.rolloutRate || 0;
if (this.isTestEmail(subject.account.get('email'))) {
choice = 'signinCodes';
diff --git a/app/scripts/lib/experiments/grouping-rules/sentry.js b/app/scripts/lib/experiments/grouping-rules/sentry.js
index 4a112ddc6..46b8157ec 100644
--- a/app/scripts/lib/experiments/grouping-rules/sentry.js
+++ b/app/scripts/lib/experiments/grouping-rules/sentry.js
@@ -16,7 +16,7 @@ module.exports = class SentryGroupingRule extends BaseGroupingRule {
}
choose (subject = {}) {
- const sampleRate = SentryGroupingRule.sampleRate(subject.env);
+ const sampleRate = SentryGroupingRule.sampleRate(subject);
return !! (subject.env && subject.uniqueUserId && this.bernoulliTrial(sampleRate, subject.uniqueUserId));
}
@@ -28,7 +28,11 @@ module.exports = class SentryGroupingRule extends BaseGroupingRule {
* @param {String} env
* @returns {Number}
*/
- static sampleRate (env) {
+ static sampleRate ({ env, featureFlags }) {
+ if (featureFlags && featureFlags.sentrySampleRate >= 0) {
+ return featureFlags.sentrySampleRate;
+ }
+
return env === 'development' ? 1.0 : 0.3;
}
};
diff --git a/app/scripts/lib/experiments/grouping-rules/token-code.js b/app/scripts/lib/experiments/grouping-rules/token-code.js
index 890c057c8..06445cd20 100644
--- a/app/scripts/lib/experiments/grouping-rules/token-code.js
+++ b/app/scripts/lib/experiments/grouping-rules/token-code.js
@@ -48,8 +48,13 @@ module.exports = class TokenCodeGroupingRule extends BaseGroupingRule {
return false;
}
+ const { featureFlags } = subject;
+
if (subject.clientId) {
- const client = this.ROLLOUT_CLIENTS[subject.clientId];
+ let client = this.ROLLOUT_CLIENTS[subject.clientId];
+ if (featureFlags && featureFlags.tokenCodeClients) {
+ client = featureFlags.tokenCodeClients[subject.clientId];
+ }
if (client) {
const groups = client.groups || GROUPS_DEFAULT;
@@ -70,7 +75,12 @@ module.exports = class TokenCodeGroupingRule extends BaseGroupingRule {
}
if (subject.service && subject.service === Constants.SYNC_SERVICE) {
- if (this.bernoulliTrial(this.SYNC_ROLLOUT_RATE, subject.uniqueUserId)) {
+ let syncRolloutRate = this.SYNC_ROLLOUT_RATE;
+ if (featureFlags && featureFlags.tokenCodeClients) {
+ syncRolloutRate = featureFlags.tokenCodeClients.sync.rolloutRate;
+ }
+
+ if (this.bernoulliTrial(syncRolloutRate, subject.uniqueUserId)) {
return this.uniformChoice(GROUPS_DEFAULT, subject.uniqueUserId);
}
}
diff --git a/app/tests/spec/lib/app-start.js b/app/tests/spec/lib/app-start.js
index 2924ff058..aa489e7ed 100644
--- a/app/tests/spec/lib/app-start.js
+++ b/app/tests/spec/lib/app-start.js
@@ -10,6 +10,7 @@ define(function (require, exports, module) {
const BaseBroker = require('models/auth_brokers/base');
const Constants = require('lib/constants');
const ErrorUtils = require('lib/error-utils');
+ const ExperimentGroupingRules = require('lib/experiments/grouping-rules');
const FxFennecV1Broker = require('models/auth_brokers/fx-fennec-v1');
const FxFirstrunV1Broker = require('models/auth_brokers/fx-firstrun-v1');
const FxFirstrunV2Broker = require('models/auth_brokers/fx-firstrun-v2');
@@ -38,6 +39,7 @@ define(function (require, exports, module) {
let appStart;
let backboneHistoryMock;
let brokerMock;
+ let config;
let notifier;
let routerMock;
let translator;
@@ -47,6 +49,12 @@ define(function (require, exports, module) {
beforeEach(() => {
brokerMock = new BaseBroker();
backboneHistoryMock = new HistoryMock();
+ config = {
+ env: 'production',
+ featureFlags: {
+ foo: 'bar'
+ }
+ };
notifier = new Notifier();
routerMock = { navigate: sinon.spy() };
translator = {
@@ -60,6 +68,7 @@ define(function (require, exports, module) {
appStart = new AppStart({
broker: brokerMock,
+ config,
history: backboneHistoryMock,
notifier,
router: routerMock,
@@ -113,34 +122,17 @@ define(function (require, exports, module) {
});
});
- it('initializeErrorMetrics skips error metrics on empty config', () => {
- appStart.initializeExperimentGroupingRules();
- const ableChoose = sinon.stub(appStart._experimentGroupingRules, 'choose').callsFake(() => {
- return true;
- });
+ it('initializeExperimentGroupingRules propagates env and featureFlags', () => {
+ assert.isUndefined(appStart._experimentGroupingRules);
- appStart.initializeErrorMetrics();
- assert.isUndefined(appStart._sentryMetrics);
- ableChoose.restore();
- });
-
- it('initializeErrorMetrics skips error metrics if env is not defined', () => {
appStart.initializeExperimentGroupingRules();
- appStart.initializeErrorMetrics();
- assert.isUndefined(appStart._sentryMetrics);
+ assert.instanceOf(appStart._experimentGroupingRules, ExperimentGroupingRules);
+ assert.equal(appStart._experimentGroupingRules._env, 'production');
+ assert.deepEqual(appStart._experimentGroupingRules._featureFlags, { foo: 'bar' });
});
it('initializeErrorMetrics creates error metrics', () => {
- const appStart = new AppStart({
- broker: brokerMock,
- config: {
- env: 'development'
- },
- history: backboneHistoryMock,
- router: routerMock,
- window: windowMock
- });
appStart.initializeExperimentGroupingRules();
const ableChoose = sinon.stub(appStart._experimentGroupingRules, 'choose').callsFake(() => {
@@ -184,6 +176,35 @@ define(function (require, exports, module) {
assert.instanceOf(appStart._refreshObserver, RefreshObserver);
});
+ describe('without config', () => {
+ beforeEach(() => {
+ appStart = new AppStart({
+ broker: brokerMock,
+ history: backboneHistoryMock,
+ router: routerMock,
+ window: windowMock
+ });
+ });
+
+ it('initializeErrorMetrics skips error metrics on empty config', () => {
+ appStart.initializeExperimentGroupingRules();
+ const ableChoose = sinon.stub(appStart._experimentGroupingRules, 'choose').callsFake(() => {
+ return true;
+ });
+
+ appStart.initializeErrorMetrics();
+ assert.isUndefined(appStart._sentryMetrics);
+ ableChoose.restore();
+ });
+
+ it('initializeErrorMetrics skips error metrics if env is not defined', () => {
+ appStart.initializeExperimentGroupingRules();
+
+ appStart.initializeErrorMetrics();
+ assert.isUndefined(appStart._sentryMetrics);
+ });
+ });
+
describe('fatalError', () => {
var err;
var sandbox;
diff --git a/app/tests/spec/lib/config-loader.js b/app/tests/spec/lib/config-loader.js
index da597df63..33622e5f1 100644
--- a/app/tests/spec/lib/config-loader.js
+++ b/app/tests/spec/lib/config-loader.js
@@ -18,6 +18,28 @@ define(function (require, exports, module) {
const VALID_HTML_CONFIG =
encodeURIComponent(JSON.stringify({ env: 'dev' }));
+ const FEATURE_FLAGS = {
+ communicationPrefLanguages: [ 'en', 'fr' ],
+ metricsSampleRate: 0.1,
+ sentrySampleRate: 1,
+ smsCountries: {
+ FR: {
+ rolloutRate: 0.5,
+ },
+ GB: {
+ rolloutRate: 1,
+ }
+ },
+ tokenCodeClients: {
+ deadbeefbaadf00d: {
+ rolloutRate: 0
+ },
+ sync: {
+ rolloutRate: 1
+ }
+ }
+ };
+ const SERIALISED_FEATURE_FLAGS = encodeURIComponent(JSON.stringify(FEATURE_FLAGS));
describe('lib/config-loader', () => {
let configLoader;
@@ -26,72 +48,66 @@ define(function (require, exports, module) {
configLoader = new ConfigLoader();
});
- describe('_readConfigFromHTML', () => {
- describe('config missing', () => {
- it('returns a `MISSING_CONFIG` error', () => {
- return configLoader._readConfigFromHTML()
- .then(assert.fail, (err) => {
- assert.isTrue(ConfigLoaderErrors.is(err, 'MISSING_CONFIG'));
- });
+ it('_readConfigFromHTML returns a `MISSING_CONFIG` error', () => {
+ return configLoader._readConfigFromHTML()
+ .then(assert.fail, (err) => {
+ assert.isTrue(ConfigLoaderErrors.is(err, 'MISSING_CONFIG'));
});
- });
-
- describe('config available', () => {
- beforeEach(() => {
- $('head').append(``);
- });
-
- afterEach(() => {
- $('meta[name="fxa-content-server/config"]').remove();
- });
-
- it('returns the expected config', () => {
- return configLoader._readConfigFromHTML()
- .then((serializedHTMLConfig) => {
- assert.equal(serializedHTMLConfig, VALID_HTML_CONFIG);
- });
- });
- });
});
- describe('_parseHTMLConfig', () => {
- describe('with an invalid URI Component', () => {
- it('throws an `INVALID_CONFIG` error', () => {
- return configLoader._parseHTMLConfig(INVALID_URI_COMPONENT_HTML_CONFIG)
- .then(assert.fail, (err) => {
- assert.isTrue(ConfigLoaderErrors.is(err, 'INVALID_CONFIG'));
- });
+ it('_parseHTMLConfig rejects with invalid encoding', () => {
+ return configLoader._parseHTMLConfig(INVALID_URI_COMPONENT_HTML_CONFIG)
+ .then(assert.fail, (err) => {
+ assert.isTrue(ConfigLoaderErrors.is(err, 'INVALID_CONFIG'));
});
- });
-
- describe('with invalid JSON', () => {
- it('throws an `INVALID_CONFIG` error', () => {
- return configLoader._parseHTMLConfig(INVALID_JSON_HTML_CONFIG)
- .then(assert.fail, (err) => {
- assert.isTrue(ConfigLoaderErrors.is(err, 'INVALID_CONFIG'));
- });
- });
- });
-
- describe('with valid config', () => {
- it('parses the config', () => {
- return configLoader._parseHTMLConfig(VALID_HTML_CONFIG)
- .then((config) => {
- assert.equal(config.env, 'dev');
- });
- });
- });
});
- describe('fetch', () => {
- describe('with valid config', () => {
+ it('_parseHTMLConfig rejects with invalid JSON', () => {
+ return configLoader._parseHTMLConfig(INVALID_JSON_HTML_CONFIG)
+ .then(assert.fail, (err) => {
+ assert.isTrue(ConfigLoaderErrors.is(err, 'INVALID_CONFIG'));
+ });
+ });
+
+ describe('insert config markup in to the DOM', () => {
+ before(() => {
+ $('head').append(``);
+ $('head').append(``);
+ });
+
+ after(() => {
+ $('meta[name="fxa-content-server/config"]').remove();
+ $('meta[name="fxa-feature-flags"]').remove();
+ });
+
+ it('_readConfigFromHTML returns the expected config', () => {
+ return configLoader._readConfigFromHTML()
+ .then((serializedHTMLConfig) => {
+ assert.equal(serializedHTMLConfig, VALID_HTML_CONFIG);
+ });
+ });
+
+ it('_parseHTMLConfig parses the config', () => {
+ return configLoader._parseHTMLConfig(VALID_HTML_CONFIG)
+ .then((config) => {
+ assert.equal(config.env, 'dev');
+ assert.deepEqual(config.featureFlags, FEATURE_FLAGS);
+ });
+ });
+
+ it('_setWebpackPublicPath sets the bundle path', () => {
+ configLoader._setWebpackPublicPath('somepath');
+ assert.equal(__webpack_public_path__, 'somepath'); //eslint-disable-line no-undef
+ configLoader._setWebpackPublicPath();
+ assert.equal(__webpack_public_path__, Constants.DEFAULT_BUNDLE_PATH); //eslint-disable-line no-undef
+ });
+
+ describe('mock internal methods', () => {
let $html;
let origLang;
let sandbox;
beforeEach(() => {
- $('head').append(``);
-
$html = $('html');
origLang = $html.attr('lang');
$html.attr('lang', 'db_LB');
@@ -102,17 +118,17 @@ define(function (require, exports, module) {
});
afterEach(() => {
- $('meta[name="fxa-content-server/config"]').remove();
$html.attr('lang', origLang);
sandbox.restore();
});
- it('returns the config', () => {
+ it('fetch returns the config', () => {
return configLoader.fetch()
.then((config) => {
assert.equal(config.env, 'dev');
assert.equal(config.lang, 'db_LB');
+ assert.deepEqual(config.featureFlags, FEATURE_FLAGS);
assert.isTrue(configLoader._readConfigFromHTML.called);
assert.isTrue(configLoader._parseHTMLConfig.called);
@@ -120,19 +136,5 @@ define(function (require, exports, module) {
});
});
});
-
-
- describe('_setWebpackPublicPath', () => {
- it('sets the bundle path', () => {
- /*eslint-disable camelcase*/
- configLoader._setWebpackPublicPath('somepath');
- assert.equal(__webpack_public_path__, 'somepath'); //eslint-disable-line no-undef
- configLoader._setWebpackPublicPath();
- assert.equal(__webpack_public_path__, Constants.DEFAULT_BUNDLE_PATH); //eslint-disable-line no-undef
- });
- });
-
});
});
-
-
diff --git a/app/tests/spec/lib/experiments/grouping-rules/communication-prefs.js b/app/tests/spec/lib/experiments/grouping-rules/communication-prefs.js
index ba3d83f5d..7079ba127 100644
--- a/app/tests/spec/lib/experiments/grouping-rules/communication-prefs.js
+++ b/app/tests/spec/lib/experiments/grouping-rules/communication-prefs.js
@@ -15,45 +15,53 @@ define(function (require, exports, module) {
experiment = new Experiment();
});
-
- it('has the expected number of available languages', () => {
- assert.lengthOf(experiment.availableLanguages, 12);
+ it('choose returns false without subject.lang', () => {
+ assert.isFalse(experiment.choose({}));
});
- describe('choose', () => {
- it('returns false without subject.lang', () => {
- assert.isFalse(experiment.choose({}));
+ [
+ 'de',
+ 'en',
+ 'en-US',
+ 'en-GB',
+ 'es',
+ 'es-ES',
+ 'es-MX',
+ 'fr',
+ 'hu',
+ 'id',
+ 'pl',
+ 'pt-br',
+ 'ru',
+ 'zh-TW',
+ ].forEach((lang) => {
+ it(`choose returns true for ${lang}`, () => {
+ assert.isTrue(experiment.choose({ lang }));
});
+ });
- it('returns true for available languages', () => {
- [
- 'de',
- 'en',
- 'en-US',
- 'en-GB',
- 'es',
- 'es-ES',
- 'es-MX',
- 'fr',
- 'hu',
- 'id',
- 'pl',
- 'pt-br',
- 'ru',
- 'zh-TW',
- ].forEach((lang) => {
- assert.isTrue(experiment.choose({ lang }));
- });
+ [
+ 'de-DE',
+ 'pt',
+ ].forEach((lang) => {
+ it(`choose returns false for ${lang}`, () => {
+ assert.isFalse(experiment.choose({ lang }));
});
+ });
- it('returns false for unsupported languages', () => {
- [
- 'de-DE',
- 'pt',
- ].forEach((lang) => {
- assert.isFalse(experiment.choose({ lang }));
- });
- });
+ it('choose gives precedence to featureFlags', () => {
+ assert.isFalse(experiment.choose({
+ featureFlags: {
+ communicationPrefLanguages: [ 'en', 'fr' ]
+ },
+ lang: 'de'
+ }));
+ assert.isTrue(experiment.choose({
+ featureFlags: {
+ communicationPrefLanguages: [ 'en', 'pt' ]
+ },
+ lang: 'pt'
+ }));
});
});
});
diff --git a/app/tests/spec/lib/experiments/grouping-rules/index.js b/app/tests/spec/lib/experiments/grouping-rules/index.js
index 71d1984fd..cea13b6fc 100644
--- a/app/tests/spec/lib/experiments/grouping-rules/index.js
+++ b/app/tests/spec/lib/experiments/grouping-rules/index.js
@@ -42,7 +42,10 @@ define(function (require, exports, module) {
rule1,
rule2,
rule3
- ]
+ ],
+ featureFlags: {
+ foo: 'bar'
+ }
});
});
@@ -64,11 +67,17 @@ define(function (require, exports, module) {
assert.isTrue(experimentGroupingRules.choose('rule1', subject));
assert.isTrue(rule1.choose.calledOnce);
- assert.isTrue(rule1.choose.calledWith(subject));
+ assert.deepEqual(rule1.choose.args[0][0], {
+ experimentGroupingRules,
+ featureFlags: {
+ foo: 'bar'
+ },
+ ...subject
+ });
assert.equal(experimentGroupingRules.choose('rule2', subject), 'treatment');
assert.isTrue(rule2.choose.calledOnce);
- assert.isTrue(rule2.choose.calledWith(subject));
+ assert.deepEqual(rule2.choose.args[0][0], rule1.choose.args[0][0]);
// rule3 is allowed even if rule2 is forced.
subject.forceExperiment = 'rule2';
diff --git a/app/tests/spec/lib/experiments/grouping-rules/is-sampled-user.js b/app/tests/spec/lib/experiments/grouping-rules/is-sampled-user.js
index 04a613707..aa27eb116 100644
--- a/app/tests/spec/lib/experiments/grouping-rules/is-sampled-user.js
+++ b/app/tests/spec/lib/experiments/grouping-rules/is-sampled-user.js
@@ -18,22 +18,46 @@ define(function (require, exports, module) {
describe('sampleRate', () => {
it('returns 1 for development', () => {
- assert.equal(Experiment.sampleRate('development'), 1);
+ assert.equal(Experiment.sampleRate({ env: 'development' }), 1);
});
it('returns 0.1 for everyone else', () => {
- assert.equal(Experiment.sampleRate('production'), 0.1);
+ assert.equal(Experiment.sampleRate({ env: 'production' }), 0.1);
});
});
describe('choose', () => {
- it('delegates to bernoulliTrial', () => {
+ beforeEach(() => {
sinon.stub(experiment, 'bernoulliTrial').callsFake(() => true);
+ });
+ afterEach(() => {
+ experiment.bernoulliTrial.restore();
+ });
+
+ it('delegates to bernoulliTrial', () => {
assert.isTrue(experiment.choose({ env: 'production', uniqueUserId: 'user-id' }));
assert.isTrue(experiment.bernoulliTrial.calledOnce);
assert.isTrue(experiment.bernoulliTrial.calledWith(0.1, 'user-id'));
});
+
+ it('passes sampleRate as 1 if env is development', () => {
+ experiment.choose({ env: 'development', uniqueUserId: 'wibble' });
+ assert.equal(experiment.bernoulliTrial.callCount, 1);
+ assert.equal(experiment.bernoulliTrial.args[0][0], 1);
+ });
+
+ it('gives precedence to featureFlags', () => {
+ experiment.choose({
+ env: 'production',
+ featureFlags: {
+ metricsSampleRate: 0
+ },
+ uniqueUserId: 'wibble'
+ });
+ assert.equal(experiment.bernoulliTrial.callCount, 1);
+ assert.equal(experiment.bernoulliTrial.args[0][0], 0);
+ });
});
});
});
diff --git a/app/tests/spec/lib/experiments/grouping-rules/send-sms-install-link.js b/app/tests/spec/lib/experiments/grouping-rules/send-sms-install-link.js
index ac9e2c48f..86c33ce5c 100644
--- a/app/tests/spec/lib/experiments/grouping-rules/send-sms-install-link.js
+++ b/app/tests/spec/lib/experiments/grouping-rules/send-sms-install-link.js
@@ -40,10 +40,28 @@ define(function (require, exports, module) {
});
describe('country does not have a `rolloutRate`', () => {
- it('returns `false', () => {
+ beforeEach(() => {
delete CountryTelephoneInfo.GB.rolloutRate;
+ });
+
+ it('returns `false', () => {
assert.isFalse(experiment.choose({ account, country, uniqueUserId: 'user-id' }));
});
+
+ it('featureFlags take precedence', () => {
+ assert.isTrue(experiment.choose({
+ account,
+ country,
+ featureFlags: {
+ smsCountries: {
+ GB: {
+ rolloutRate: 1
+ }
+ }
+ },
+ uniqueUserId: 'wibble'
+ }));
+ });
});
describe('country has a `rolloutRate`', () => {
@@ -79,6 +97,18 @@ define(function (require, exports, module) {
CountryTelephoneInfo.GB.rolloutRate = 1.0;
assert.isTrue(experiment.choose({ account, country, uniqueUserId: 'user-id' }));
});
+
+ it('featureFlags take precedence', () => {
+ CountryTelephoneInfo.GB.rolloutRate = 1.0;
+ assert.isFalse(experiment.choose({
+ account,
+ country,
+ featureFlags: {
+ smsCountries: {}
+ },
+ uniqueUserId: 'wibble'
+ }));
+ });
});
});
});
diff --git a/app/tests/spec/lib/experiments/grouping-rules/sentry.js b/app/tests/spec/lib/experiments/grouping-rules/sentry.js
index 168e1c454..520b6b8c1 100644
--- a/app/tests/spec/lib/experiments/grouping-rules/sentry.js
+++ b/app/tests/spec/lib/experiments/grouping-rules/sentry.js
@@ -18,22 +18,46 @@ define(function (require, exports, module) {
describe('sampleRate', () => {
it('returns 1 for development', () => {
- assert.equal(Experiment.sampleRate('development'), 1);
+ assert.equal(Experiment.sampleRate({ env: 'development' }), 1);
});
it('returns 0.3 for everyone else', () => {
- assert.equal(Experiment.sampleRate('production'), 0.3);
+ assert.equal(Experiment.sampleRate({ env: 'production' }), 0.3);
});
});
describe('choose', () => {
- it('delegates to bernoulliTrial', () => {
+ beforeEach(() => {
sinon.stub(experiment, 'bernoulliTrial').callsFake(() => true);
+ });
+ afterEach(() => {
+ experiment.bernoulliTrial.restore();
+ });
+
+ it('delegates to bernoulliTrial', () => {
assert.isTrue(experiment.choose({ env: 'production', uniqueUserId: 'user-id' }));
assert.isTrue(experiment.bernoulliTrial.calledOnce);
assert.isTrue(experiment.bernoulliTrial.calledWith(0.3, 'user-id'));
});
+
+ it('passes sampleRate as 1 if env is development', () => {
+ experiment.choose({ env: 'development', uniqueUserId: 'wibble' });
+ assert.equal(experiment.bernoulliTrial.callCount, 1);
+ assert.equal(experiment.bernoulliTrial.args[0][0], 1);
+ });
+
+ it('gives precedence to featureFlags', () => {
+ experiment.choose({
+ env: 'production',
+ featureFlags: {
+ sentrySampleRate: 0
+ },
+ uniqueUserId: 'wibble'
+ });
+ assert.equal(experiment.bernoulliTrial.callCount, 1);
+ assert.equal(experiment.bernoulliTrial.args[0][0], 0);
+ });
});
});
});
diff --git a/app/tests/spec/lib/experiments/grouping-rules/token-code.js b/app/tests/spec/lib/experiments/grouping-rules/token-code.js
index 4ba567643..0175eacc6 100644
--- a/app/tests/spec/lib/experiments/grouping-rules/token-code.js
+++ b/app/tests/spec/lib/experiments/grouping-rules/token-code.js
@@ -76,6 +76,21 @@ describe('lib/experiments/grouping-rules/token-code', () => {
assert.isTrue(experiment.uniformChoice.calledOnce);
assert.isTrue(experiment.uniformChoice.calledWith(['treatment-code'], 'user-id'));
});
+
+ it('featureFlags take precedence', () => {
+ subject.clientId = 'invalidClientId';
+ assert.equal(experiment.choose({
+ ...subject,
+ featureFlags: {
+ tokenCodeClients: {
+ invalidClientId: {
+ groups: [ 'treatment-code' ],
+ rolloutRate: 1
+ }
+ }
+ }
+ }), 'treatment-code');
+ });
});
describe('with sync', () => {
@@ -99,6 +114,21 @@ describe('lib/experiments/grouping-rules/token-code', () => {
assert.isTrue(experiment.uniformChoice.calledOnce, 'called once');
assert.isTrue(experiment.uniformChoice.calledWith(['treatment-code'], 'user-id'));
});
+
+ it('featureFlags take precedence', () => {
+ subject.service = 'sync';
+ assert.isFalse(experiment.choose({
+ ...subject,
+ featureFlags: {
+ tokenCodeClients: {
+ sync: {
+ groups: [ 'treatment-code' ],
+ rolloutRate: 0
+ }
+ }
+ }
+ }));
+ });
});
});
});
diff --git a/grunttasks/l10n-generate-pages.js b/grunttasks/l10n-generate-pages.js
index f4e9b05a0..4cf156264 100644
--- a/grunttasks/l10n-generate-pages.js
+++ b/grunttasks/l10n-generate-pages.js
@@ -25,6 +25,7 @@ module.exports = function (grunt) {
var PROPAGATED_ESCAPED_TEMPLATE_FIELDS = [
'config',
+ 'featureFlags',
'flowId',
'flowBeginTime',
'message'
diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json
index 4ad068af1..5b3d55f2e 100644
--- a/npm-shrinkwrap.json
+++ b/npm-shrinkwrap.json
@@ -1,6 +1,6 @@
{
"name": "fxa-content-server",
- "version": "1.132.0",
+ "version": "1.132.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -491,9 +491,9 @@
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
},
"@sinonjs/commons": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.3.1.tgz",
- "integrity": "sha512-rgmZk5CrBGAMATk0HlHOFvo8V44/r+On6cKS80tqid0Eljd+fFBWBOXZp9H2/EB3faxdNdzXTx6QZIKLkbJ7mA==",
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz",
+ "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==",
"dev": true,
"requires": {
"type-detect": "4.0.8"
@@ -517,9 +517,9 @@
}
},
"@sinonjs/samsam": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.2.0.tgz",
- "integrity": "sha512-j5F1rScewLtx6pbTK0UAjA3jJj4RYiSKOix53YWv+Jzy/AZ69qHxUpU8fwVLjyKbEEud9QrLpv6Ggs7WqTimYw==",
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.0.tgz",
+ "integrity": "sha512-beHeJM/RRAaLLsMJhsCvHK31rIqZuobfPLa/80yGH5hnD8PV1hyh9xJBJNFfNmO7yWqm+zomijHsXpI6iTQJfQ==",
"dev": true,
"requires": {
"@sinonjs/commons": "^1.0.2",
@@ -713,9 +713,9 @@
}
},
"@types/lodash": {
- "version": "4.14.122",
- "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.122.tgz",
- "integrity": "sha512-9IdED8wU93ty8gP06ninox+42SBSJHp2IAamsSYMUY76mshRTeUsid/gtbl8ovnOwy8im41ib4cxTiIYMXGKew==",
+ "version": "4.14.123",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.123.tgz",
+ "integrity": "sha512-pQvPkc4Nltyx7G1Ww45OjVqUsJP4UsZm+GWJpigXgkikZqJgRm4c48g027o6tdgubWHwFRF15iFd+Y4Pmqv6+Q==",
"dev": true
},
"@types/mime": {
@@ -731,9 +731,9 @@
"dev": true
},
"@types/node": {
- "version": "11.10.4",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-11.10.4.tgz",
- "integrity": "sha512-wa09itaLE8L705aXd8F80jnFpxz3Y1/KRHfKsYL2bPc0XF+wEWu8sR9n5bmeu8Ba1N9z2GRNzm/YdHcghLkLKg==",
+ "version": "11.11.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.3.tgz",
+ "integrity": "sha512-wp6IOGu1lxsfnrD+5mX6qwSwWuqsdkKKxTN4aQc4wByHAKZJf9/D4KXPQ1POUjEbnCP5LMggB0OEFNY9OTsMqg==",
"dev": true
},
"@types/platform": {
@@ -1440,9 +1440,9 @@
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo="
},
"async-each": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.1.tgz",
- "integrity": "sha1-GdOGodntxufByF04iu28xW0zYC0="
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.2.tgz",
+ "integrity": "sha512-6xrbvN0MOBKSJDdonmSSz2OwFSgxRaVtBDes26mj9KIGtDo+g9xosFRSC+i1gQh2oAN/tQ62AI/pGZGQjVOiRg=="
},
"async-foreach": {
"version": "0.1.3",
@@ -1641,13 +1641,15 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"is-glob": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"dev": true,
+ "optional": true,
"requires": {
"is-extglob": "^1.0.0"
}
@@ -1689,6 +1691,7 @@
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz",
"integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=",
"dev": true,
+ "optional": true,
"requires": {
"remove-trailing-separator": "^1.0.1"
}
@@ -2832,9 +2835,9 @@
"integrity": "sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs="
},
"caniuse-lite": {
- "version": "1.0.30000941",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000941.tgz",
- "integrity": "sha512-4vzGb2MfZcO20VMPj1j6nRAixhmtlhkypM4fL4zhgzEucQIYiRzSqPcWIu1OF8i0FETD93FMIPWfUJCAcFvrqA=="
+ "version": "1.0.30000947",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000947.tgz",
+ "integrity": "sha512-ubgBUfufe5Oi3W1+EHyh2C3lfBIEcZ6bTuvl5wNOpIuRB978GF/Z+pQ7pGGUpeYRB0P+8C7i/3lt6xkeu2hwnA=="
},
"capture-stack-trace": {
"version": "1.0.1",
@@ -4520,6 +4523,11 @@
"is-obj": "^1.0.0"
}
},
+ "double-ended-queue": {
+ "version": "2.1.0-0",
+ "resolved": "https://registry.npmjs.org/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz",
+ "integrity": "sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw="
+ },
"duplexer": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz",
@@ -4561,9 +4569,9 @@
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"electron-to-chromium": {
- "version": "1.3.113",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.113.tgz",
- "integrity": "sha512-De+lPAxEcpxvqPTyZAXELNpRZXABRxf+uL/rSykstQhzj/B0l1150G/ExIIxKc16lI89Hgz81J0BHAcbTqK49g=="
+ "version": "1.3.116",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.116.tgz",
+ "integrity": "sha512-NKwKAXzur5vFCZYBHpdWjTMO8QptNLNP80nItkSIgUOapPAo9Uia+RvkCaZJtO7fhQaVElSvBPWEc2ku6cKsPA=="
},
"elliptic": {
"version": "6.4.1",
@@ -4661,14 +4669,14 @@
}
},
"es5-ext": {
- "version": "0.10.48",
- "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.48.tgz",
- "integrity": "sha512-CdRvPlX/24Mj5L4NVxTs4804sxiS2CjVprgCmrgoDkdmjdY4D+ySHa7K3jJf8R40dFg0tIm3z/dk326LrnuSGw==",
+ "version": "0.10.49",
+ "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.49.tgz",
+ "integrity": "sha512-3NMEhi57E31qdzmYp2jwRArIUsj1HI/RxbQ4bgnSB+AIKIxsAmTiK83bYMifIcpWvEc3P1X30DhUKOqEtF/kvg==",
"dev": true,
"requires": {
"es6-iterator": "~2.0.3",
"es6-symbol": "~3.1.1",
- "next-tick": "1"
+ "next-tick": "^1.0.0"
}
},
"es6-iterator": {
@@ -4997,9 +5005,9 @@
}
},
"eslint-plugin-sorting": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/eslint-plugin-sorting/-/eslint-plugin-sorting-0.4.0.tgz",
- "integrity": "sha512-2edN1MfCdHN3XBr/oMNRkg9TkHvUkzivMjfs9glyPEyqVsQrSJuh/iDidUjg7LXVATJuDQsD8bVELldBj4bFZQ==",
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-sorting/-/eslint-plugin-sorting-0.4.1.tgz",
+ "integrity": "sha512-MYMdZnRN9+YsypSP6clX37AgVh3dt1Esgo+DynoubjCMm+B8Gf2t80xdqjWt7poyDUzkwSfzu5pOVF2HvVfYug==",
"dev": true
},
"eslint-scope": {
@@ -5861,7 +5869,8 @@
},
"ansi-regex": {
"version": "2.1.1",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"aproba": {
"version": "1.2.0",
@@ -5879,11 +5888,13 @@
},
"balanced-match": {
"version": "1.0.0",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
+ "optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -5896,15 +5907,18 @@
},
"code-point-at": {
"version": "1.1.0",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"concat-map": {
"version": "0.0.1",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"console-control-strings": {
"version": "1.1.0",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"core-util-is": {
"version": "1.0.2",
@@ -6007,7 +6021,8 @@
},
"inherits": {
"version": "2.0.3",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"ini": {
"version": "1.3.5",
@@ -6017,6 +6032,7 @@
"is-fullwidth-code-point": {
"version": "1.0.0",
"bundled": true,
+ "optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -6029,17 +6045,20 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
+ "optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "0.0.8",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"minipass": {
"version": "2.3.5",
"bundled": true,
+ "optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@@ -6056,6 +6075,7 @@
"mkdirp": {
"version": "0.5.1",
"bundled": true,
+ "optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -6128,7 +6148,8 @@
},
"number-is-nan": {
"version": "1.0.1",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"object-assign": {
"version": "4.1.1",
@@ -6138,6 +6159,7 @@
"once": {
"version": "1.4.0",
"bundled": true,
+ "optional": true,
"requires": {
"wrappy": "1"
}
@@ -6213,7 +6235,8 @@
},
"safe-buffer": {
"version": "5.1.2",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"safer-buffer": {
"version": "2.1.2",
@@ -6243,6 +6266,7 @@
"string-width": {
"version": "1.0.2",
"bundled": true,
+ "optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -6260,6 +6284,7 @@
"strip-ansi": {
"version": "3.0.1",
"bundled": true,
+ "optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -6298,11 +6323,13 @@
},
"wrappy": {
"version": "1.0.2",
- "bundled": true
+ "bundled": true,
+ "optional": true
},
"yallist": {
"version": "3.0.3",
- "bundled": true
+ "bundled": true,
+ "optional": true
}
}
},
@@ -6391,7 +6418,7 @@
"integrity": "sha512-1Nq8FtlF0Xb5V/4oKmJuKLKD2nGnM4DuhWCdY5obZEIDtBUHgPb0CUf9FVLY54rHJGzfoItFqrVI+SNpY1GAdw==",
"requires": {
"es6-promise": "4.1.1",
- "sjcl": "git://github.com/bitwiseshiftleft/sjcl.git#a03ea8ef32329bc8d7bc28a438372b5acb46616b",
+ "sjcl": "git://github.com/bitwiseshiftleft/sjcl.git#a03ea8e",
"xhr2": "0.0.7"
},
"dependencies": {
@@ -6421,12 +6448,16 @@
}
},
"fxa-shared": {
- "version": "1.0.18",
- "resolved": "https://registry.npmjs.org/fxa-shared/-/fxa-shared-1.0.18.tgz",
- "integrity": "sha512-5un2xXGpIfjPramIx/fJtuxojw/zuJ884VYb/N4xhkW4qXKGOahAJIzcgERAUxCgAqQpcv3K7RA+Z33P+MH5ug==",
+ "version": "1.0.20",
+ "resolved": "https://registry.npmjs.org/fxa-shared/-/fxa-shared-1.0.20.tgz",
+ "integrity": "sha512-olnR3xMo0m2+eL11yAj2A7Kf9l1kbzk1hHieYu3BmQDN8Otf6o/erPlGWOt2/G8AJdWAtxOBZ//W4SR5K+hB1w==",
"requires": {
"accept-language": "2.0.17",
- "moment": "2.20.1"
+ "ajv": "6.10.0",
+ "bluebird": "3.5.3",
+ "generic-pool": "3.6.1",
+ "moment": "2.20.1",
+ "redis": "2.8.0"
},
"dependencies": {
"moment": {
@@ -6477,6 +6508,11 @@
"is-property": "^1.0.0"
}
},
+ "generic-pool": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.6.1.tgz",
+ "integrity": "sha512-iMmD/pY4q0+V+f8o4twE9JPeqfNuX+gJAaIPB3B0W1lFkBOtTxBo6B0HxHPgGhzQA8jego7EWopcYq/UDJO2KA=="
+ },
"get-caller-file": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz",
@@ -6898,13 +6934,15 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"is-glob": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz",
"integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=",
"dev": true,
+ "optional": true,
"requires": {
"is-extglob": "^1.0.0"
}
@@ -8139,17 +8177,17 @@
}
},
"strip-ansi": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.0.0.tgz",
- "integrity": "sha512-Uu7gQyZI7J7gn5qLn1Np3G9vcYGTVqB+lFTytnDJv83dd8T22aGH451P3jueT2/QemInJDfxHB5Tde5OzgG1Ow==",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.1.0.tgz",
+ "integrity": "sha512-TjxrkPONqO2Z8QDCpeE2j6n0M6EwxzyDgzEeGp+FbdvaJAt//ClYi6W5my+3ROlC/hZX2KACUwDfK49Ka5eDvg==",
"requires": {
- "ansi-regex": "^4.0.0"
+ "ansi-regex": "^4.1.0"
},
"dependencies": {
"ansi-regex": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.0.0.tgz",
- "integrity": "sha512-iB5Dda8t/UqpPI/IjsejXu5jOGDrzn41wJyljwPH65VCIbk6+1BzFIMJGFwTNrYXT1CrD+B4l19U7awiQ8rk7w=="
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
}
}
}
@@ -9303,7 +9341,7 @@
}
},
"legal-docs": {
- "version": "git://github.com/mozilla/legal-docs.git#6827032416da57e4b77920046dc6abbfe582fbcf",
+ "version": "git://github.com/mozilla/legal-docs.git#30c5c916dc3f153df9cf2a55a263329ae403febb",
"from": "git://github.com/mozilla/legal-docs.git#master"
},
"levn": {
@@ -9820,12 +9858,12 @@
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
},
"mem": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/mem/-/mem-4.1.0.tgz",
- "integrity": "sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg==",
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/mem/-/mem-4.2.0.tgz",
+ "integrity": "sha512-5fJxa68urlY0Ir8ijatKa3eRz5lwXnRCTvo9+TbTGAuTFJOwpGcY0X05moBd0nW45965Njt4CDI2GFQoG8DvqA==",
"requires": {
"map-age-cleaner": "^0.1.1",
- "mimic-fn": "^1.0.0",
+ "mimic-fn": "^2.0.0",
"p-is-promise": "^2.0.0"
}
},
@@ -9930,9 +9968,9 @@
}
},
"mimic-fn": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
- "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.0.0.tgz",
+ "integrity": "sha512-jbex9Yd/3lmICXwYT6gA/j2mNQGU48wCh/VzRd+/Y/PjYQtlg1gLMdZqvu9s/xH7qKvngxRObl56XZR609IMbA=="
},
"min-document": {
"version": "2.19.0",
@@ -10196,9 +10234,9 @@
"integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s="
},
"nan": {
- "version": "2.12.1",
- "resolved": "https://registry.npmjs.org/nan/-/nan-2.12.1.tgz",
- "integrity": "sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw=="
+ "version": "2.13.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.0.tgz",
+ "integrity": "sha512-5DDQvN0luhXdut8SCwzm/ZuAX2W+fwhqNzfq7CZ+OJzQ6NwpcqmIGyLD1R8MEt7BeErzcsI0JLr4pND2pNp2Cw=="
},
"nanomatch": {
"version": "1.2.13",
@@ -10401,9 +10439,9 @@
}
},
"node-releases": {
- "version": "1.1.9",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.9.tgz",
- "integrity": "sha512-oic3GT4OtbWWKfRolz5Syw0Xus0KRFxeorLNj0s93ofX6PWyuzKjsiGxsCtWktBwwmTF6DdRRf2KreGqeOk5KA==",
+ "version": "1.1.10",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.10.tgz",
+ "integrity": "sha512-KbUPCpfoBvb3oBkej9+nrU0/7xPlVhmhhUJ1PZqwIP5/1dJkRWKWD3OONjo6M2J7tSCBtDCumLwwqeI+DWWaLQ==",
"requires": {
"semver": "^5.3.0"
}
@@ -10543,7 +10581,7 @@
"from": "git://github.com/vladikoff/node-uap.git#9cdd16247",
"requires": {
"array.prototype.find": "2.0.0",
- "uap-core": "git://github.com/ua-parser/uap-core.git#add7bafbb3ba57256d1b919103add1b2cab97aa7",
+ "uap-core": "git://github.com/ua-parser/uap-core.git",
"uap-ref-impl": "0.2.0",
"yamlparser": "0.0.2"
}
@@ -10790,6 +10828,13 @@
"integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
"requires": {
"mimic-fn": "^1.0.0"
+ },
+ "dependencies": {
+ "mimic-fn": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+ "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="
+ }
}
},
"optimist": {
@@ -10992,7 +11037,8 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz",
"integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=",
- "dev": true
+ "dev": true,
+ "optional": true
},
"is-glob": {
"version": "2.0.1",
@@ -11226,9 +11272,9 @@
"dev": true
},
"pngjs": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.3.3.tgz",
- "integrity": "sha512-1n3Z4p3IOxArEs1VRXnZ/RXdfEniAUS9jb68g58FIXMNkPJeZd+Qh4Uq7/e0LVxAQGos1eIUrqrt4FpjdnEd+Q==",
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz",
+ "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==",
"dev": true
},
"po2json": {
@@ -12036,6 +12082,26 @@
"strip-indent": "^1.0.1"
}
},
+ "redis": {
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/redis/-/redis-2.8.0.tgz",
+ "integrity": "sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A==",
+ "requires": {
+ "double-ended-queue": "^2.1.0-0",
+ "redis-commands": "^1.2.0",
+ "redis-parser": "^2.6.0"
+ }
+ },
+ "redis-commands": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.4.0.tgz",
+ "integrity": "sha512-cu8EF+MtkwI4DLIT0x9P8qNTLFhQD4jLfxLR0cCNkeGzs87FN6879JOJwNQR/1zD7aSYNbU0hgsV9zGY71Itvw=="
+ },
+ "redis-parser": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-2.6.0.tgz",
+ "integrity": "sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs="
+ },
"referrer-policy": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/referrer-policy/-/referrer-policy-1.1.0.tgz",
@@ -13804,7 +13870,7 @@
"from": "git://github.com/vladikoff/ua-parser-js.git#fxa-version"
},
"uap-core": {
- "version": "git://github.com/ua-parser/uap-core.git#add7bafbb3ba57256d1b919103add1b2cab97aa7",
+ "version": "git://github.com/ua-parser/uap-core.git#b4a50d040ad03b163b675d468d7ee011e9ad5436",
"from": "git://github.com/ua-parser/uap-core.git"
},
"uap-ref-impl": {
@@ -13991,9 +14057,9 @@
"integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c="
},
"upath": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.0.tgz",
- "integrity": "sha512-bzpH/oBhoS/QI/YtbkqCg6VEiPYjSZtrHQM6/QnJS6OL9pKUFLqb3aFh4Scvwm45+7iAgiMkLhSbaZxUqmrprw=="
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz",
+ "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q=="
},
"upper-case": {
"version": "1.1.3",
@@ -14635,6 +14701,12 @@
"mimic-fn": "^1.0.0"
}
},
+ "mimic-fn": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+ "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
+ "dev": true
+ },
"os-locale": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz",
diff --git a/package.json b/package.json
index 5597e8828..ca3f35e51 100644
--- a/package.json
+++ b/package.json
@@ -63,7 +63,7 @@
"fxa-js-client": "1.0.8",
"fxa-mustache-loader": "0.0.2",
"fxa-pairing-channel": "1.0.1",
- "fxa-shared": "1.0.18",
+ "fxa-shared": "1.0.20",
"got": "6.7.1",
"grunt": "1.0.3",
"grunt-babel": "6.0.0",
@@ -133,7 +133,7 @@
"css": "2.2.3",
"eslint": "4.16.0",
"eslint-plugin-fxa": "git://github.com/mozilla/eslint-plugin-fxa.git#1153ff4bbf7e2c074363253c555fb7f71bac09a1",
- "eslint-plugin-sorting": "0.4.0",
+ "eslint-plugin-sorting": "0.4.1",
"firefox-profile": "1.2.0",
"fxa-conventional-changelog": "1.1.0",
"grunt-ban-word": "0.1.1",
diff --git a/server/lib/configuration.js b/server/lib/configuration.js
index ec3997c65..bc3f0832c 100644
--- a/server/lib/configuration.js
+++ b/server/lib/configuration.js
@@ -147,6 +147,52 @@ const conf = module.exports = convict({
'development'
]
},
+ featureFlags: {
+ enabled: {
+ default: true,
+ doc: 'Enable feature flagging',
+ env: 'FEATURE_FLAGS_ENABLED',
+ format: Boolean
+ },
+ interval: {
+ default: '30 seconds',
+ doc: 'The refresh interval for feature-flagging',
+ env: 'FEATURE_FLAGS_INTERVAL',
+ format: 'duration'
+ },
+ redis: {
+ host: {
+ default: '127.0.0.1',
+ doc: 'Redis host name or IP address',
+ env: 'FEATURE_FLAGS_REDIS_HOST',
+ format: String
+ },
+ maxConnections: {
+ default: 1,
+ doc: 'Maximum connection count for feature-flagging Redis pool',
+ env: 'FEATURE_FLAGS_REDIS_MAX_CONNECTIONS',
+ format: 'nat'
+ },
+ maxPending: {
+ default: 1,
+ doc: 'Maximum waiting client count for feature-flagging Redis pool',
+ env: 'FEATURE_FLAGS_REDIS_MAX_PENDING',
+ format: 'nat'
+ },
+ minConnections: {
+ default: 1,
+ doc: 'Minimum connection count for feature-flagging Redis pool',
+ env: 'FEATURE_FLAGS_REDIS_MIN_CONNECTIONS',
+ format: 'nat'
+ },
+ port: {
+ default: 6379,
+ doc: 'Redis port',
+ env: 'FEATURE_FLAGS_REDIS_PORT',
+ format: 'port'
+ }
+ }
+ },
flow_id_expiry: {
default: '2 hours',
doc: 'Time after which flow ids are considered stale',
diff --git a/server/lib/routes/get-index.js b/server/lib/routes/get-index.js
index 9214931c2..d26b29eab 100644
--- a/server/lib/routes/get-index.js
+++ b/server/lib/routes/get-index.js
@@ -8,6 +8,14 @@ const flowMetrics = require('../flow-metrics');
const logger = require('../logging/log')('routes.index');
module.exports = function (config) {
+ let featureFlags;
+ const featureFlagConfig = config.get('featureFlags');
+ if (featureFlagConfig.enabled) {
+ featureFlags = require('fxa-shared/feature-flags')(featureFlagConfig, logger);
+ } else {
+ featureFlags = { get: () => ({}) };
+ }
+
const AUTH_SERVER_URL = config.get('fxaccount_url');
const CLIENT_ID = config.get('oauth_client_id');
const COPPA_ENABLED = config.get('coppa.enabled');
@@ -54,17 +62,25 @@ module.exports = function (config) {
return {
method: 'get',
path: '/',
- process: function (req, res) {
+ process: async function (req, res) {
const flowEventData = flowMetrics.create(FLOW_ID_KEY, req.headers['user-agent']);
if (NO_LONGER_SUPPORTED_CONTEXTS.has(req.query.context)) {
return res.redirect(`/update_firefox?${req.originalUrl.split('?')[1]}`);
}
+ let flags;
+ try {
+ flags = await featureFlags.get();
+ } catch (err) {
+ logger.error('featureFlags.error', err);
+ flags = {};
+ }
res.render('index', {
// Note that bundlePath is added to templates as a build step
bundlePath: '/bundle',
config: serializedConfig,
+ featureFlags: encodeURIComponent(JSON.stringify(flags)),
flowBeginTime: flowEventData.flowBeginTime,
flowId: flowEventData.flowId,
// Note that staticResourceUrl is added to templates as a build step
@@ -74,6 +90,7 @@ module.exports = function (config) {
if (req.headers.dnt === '1') {
logger.info('request.headers.dnt');
}
- }
+ },
+ terminate: featureFlags.terminate
};
};
diff --git a/server/templates/pages/src/index.html b/server/templates/pages/src/index.html
index 5b8fcb0ed..48a8a30a3 100644
--- a/server/templates/pages/src/index.html
+++ b/server/templates/pages/src/index.html
@@ -8,6 +8,7 @@
+
diff --git a/tests/server/routes/get-index.js b/tests/server/routes/get-index.js
index bff93a072..ecee8f0d0 100644
--- a/tests/server/routes/get-index.js
+++ b/tests/server/routes/get-index.js
@@ -20,10 +20,14 @@ registerSuite('routes/get-index', {
instance = route(config);
},
+ after: () => {
+ instance.terminate();
+ },
+
tests: {
'instance interface is correct': function () {
assert.isObject(instance);
- assert.lengthOf(Object.keys(instance), 3);
+ assert.lengthOf(Object.keys(instance), 4);
assert.equal(instance.method, 'get');
assert.equal(instance.path, '/');
assert.isFunction(instance.process);
@@ -37,7 +41,7 @@ registerSuite('routes/get-index', {
query: {}
};
response = {render: sinon.spy()};
- instance.process(request, response);
+ return instance.process(request, response);
},
tests: {
@@ -51,10 +55,11 @@ registerSuite('routes/get-index', {
var renderParams = args[1];
assert.isObject(renderParams);
- assert.lengthOf(Object.keys(renderParams), 5);
+ assert.lengthOf(Object.keys(renderParams), 6);
assert.ok(/[0-9a-f]{64}/.exec(renderParams.flowId));
assert.isAbove(renderParams.flowBeginTime, 0);
assert.equal(renderParams.bundlePath, '/bundle');
+ assert.isObject(JSON.parse(decodeURIComponent(renderParams.featureFlags)));
assert.equal(renderParams.staticResourceUrl, config.get('static_resource_url'));
assert.isString(renderParams.config);
@@ -88,7 +93,7 @@ registerSuite('routes/get-index', {
},
};
response = {redirect: sinon.spy()};
- instance.process(request, response);
+ return instance.process(request, response);
},
tests: {
@@ -109,7 +114,7 @@ registerSuite('routes/get-index', {
},
};
response = {redirect: sinon.spy()};
- instance.process(request, response);
+ return instance.process(request, response);
},
tests: {
@@ -130,7 +135,7 @@ registerSuite('routes/get-index', {
},
};
response = {render: sinon.spy()};
- instance.process(request, response);
+ return instance.process(request, response);
},
tests: {