Merge pull request #16464 from mozilla/FXA-9080

task(settings, content): Create configurable rollout rates for v2 key stretching
This commit is contained in:
Dan Schomburg 2024-03-01 09:47:39 -08:00 коммит произвёл GitHub
Родитель d010da3f2d 23cae02c07
Коммит c35d9a0266
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
17 изменённых файлов: 203 добавлений и 32 удалений

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

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