feat(feature-flags): wire in experiments to the feature-flag api

Originally added in 62f4b637, then reverted in e64c571b because it broke
fxa-dev. That breakage was caused by missing Redis config for backoff
and retry, and a too-low limit on pending connections. Those required
config tweaks are added in the next commit.
This commit is contained in:
Phil Booth 2019-02-01 13:06:02 +00:00
Родитель 1345f4db4b
Коммит 274d3cc980
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 36FBB106F9C32516
24 изменённых файлов: 585 добавлений и 241 удалений

Просмотреть файл

@ -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

Просмотреть файл

@ -95,7 +95,8 @@ Start.prototype = {
initializeExperimentGroupingRules () {
this._experimentGroupingRules = new ExperimentGroupingRules({
env: this._config.env
env: this._config.env,
featureFlags: this._config.featureFlags
});
},

Просмотреть файл

@ -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'));
}

Просмотреть файл

@ -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);
}
};

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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;
}
};

Просмотреть файл

@ -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';

Просмотреть файл

@ -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;
}
};

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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;

Просмотреть файл

@ -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(`<meta name="fxa-content-server/config" content="${VALID_HTML_CONFIG}" />`);
});
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(`<meta name="fxa-content-server/config" content="${VALID_HTML_CONFIG}" />`);
$('head').append(`<meta name="fxa-feature-flags" content="${SERIALISED_FEATURE_FLAGS}" />`);
});
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(`<meta name="fxa-content-server/config" content="${VALID_HTML_CONFIG}" />`);
$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
});
});
});
});

Просмотреть файл

@ -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'
}));
});
});
});

Просмотреть файл

@ -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';

Просмотреть файл

@ -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);
});
});
});
});

Просмотреть файл

@ -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'
}));
});
});
});
});

Просмотреть файл

@ -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);
});
});
});
});

Просмотреть файл

@ -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
}
}
}
}));
});
});
});
});

Просмотреть файл

@ -25,6 +25,7 @@ module.exports = function (grunt) {
var PROPAGATED_ESCAPED_TEMPLATE_FIELDS = [
'config',
'featureFlags',
'flowId',
'flowBeginTime',
'message'

226
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",

Просмотреть файл

@ -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",

Просмотреть файл

@ -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',

Просмотреть файл

@ -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
};
};

Просмотреть файл

@ -8,6 +8,7 @@
<meta name="referrer" content="origin">
<meta name="robots" content="noindex,nofollow">
<meta name="fxa-content-server/config" content="{{ config }}" />
<meta name="fxa-feature-flags" content="{{ featureFlags }}" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=2.0, user-scalable=yes" />
<!--iOS Smart Banner-->

Просмотреть файл

@ -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: {