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/. */
5 import { BaseAction } from "resource://normandy/actions/BaseAction.sys.mjs";
9 ChromeUtils.defineESModuleGetters(lazy, {
10 ActionSchemas: "resource://normandy/actions/schemas/index.sys.mjs",
11 AddonRollouts: "resource://normandy/lib/AddonRollouts.sys.mjs",
12 NormandyAddonManager: "resource://normandy/lib/NormandyAddonManager.sys.mjs",
13 NormandyApi: "resource://normandy/lib/NormandyApi.sys.mjs",
14 TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
15 TelemetryEvents: "resource://normandy/lib/TelemetryEvents.sys.mjs",
18 class AddonRolloutError extends Error {
20 * @param {string} slug
21 * @param {object} extra Extra details to include when reporting the error to telemetry.
22 * @param {string} extra.reason The specific reason for the failure.
24 constructor(slug, extra) {
26 let { reason } = extra;
29 message = "an existing rollout already exists for this add-on";
32 case "addon-id-changed": {
33 message = "upgrade add-on ID does not match installed add-on ID";
36 case "upgrade-required": {
37 message = "a newer version of the add-on is already installed";
40 case "download-failure": {
41 message = "the add-on failed to download";
44 case "metadata-mismatch": {
45 message = "the server metadata does not match the downloaded add-on";
48 case "install-failure": {
49 message = "the add-on failed to install";
53 throw new Error(`Unexpected AddonRolloutError reason: ${reason}`);
56 super(`Cannot install add-on for rollout (${slug}): ${message}.`);
62 export class AddonRolloutAction extends BaseAction {
64 return lazy.ActionSchemas["addon-rollout"];
68 const { extensionApiId, slug } = recipe.arguments;
70 const existingRollout = await lazy.AddonRollouts.get(slug);
71 const eventName = existingRollout ? "update" : "enroll";
72 const extensionDetails =
73 await lazy.NormandyApi.fetchExtensionDetails(extensionApiId);
75 // Check if the existing rollout matches the current rollout
78 existingRollout.addonId === extensionDetails.extension_id
80 const versionCompare = Services.vc.compare(
81 existingRollout.addonVersion,
82 extensionDetails.version
85 if (versionCompare === 0) {
90 const createError = (reason, extra = {}) => {
91 return new AddonRolloutError(slug, {
97 // Check for a conflict (addon already installed by another rollout)
98 const activeRollouts = await lazy.AddonRollouts.getAllActive();
99 const conflictingRollout = activeRollouts.find(
101 rollout.slug !== slug &&
102 rollout.addonId === extensionDetails.extension_id
104 if (conflictingRollout) {
105 const conflictError = createError("conflict", {
106 addonId: conflictingRollout.addonId,
107 conflictingSlug: conflictingRollout.slug,
109 this.reportError(conflictError, "enrollFailed");
113 const onInstallStarted = (install, installDeferred) => {
114 const existingAddon = install.existingAddon;
116 if (existingRollout && existingRollout.addonId !== install.addon.id) {
117 installDeferred.reject(createError("addon-id-changed"));
118 return false; // cancel the upgrade, the add-on ID has changed
123 Services.vc.compare(existingAddon.version, install.addon.version) > 0
125 installDeferred.reject(createError("upgrade-required"));
126 return false; // cancel the installation, must be an upgrade
132 const applyNormandyChanges = async install => {
134 addonId: install.addon.id,
135 addonVersion: install.addon.version,
137 xpiUrl: extensionDetails.xpi,
138 xpiHash: extensionDetails.hash,
139 xpiHashAlgorithm: extensionDetails.hash_algorithm,
142 if (existingRollout) {
143 await lazy.AddonRollouts.update({
148 await lazy.AddonRollouts.add({
150 state: lazy.AddonRollouts.STATE_ACTIVE,
157 const undoNormandyChanges = async () => {
158 if (existingRollout) {
159 await lazy.AddonRollouts.update(existingRollout);
161 await lazy.AddonRollouts.delete(recipe.id);
165 const [installedId, installedVersion] =
166 await lazy.NormandyAddonManager.downloadAndInstall({
169 applyNormandyChanges,
172 reportError: error => this.reportError(error, `${eventName}Failed`),
175 if (existingRollout) {
176 this.log.debug(`Updated addon rollout ${slug}`);
178 this.log.debug(`Enrolled in addon rollout ${slug}`);
179 lazy.TelemetryEnvironment.setExperimentActive(
181 lazy.AddonRollouts.STATE_ACTIVE,
183 type: "normandy-addonrollout",
188 // All done, report success to Telemetry
189 lazy.TelemetryEvents.sendEvent(eventName, "addon_rollout", slug, {
190 addonId: installedId,
191 addonVersion: installedVersion,
195 reportError(error, eventName) {
196 if (error instanceof AddonRolloutError) {
197 // One of our known errors. Report it nicely to telemetry
198 lazy.TelemetryEvents.sendEvent(
206 * Some unknown error. Add some helpful details, and report it to
207 * telemetry. The actual stack trace and error message could possibly
208 * contain PII, so we don't include them here. Instead include some
209 * information that should still be helpful, and is less likely to be
212 const safeErrorMessage = `${error.fileName}:${error.lineNumber}:${error.columnNumber} ${error.name}`;
213 lazy.TelemetryEvents.sendEvent(eventName, "addon_rollout", error.slug, {
214 reason: safeErrorMessage.slice(0, 80), // max length is 80 chars