зеркало из https://github.com/mozilla/fxa.git
Merge pull request #16464 from mozilla/FXA-9080
task(settings, content): Create configurable rollout rates for v2 key stretching
This commit is contained in:
Коммит
c35d9a0266
|
@ -98,6 +98,7 @@ Start.prototype = {
|
|||
this._experimentGroupingRules = new ExperimentGroupingRules({
|
||||
env: this._config.env,
|
||||
featureFlags: this._config.featureFlags,
|
||||
rolloutRates: this._config.rolloutRates,
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ const UA_OVERRIDE = 'FxATester';
|
|||
*/
|
||||
const STARTUP_EXPERIMENTS = {
|
||||
generalizedReactApp: BaseExperiment,
|
||||
keyStretchV2: BaseExperiment,
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,6 +25,18 @@ class ExperimentChoiceIndex {
|
|||
this._experimentGroupingRules =
|
||||
options.experimentGroupingRules || experimentGroupingRules;
|
||||
this._featureFlags = options.featureFlags;
|
||||
|
||||
// Apply configured rollout rates
|
||||
if (options.rolloutRates) {
|
||||
for (const rule of this._experimentGroupingRules) {
|
||||
if (typeof rule.setRolloutRate === 'function') {
|
||||
const rate = options.rolloutRates[rule.name];
|
||||
if (typeof rate === 'number') {
|
||||
rule.setRolloutRate(rate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -6,19 +6,20 @@
|
|||
|
||||
const BaseGroupingRule = require('./base');
|
||||
|
||||
const GROUPS = [
|
||||
'control',
|
||||
'v2',
|
||||
];
|
||||
const GROUPS = ['control', 'v2'];
|
||||
|
||||
const ROLLOUT_RATE = 0.0;
|
||||
const DEFAULT_ROLLOUT_RATE = 0.0;
|
||||
|
||||
module.exports = class KeyStretchGroupingRule extends BaseGroupingRule {
|
||||
constructor() {
|
||||
super();
|
||||
this.name = 'key-stretch';
|
||||
this.name = 'keyStretchV2';
|
||||
this.groups = GROUPS;
|
||||
this.rolloutRate = ROLLOUT_RATE;
|
||||
this.rolloutRate = DEFAULT_ROLLOUT_RATE;
|
||||
}
|
||||
|
||||
setRolloutRate(rate) {
|
||||
this.rolloutRate = rate;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -139,7 +139,7 @@ function determineKeyStretchVersion() {
|
|||
if (params.get('stretch') === '2') {
|
||||
return 2;
|
||||
}
|
||||
if (ExperimentMixin.isInExperimentGroup('key-stretch', 'v2')) {
|
||||
if (ExperimentMixin.isInExperimentGroup('keyStretchV2', 'v2')) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
|
|
|
@ -8,9 +8,7 @@
|
|||
dependsOn: [ExperimentMixin],
|
||||
|
||||
isInKeyStretchExperiment() {
|
||||
const experimentGroup = this.getAndReportExperimentGroup(
|
||||
'key-stretch'
|
||||
);
|
||||
const experimentGroup = this.getAndReportExperimentGroup('keyStretchV2');
|
||||
return experimentGroup === 'v2';
|
||||
},
|
||||
};
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { assert } from 'chai';
|
||||
import Experiment from 'lib/experiments/grouping-rules/key-stretch-v2';
|
||||
import Experiment from 'lib/experiments/grouping-rules/key-stretch';
|
||||
|
||||
describe('lib/experiments/grouping-rules/key-stretch-v2', () => {
|
||||
describe('lib/experiments/grouping-rules/key-stretch', () => {
|
||||
let experiment;
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -36,13 +36,13 @@
|
|||
});
|
||||
|
||||
it('returns true if rollout 100%', () => {
|
||||
experiment.rolloutRate = 1.0;
|
||||
assert.isTrue(
|
||||
experiment.choose({
|
||||
experimentGroupingRules: { choose: () => experiment.name },
|
||||
uniqueUserId: 'user-id',
|
||||
})
|
||||
);
|
||||
});
|
||||
experiment.rolloutRate = 1.0;
|
||||
assert.isTrue(
|
||||
experiment.choose({
|
||||
experimentGroupingRules: { choose: () => experiment.name },
|
||||
uniqueUserId: 'user-id',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -81,6 +81,12 @@ const settingsConfig = {
|
|||
signUpRoutes: config.get('showReactApp.signUpRoutes'),
|
||||
signInRoutes: config.get('showReactApp.signInRoutes'),
|
||||
},
|
||||
rolloutRates: {
|
||||
keyStretchV2: config.get('rolloutRates.keyStretchV2'),
|
||||
},
|
||||
featureFlags: {
|
||||
keyStretchV2: config.get('featureFlags.keyStretchV2'),
|
||||
},
|
||||
};
|
||||
|
||||
// Inject Beta Settings meta content
|
||||
|
|
|
@ -220,6 +220,12 @@ const conf = (module.exports = convict({
|
|||
format: Boolean,
|
||||
env: 'FEATURE_FLAGS_FXA_STATUS_ON_SETTINGS',
|
||||
},
|
||||
keyStretchV2: {
|
||||
default: true,
|
||||
doc: 'Enables V2 key stretching',
|
||||
format: Boolean,
|
||||
env: 'FEATURE_FLAGS_KEY_STRETCH_V2',
|
||||
},
|
||||
},
|
||||
showReactApp: {
|
||||
simpleRoutes: {
|
||||
|
@ -789,6 +795,14 @@ const conf = (module.exports = convict({
|
|||
env: 'WEBPACK_MODE_OVERRIDE',
|
||||
format: String,
|
||||
},
|
||||
rolloutRates: {
|
||||
keyStretchV2: {
|
||||
default: 0,
|
||||
doc: 'The rollout rate for key stretching changes. Valid values are from 0 to 1.0',
|
||||
env: 'ROLLOUT_KEY_STRETCH_V2',
|
||||
format: Number,
|
||||
},
|
||||
},
|
||||
statsd: {
|
||||
enabled: {
|
||||
doc: 'Enable StatsD',
|
||||
|
|
|
@ -59,6 +59,9 @@ module.exports = function (config) {
|
|||
const GLEAN_LOG_PINGS = config.get('glean.logPings');
|
||||
const GLEAN_DEBUG_VIEW_TAG = config.get('glean.debugViewTag');
|
||||
|
||||
// Rather than relay all rollout rates, hand pick the ones that are applicable
|
||||
const ROLLOUT_RATES = config.get('rolloutRates');
|
||||
|
||||
// Note that this list is only enforced for clients that use login_hint/email
|
||||
// with prompt=none. id_token_hint clients are not subject to this check.
|
||||
const PROMPT_NONE_ENABLED_CLIENT_IDS = new Set(
|
||||
|
@ -89,6 +92,7 @@ module.exports = function (config) {
|
|||
profileUrl: PROFILE_SERVER_URL,
|
||||
release: RELEASE,
|
||||
redirectAllowlist: REDIRECT_CHECK_ALLOW_LIST,
|
||||
rolloutRates: ROLLOUT_RATES,
|
||||
scopedKeysEnabled: SCOPED_KEYS_ENABLED,
|
||||
scopedKeysValidation: SCOPED_KEYS_VALIDATION,
|
||||
sentry: {
|
||||
|
|
|
@ -82,6 +82,12 @@ export interface Config {
|
|||
signUpRoutes: boolean;
|
||||
signInRoutes: boolean;
|
||||
};
|
||||
rolloutRates?: {
|
||||
keyStretchV2?: number;
|
||||
};
|
||||
featureFlags?: {
|
||||
keyStretchV2?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export function getDefault() {
|
||||
|
|
|
@ -38,7 +38,7 @@ export function initializeAppContext() {
|
|||
new UrlQueryData(new ReachRouterWindow())
|
||||
);
|
||||
const authClient = new AuthClient(config.servers.auth.url, {
|
||||
keyStretchVersion: keyStretchExperiment.isV2() ? 2 : 1
|
||||
keyStretchVersion: keyStretchExperiment.isV2(config) ? 2 : 1,
|
||||
});
|
||||
const apolloClient = createApolloClient(config.servers.gql.url);
|
||||
const account = new Account(authClient, apolloClient);
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import { GenericData } from '../../lib/model-data';
|
||||
import { ReachRouterWindow } from '../../lib/window';
|
||||
import { KeyStretchExperiment } from './key-stretch-experiment';
|
||||
|
||||
describe('Key Stretch Experiment Model', function () {
|
||||
const ffOn = {
|
||||
featureFlags: {
|
||||
keyStretchV2: true,
|
||||
},
|
||||
};
|
||||
|
||||
const ffOff = {
|
||||
featureFlags: {
|
||||
keyStretchV2: false,
|
||||
},
|
||||
};
|
||||
|
||||
const window = new ReachRouterWindow();
|
||||
let model: KeyStretchExperiment;
|
||||
beforeEach(function () {
|
||||
model = new KeyStretchExperiment(new GenericData({}));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
window.localStorage.removeItem(`__fxa_storage.experiment.keyStretchV2`);
|
||||
});
|
||||
|
||||
it('is disabled by default', () => {
|
||||
const model = new KeyStretchExperiment(new GenericData({}));
|
||||
expect(model.isV2(ffOn)).toBeFalsy();
|
||||
expect(model.isV2(ffOff)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('enables with stretch query parameter', () => {
|
||||
const model = new KeyStretchExperiment(new GenericData({ stretch: '2' }));
|
||||
expect(model.isV2(ffOn)).toBeTruthy();
|
||||
expect(model.isV2(ffOff)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('enables with force experiment query parameters', () => {
|
||||
const model = new KeyStretchExperiment(
|
||||
new GenericData({
|
||||
forceExperiment: 'keyStretchV2',
|
||||
forceExperimentGroup: 'v2',
|
||||
})
|
||||
);
|
||||
expect(model.isV2(ffOn)).toBeTruthy();
|
||||
expect(model.isV2(ffOff)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('enables with content-server experiment', () => {
|
||||
window.localStorage.setItem(
|
||||
`__fxa_storage.experiment.keyStretchV2`,
|
||||
JSON.stringify(JSON.stringify({ enrolled: true }))
|
||||
);
|
||||
const model = new KeyStretchExperiment(new GenericData({}));
|
||||
expect(model.isV2(ffOn)).toBeTruthy();
|
||||
expect(model.isV2(ffOff)).toBeFalsy();
|
||||
});
|
||||
});
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
import { bind, ModelDataProvider } from '../../lib/model-data';
|
||||
import * as Sentry from '@sentry/browser';
|
||||
|
||||
export class KeyStretchExperiment extends ModelDataProvider {
|
||||
@IsOptional()
|
||||
|
@ -21,11 +22,68 @@ export class KeyStretchExperiment extends ModelDataProvider {
|
|||
@bind()
|
||||
forceExperimentGroup?: string;
|
||||
|
||||
isV2() {
|
||||
return (
|
||||
this.stretch === '2' ||
|
||||
(this.forceExperiment === 'key-stretch' &&
|
||||
this.forceExperimentGroup === 'v2')
|
||||
);
|
||||
isV2(config: { featureFlags?: { keyStretchV2?: boolean } }) {
|
||||
// If the feature flag is off or unspecified, always disable the functionality!
|
||||
if (!config.featureFlags?.keyStretchV2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If stretch=2 in the URL, then force V2 key stretching for this page render,
|
||||
// This is used for dev/test purposes.
|
||||
if (this.stretch === '2') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If force experiment params are in URL, then force V2 key stretching, and
|
||||
// automatically enroll in experiment, so that content server will pick it up.
|
||||
if (
|
||||
this.forceExperiment === 'keyStretchV2' &&
|
||||
this.forceExperimentGroup === 'v2'
|
||||
) {
|
||||
enrollInExp('keyStretchV2', true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isEnrolledIn('keyStretchV2')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Typical state. Not enrolled and not using V2 key stretching.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets state for a local experiment. Typically this is set by
|
||||
* content-server/backbone, but we are in the midst of a migration to
|
||||
* react. So for now we will sort of go behind the content server's back
|
||||
* here. Note, that this will only happen if force experiment query params
|
||||
* are set. Otherwise, the experiment will not be activated, or have to be
|
||||
* activated by a content server / backbone page.
|
||||
*/
|
||||
function enrollInExp(key: string, enrolled: boolean) {
|
||||
window.localStorage.setItem(
|
||||
`__fxa_storage.experiment.${key}`,
|
||||
JSON.stringify(JSON.stringify({ enrolled }))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if local storage indicates an active key stretch experiment.
|
||||
* This is set by content-server/backbone. In the future we will be
|
||||
* porting over experimentation code into settings, but this is in flux
|
||||
* at the moment. See FXA-9183 for more info and latest status
|
||||
*/
|
||||
function isEnrolledIn(key: string) {
|
||||
try {
|
||||
let value: any = window.localStorage.getItem(
|
||||
`__fxa_storage.experiment.${key}`
|
||||
);
|
||||
const json = value ? JSON.parse(JSON.parse(value)) : { enrolled: false };
|
||||
return json.enrolled === true;
|
||||
} catch (error) {
|
||||
Sentry.captureException(error);
|
||||
// If value was malformed then assume false.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
useFtlMsgResolver,
|
||||
isSyncDesktopV3Integration,
|
||||
isSyncOAuthIntegration,
|
||||
useConfig,
|
||||
} from '../../../models';
|
||||
import WarningMessage from '../../../components/WarningMessage';
|
||||
import LinkRememberPassword from '../../../components/LinkRememberPassword';
|
||||
|
@ -71,6 +72,7 @@ const CompleteResetPassword = ({
|
|||
const navigate = useNavigate();
|
||||
const navigateWithoutRerender = useNavigateWithoutRerender();
|
||||
const keyStretchExperiment = useValidatedQueryParams(KeyStretchExperiment);
|
||||
const config = useConfig();
|
||||
const account = useAccount();
|
||||
const location = useLocation() as ReturnType<typeof useLocation> & {
|
||||
state: CompleteResetPasswordLocationState;
|
||||
|
@ -217,7 +219,7 @@ const CompleteResetPassword = ({
|
|||
GleanMetrics.resetPassword.createNewSubmit();
|
||||
|
||||
const accountResetData = await account.completeResetPassword(
|
||||
keyStretchExperiment.queryParamModel.isV2(),
|
||||
keyStretchExperiment.queryParamModel.isV2(config),
|
||||
token,
|
||||
code,
|
||||
emailToUse,
|
||||
|
@ -268,6 +270,7 @@ const CompleteResetPassword = ({
|
|||
ftlMsgResolver,
|
||||
setLinkStatus,
|
||||
keyStretchExperiment,
|
||||
config,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
isSyncDesktopV3Integration,
|
||||
useAuthClient,
|
||||
useFtlMsgResolver,
|
||||
useConfig,
|
||||
} from '../../models';
|
||||
import { MozServices } from '../../lib/types';
|
||||
import { useValidatedQueryParams } from '../../lib/hooks/useValidate';
|
||||
|
@ -82,6 +83,7 @@ const SigninContainer = ({
|
|||
integration: SigninContainerIntegration;
|
||||
serviceName: MozServices;
|
||||
} & RouteComponentProps) => {
|
||||
const config = useConfig();
|
||||
const authClient = useAuthClient();
|
||||
const ftlMsgResolver = useFtlMsgResolver();
|
||||
const navigate = useNavigate();
|
||||
|
@ -205,7 +207,7 @@ const SigninContainer = ({
|
|||
const v1Credentials = await getCredentials(email, password);
|
||||
let v2Credentials = null;
|
||||
|
||||
if (keyStretchExp.queryParamModel.isV2()) {
|
||||
if (keyStretchExp.queryParamModel.isV2(config)) {
|
||||
const credentialStatusData = await credentialStatus({
|
||||
variables: {
|
||||
input: email,
|
||||
|
@ -355,6 +357,7 @@ const SigninContainer = ({
|
|||
keyStretchExp.queryParamModel,
|
||||
passwordChangeFinish,
|
||||
passwordChangeStart,
|
||||
config,
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -205,7 +205,7 @@ const SignupContainer = ({
|
|||
// If enabled, add in V2 key stretching support
|
||||
let credentialsV2 = undefined;
|
||||
let passwordV2 = undefined;
|
||||
if (keyStretchExp.queryParamModel.isV2()) {
|
||||
if (keyStretchExp.queryParamModel.isV2(config)) {
|
||||
credentialsV2 = await getCredentialsV2({
|
||||
password,
|
||||
clientSalt: await createSaltV2(),
|
||||
|
@ -258,7 +258,7 @@ const SignupContainer = ({
|
|||
return handleGQLError(error);
|
||||
}
|
||||
},
|
||||
[beginSignup, integration, isSyncDesktopV3, isOAuth, keyStretchExp]
|
||||
[beginSignup, integration, isSyncDesktopV3, isOAuth, keyStretchExp, config]
|
||||
);
|
||||
|
||||
// TODO: probably a better way to read this?
|
||||
|
|
Загрузка…
Ссылка в новой задаче