Bug 1503402 - Replace Report Site Issue in Fennec with a webextension version; r=aswan
authorThomas Wisniewski <twisniewski@mozilla.com>
Fri, 16 Nov 2018 22:07:31 +0000
changeset 446874 9a2cf737f3e3d623ad99857070f4646e4be45388
parent 446873 c21bf3a5e752044715f0026f8407eed642c794c6
child 446875 cd0bb0a35c3d4d8de65e504e11095b3f306b6815
push id35052
push userapavel@mozilla.com
push dateSat, 17 Nov 2018 11:25:40 +0000
treeherdermozilla-central@efc1da42132b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersaswan
bugs1503402
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1503402 - Replace Report Site Issue in Fennec with a webextension version; r=aswan Replace Report Site Issue in Fennec with a webextension version Differential Revision: https://phabricator.services.mozilla.com/D10447
mobile/android/chrome/content/WebcompatReporter.js
mobile/android/chrome/content/browser.js
mobile/android/chrome/jar.mn
mobile/android/extensions/moz.build
mobile/android/extensions/report-site-issue/.eslintrc.js
mobile/android/extensions/report-site-issue/background.js
mobile/android/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.js
mobile/android/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.json
mobile/android/extensions/report-site-issue/experimentalAPIs/browserInfo.js
mobile/android/extensions/report-site-issue/experimentalAPIs/browserInfo.json
mobile/android/extensions/report-site-issue/experimentalAPIs/l10n.js
mobile/android/extensions/report-site-issue/experimentalAPIs/l10n.json
mobile/android/extensions/report-site-issue/experimentalAPIs/nativeMenu.js
mobile/android/extensions/report-site-issue/experimentalAPIs/nativeMenu.json
mobile/android/extensions/report-site-issue/experimentalAPIs/snackbars.js
mobile/android/extensions/report-site-issue/experimentalAPIs/snackbars.json
mobile/android/extensions/report-site-issue/experimentalAPIs/tabExtras.js
mobile/android/extensions/report-site-issue/experimentalAPIs/tabExtras.json
mobile/android/extensions/report-site-issue/manifest.json
mobile/android/extensions/report-site-issue/moz.build
mobile/android/modules/Snackbars.jsm
deleted file mode 100644
--- a/mobile/android/chrome/content/WebcompatReporter.js
+++ /dev/null
@@ -1,143 +0,0 @@
-/* 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/. */
-
-ChromeUtils.import("resource://gre/modules/Services.jsm");
-ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
-
-ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
-                               "resource://gre/modules/PrivateBrowsingUtils.jsm");
-ChromeUtils.defineModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
-
-var WebcompatReporter = {
-  menuItem: null,
-  menuItemEnabled: null,
-  init: function() {
-    GlobalEventDispatcher.registerListener(this, "DesktopMode:Change");
-    Services.obs.addObserver(this, "chrome-document-global-created");
-    Services.obs.addObserver(this, "content-document-global-created");
-
-    let visible = true;
-    if ("@mozilla.org/parental-controls-service;1" in Cc) {
-      let pc = Cc["@mozilla.org/parental-controls-service;1"].createInstance(Ci.nsIParentalControlsService);
-      visible = !pc.parentalControlsEnabled;
-    }
-
-    this.addMenuItem(visible);
-  },
-
-  onEvent: function(event, data, callback) {
-    if (event === "DesktopMode:Change") {
-      let tab = BrowserApp.getTabForId(data.tabId);
-      let currentURI = tab.browser.currentURI.spec;
-      if (data.desktopMode && this.isReportableUrl(currentURI)) {
-        this.reportDesktopModePrompt(tab);
-      }
-    }
-  },
-
-  observe: function(subject, topic, data) {
-    if (topic == "content-document-global-created" || topic == "chrome-document-global-created") {
-      let win = subject;
-      let currentURI = win.document.documentURI;
-
-      // Ignore non top-level documents
-      if (currentURI !== win.top.location.href) {
-        return;
-      }
-
-      if (!this.menuItemEnabled && this.isReportableUrl(currentURI)) {
-        NativeWindow.menu.update(this.menuItem, {enabled: true});
-        this.menuItemEnabled = true;
-      } else if (this.menuItemEnabled && !this.isReportableUrl(currentURI)) {
-        NativeWindow.menu.update(this.menuItem, {enabled: false});
-        this.menuItemEnabled = false;
-      }
-    }
-  },
-
-  addMenuItem: function(visible) {
-    this.menuItem = NativeWindow.menu.add({
-      name: this.strings.GetStringFromName("webcompat.menu.name"),
-      callback: () => {
-        Promise.resolve(BrowserApp.selectedTab).then(this.getScreenshot)
-                                               .then(this.reportIssue)
-                                               .catch(Cu.reportError);
-      },
-      enabled: false,
-      visible: visible,
-    });
-  },
-
-  getScreenshot: (tab) => {
-    return new Promise((resolve) => {
-      try {
-        let win = tab.window;
-        let dpr = win.devicePixelRatio;
-        let canvas = win.document.createElement("canvas");
-        let ctx = canvas.getContext("2d");
-        // Grab the visible viewport coordinates
-        let x = win.document.documentElement.scrollLeft;
-        let y = win.document.documentElement.scrollTop;
-        let w = win.innerWidth;
-        let h = win.innerHeight;
-        // Scale according to devicePixelRatio and coordinates
-        canvas.width = dpr * w;
-        canvas.height = dpr * h;
-        ctx.scale(dpr, dpr);
-        ctx.drawWindow(win, x, y, w, h, "#ffffff");
-        let screenshot = canvas.toDataURL();
-        resolve({tab: tab, data: screenshot});
-      } catch (e) {
-        // drawWindow can fail depending on memory or surface size. Rather than reject here,
-        // we resolve the URL so the user can continue to file an issue without a screenshot.
-        Cu.reportError("WebCompatReporter: getting a screenshot failed: " + e);
-        resolve({tab: tab});
-      }
-    });
-  },
-
-  isReportableUrl: function(url) {
-    return url && !(url.startsWith("about") ||
-                    url.startsWith("chrome") ||
-                    url.startsWith("file") ||
-                    url.startsWith("resource"));
-  },
-
-  reportDesktopModePrompt: function(tab) {
-    let message = this.strings.GetStringFromName("webcompat.reportDesktopMode.message");
-    let options = {
-      action: {
-        label: this.strings.GetStringFromName("webcompat.reportDesktopModeYes.label"),
-        callback: () => this.reportIssue({tab: tab}),
-      },
-    };
-    Snackbars.show(message, Snackbars.LENGTH_LONG, options);
-  },
-
-  reportIssue: (tabData) => {
-    return new Promise((resolve) => {
-      const WEBCOMPAT_ORIGIN = "https://webcompat.com";
-      let url = tabData.tab.browser.currentURI.spec;
-      let webcompatURL = `${WEBCOMPAT_ORIGIN}/issues/new?url=${url}&src=mobile-reporter`;
-
-      if (tabData.data && typeof tabData.data === "string") {
-        BrowserApp.deck.addEventListener("DOMContentLoaded", function(event) {
-          if (event.target.defaultView.location.origin === WEBCOMPAT_ORIGIN) {
-            // Waive Xray vision so event.origin is not chrome://browser on the other side.
-            let win = Cu.waiveXrays(event.target.defaultView);
-            win.postMessage(tabData.data, WEBCOMPAT_ORIGIN);
-          }
-        }, {once: true});
-      }
-
-      let isPrivateTab = PrivateBrowsingUtils.isBrowserPrivate(tabData.tab.browser);
-      BrowserApp.addTab(webcompatURL, {parentId: tabData.tab.id, isPrivate: isPrivateTab});
-      resolve();
-    });
-  },
-};
-
-XPCOMUtils.defineLazyGetter(WebcompatReporter, "strings", function() {
-  return Services.strings.createBundle("chrome://browser/locale/webcompatReporter.properties");
-});
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -122,20 +122,16 @@ var WindowEventDispatcher = EventDispatc
 var lazilyLoadedBrowserScripts = [
   ["MasterPassword", "chrome://browser/content/MasterPassword.js"],
   ["OfflineApps", "chrome://browser/content/OfflineApps.js"],
   ["Linkifier", "chrome://browser/content/Linkify.js"],
   ["CastingApps", "chrome://browser/content/CastingApps.js"],
   ["RemoteDebugger", "chrome://browser/content/RemoteDebugger.js"],
   ["gViewSourceUtils", "chrome://global/content/viewSourceUtils.js"],
 ];
-if (!["release", "esr"].includes(AppConstants.MOZ_UPDATE_CHANNEL)) {
-  lazilyLoadedBrowserScripts.push(
-    ["WebcompatReporter", "chrome://browser/content/WebcompatReporter.js"]);
-}
 
 lazilyLoadedBrowserScripts.forEach(function (aScript) {
   let [name, script] = aScript;
   XPCOMUtils.defineLazyGetter(window, name, function() {
     let sandbox = {};
     Services.scriptloader.loadSubScript(script, sandbox);
     return sandbox[name];
   });
@@ -529,20 +525,16 @@ var BrowserApp = {
       ChromeUtils.import("resource://gre/modules/NotificationDB.jsm");
 
       InitLater(() => Services.obs.notifyObservers(window, "browser-delayed-startup-finished"));
       InitLater(() => GlobalEventDispatcher.sendRequest({ type: "Gecko:DelayedStartup" }));
 
       // AsyncPrefs is needed for reader mode.
       InitLater(() => AsyncPrefs.init());
 
-      if (!["release", "esr"].includes(AppConstants.MOZ_UPDATE_CHANNEL)) {
-        InitLater(() => WebcompatReporter.init());
-      }
-
       // Collect telemetry data.
       // We do this at startup because we want to move away from "gather-telemetry" (bug 1127907)
       InitLater(() => {
         Telemetry.addData("FENNEC_TRACKING_PROTECTION_STATE", parseInt(BrowserApp.getTrackingProtectionState()));
       });
 
       InitLater(() => LightWeightThemeWebInstaller.init());
       InitLater(() => CastingApps.init(), window, "CastingApps");
@@ -5215,21 +5207,36 @@ var XPInstallObserver = {
       case "browser-delayed-startup-finished": {
         let disabledAddons = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_DISABLED);
         for (let id of disabledAddons) {
           if (AddonManager.getAddonByID(id).signedState <= AddonManager.SIGNEDSTATE_MISSING) {
             this._notifyUnsignedAddonsDisabled();
             break;
           }
         }
+        this._monitorReportSiteIssueEnabledPref();
         break;
       }
     }
   },
 
+  _monitorReportSiteIssueEnabledPref: function() {
+    const PREF = "extensions.webcompat-reporter.enabled";
+    const ID = "webcompat-reporter@mozilla.org";
+    Services.prefs.addObserver(PREF, async () => {
+      let addon = await AddonManager.getAddonByID(ID);
+      let enabled = Services.prefs.getBoolPref(PREF, false);
+      if (enabled && !addon.isActive) {
+        await addon.enable({allowSystemAddons: true});
+      } else if (!enabled && addon.isActive) {
+        await addon.disable({allowSystemAddons: true});
+      }
+    });
+  },
+
   _notifyUnsignedAddonsDisabled: function() {
     new Prompt({
       window: window,
       title: Strings.browser.GetStringFromName("unsignedAddonsDisabled.title"),
       message: Strings.browser.GetStringFromName("unsignedAddonsDisabled.message"),
       buttons: [
         Strings.browser.GetStringFromName("unsignedAddonsDisabled.viewAddons"),
         Strings.browser.GetStringFromName("unsignedAddonsDisabled.dismiss")
--- a/mobile/android/chrome/jar.mn
+++ b/mobile/android/chrome/jar.mn
@@ -42,19 +42,16 @@ chrome.jar:
   content/CastingApps.js               (content/CastingApps.js)
   content/RemoteDebugger.js            (content/RemoteDebugger.js)
   content/aboutAccounts.xhtml          (content/aboutAccounts.xhtml)
   content/aboutAccounts.js             (content/aboutAccounts.js)
   content/aboutExperiments.xhtml       (content/aboutExperiments.xhtml)
   content/aboutExperiments.js          (content/aboutExperiments.js)
   content/aboutLogins.xhtml            (content/aboutLogins.xhtml)
   content/aboutLogins.js               (content/aboutLogins.js)
-#if MOZ_UPDATE_CHANNEL != release && MOZ_UPDATE_CHANNEL != esr
-  content/WebcompatReporter.js         (content/WebcompatReporter.js)
-#endif
   content/ExtensionPermissions.js      (content/ExtensionPermissions.js)
 
 % content branding %content/branding/
 
 % override chrome://global/content/netError.xhtml chrome://browser/content/netError.xhtml
 % override chrome://mozapps/content/extensions/extensions.xul chrome://browser/content/aboutAddons.xhtml
 
 # L10n resource overrides.
--- a/mobile/android/extensions/moz.build
+++ b/mobile/android/extensions/moz.build
@@ -3,10 +3,14 @@
 # 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/.
 
 with Files('**'):
     BUG_COMPONENT = ('Firefox for Android', 'General')
 
 DIRS += [
-    'webcompat',
+    'webcompat'
 ]
+if not CONFIG['MOZ_UPDATE_CHANNEL'] in ('release', 'esr'):
+    DIRS += [
+        'report-site-issue'
+    ]
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/report-site-issue/.eslintrc.js
@@ -0,0 +1,108 @@
+"use strict";
+
+module.exports = {
+  "rules": {
+    // Rules from the mozilla plugin
+    "mozilla/balanced-listeners": "error",
+    "mozilla/no-aArgs": "error",
+    "mozilla/var-only-at-top-level": "error",
+
+    "valid-jsdoc": ["error", {
+      "prefer": {
+        "return": "returns",
+      },
+      "preferType": {
+        "Boolean": "boolean",
+        "Number": "number",
+        "String": "string",
+        "bool": "boolean",
+      },
+      "requireParamDescription": false,
+      "requireReturn": false,
+      "requireReturnDescription": false,
+    }],
+
+    // Forbid spaces inside the square brackets of array literals.
+    "array-bracket-spacing": ["error", "never"],
+
+    // Forbid spaces inside the curly brackets of object literals.
+    "object-curly-spacing": ["error", "never"],
+
+    // No space padding in parentheses
+    "space-in-parens": ["error", "never"],
+
+    // Require braces around blocks that start a new line
+    "curly": ["error", "all"],
+
+    // Two space indent
+    "indent-legacy": ["error", 2, {"SwitchCase": 1}],
+
+    // Always require parenthesis for new calls
+    "new-parens": "error",
+
+    // No expressions where a statement is expected
+    "no-unused-expressions": "error",
+
+    // No declaring variables that are never used
+    "no-unused-vars": "error",
+
+    // Disallow using variables outside the blocks they are defined (especially
+    // since only let and const are used, see "no-var").
+    "block-scoped-var": "error",
+
+    // Warn about cyclomatic complexity in functions.
+    "complexity": ["error", {"max": 26}],
+
+    // Enforce dots on the next line with property name.
+    "dot-location": ["error", "property"],
+
+    // Maximum length of a line.
+    // This should be 100 but too many lines were longer than that so set a
+    // conservative upper bound for now.
+    "max-len": ["error", 140],
+
+    // Maximum depth callbacks can be nested.
+    "max-nested-callbacks": ["error", 4],
+
+    // Allow the console API aside from console.log.
+    "no-console": ["error", {allow: ["error", "info", "trace", "warn"]}],
+
+    // Disallow fallthrough of case statements, except if there is a comment.
+    "no-fallthrough": "error",
+
+    // Disallow use of multiline strings (use template strings instead).
+    "no-multi-str": "error",
+
+    // Disallow multiple empty lines.
+    "no-multiple-empty-lines": ["error", {"max": 2}],
+
+    // Disallow usage of __proto__ property.
+    "no-proto": "error",
+
+    // Disallow use of assignment in return statement. It is preferable for a
+    // single line of code to have only one easily predictable effect.
+    "no-return-assign": "error",
+
+    // Disallow throwing literals (eg. throw "error" instead of
+    // throw new Error("error")).
+    "no-throw-literal": "error",
+
+    // Disallow padding within blocks.
+    "padded-blocks": ["error", "never"],
+
+    // Require use of the second argument for parseInt().
+    "radix": "error",
+
+    // Enforce spacing after semicolons.
+    "semi-spacing": ["error", {"before": false, "after": true}],
+
+    // Require "use strict" to be defined globally in the script.
+    "strict": ["error", "global"],
+
+    // Disallow Yoda conditions (where literal value comes first).
+    "yoda": "error",
+
+    // Disallow function or variable declarations in nested blocks
+    "no-inner-declarations": "error",
+  },
+};
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/report-site-issue/background.js
@@ -0,0 +1,191 @@
+/* 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";
+
+/* globals browser */
+
+const Config = {
+  newIssueEndpoint: "https://webcompat.com/issues/new",
+  newIssueEndpointPref: "newIssueEndpoint",
+  screenshotFormat: {
+    format: "jpeg",
+    quality: 75,
+  },
+};
+
+// If parental controls are on, we don't activate (that is, we don't show our
+// menu item or prompt the user when they use "request desktop site".
+browser.browserInfo.getParentalControlsEnabled().then(enabled => {
+  if (enabled) {
+    return;
+  }
+
+  browser.aboutConfigPrefs.onEndpointPrefChange.addListener(checkEndpointPref);
+  checkEndpointPref();
+
+  activateMenuItem();
+  activateDesktopViewPrompts();
+});
+
+function activateDesktopViewPrompts() {
+  Promise.all([
+    browser.l10n.getMessage("webcompat.reportDesktopMode.message"),
+    browser.l10n.getMessage("webcompat.reportDesktopModeYes.label"),
+  ]).then(([message, button]) => {
+    browser.tabExtras.onDesktopSiteRequested.addListener(async tabId => {
+      browser.tabs.get(tabId).then(tab => {
+        browser.snackbars.show(message, button).then(() => {
+          reportForTab(tab);
+        }).catch(() => {});
+      }).catch(() => {});
+    });
+  }).catch(() => {});
+}
+
+function activateMenuItem() {
+  browser.nativeMenu.show();
+
+  browser.l10n.getMessage("webcompat.menu.name").then(label => {
+    // We use Fennec NativeMenus because its BrowserAction implementation
+    // lacks support for enabling/disabling its items.
+    browser.nativeMenu.setLabel(label);
+  }).catch(() => {});
+
+  browser.nativeMenu.onClicked.addListener(async () => {
+    const tabs = await browser.tabs.query({active: true});
+    reportForTab(tabs[0]);
+  });
+
+  async function updateMenuItem(url) {
+    if (isReportableUrl(url)) {
+      await browser.nativeMenu.enable();
+    } else {
+      await browser.nativeMenu.disable();
+    }
+  }
+
+  browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+    if ("url" in changeInfo && tab.active) {
+      updateMenuItem(tab.url);
+    }
+  });
+
+  browser.tabs.onActivated.addListener(({tabId}) => {
+    browser.tabs.get(tabId).then(({url}) => {
+      updateMenuItem(url);
+    }).catch(() => {
+      updateMenuItem("about"); // So the action is disabled
+    });
+  });
+
+  browser.tabs.query({active: true}).then(tabs => {
+    updateMenuItem(tabs[0].url);
+  }).catch(() => {});
+}
+
+function isReportableUrl(url) {
+  return url && !(url.startsWith("about") ||
+                  url.startsWith("chrome") ||
+                  url.startsWith("file") ||
+                  url.startsWith("resource"));
+}
+
+async function reportForTab(tab) {
+  return getWebCompatInfoForTab(tab).then(async info => {
+    return openWebCompatTab(info, tab.incognito);
+  }).catch(err => {
+    console.error("Report Site Issue: unexpected error", err);
+  });
+}
+
+async function checkEndpointPref() {
+  const value = await browser.aboutConfigPrefs.getEndpointPref();
+  if (value === undefined) {
+    browser.aboutConfigPrefs.setEndpointPref(Config.newIssueEndpoint);
+  } else {
+    Config.newIssueEndpoint = value;
+  }
+}
+
+function getWebCompatInfoForTab(tab) {
+  const {id, windiwId, url} = tab;
+  return Promise.all([
+    browser.browserInfo.getBlockList(),
+    browser.browserInfo.getBuildID(),
+    browser.browserInfo.getGraphicsPrefs(),
+    browser.browserInfo.getUpdateChannel(),
+    browser.browserInfo.hasTouchScreen(),
+    browser.tabExtras.getWebcompatInfo(id),
+    browser.tabs.captureVisibleTab(windiwId, Config.screenshotFormat).catch(e => {
+      console.error("Report Site Issue: getting a screenshot failed", e);
+      return Promise.resolve(undefined);
+    }),
+  ]).then(([blockList, buildID, graphicsPrefs, channel, hasTouchScreen,
+            frameInfo, screenshot]) => {
+    if (channel !== "linux") {
+      delete graphicsPrefs["layers.acceleration.force-enabled"];
+    }
+
+    const consoleLog = frameInfo.log;
+    delete frameInfo.log;
+
+    return Object.assign(frameInfo, {
+      tabId: id,
+      blockList,
+      details: Object.assign(graphicsPrefs, {
+        buildID,
+        channel,
+        consoleLog,
+        hasTouchScreen,
+        "mixed active content blocked": frameInfo.hasMixedActiveContentBlocked,
+        "mixed passive content blocked": frameInfo.hasMixedDisplayContentBlocked,
+        "tracking content blocked": frameInfo.hasTrackingContentBlocked ?
+                                    `true (${blockList})` : "false",
+      }),
+      screenshot,
+      url,
+    });
+  });
+}
+
+function stripNonASCIIChars(str) {
+  // eslint-disable-next-line no-control-regex
+  return str.replace(/[^\x00-\x7F]/g, "");
+}
+
+async function openWebCompatTab(compatInfo, usePrivateTab) {
+  const url = new URL(Config.newIssueEndpoint);
+  const {details} = compatInfo;
+  const params = {
+    url: `${compatInfo.url}`,
+    src: "mobile-reporter",
+    details,
+    label: [],
+  };
+  if (details["gfx.webrender.all"] || details["gfx.webrender.enabled"]) {
+    params.label.push("type-webrender-enabled");
+  }
+  if (compatInfo.hasTrackingContentBlocked) {
+    params.label.push(`type-tracking-protection-${compatInfo.blockList}`);
+  }
+
+  // Need custom API for private tabs until https://bugzil.la/1372178 is fixed
+  const tab = usePrivateTab ? await browser.tabExtras.createPrivateTab() :
+                              await browser.tabs.create({url: "about:blank"});
+  const json = stripNonASCIIChars(JSON.stringify(params));
+  await browser.tabExtras.loadURIWithPostData(tab.id, url.href, json,
+                                              "application/json");
+  await browser.tabs.executeScript(tab.id, {
+    runAt: "document_end",
+    code: `(function() {
+      async function sendScreenshot(dataURI) {
+        const res = await fetch(dataURI);
+        const blob = await res.blob();
+        postMessage(blob, "${url.origin}");
+      }
+      sendScreenshot("${compatInfo.screenshot}");
+    })()`,
+  });
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.js
@@ -0,0 +1,41 @@
+/* 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";
+
+/* global ExtensionAPI, ExtensionCommon */
+
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+this.aboutConfigPrefs = class extends ExtensionAPI {
+  getAPI(context) {
+    const EventManager = ExtensionCommon.EventManager;
+    const extensionIDBase = context.extension.id.split("@")[0];
+    const endpointPrefName = `extensions.${extensionIDBase}.newIssueEndpoint`;
+
+    return {
+      aboutConfigPrefs: {
+        onEndpointPrefChange: new EventManager({
+          context,
+          name: "aboutConfigPrefs.onEndpointPrefChange",
+          register: (fire) => {
+            const callback = () => {
+              fire.async().catch(() => {}); // ignore Message Manager disconnects
+            };
+            Services.prefs.addObserver(endpointPrefName, callback);
+            return () => {
+              Services.prefs.removeObserver(endpointPrefName, callback);
+            };
+          },
+        }).api(),
+        async getEndpointPref() {
+          return Services.prefs.getStringPref(endpointPrefName, undefined);
+        },
+        async setEndpointPref(value) {
+          Services.prefs.setStringPref(endpointPrefName, value);
+        },
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/report-site-issue/experimentalAPIs/aboutConfigPrefs.json
@@ -0,0 +1,35 @@
+[
+  {
+    "namespace": "aboutConfigPrefs",
+    "description": "experimental API extension to allow access to about:config preferences",
+    "events": [
+      {
+        "name": "onEndpointPrefChange",
+        "type": "function",
+        "parameters": []
+      }
+    ],
+    "functions": [
+      {
+        "name": "getEndpointPref",
+        "type": "function",
+        "description": "Get the endpoint preference's value",
+        "parameters": [],
+        "async": true
+      },
+      {
+        "name": "setEndpointPref",
+        "type": "function",
+        "description": "Set the endpoint preference's value",
+        "parameters": [
+          {
+            "name": "value",
+            "type": "string",
+            "description": "The new value"
+          }
+        ],
+        "async": true
+      }
+    ]
+  }
+]
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/report-site-issue/experimentalAPIs/browserInfo.js
@@ -0,0 +1,65 @@
+/* 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";
+
+/* global ExtensionAPI */
+
+ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+const gParentalControls = (function() {
+  if ("@mozilla.org/parental-controls-service;1" in Cc) {
+    return Cc["@mozilla.org/parental-controls-service;1"]
+           .createInstance(Ci.nsIParentalControlsService);
+  }
+  return {parentalControlsEnabled: false};
+})();
+
+this.browserInfo = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      browserInfo: {
+        async getGraphicsPrefs() {
+          const prefs = {};
+          for (const [name, dflt] of Object.entries({
+            "layers.acceleration.force-enabled": false,
+            "gfx.webrender.all": false,
+            "gfx.webrender.blob-images": true,
+            "gfx.webrender.enabled": false,
+            "image.mem.shared": true,
+          })) {
+            prefs[name] = Services.prefs.getBoolPref(name, dflt);
+          }
+          return prefs;
+        },
+        async getAppVersion() {
+          return AppConstants.MOZ_APP_VERSION;
+        },
+        async getBlockList() {
+          const trackingTable = Services.prefs.getCharPref("urlclassifier.trackingTable");
+          // If content-track-digest256 is in the tracking table,
+          // the user has enabled the strict list.
+          return trackingTable.includes("content") ? "strict" : "basic";
+        },
+        async getBuildID() {
+          return Services.appinfo.appBuildID;
+        },
+        async getUpdateChannel() {
+          return AppConstants.MOZ_UPDATE_CHANNEL;
+        },
+        async getParentalControlsEnabled() {
+          return gParentalControls.parentalControlsEnabled;
+        },
+        async getPlatform() {
+          return AppConstants.platform;
+        },
+        async hasTouchScreen() {
+          const gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+          return gfxInfo.getInfo().ApzTouchInput == 1;
+        },
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/report-site-issue/experimentalAPIs/browserInfo.json
@@ -0,0 +1,64 @@
+[
+  {
+    "namespace": "browserInfo",
+    "description": "experimental API extensions to get browser info not exposed via web APIs",
+    "functions": [
+      {
+        "name": "getAppVersion",
+        "type": "function",
+        "description": "Gets the app version",
+        "parameters": [],
+        "async": true
+      },
+      {
+        "name": "getBlockList",
+        "type": "function",
+        "description": "Gets the current blocklist",
+        "parameters": [],
+        "async": true
+      },
+      {
+        "name": "getBuildID",
+        "type": "function",
+        "description": "Gets the build ID",
+        "parameters": [],
+        "async": true
+      },
+      {
+        "name": "getGraphicsPrefs",
+        "type": "function",
+        "description": "Gets interesting about:config prefs for graphics",
+        "parameters": [],
+        "async": true
+      },
+      {
+        "name": "getParentalControlsEnabled",
+        "type": "function",
+        "description": "Gets whether the Parent Controls are enabled",
+        "parameters": [],
+        "async": true
+      },
+      {
+        "name": "getPlatform",
+        "type": "function",
+        "description": "Gets the platform",
+        "parameters": [],
+        "async": true
+      },
+      {
+        "name": "getUpdateChannel",
+        "type": "function",
+        "description": "Gets the update channel",
+        "parameters": [],
+        "async": true
+      },
+      {
+        "name": "hasTouchScreen",
+        "type": "function",
+        "description": "Gets whether a touchscreen is present",
+        "parameters": [],
+        "async": true
+      }
+    ]
+  }
+]
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/report-site-issue/experimentalAPIs/l10n.js
@@ -0,0 +1,32 @@
+/* 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";
+
+/* global ExtensionAPI, XPCOMUtils */
+
+ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "l10nStrings", function() {
+  return Services.strings.createBundle(
+    "chrome://browser/locale/webcompatReporter.properties");
+});
+
+let l10nManifest;
+
+this.l10n = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      l10n: {
+        getMessage(name) {
+          try {
+            return Promise.resolve(l10nStrings.GetStringFromName(name));
+          } catch (e) {
+            return Promise.reject(e);
+          }
+        },
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/report-site-issue/experimentalAPIs/l10n.json
@@ -0,0 +1,19 @@
+[
+  {
+    "namespace": "l10n",
+    "description": "A stop-gap L10N API only meant to be used until a Fluent-based API is added in bug 1425104",
+    "functions": [
+      {
+        "name": "getMessage",
+        "type": "function",
+        "description": "Gets the message with the given name",
+        "parameters": [{
+          "name": "name",
+          "type": "string",
+          "description": "The name of the message"
+        }],
+        "async": true
+      }
+    ]
+  }
+]
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/report-site-issue/experimentalAPIs/nativeMenu.js
@@ -0,0 +1,62 @@
+/* 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";
+
+/* global ExtensionAPI, ExtensionCommon */
+
+const {Management: {global: {windowTracker}}} =
+            ChromeUtils.import("resource://gre/modules/Extension.jsm", {});
+
+function getNativeWindow() {
+  return windowTracker.topWindow.NativeWindow;
+}
+
+const clickHandlers = new ExtensionCommon.EventEmitter();
+
+const menuItem = getNativeWindow().menu.add({
+  name: "Report site issue",
+  callback: () => {
+    clickHandlers.emit("click");
+  },
+  enabled: false,
+  visible: false,
+});
+
+this.nativeMenu = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      nativeMenu: {
+        onClicked: new ExtensionCommon.EventManager({
+          context,
+          name: "nativeMenu.onClicked",
+          register: (fire) => {
+            const callback = () => {
+              fire.async().catch(() => {}); // ignore Message Manager disconnects
+            };
+            clickHandlers.on("click", callback);
+            return () => {
+              clickHandlers.off("click", callback);
+            };
+          },
+        }).api(),
+        async disable() {
+          getNativeWindow().menu.update(menuItem, {enabled: false});
+        },
+        async enable() {
+          getNativeWindow().menu.update(menuItem, {enabled: true});
+        },
+        async hide() {
+          getNativeWindow().menu.update(menuItem, {visible: false});
+        },
+        async show() {
+          getNativeWindow().menu.update(menuItem, {visible: true});
+        },
+        async setLabel(label) {
+          getNativeWindow().menu.update(menuItem, {name: label});
+        },
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/report-site-issue/experimentalAPIs/nativeMenu.json
@@ -0,0 +1,53 @@
+[
+  {
+    "namespace": "nativeMenu",
+    "description": "experimental extension wrapping around a Fennec NativeMenu",
+    "events": [
+      {
+        "name": "onClicked",
+        "type": "function",
+        "parameters": []
+      }
+    ],
+    "functions": [
+      {
+        "name": "disable",
+        "type": "function",
+        "async": true,
+        "description": "Disable the addon's menu item",
+        "parameters": []
+      },
+      {
+        "name": "enable",
+        "type": "function",
+        "async": true,
+        "description": "Enable the addon's menu item",
+        "parameters": []
+      },
+      {
+        "name": "hide",
+        "type": "function",
+        "async": true,
+        "description": "Hide the addon's menu item",
+        "parameters": []
+      },
+      {
+        "name": "show",
+        "type": "function",
+        "async": true,
+        "description": "Show the addon's menu item",
+        "parameters": []
+      },
+      {
+        "name": "setLabel",
+        "type": "function",
+        "async": true,
+        "description": "Set the label of the addon's menu item",
+        "parameters": [{
+          "name": "label",
+          "type": "string"
+        }]
+      }
+    ]
+  }
+]
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/report-site-issue/experimentalAPIs/snackbars.js
@@ -0,0 +1,33 @@
+/* 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";
+
+/* global ExtensionAPI */
+
+ChromeUtils.defineModuleGetter(this, "clearTimeout",
+                               "resource://gre/modules/Timer.jsm");
+ChromeUtils.defineModuleGetter(this, "setTimeout",
+                               "resource://gre/modules/Timer.jsm");
+ChromeUtils.defineModuleGetter(this, "Snackbars", "resource://gre/modules/Snackbars.jsm");
+
+this.snackbars = class extends ExtensionAPI {
+  getAPI(context) {
+    return {
+      snackbars: {
+        show(message, button) {
+          return new Promise((callback, rejection) => {
+            Snackbars.show(message, Snackbars.LENGTH_LONG, {
+              action: {
+                label: button,
+                callback,
+                rejection,
+              },
+            });
+          });
+        },
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/report-site-issue/experimentalAPIs/snackbars.json
@@ -0,0 +1,21 @@
+[
+  {
+    "namespace": "snackbars",
+    "description": "experimental API extensions for prompting the user via Android Snackbar notifications",
+    "functions": [
+      {
+        "name": "show",
+        "type": "function",
+        "description": "Shows a Snackbar with the given message and button",
+        "parameters": [{
+          "name": "message",
+          "type": "string"
+        },{
+          "name": "button",
+          "type": "string"
+        }],
+        "async": true
+      }
+    ]
+  }
+]
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/report-site-issue/experimentalAPIs/tabExtras.js
@@ -0,0 +1,186 @@
+/* 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";
+
+/* global ChromeUtils, ExtensionAPI, ExtensionCommon, XPCOMUtils */
+
+ChromeUtils.defineModuleGetter(this, "EventDispatcher",
+                               "resource://gre/modules/Messaging.jsm");
+
+ChromeUtils.defineModuleGetter(this, "PrivateBrowsingUtils",
+                               "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
+
+XPCOMUtils.defineLazyGetter(this, "GlobalEventDispatcher", () => EventDispatcher.instance);
+
+function getInfoFrameScript(messageName) {
+  /* eslint-env mozilla/frame-script */
+
+  ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+  function getInnerWindowId(window) {
+    return window.windowUtils.currentInnerWindowID;
+  }
+
+  function getInnerWindowIDsForAllFrames(window) {
+    const innerWindowID = getInnerWindowId(window);
+    let ids = [innerWindowID];
+
+    if (window.frames) {
+      for (let i = 0; i < window.frames.length; i++) {
+        ids = ids.concat(getInnerWindowIDsForAllFrames(window.frames[i]));
+      }
+    }
+
+    return ids;
+  }
+
+  function getLoggedMessages(window, includePrivate = false) {
+    const ids = getInnerWindowIDsForAllFrames(window);
+    return getConsoleMessages(ids).concat(getScriptErrors(ids, includePrivate))
+                                  .sort((a, b) => a.timeStamp - b.timeStamp)
+                                  .map(m => m.message);
+  }
+
+  function getConsoleMessages(windowIds) {
+    const ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"]
+                              .getService(Ci.nsIConsoleAPIStorage);
+    let messages = [];
+    for (const id of windowIds) {
+      messages = messages.concat(ConsoleAPIStorage.getEvents(id) || []);
+    }
+    return messages.map(evt => {
+      const {columnNumber, filename, level, lineNumber, timeStamp} = evt;
+      const args = evt.arguments.map(arg => {
+        return "" + arg;
+      }).join(", ");
+      const message = `[console.${level}(${args}) ${filename}:${lineNumber}:${columnNumber}]`;
+      return {timeStamp, message};
+    });
+  }
+
+  function getScriptErrors(windowIds, includePrivate = false) {
+    const messages = Services.console.getMessageArray() || [];
+    return messages.filter(message => {
+      if (message instanceof Ci.nsIScriptError) {
+        if (!includePrivate && message.isFromPrivateWindow) {
+          return false;
+        }
+
+        if (windowIds && !windowIds.includes(message.innerWindowID)) {
+          return false;
+        }
+
+        return true;
+      }
+
+      // If this is not an nsIScriptError and we need to do window-based
+      // filtering we skip this message.
+      return false;
+    }).map(error => {
+      const {timeStamp, message} = error;
+      return {timeStamp, message};
+    });
+  }
+
+  sendAsyncMessage(messageName, {
+    hasMixedActiveContentBlocked: docShell.hasMixedActiveContentBlocked,
+    hasMixedDisplayContentBlocked: docShell.hasMixedDisplayContentBlocked,
+    hasTrackingContentBlocked: docShell.hasTrackingContentBlocked,
+    log: getLoggedMessages(content, true), // also on private tabs
+  });
+}
+
+this.tabExtras = class extends ExtensionAPI {
+  getAPI(context) {
+    const EventManager = ExtensionCommon.EventManager;
+    const {tabManager} = context.extension;
+    const {Management: {global: {windowTracker}}} =
+                ChromeUtils.import("resource://gre/modules/Extension.jsm", {});
+    return {
+      tabExtras: {
+        onDesktopSiteRequested: new EventManager({
+          context,
+          name: "tabExtras.onDesktopSiteRequested",
+          register: (fire) => {
+            const callback = tab => {
+              fire.async(tab).catch(() => {}); // ignore Message Manager disconnects
+            };
+            const listener = {
+              onEvent: (event, data, _callback) => {
+                if (event === "DesktopMode:Change" && data.desktopMode) {
+                  callback(data.tabId);
+                }
+              },
+            };
+            GlobalEventDispatcher.registerListener(listener, "DesktopMode:Change");
+            return () => {
+              GlobalEventDispatcher.unregisterListener(listener, "DesktopMode:Change");
+            };
+          },
+        }).api(),
+        async createPrivateTab() {
+          const {BrowserApp} = windowTracker.topWindow;
+          const nativeTab = BrowserApp.addTab("about:blank", {selected: true, isPrivate: true});
+          return Promise.resolve(tabManager.convert(nativeTab));
+        },
+        async loadURIWithPostData(tabId, url, postData, postDataContentType) {
+          const tab = tabManager.get(tabId);
+          if (!tab || !tab.browser) {
+            return Promise.reject("Invalid tab");
+          }
+
+          try {
+            new URL(url);
+          } catch (_) {
+            return Promise.reject("Invalid url");
+          }
+
+          if (typeof postData !== "string" && !(postData instanceof String)) {
+            return Promise.reject("postData must be a string");
+          }
+
+          const stringStream = Cc["@mozilla.org/io/string-input-stream;1"]
+                               .createInstance(Ci.nsIStringInputStream);
+          stringStream.data = postData;
+          const post = Cc["@mozilla.org/network/mime-input-stream;1"]
+                       .createInstance(Ci.nsIMIMEInputStream);
+          post.addHeader("Content-Type", postDataContentType ||
+                                         "application/x-www-form-urlencoded");
+          post.setData(stringStream);
+
+          return new Promise(resolve => {
+            const listener = {
+              onLocationChange(browser, webProgress, request, locationURI, flags) {
+                if (webProgress.isTopLevel &&
+                    browser === tab.browser &&
+                    locationURI.spec === url) {
+                  windowTracker.removeListener("progress", listener);
+                  resolve();
+                }
+              },
+            };
+            windowTracker.addListener("progress", listener);
+            tab.browser.webNavigation.loadURIWithOptions(url, null, null, null,
+                                                         post, null, null, null);
+          });
+        },
+        async getWebcompatInfo(tabId) {
+          return new Promise(resolve => {
+            const messageName = "WebExtension:GetWebcompatInfo";
+            const code = `${getInfoFrameScript.toString()};getInfoFrameScript("${messageName}")`;
+            const mm = tabManager.get(tabId).browser.messageManager;
+            mm.loadFrameScript(`data:,${encodeURI(code)}`, false);
+            mm.addMessageListener(messageName, function receiveFn(message) {
+              mm.removeMessageListener(messageName, receiveFn);
+              resolve(message.json);
+            });
+          });
+        },
+      },
+    };
+  }
+};
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/report-site-issue/experimentalAPIs/tabExtras.json
@@ -0,0 +1,57 @@
+[
+  {
+    "namespace": "tabExtras",
+    "description": "experimental tab API extensions",
+    "events": [{
+      "name": "onDesktopSiteRequested",
+      "type": "function",
+      "parameters": [
+        {
+          "name": "tabId",
+          "type": "integer",
+          "description": "The related tab's id"
+        }
+      ]
+    }],
+    "functions": [
+      {
+        "name": "createPrivateTab",
+        "type": "function",
+        "description": "Create and select a new private about:blank tab",
+        "parameters": [],
+        "async": true
+      },
+      {
+        "name": "getWebcompatInfo",
+        "type": "function",
+        "description": "Gets the content blocking status and script log for a given tab",
+        "parameters": [{
+          "type": "integer",
+          "name": "tabId",
+          "minimum": 0
+        }],
+        "async": true
+      },
+      {
+        "name": "loadURIWithPostData",
+        "type": "function",
+        "description": "Loads a URI on the given tab using a POST request",
+        "parameters": [{
+          "type": "integer",
+          "name": "tabId",
+          "minimum": 0
+        }, {
+          "type": "string",
+          "name": "url"
+        }, {
+          "type": "string",
+          "name": "postData"
+        }, {
+          "type": "string",
+          "name": "postDataContentType"
+        }],
+        "async": true
+      }
+    ]
+  }
+]
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/report-site-issue/manifest.json
@@ -0,0 +1,72 @@
+{
+  "manifest_version": 2,
+  "name": "WebCompat Reporter",
+  "description": "Report site compatibility issues on webcompat.com",
+  "author": "Thomas Wisniewski <twisniewski@mozilla.com>",
+  "version": "1.1.0",
+  "homepage_url": "https://github.com/mozilla/webcompat-reporter",
+  "applications": {
+    "gecko": {
+      "id": "webcompat-reporter@mozilla.org"
+    }
+  },
+  "experiment_apis": {
+    "aboutConfigPrefs": {
+      "schema": "experimentalAPIs/aboutConfigPrefs.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "experimentalAPIs/aboutConfigPrefs.js",
+        "paths": [["aboutConfigPrefs"]]
+      }
+    },
+    "browserInfo": {
+      "schema": "experimentalAPIs/browserInfo.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "experimentalAPIs/browserInfo.js",
+        "paths": [["browserInfo"]]
+      }
+    },
+    "l10n": {
+      "schema": "experimentalAPIs/l10n.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "experimentalAPIs/l10n.js",
+        "paths": [["l10n"]]
+      }
+    },
+    "nativeMenu": {
+      "schema": "experimentalAPIs/nativeMenu.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "experimentalAPIs/nativeMenu.js",
+        "paths": [["nativeMenu"]]
+      }
+    },
+    "snackbars": {
+      "schema": "experimentalAPIs/snackbars.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "experimentalAPIs/snackbars.js",
+        "paths": [["snackbars"]]
+      }
+    },
+    "tabExtras": {
+      "schema": "experimentalAPIs/tabExtras.json",
+      "parent": {
+        "scopes": ["addon_parent"],
+        "script": "experimentalAPIs/tabExtras.js",
+        "paths": [["tabExtras"]]
+      }
+    }
+  },
+  "permissions": [
+    "tabs",
+    "<all_urls>"
+  ],
+  "background": {
+    "scripts": [
+      "background.js"
+    ]
+  }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/extensions/report-site-issue/moz.build
@@ -0,0 +1,31 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DEFINES['MOZ_APP_VERSION'] = CONFIG['MOZ_APP_VERSION']
+DEFINES['MOZ_APP_MAXVERSION'] = CONFIG['MOZ_APP_MAXVERSION']
+
+FINAL_TARGET_FILES.features['webcompat-reporter@mozilla.org'] += [
+  'background.js',
+  'manifest.json'
+]
+
+FINAL_TARGET_FILES.features['webcompat-reporter@mozilla.org'].experimentalAPIs += [
+  'experimentalAPIs/aboutConfigPrefs.js',
+  'experimentalAPIs/aboutConfigPrefs.json',
+  'experimentalAPIs/browserInfo.js',
+  'experimentalAPIs/browserInfo.json',
+  'experimentalAPIs/l10n.js',
+  'experimentalAPIs/l10n.json',
+  'experimentalAPIs/nativeMenu.js',
+  'experimentalAPIs/nativeMenu.json',
+  'experimentalAPIs/snackbars.js',
+  'experimentalAPIs/snackbars.json',
+  'experimentalAPIs/tabExtras.js',
+  'experimentalAPIs/tabExtras.json'
+]
+
+with Files('**'):
+    BUG_COMPONENT = ('Web Compatibility Tools', 'General')
--- a/mobile/android/modules/Snackbars.jsm
+++ b/mobile/android/modules/Snackbars.jsm
@@ -39,17 +39,19 @@ var Snackbars = {
 
       if (aOptions.action.label) {
         msg.action.label = aOptions.action.label;
       }
 
       EventDispatcher.instance.sendRequestForResult(msg)
         .then(result => aOptions.action.callback())
         .catch(result => {
-          if (result === null) {
+          if (aOptions.action.rejection) {
+            aOptions.action.rejection(result);
+          } else if (result === null) {
             /* The snackbar was dismissed without executing the callback, nothing to do here. */
           } else {
             Cu.reportError(result);
           }
         });
     } else {
       EventDispatcher.instance.sendRequest(msg);
     }