/
githubmirror
/
amphtml
Обзор
Документация
Войти
/
githubmirror
/
amphtml
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
src/experiments/index.js
377 строк
11 KB
Viktor Szépe
Fix typos in src/ (#39606)
12 авг 2025, 21:35
Не верифицирован
12 авг 2025, 21:35
fca692e
Код
Авторство
О чём код?
/** * @fileoverview Experiments system allows a developer to opt-in to test * features that are not yet fully tested. * * Experiments page: https://cdn.ampproject.org/experiments.html */ import {devAssert} from '#core/assert'; import {isArray} from '#core/types'; import {hasOwn, map} from '#core/types/object'; import {parseJson} from '#core/types/object/json'; import {parseQueryString} from '#core/types/string/url'; import {dev, user} from '#utils/log'; import {getMode} from '../mode'; import {getTopWindow} from '../service-helpers'; /** @typedef {import('./types.d').ExperimentBranchMap} ExperimentBranchMap */ /** @typedef {{[key: string]: boolean}} ExperimentTogglesMap */ /** * @const * @type {string} */ const TAG = 'EXPERIMENTS'; /** * @const * @type {string} */ const LOCAL_STORAGE_KEY = 'amp-experiment-toggles'; const TOGGLES_WINDOW_PROPERTY = '__AMP__EXPERIMENT_TOGGLES'; /** * Whether we are in canary. * @param {Window} win * @return {boolean} */ export function isCanary(win) { return !!win.AMP_CONFIG?.canary; } /** * Returns binary type, e.g., canary, production, control, or rc. * @param {Window} win * @return {string} */ export function getBinaryType(win) { return win.AMP_CONFIG?.type || 'unknown'; } /** * Whether the specified experiment is on or off. * @param {Window} win * @param {string} experimentId * @return {boolean} */ export function isExperimentOn(win, experimentId) { const toggles = experimentToggles(win); return !!toggles[experimentId]; } /** * Toggles the experiment on or off. Returns the actual value of the experiment * after toggling is done. * @param {Window} win * @param {string} experimentId * @param {boolean=} opt_on * @param {boolean=} opt_transientExperiment Whether to toggle the * experiment state "transiently" (i.e., for this page load only) or * durably (by saving the experiment IDs after toggling). * Default: false (save durably). * @return {boolean} New state for experimentId. */ export function toggleExperiment( win, experimentId, opt_on, opt_transientExperiment ) { const currentlyOn = isExperimentOn(win, /*OK*/ experimentId); const on = opt_on ?? !currentlyOn; if (on != currentlyOn) { const toggles = experimentToggles(win); toggles[experimentId] = on; if (!opt_transientExperiment) { const storedToggles = getExperimentToggles(win); storedToggles[experimentId] = on; saveExperimentToggles(win, storedToggles); // Avoid affecting tests that spy/stub warn(). if (!getMode().test) { user().warn( TAG, '"%s" experiment %s for the domain "%s". See: https://amp.dev/documentation/guides-and-tutorials/learn/experimental', experimentId, on ? 'enabled' : 'disabled', win.location.hostname ); } } } return on; } /** * Calculate whether the experiment is on or off based off of its default value, * stored overridden value, or the global config frequency given. * @param {Window} win * @return {ExperimentTogglesMap} */ export function experimentToggles(win) { if (win[TOGGLES_WINDOW_PROPERTY]) { return /** @type {ExperimentTogglesMap} */ (win[TOGGLES_WINDOW_PROPERTY]); } win[TOGGLES_WINDOW_PROPERTY] = /** @type {ExperimentTogglesMap} */ (map()); const toggles = win[TOGGLES_WINDOW_PROPERTY]; devAssert(toggles); // Read default and injected configs of this build. const buildExperimentConfigs = { ...(win.AMP_CONFIG ?? {}), ...(win.AMP_EXP ?? parseJson(win.__AMP_EXP?.textContent || '{}')), }; for (const experimentId in buildExperimentConfigs) { const frequency = buildExperimentConfigs[experimentId]; if (typeof frequency === 'number' && frequency >= 0 && frequency <= 1) { toggles[experimentId] = Math.random() < frequency; } } // Read document level override from meta tag. const allowedDocOptIn = win.AMP_CONFIG?.['allow-doc-opt-in']; if (isArray(allowedDocOptIn) && allowedDocOptIn.length) { const meta = win.document.head.querySelector( 'meta[name="amp-experiments-opt-in"]' ); if (meta) { const optedInExperiments = meta.getAttribute('content')?.split(',') || []; for (const experiment of optedInExperiments) { if (allowedDocOptIn.includes(experiment)) { toggles[experiment] = true; } } } } Object.assign(toggles, getExperimentToggles(win)); const allowedUrlOptIn = win.AMP_CONFIG?.['allow-url-opt-in']; if (isArray(allowedUrlOptIn) && allowedUrlOptIn.length) { const hash = win.location['originalHash'] || win.location.hash; const params = parseQueryString(hash); for (const experiment of allowedUrlOptIn) { const param = params[`e-${experiment}`]; if (param == '1') { toggles[experiment] = true; } if (param == '0') { toggles[experiment] = false; } } } return toggles; } /** * Returns the cached experiments toggles, or null if they have not been * computed yet. * @param {Window} win * @return {?ExperimentTogglesMap} */ export function experimentTogglesOrNull(win) { return win[TOGGLES_WINDOW_PROPERTY] || null; } /** * Returns a set of experiment IDs currently on. * @param {Window} win * @return {ExperimentTogglesMap} */ function getExperimentToggles(win) { let experimentsString = ''; try { if ('localStorage' in win) { experimentsString = win.localStorage.getItem(LOCAL_STORAGE_KEY) ?? ''; } } catch { dev().warn(TAG, 'Failed to retrieve experiments from localStorage.'); } const tokens = experimentsString?.split(/\s*,\s*/g) || []; const toggles = map(); for (const token of tokens) { if (!token) { continue; } if (token[0] == '-') { toggles[token.substr(1)] = false; } else { toggles[token] = true; } } return toggles; } /** * Saves a set of experiment IDs currently on. * @param {Window} win * @param {ExperimentTogglesMap} toggles */ function saveExperimentToggles(win, toggles) { const experimentIds = []; for (const experiment in toggles) { experimentIds.push((toggles[experiment] === false ? '-' : '') + experiment); } try { win.localStorage?.setItem(LOCAL_STORAGE_KEY, experimentIds.join(',')); } catch (e) { user().error(TAG, 'Failed to save experiments to localStorage.'); } } /** * See getExperimentToggles(). * @param {Window} win * @return {ExperimentTogglesMap} * @visibleForTesting */ export function getExperimentTogglesForTesting(win) { return getExperimentToggles(win); } /** * Resets the experimentsToggle cache for testing purposes. * @param {Window} win * @visibleForTesting */ export function resetExperimentTogglesForTesting(win) { saveExperimentToggles(win, {}); win[TOGGLES_WINDOW_PROPERTY] = undefined; } /** * In some browser implementations of Math.random(), sequential calls of * Math.random() are correlated and can cause a bias. In particular, * if the previous random() call was < 0.001 (as it will be if we select * into an experiment), the next value could be less than 0.5 more than * 50.7% of the time. This provides an implementation that roots down into * the crypto API, when available, to produce less biased samples. * * @return {number} Pseudo-random floating-point value on the range [0, 1). */ function slowButAccuratePrng() { // TODO(tdrl): Implement. return Math.random(); } /** * Container for alternate random number generator implementations. This * allows us to set an "accurate" PRNG for branch selection, but to mock it * out easily in tests. * * @visibleForTesting * @const * @type {{accuratePrng: function():number}} */ export const RANDOM_NUMBER_GENERATORS = { accuratePrng: slowButAccuratePrng, }; /** * Selects, uniformly at random, a single item from the array. * @param {string[]} arr Object to select from. * @return {?string} Single item from arr or null if arr was empty. */ function selectRandomItem(arr) { const rn = RANDOM_NUMBER_GENERATORS.accuratePrng(); return arr[Math.floor(rn * arr.length)] || null; } /** * Selects which page-level experiment branches are enabled. If a given * experiment name is already set (including to the null / no branches selected * state), this won't alter its state. * * Check whether a given experiment is set using isExperimentOn(win, * experimentName) and, if it is on, look for which branch is selected in * win.__AMP_EXPERIMENT_BRANCHES[experimentName]. * * @param {Window} win Window context on which to save experiment * selection state. * @param {import('./types.d').ExperimentInfo[]} experiments Set of experiments * to configure for this page load. * @return {ExperimentBranchMap} Map of experiment names to selected * branches. */ export function randomlySelectUnsetExperiments(win, experiments) { win.__AMP_EXPERIMENT_BRANCHES = win.__AMP_EXPERIMENT_BRANCHES || {}; /** @type {ExperimentBranchMap} */ const selectedExperiments = {}; for (const experiment of experiments) { const experimentName = experiment.experimentId; if (hasOwn(win.__AMP_EXPERIMENT_BRANCHES, experimentName)) { selectedExperiments[experimentName] = win.__AMP_EXPERIMENT_BRANCHES[experimentName]; continue; } if (!experiment.isTrafficEligible?.(win)) { win.__AMP_EXPERIMENT_BRANCHES[experimentName] = null; continue; } // If we're in the experiment, but we haven't already forced a specific // experiment branch (e.g., via a test setup), then randomize the branch // choice. if ( !win.__AMP_EXPERIMENT_BRANCHES[experimentName] && isExperimentOn(win, /*OK*/ experimentName) ) { win.__AMP_EXPERIMENT_BRANCHES[experimentName] = selectRandomItem( experiment.branches ); selectedExperiments[experimentName] = win.__AMP_EXPERIMENT_BRANCHES[experimentName]; } } return selectedExperiments; } /** * Returns the experiment branch enabled for the given experiment ID. * For example, 'control' or 'experiment'. * * @param {Window} win Window context to check for experiment state. * @param {string} experimentName Name of the experiment to check. * @return {?string} Active experiment branch ID for experimentName (possibly * null if experimentName has been tested but no branch was enabled). */ export function getExperimentBranch(win, experimentName) { return win.__AMP_EXPERIMENT_BRANCHES ? win.__AMP_EXPERIMENT_BRANCHES[experimentName] : null; } /** * Returns an object containing all active experiment branches on the * top Window. * * @param {Window} win Window context to check for experiment state. * @return {object} contains all experiment branches and their ids. */ export function getActiveExperimentBranches(win) { const topWin = getTopWindow(win); if (!topWin.__AMP_EXPERIMENT_BRANCHES) { topWin.__AMP_EXPERIMENT_BRANCHES = {}; } return {...topWin.__AMP_EXPERIMENT_BRANCHES}; } /** * Force enable (or disable) a specific branch of a given experiment name. * Disables the experiment name altogether if branchId is falseish. * * @param {Window} win Window context to check for experiment state. * @param {string} experimentName Name of the experiment to check. * @param {?string} branchId ID of branch to force or null to disable * altogether. * @visibleForTesting */ export function forceExperimentBranch(win, experimentName, branchId) { win.__AMP_EXPERIMENT_BRANCHES = win.__AMP_EXPERIMENT_BRANCHES || {}; toggleExperiment(win, experimentName, !!branchId, true); win.__AMP_EXPERIMENT_BRANCHES[experimentName] = branchId; }