Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


SSL ExperimentStore.sys.mjs   Sprache: unbekannt

 
rahmenlose Ansicht.mjs DruckansichtUnknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

/* 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 { SharedDataMap } from "resource://nimbus/lib/SharedDataMap.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  FeatureManifest: "resource://nimbus/FeatureManifest.sys.mjs",
  PrefUtils: "resource://normandy/lib/PrefUtils.sys.mjs",
});

const IS_MAIN_PROCESS =
  Services.appinfo.processType === Services.appinfo.PROCESS_TYPE_DEFAULT;

// This branch is used to store experiment data
const SYNC_DATA_PREF_BRANCH = "nimbus.syncdatastore.";
// This branch is used to store remote rollouts
const SYNC_DEFAULTS_PREF_BRANCH = "nimbus.syncdefaultsstore.";
let tryJSONParse = data => {
  try {
    return JSON.parse(data);
  } catch (e) {}

  return null;
};
ChromeUtils.defineLazyGetter(lazy, "syncDataStore", () => {
  let experimentsPrefBranch = Services.prefs.getBranch(SYNC_DATA_PREF_BRANCH);
  let defaultsPrefBranch = Services.prefs.getBranch(SYNC_DEFAULTS_PREF_BRANCH);
  return {
    _tryParsePrefValue(branch, pref) {
      try {
        return tryJSONParse(branch.getStringPref(pref, ""));
      } catch (e) {
        /* This is expected if we don't have anything stored */
      }

      return null;
    },
    _trySetPrefValue(branch, pref, value) {
      try {
        branch.setStringPref(pref, JSON.stringify(value));
      } catch (e) {
        console.error(e);
      }
    },
    _trySetTypedPrefValue(pref, value) {
      let variableType = typeof value;
      switch (variableType) {
        case "boolean":
          Services.prefs.setBoolPref(pref, value);
          break;
        case "number":
          Services.prefs.setIntPref(pref, value);
          break;
        case "string":
          Services.prefs.setStringPref(pref, value);
          break;
        case "object":
          Services.prefs.setStringPref(pref, JSON.stringify(value));
          break;
      }
    },
    _clearBranchChildValues(prefBranch) {
      const variablesBranch = Services.prefs.getBranch(prefBranch);
      const prefChildList = variablesBranch.getChildList("");
      for (let variable of prefChildList) {
        variablesBranch.clearUserPref(variable);
      }
    },
    /**
     * Given a branch pref returns all child prefs and values
     * { childPref: value }
     * where value is parsed to the appropriate type
     *
     * @returns {Object[]}
     */
    _getBranchChildValues(prefBranch, featureId) {
      const branch = Services.prefs.getBranch(prefBranch);
      const prefChildList = branch.getChildList("");
      let values = {};
      if (!prefChildList.length) {
        return null;
      }
      for (const childPref of prefChildList) {
        let prefName = `${prefBranch}${childPref}`;
        let value = lazy.PrefUtils.getPref(prefName);
        // Try to parse string values that could be stringified objects
        if (
          lazy.FeatureManifest[featureId]?.variables[childPref]?.type === "json"
        ) {
          let parsedValue = tryJSONParse(value);
          if (parsedValue) {
            value = parsedValue;
          }
        }
        values[childPref] = value;
      }

      return values;
    },
    get(featureId) {
      let metadata = this._tryParsePrefValue(experimentsPrefBranch, featureId);
      if (!metadata) {
        return null;
      }
      let prefBranch = `${SYNC_DATA_PREF_BRANCH}${featureId}.`;
      metadata.branch.feature.value = this._getBranchChildValues(
        prefBranch,
        featureId
      );

      return metadata;
    },
    getDefault(featureId) {
      let metadata = this._tryParsePrefValue(defaultsPrefBranch, featureId);
      if (!metadata) {
        return null;
      }
      let prefBranch = `${SYNC_DEFAULTS_PREF_BRANCH}${featureId}.`;
      metadata.branch.feature.value = this._getBranchChildValues(
        prefBranch,
        featureId
      );

      return metadata;
    },
    set(featureId, value) {
      /* If the enrollment branch has variables we store those separately
       * in pref branches of appropriate type:
       * { featureId: "foo", value: { enabled: true } }
       * gets stored as `${SYNC_DATA_PREF_BRANCH}foo.enabled=true`
       */
      if (value.branch?.feature?.value) {
        for (let variable of Object.keys(value.branch.feature.value)) {
          let prefName = `${SYNC_DATA_PREF_BRANCH}${featureId}.${variable}`;
          this._trySetTypedPrefValue(
            prefName,
            value.branch.feature.value[variable]
          );
        }
        this._trySetPrefValue(experimentsPrefBranch, featureId, {
          ...value,
          branch: {
            ...value.branch,
            feature: {
              ...value.branch.feature,
              value: null,
            },
          },
        });
      } else {
        this._trySetPrefValue(experimentsPrefBranch, featureId, value);
      }
    },
    setDefault(featureId, enrollment) {
      /* We store configuration variables separately in pref branches of
       * appropriate type:
       * (feature: "foo") { variables: { enabled: true } }
       * gets stored as `${SYNC_DEFAULTS_PREF_BRANCH}foo.enabled=true`
       */
      let { feature } = enrollment.branch;
      for (let variable of Object.keys(feature.value)) {
        let prefName = `${SYNC_DEFAULTS_PREF_BRANCH}${featureId}.${variable}`;
        this._trySetTypedPrefValue(prefName, feature.value[variable]);
      }
      this._trySetPrefValue(defaultsPrefBranch, featureId, {
        ...enrollment,
        branch: {
          ...enrollment.branch,
          feature: {
            ...enrollment.branch.feature,
            value: null,
          },
        },
      });
    },
    getAllDefaultBranches() {
      return defaultsPrefBranch.getChildList("").filter(
        // Filter out remote defaults variable prefs
        pref => !pref.includes(".")
      );
    },
    delete(featureId) {
      const prefBranch = `${SYNC_DATA_PREF_BRANCH}${featureId}.`;
      this._clearBranchChildValues(prefBranch);
      try {
        experimentsPrefBranch.clearUserPref(featureId);
      } catch (e) {}
    },
    deleteDefault(featureId) {
      let prefBranch = `${SYNC_DEFAULTS_PREF_BRANCH}${featureId}.`;
      this._clearBranchChildValues(prefBranch);
      try {
        defaultsPrefBranch.clearUserPref(featureId);
      } catch (e) {}
    },
  };
});

const DEFAULT_STORE_ID = "ExperimentStoreData";

/**
 * Returns all feature ids associated with the branch provided.
 * Fallback for when `featureIds` was not persisted to disk. Can be removed
 * after bug 1725240 has reached release.
 *
 * @param {Branch} branch
 * @returns {string[]}
 */
function getAllBranchFeatureIds(branch) {
  return featuresCompat(branch).map(f => f.featureId);
}

function featuresCompat(branch) {
  if (!branch || (!branch.feature && !branch.features)) {
    return [];
  }
  let { features } = branch;
  // In <=v1.5.0 of the Nimbus API, experiments had single feature
  if (!features) {
    features = [branch.feature];
  }

  return features;
}

export class ExperimentStore extends SharedDataMap {
  static SYNC_DATA_PREF_BRANCH = SYNC_DATA_PREF_BRANCH;
  static SYNC_DEFAULTS_PREF_BRANCH = SYNC_DEFAULTS_PREF_BRANCH;

  constructor(sharedDataKey, options = { isParent: IS_MAIN_PROCESS }) {
    super(sharedDataKey || DEFAULT_STORE_ID, options);
  }

  async init() {
    await super.init();

    this.getAllActiveExperiments().forEach(({ branch, featureIds }) => {
      (featureIds || getAllBranchFeatureIds(branch)).forEach(featureId =>
        this._emitFeatureUpdate(featureId, "feature-experiment-loaded")
      );
    });
    this.getAllActiveRollouts().forEach(({ featureIds }) => {
      featureIds.forEach(featureId =>
        this._emitFeatureUpdate(featureId, "feature-rollout-loaded")
      );
    });

    Services.tm.idleDispatchToMainThread(() => this._cleanupOldRecipes());
  }

  /**
   * Given a feature identifier, find an active experiment that matches that feature identifier.
   * This assumes, for now, that there is only one active experiment per feature per browser.
   * Does not activate the experiment (send an exposure event)
   *
   * @param {string} featureId
   * @returns {Enrollment|undefined} An active experiment if it exists
   * @memberof ExperimentStore
   */
  getExperimentForFeature(featureId) {
    return (
      this.getAllActiveExperiments().find(
        experiment =>
          experiment.featureIds?.includes(featureId) ||
          // Supports <v1.3.0, which was when .featureIds was added
          getAllBranchFeatureIds(experiment.branch).includes(featureId)
        // Default to the pref store if data is not yet ready
      ) || lazy.syncDataStore.get(featureId)
    );
  }

  /**
   * Check if an active experiment already exists for a feature.
   * Does not activate the experiment (send an exposure event)
   *
   * @param {string} featureId
   * @returns {boolean} Does an active experiment exist for that feature?
   * @memberof ExperimentStore
   */
  hasExperimentForFeature(featureId) {
    if (!featureId) {
      return false;
    }
    return !!this.getExperimentForFeature(featureId);
  }

  /**
   * @returns {Enrollment[]}
   */
  getAll() {
    let data = [];
    try {
      data = Object.values(this._data || {});
    } catch (e) {
      console.error(e);
    }

    return data;
  }

  /**
   * Returns all active experiments
   * @returns {Enrollment[]}
   */
  getAllActiveExperiments() {
    return this.getAll().filter(
      enrollment => enrollment.active && !enrollment.isRollout
    );
  }

  /**
   * Returns all active rollouts
   * @returns {Enrollment[]}
   */
  getAllActiveRollouts() {
    return this.getAll().filter(
      enrollment => enrollment.active && enrollment.isRollout
    );
  }

  /**
   * Query the store for the remote configuration of a feature
   * @param {string} featureId The feature we want to query for
   * @returns {{Rollout}|undefined} Remote defaults if available
   */
  getRolloutForFeature(featureId) {
    return (
      this.getAllActiveRollouts().find(r => r.featureIds.includes(featureId)) ||
      lazy.syncDataStore.getDefault(featureId)
    );
  }

  /**
   * Check if an active rollout already exists for a feature.
   * Does not active the experiment (send an exposure event).
   *
   * @param {string} featureId
   * @returns {boolean} Does an active rollout exist for that feature?
   */
  hasRolloutForFeature(featureId) {
    if (!featureId) {
      return false;
    }
    return !!this.getRolloutForFeature(featureId);
  }

  /**
   * Remove inactive enrollments older than 12 months
   */
  _cleanupOldRecipes() {
    const threshold = 365.25 * 24 * 3600 * 1000;
    const nowTimestamp = new Date().getTime();
    const recipesToRemove = this.getAll().filter(
      experiment =>
        !experiment.active &&
        // Flip the comparison here to catch scenarios in which lastSeen is
        // invalid or undefined. The result with be a comparison with NaN
        // which is always false
        !(nowTimestamp - new Date(experiment.lastSeen).getTime() < threshold)
    );
    this._removeEntriesByKeys(recipesToRemove.map(r => r.slug));
  }

  _emitUpdates(enrollment) {
    this.emit(`update:${enrollment.slug}`, enrollment);
    const featureIds =
      enrollment.featureIds || getAllBranchFeatureIds(enrollment.branch);
    const reason = enrollment.isRollout
      ? "rollout-updated"
      : "experiment-updated";

    for (const featureId of featureIds) {
      this._emitFeatureUpdate(featureId, reason);
    }
  }

  _emitFeatureUpdate(featureId, reason) {
    this.emit(`featureUpdate:${featureId}`, reason);
  }

  _onFeatureUpdate(featureId, callback) {
    if (this._isReady) {
      const hasExperiment = this.hasExperimentForFeature(featureId);
      if (hasExperiment || this.hasRolloutForFeature(featureId)) {
        callback(
          `featureUpdate:${featureId}`,
          hasExperiment ? "experiment-updated" : "rollout-updated"
        );
      }
    }

    this.on(`featureUpdate:${featureId}`, callback);
  }

  _offFeatureUpdate(featureId, callback) {
    this.off(`featureUpdate:${featureId}`, callback);
  }

  /**
   * Persists early startup experiments or rollouts
   * @param {Enrollment} enrollment Experiment or rollout
   */
  _updateSyncStore(enrollment) {
    let features = featuresCompat(enrollment.branch);
    for (let feature of features) {
      if (
        lazy.FeatureManifest[feature.featureId]?.isEarlyStartup ||
        feature.isEarlyStartup
      ) {
        if (!enrollment.active) {
          // Remove experiments on un-enroll, no need to check if it exists
          if (enrollment.isRollout) {
            lazy.syncDataStore.deleteDefault(feature.featureId);
          } else {
            lazy.syncDataStore.delete(feature.featureId);
          }
        } else {
          let updateEnrollmentSyncStore = enrollment.isRollout
            ? lazy.syncDataStore.setDefault.bind(lazy.syncDataStore)
            : lazy.syncDataStore.set.bind(lazy.syncDataStore);
          updateEnrollmentSyncStore(feature.featureId, {
            ...enrollment,
            branch: {
              ...enrollment.branch,
              feature,
              // Only store the early startup feature
              features: null,
            },
          });
        }
      }
    }
  }

  /**
   * Add an enrollment and notify listeners
   * @param {Enrollment} enrollment
   */
  addEnrollment(enrollment) {
    if (!enrollment || !enrollment.slug) {
      throw new Error(
        `Tried to add an experiment but it didn't have a .slug property.`
      );
    }

    this.set(enrollment.slug, enrollment);
    this._updateSyncStore(enrollment);
    this._emitUpdates(enrollment);
  }

  /**
   * Merge new properties into the properties of an existing experiment
   * @param {string} slug
   * @param {Partial<Enrollment>} newProperties
   */
  updateExperiment(slug, newProperties) {
    const oldProperties = this.get(slug);
    if (!oldProperties) {
      throw new Error(
        `Tried to update experiment ${slug} but it doesn't exist`
      );
    }
    const updatedExperiment = { ...oldProperties, ...newProperties };
    this.set(slug, updatedExperiment);
    this._updateSyncStore(updatedExperiment);
    this._emitUpdates(updatedExperiment);
  }

  /**
   * Test only helper for cleanup
   *
   * @param slugOrFeatureId Can be called with slug (which removes the SharedDataMap entry) or
   * with featureId which removes the SyncDataStore entry for the feature
   */
  _deleteForTests(slugOrFeatureId) {
    super._deleteForTests(slugOrFeatureId);
    lazy.syncDataStore.deleteDefault(slugOrFeatureId);
    lazy.syncDataStore.delete(slugOrFeatureId);
  }
}

[ Verzeichnis aufwärts0.51unsichere Verbindung  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge