1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5"use strict";
6
7const { BaseAction } = ChromeUtils.import(
8  "resource://normandy/actions/BaseAction.jsm"
9);
10ChromeUtils.defineModuleGetter(
11  this,
12  "TelemetryEnvironment",
13  "resource://gre/modules/TelemetryEnvironment.jsm"
14);
15ChromeUtils.defineModuleGetter(
16  this,
17  "PreferenceRollouts",
18  "resource://normandy/lib/PreferenceRollouts.jsm"
19);
20ChromeUtils.defineModuleGetter(
21  this,
22  "PrefUtils",
23  "resource://normandy/lib/PrefUtils.jsm"
24);
25ChromeUtils.defineModuleGetter(
26  this,
27  "ActionSchemas",
28  "resource://normandy/actions/schemas/index.js"
29);
30ChromeUtils.defineModuleGetter(
31  this,
32  "TelemetryEvents",
33  "resource://normandy/lib/TelemetryEvents.jsm"
34);
35
36var EXPORTED_SYMBOLS = ["PreferenceRollbackAction"];
37
38class PreferenceRollbackAction extends BaseAction {
39  get schema() {
40    return ActionSchemas["preference-rollback"];
41  }
42
43  async _run(recipe) {
44    const { rolloutSlug } = recipe.arguments;
45    const rollout = await PreferenceRollouts.get(rolloutSlug);
46
47    if (PreferenceRollouts.GRADUATION_SET.has(rolloutSlug)) {
48      // graduated rollouts can't be rolled back
49      TelemetryEvents.sendEvent(
50        "unenrollFailed",
51        "preference_rollback",
52        rolloutSlug,
53        {
54          reason: "in-graduation-set",
55          enrollmentId:
56            rollout?.enrollmentId ?? TelemetryEvents.NO_ENROLLMENT_ID_MARKER,
57        }
58      );
59      throw new Error(
60        `Cannot rollback rollout in graduation set "${rolloutSlug}".`
61      );
62    }
63
64    if (!rollout) {
65      this.log.debug(`Rollback ${rolloutSlug} not applicable, skipping`);
66      return;
67    }
68
69    switch (rollout.state) {
70      case PreferenceRollouts.STATE_ACTIVE: {
71        this.log.info(`Rolling back ${rolloutSlug}`);
72        rollout.state = PreferenceRollouts.STATE_ROLLED_BACK;
73        for (const { preferenceName, previousValue } of rollout.preferences) {
74          PrefUtils.setPref(preferenceName, previousValue, {
75            branch: "default",
76          });
77        }
78        await PreferenceRollouts.update(rollout);
79        TelemetryEvents.sendEvent(
80          "unenroll",
81          "preference_rollback",
82          rolloutSlug,
83          {
84            reason: "rollback",
85            enrollmentId:
86              rollout.enrollmentId || TelemetryEvents.NO_ENROLLMENT_ID_MARKER,
87          }
88        );
89        TelemetryEnvironment.setExperimentInactive(rolloutSlug);
90        break;
91      }
92      case PreferenceRollouts.STATE_ROLLED_BACK: {
93        // The rollout has already been rolled back, so nothing to do here.
94        break;
95      }
96      case PreferenceRollouts.STATE_GRADUATED: {
97        // graduated rollouts can't be rolled back
98        TelemetryEvents.sendEvent(
99          "unenrollFailed",
100          "preference_rollback",
101          rolloutSlug,
102          {
103            reason: "graduated",
104            enrollmentId:
105              rollout.enrollmentId || TelemetryEvents.NO_ENROLLMENT_ID_MARKER,
106          }
107        );
108        throw new Error(
109          `Cannot rollback already graduated rollout ${rolloutSlug}`
110        );
111      }
112      default: {
113        throw new Error(
114          `Unexpected state when rolling back ${rolloutSlug}: ${rollout.state}`
115        );
116      }
117    }
118  }
119
120  async _finalize() {
121    await PreferenceRollouts.saveStartupPrefs();
122  }
123}
124