forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			198 lines
		
	
	
	
		
			5.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			198 lines
		
	
	
	
		
			5.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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/. */
 | 
						|
 | 
						|
"use strict";
 | 
						|
 | 
						|
/**
 | 
						|
 * @typedef {import("../experiments/@types/ExperimentManager").Recipe} Recipe
 | 
						|
 */
 | 
						|
 | 
						|
const EXPORTED_SYMBOLS = [
 | 
						|
  "_RemoteSettingsExperimentLoader",
 | 
						|
  "RemoteSettingsExperimentLoader",
 | 
						|
];
 | 
						|
 | 
						|
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
 | 
						|
const { XPCOMUtils } = ChromeUtils.import(
 | 
						|
  "resource://gre/modules/XPCOMUtils.jsm"
 | 
						|
);
 | 
						|
 | 
						|
XPCOMUtils.defineLazyModuleGetters(this, {
 | 
						|
  ASRouterTargeting: "resource://activity-stream/lib/ASRouterTargeting.jsm",
 | 
						|
  ExperimentManager:
 | 
						|
    "resource://messaging-system/experiments/ExperimentManager.jsm",
 | 
						|
  RemoteSettings: "resource://services-settings/remote-settings.js",
 | 
						|
  CleanupManager: "resource://normandy/lib/CleanupManager.jsm",
 | 
						|
});
 | 
						|
 | 
						|
XPCOMUtils.defineLazyGetter(this, "log", () => {
 | 
						|
  const { Logger } = ChromeUtils.import(
 | 
						|
    "resource://messaging-system/lib/Logger.jsm"
 | 
						|
  );
 | 
						|
  return new Logger("RSLoader");
 | 
						|
});
 | 
						|
 | 
						|
XPCOMUtils.defineLazyServiceGetter(
 | 
						|
  this,
 | 
						|
  "timerManager",
 | 
						|
  "@mozilla.org/updates/timer-manager;1",
 | 
						|
  "nsIUpdateTimerManager"
 | 
						|
);
 | 
						|
 | 
						|
const COLLECTION_ID = "messaging-experiments";
 | 
						|
const ENABLED_PREF = "messaging-system.rsexperimentloader.enabled";
 | 
						|
 | 
						|
const TIMER_NAME = "rs-experiment-loader-timer";
 | 
						|
const TIMER_LAST_UPDATE_PREF = `app.update.lastUpdateTime.${TIMER_NAME}`;
 | 
						|
// Use the same update interval as normandy
 | 
						|
const RUN_INTERVAL_PREF = "app.normandy.run_interval_seconds";
 | 
						|
 | 
						|
class _RemoteSettingsExperimentLoader {
 | 
						|
  constructor() {
 | 
						|
    // Has the timer been set?
 | 
						|
    this._initialized = false;
 | 
						|
    // Are we in the middle of updating recipes already?
 | 
						|
    this._updating = false;
 | 
						|
 | 
						|
    // Make it possible to override for testing
 | 
						|
    this.manager = ExperimentManager;
 | 
						|
 | 
						|
    XPCOMUtils.defineLazyGetter(this, "remoteSettingsClient", () => {
 | 
						|
      return RemoteSettings(COLLECTION_ID);
 | 
						|
    });
 | 
						|
 | 
						|
    XPCOMUtils.defineLazyPreferenceGetter(
 | 
						|
      this,
 | 
						|
      "enabled",
 | 
						|
      ENABLED_PREF,
 | 
						|
      false,
 | 
						|
      this.onEnabledPrefChange.bind(this)
 | 
						|
    );
 | 
						|
 | 
						|
    XPCOMUtils.defineLazyPreferenceGetter(
 | 
						|
      this,
 | 
						|
      "intervalInSeconds",
 | 
						|
      RUN_INTERVAL_PREF,
 | 
						|
      21600,
 | 
						|
      () => this.setTimer()
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  async init() {
 | 
						|
    if (this._initialized || !this.enabled) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this.setTimer();
 | 
						|
    CleanupManager.addCleanupHandler(() => this.uninit());
 | 
						|
    this._initialized = true;
 | 
						|
 | 
						|
    await this.updateRecipes();
 | 
						|
  }
 | 
						|
 | 
						|
  uninit() {
 | 
						|
    if (!this._initialized) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    timerManager.unregisterTimer(TIMER_NAME);
 | 
						|
    this._initialized = false;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Checks targeting of a recipe if it is defined
 | 
						|
   * @param {Recipe} recipe
 | 
						|
   * @param {{[key: string]: any}} customContext A custom filter context
 | 
						|
   * @returns {Promise<boolean>} Should we process the recipe?
 | 
						|
   */
 | 
						|
  async checkTargeting(recipe, customContext = {}) {
 | 
						|
    const { targeting } = recipe;
 | 
						|
    if (!targeting) {
 | 
						|
      log.debug("No targeting for recipe, so it matches automatically");
 | 
						|
      return true;
 | 
						|
    }
 | 
						|
    log.debug("Testing targeting expression:", targeting);
 | 
						|
    const result = await ASRouterTargeting.isMatch(
 | 
						|
      targeting,
 | 
						|
      customContext,
 | 
						|
      err => {
 | 
						|
        log.debug("Targeting failed because of an error");
 | 
						|
        Cu.reportError(err);
 | 
						|
      }
 | 
						|
    );
 | 
						|
    return Boolean(result);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Get all recipes from remote settings
 | 
						|
   * @param {string} trigger What caused the update to occur?
 | 
						|
   */
 | 
						|
  async updateRecipes(trigger) {
 | 
						|
    if (this._updating || !this._initialized) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
    this._updating = true;
 | 
						|
 | 
						|
    log.debug("Updating recipes" + (trigger ? ` with trigger ${trigger}` : ""));
 | 
						|
 | 
						|
    let recipes;
 | 
						|
    let loadingError = false;
 | 
						|
 | 
						|
    try {
 | 
						|
      recipes = await this.remoteSettingsClient.get();
 | 
						|
      log.debug(`Got ${recipes.length} recipes from Remote Settings`);
 | 
						|
    } catch (e) {
 | 
						|
      log.debug("Error getting recipes from remote settings.");
 | 
						|
      loadingError = true;
 | 
						|
      Cu.reportError(e);
 | 
						|
    }
 | 
						|
 | 
						|
    let matches = 0;
 | 
						|
    if (recipes && !loadingError) {
 | 
						|
      const context = this.manager.createTargetingContext();
 | 
						|
 | 
						|
      for (const r of recipes) {
 | 
						|
        if (await this.checkTargeting(r, context)) {
 | 
						|
          matches++;
 | 
						|
          log.debug(`${r.id} matched`);
 | 
						|
          await this.manager.onRecipe(r.arguments, "rs-loader");
 | 
						|
        } else {
 | 
						|
          log.debug(`${r.id} did not match due to targeting`);
 | 
						|
        }
 | 
						|
      }
 | 
						|
 | 
						|
      log.debug(`${matches} recipes matched. Finalizing ExperimentManager.`);
 | 
						|
      this.manager.onFinalize("rs-loader");
 | 
						|
    }
 | 
						|
 | 
						|
    if (trigger !== "timer") {
 | 
						|
      const lastUpdateTime = Math.round(Date.now() / 1000);
 | 
						|
      Services.prefs.setIntPref(TIMER_LAST_UPDATE_PREF, lastUpdateTime);
 | 
						|
    }
 | 
						|
 | 
						|
    this._updating = false;
 | 
						|
  }
 | 
						|
 | 
						|
  onEnabledPrefChange(prefName, oldValue, newValue) {
 | 
						|
    if (this._initialized && !newValue) {
 | 
						|
      this.uninit();
 | 
						|
    } else if (!this._initialized && newValue) {
 | 
						|
      this.init();
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Sets a timer to update recipes every this.intervalInSeconds
 | 
						|
   */
 | 
						|
  setTimer() {
 | 
						|
    // When this function is called, updateRecipes is also called immediately
 | 
						|
    timerManager.registerTimer(
 | 
						|
      TIMER_NAME,
 | 
						|
      () => this.updateRecipes("timer"),
 | 
						|
      this.intervalInSeconds
 | 
						|
    );
 | 
						|
    log.debug("Registered update timer");
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
const RemoteSettingsExperimentLoader = new _RemoteSettingsExperimentLoader();
 |