Bug 1528953 - Add pref to opt out of recommended features r=k88hudson
authorAndrei Oprea <andrei.br92@gmail.com>
Tue, 05 Mar 2019 10:55:14 +0000
changeset 462656 c989a479caa5cc5516e8d6abc55fae4ce16c0f2a
parent 462655 34f94fc00f2191bfd709eb87d8cc367a80b5f0f1
child 462657 5d8d62f021f6296a6e5216258997d2d451187008
push id112326
push userccoroiu@mozilla.com
push dateThu, 07 Mar 2019 04:41:19 +0000
treeherdermozilla-inbound@a6f8093bf1a2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersk88hudson
bugs1528953
milestone67.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 1528953 - Add pref to opt out of recommended features r=k88hudson To be reviewed together with https://github.com/mozilla/activity-stream/pull/4819 Differential Revision: https://phabricator.services.mozilla.com/D21408
browser/app/profile/firefox.js
browser/components/enterprisepolicies/Policies.jsm
browser/components/enterprisepolicies/tests/browser/browser_policies_simple_pref_policies.js
browser/components/newtab/lib/ASRouter.jsm
browser/components/newtab/lib/ASRouterPreferences.jsm
browser/components/newtab/lib/ActivityStream.jsm
browser/components/newtab/lib/CFRMessageProvider.jsm
browser/components/newtab/test/unit/asrouter/ASRouter.test.js
browser/components/newtab/test/unit/asrouter/ASRouterPreferences.test.js
browser/components/newtab/test/unit/asrouter/CFRMessageProvider.test.js
browser/components/preferences/in-content/main.js
browser/components/preferences/in-content/main.xul
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1297,17 +1297,17 @@ pref("browser.library.activity-stream.en
 
 // The remote FxA root content URL for the Activity Stream firstrun page.
 pref("browser.newtabpage.activity-stream.fxaccounts.endpoint", "https://accounts.firefox.com/");
 
 // The pref that controls if the search shortcuts experiment is on
 pref("browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts", true);
 
 // ASRouter provider configuration
-pref("browser.newtabpage.activity-stream.asrouter.providers.cfr", "{\"id\":\"cfr\",\"enabled\":true,\"type\":\"local\",\"localProvider\":\"CFRMessageProvider\",\"frequency\":{\"custom\":[{\"period\":\"daily\",\"cap\":1}]}}");
+pref("browser.newtabpage.activity-stream.asrouter.providers.cfr", "{\"id\":\"cfr\",\"enabled\":true,\"type\":\"local\",\"localProvider\":\"CFRMessageProvider\",\"frequency\":{\"custom\":[{\"period\":\"daily\",\"cap\":1}]},\"categories\":[\"cfrAddons\",\"cfrFeatures\"]}");
 pref("browser.newtabpage.activity-stream.asrouter.providers.snippets", "{\"id\":\"snippets\",\"enabled\":true,\"type\":\"remote\",\"url\":\"https://snippets.cdn.mozilla.net/%STARTPAGE_VERSION%/%NAME%/%VERSION%/%APPBUILDID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/\",\"updateCycleInMs\":14400000}");
 
 // The pref controls if search hand-off is enabled for Activity Stream.
 #ifdef NIGHTLY_BUILD
 pref("browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", true);
 #else
 pref("browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar", false);
 #endif
--- a/browser/components/enterprisepolicies/Policies.jsm
+++ b/browser/components/enterprisepolicies/Policies.jsm
@@ -323,17 +323,18 @@ var Policies = {
       }
     },
   },
 
   "DisableFirefoxStudies": {
     onBeforeAddons(manager, param) {
       if (param) {
         manager.disallowFeature("Shield");
-        setAndLockPref("browser.newtabpage.activity-stream.asrouter.userprefs.cfr", false);
+        setAndLockPref("browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons", false);
+        setAndLockPref("browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", false);
       }
     },
   },
 
   "DisableForgetButton": {
     onProfileAfterChange(manager, param) {
       if (param) {
         setAndLockPref("privacy.panicButton.enabled", false);
@@ -678,17 +679,18 @@ var Policies = {
     onBeforeUIStartup(manager, param) {
       if ("Allow" in param) {
         addAllowDenyPermissions("install", param.Allow, null);
       }
       if ("Default" in param) {
         setAndLockPref("xpinstall.enabled", param.Default);
         if (!param.Default) {
           blockAboutPage(manager, "about:debugging");
-          setAndLockPref("browser.newtabpage.activity-stream.asrouter.userprefs.cfr", false);
+          setAndLockPref("browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons", false);
+          setAndLockPref("browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", false);
           manager.disallowFeature("xpinstall");
         }
       }
     },
   },
 
   "NetworkPrediction": {
     onBeforeAddons(manager, param) {
--- a/browser/components/enterprisepolicies/tests/browser/browser_policies_simple_pref_policies.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policies_simple_pref_policies.js
@@ -146,17 +146,18 @@ const POLICIES_TESTS = [
   {
     policies: {
       "InstallAddonsPermission": {
         "Default": false,
       },
     },
     lockedPrefs: {
       "xpinstall.enabled": false,
-      "browser.newtabpage.activity-stream.asrouter.userprefs.cfr": false,
+      "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons": false,
+      "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features": false,
     },
   },
 
   // POLICY: SanitizeOnShutdown
   {
     policies: {
       "SanitizeOnShutdown": true,
     },
@@ -258,17 +259,18 @@ const POLICIES_TESTS = [
   },
 
   // POLICY: DisableShield
   {
     policies: {
       "DisableFirefoxStudies": true,
     },
     lockedPrefs: {
-      "browser.newtabpage.activity-stream.asrouter.userprefs.cfr": false,
+      "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons": false,
+      "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features": false,
     },
   },
 ];
 
 add_task(async function test_policy_remember_passwords() {
   for (let test of POLICIES_TESTS) {
     await setupPolicyEngineWithJson({
       "policies": test.policies,
--- a/browser/components/newtab/lib/ASRouter.jsm
+++ b/browser/components/newtab/lib/ASRouter.jsm
@@ -333,18 +333,23 @@ class _ASRouter {
   _updateMessageProviders() {
     const previousProviders =  this.state.providers;
     const providers = [
       // If we have added a `preview` provider, hold onto it
       ...previousProviders.filter(p => p.id === "preview"),
       // The provider should be enabled and not have a user preference set to false
       ...ASRouterPreferences.providers.filter(p => (
         p.enabled &&
-        ASRouterPreferences.getUserPreference(p.id) !== false)
-      ),
+        (
+          ASRouterPreferences.getUserPreference(p.id) !== false &&
+          // Provider is enabled or if provider has multiple categories
+          // check that at least one category is enabled
+          (!p.categories || p.categories.some(c => ASRouterPreferences.getUserPreference(c) !== false))
+        )
+      )),
     ].map(_provider => {
       // make a copy so we don't modify the source of the pref
       const provider = {..._provider};
 
       if (provider.type === "local" && !provider.messages) {
         // Get the messages from the local message provider
         const localProvider = this._localProviders[provider.localProvider];
         provider.messages = localProvider ? localProvider.getMessages() : [];
@@ -408,17 +413,18 @@ class _ASRouter {
    */
   async loadMessagesFromAllProviders() {
     const needsUpdate = this.state.providers.filter(provider => MessageLoaderUtils.shouldProviderUpdate(provider));
     // Don't do extra work if we don't need any updates
     if (needsUpdate.length) {
       let newState = {messages: [], providers: []};
       for (const provider of this.state.providers) {
         if (needsUpdate.includes(provider)) {
-          const {messages, lastUpdated} = await MessageLoaderUtils.loadMessagesForProvider(provider, this._storage);
+          let {messages, lastUpdated} = await MessageLoaderUtils.loadMessagesForProvider(provider, this._storage);
+          messages = messages.filter(({category}) => !category || ASRouterPreferences.getUserPreference(category));
           newState.providers.push({...provider, lastUpdated});
           newState.messages = [...newState.messages, ...messages];
         } else {
           // Skip updating this provider's messages if no update is required
           let messages = this.state.messages.filter(msg => msg.provider === provider.id);
           newState.providers.push(provider);
           newState.messages = [...newState.messages, ...messages];
         }
--- a/browser/components/newtab/lib/ASRouterPreferences.jsm
+++ b/browser/components/newtab/lib/ASRouterPreferences.jsm
@@ -11,19 +11,25 @@ const DEVTOOLS_PREF = "browser.newtabpag
 const DEFAULT_STATE = {
   _initialized: false,
   _providers: null,
   _providerPrefBranch: PROVIDER_PREF_BRANCH,
   _devtoolsEnabled: null,
   _devtoolsPref: DEVTOOLS_PREF,
 };
 
+const MIGRATE_PREFS = [
+  // Old pref, New pref
+  ["browser.newtabpage.activity-stream.asrouter.userprefs.cfr", "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons"],
+];
+
 const USER_PREFERENCES = {
   snippets: "browser.newtabpage.activity-stream.feeds.snippets",
-  cfr: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr",
+  cfrAddons: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons",
+  cfrFeatures: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features",
 };
 
 const TEST_PROVIDER = {
   id: "snippets_local_testing",
   type: "local",
   localProvider: "SnippetsTestMessageProvider",
   enabled: true,
 };
@@ -45,16 +51,34 @@ class _ASRouterPreferences {
       }
       if (value) {
         filtered.push(value);
       }
       return filtered;
     }, []);
   }
 
+  // XXX Bug 1531734
+  // Required for 67 when the pref change will happen
+  _migratePrefs() {
+    for (let [oldPref, newPref] of MIGRATE_PREFS) {
+      if (!Services.prefs.prefHasUserValue(oldPref)) {
+        continue;
+      }
+      if (Services.prefs.prefHasUserValue(newPref)) {
+        Services.prefs.clearUserPref(oldPref);
+        continue;
+      }
+      // If the pref was user modified we assume it was set to false
+      const oldValue = Services.prefs.getBoolPref(oldPref, false);
+      Services.prefs.clearUserPref(oldPref);
+      Services.prefs.setBoolPref(newPref, oldValue);
+    }
+  }
+
   get providers() {
     if (!this._initialized || this._providers === null) {
       const config = this._getProviderConfig();
       const providers = config.map(provider => Object.freeze(provider));
       if (this.devtoolsEnabled) {
         providers.unshift(TEST_PROVIDER);
       }
       this._providers = Object.freeze(providers);
@@ -139,16 +163,17 @@ class _ASRouterPreferences {
   removeListener(callback) {
     this._callbacks.delete(callback);
   }
 
   init() {
     if (this._initialized) {
       return;
     }
+    this._migratePrefs();
     Services.prefs.addObserver(this._providerPrefBranch, this);
     Services.prefs.addObserver(this._devtoolsPref, this);
     for (const id of Object.keys(USER_PREFERENCES)) {
       Services.prefs.addObserver(USER_PREFERENCES[id], this);
     }
     this._initialized = true;
   }
 
--- a/browser/components/newtab/lib/ActivityStream.jsm
+++ b/browser/components/newtab/lib/ActivityStream.jsm
@@ -201,18 +201,22 @@ const PREFS_CONFIG = new Map([
   ["improvesearch.topSiteSearchShortcuts.havePinned", {
     title: "A comma-delimited list of search shortcuts that have previously been pinned",
     value: "",
   }],
   ["asrouter.devtoolsEnabled", {
     title: "Are the asrouter devtools enabled?",
     value: false,
   }],
-  ["asrouter.userprefs.cfr", {
-    title: "Does the user allow CFR recommendations?",
+  ["asrouter.userprefs.cfr.addons", {
+    title: "Does the user allow CFR addon recommendations?",
+    value: true,
+  }],
+  ["asrouter.userprefs.cfr.features", {
+    title: "Does the user allow CFR feature recommendations?",
     value: true,
   }],
   ["asrouter.providers.onboarding", {
     title: "Configuration for onboarding provider",
     value: JSON.stringify({
       id: "onboarding",
       type: "local",
       localProvider: "OnboardingMessageProvider",
--- a/browser/components/newtab/lib/CFRMessageProvider.jsm
+++ b/browser/components/newtab/lib/CFRMessageProvider.jsm
@@ -38,16 +38,17 @@ const REDDIT_ENHANCEMENT_PARAMS = {
   min_frecency: 10000,
 };
 const PINNED_TABS_TARGET_SITES = ["trello.com", "www.trello.com", "wunderlist.com", "www.wunderlist.com", "docs.google.com", "www.docs.google.com", "calendar.google.com", "www.calendar.google.com", "simplenote.com", "www.simplenote.com", "airtable.com", "www.airtable.com", "todoist.com", "www.todoist.com", "slack.com", "www.slack.com", "irccloud.com", "www.irccloud.com", "products.office.com", "www.products.office.com", "messenger.com", "www.messenger.com", "discordapp.com", "www.discordapp.com", "web.wechat.com", "www.web.wechat.com", "web.whatsapp.com", "www.web.whatsapp.com", "gmail.com", "www.gmail.com", "mail.yahoo.com", "www.mail.yahoo.com", "outlook.com", "www.outlook.com", "polymail.io", "www.polymail.io", "icloud.com", "www.icloud.com", "mail.aol.com", "www.mail.aol.com", "lightroom.adobe.com", "www.lightroom.adobe.com", "facebook.com", "www.facebook.com", "twitter.com", "www.twitter.com", "instagram.com", "www.instagram.com", "pinterest.com", "www.pinterest.com", "reddit.com", "www.reddit.com", "coursera.org", "www.coursera.org", "edx.org", "www.edx.org", "udemy.com", "www.udemy.com", "skillshare.com", "www.skillshare.com", "pluralsight.com", "www.pluralsight.com", "udacity.com", "www.udacity.com", "tumblr.com", "www.tumblr.com", "quora.com", "www.quora.com", "deviantart.com", "www.deviantart.com", "github.com", "www.github.com", "kaggle.com", "www.kaggle.com", "dropbox.com", "www.dropbox.com", "drive.google.com", "www.drive.google.com", "box.com", "www.box.com", "netflix.com", "www.netflix.com", "primevideo.com", "www.primevideo.com", "hulu.com", "www.hulu.com", "crave.ca", "www.crave.ca", "twitch.tv", "www.twitch.tv", "youtube.com", "www.youtube.com", "craigslist.org", "www.craigslist.org", "kijiji.ca", "www.kijiji.ca"];
 
 const CFR_MESSAGES = [
   {
     id: "FACEBOOK_CONTAINER_3",
     template: "cfr_doorhanger",
+    category: "cfrAddons",
     content: {
       bucket_id: "CFR_M1",
       notification_text: {string_id: "cfr-doorhanger-extension-notification"},
       heading_text: {string_id: "cfr-doorhanger-extension-heading"},
       info_icon: {
         label: {string_id: "cfr-doorhanger-extension-sumo-link"},
         sumo_path: FACEBOOK_CONTAINER_PARAMS.sumo_path,
       },
@@ -73,32 +74,33 @@ const CFR_MESSAGES = [
           label: {string_id: "cfr-doorhanger-extension-cancel-button"},
           action: {type: "CANCEL"},
         }, {
           label: {string_id: "cfr-doorhanger-extension-never-show-recommendation"},
         }, {
           label: {string_id: "cfr-doorhanger-extension-manage-settings-button"},
           action: {
             type: "OPEN_PREFERENCES_PAGE",
-            data: {category: "general-cfr", origin: "CFR"},
+            data: {category: "general-cfraddons", origin: "CFR"},
           },
         }],
       },
     },
     frequency: {lifetime: 3},
     targeting: `
       localeLanguageCode == "en" &&
       (xpinstallEnabled == true) &&
       (${JSON.stringify(FACEBOOK_CONTAINER_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
       (${JSON.stringify(FACEBOOK_CONTAINER_PARAMS.open_urls)} intersect topFrecentSites[.frecency >= ${FACEBOOK_CONTAINER_PARAMS.min_frecency}]|mapToProperty('host'))|length > 0`,
     trigger: {id: "openURL", params: FACEBOOK_CONTAINER_PARAMS.open_urls},
   },
   {
     id: "GOOGLE_TRANSLATE_3",
     template: "cfr_doorhanger",
+    category: "cfrAddons",
     content: {
       bucket_id: "CFR_M1",
       notification_text: {string_id: "cfr-doorhanger-extension-notification"},
       heading_text: {string_id: "cfr-doorhanger-extension-heading"},
       info_icon: {
         label: {string_id: "cfr-doorhanger-extension-sumo-link"},
         sumo_path: GOOGLE_TRANSLATE_PARAMS.sumo_path,
       },
@@ -124,32 +126,33 @@ const CFR_MESSAGES = [
           label: {string_id: "cfr-doorhanger-extension-cancel-button"},
           action: {type: "CANCEL"},
         }, {
           label: {string_id: "cfr-doorhanger-extension-never-show-recommendation"},
         }, {
           label: {string_id: "cfr-doorhanger-extension-manage-settings-button"},
           action: {
             type: "OPEN_PREFERENCES_PAGE",
-            data: {category: "general-cfr", origin: "CFR"},
+            data: {category: "general-cfraddons", origin: "CFR"},
           },
         }],
       },
     },
     frequency: {lifetime: 3},
     targeting: `
       localeLanguageCode == "en" &&
       (xpinstallEnabled == true) &&
       (${JSON.stringify(GOOGLE_TRANSLATE_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
       (${JSON.stringify(GOOGLE_TRANSLATE_PARAMS.open_urls)} intersect topFrecentSites[.frecency >= ${GOOGLE_TRANSLATE_PARAMS.min_frecency}]|mapToProperty('host'))|length > 0`,
     trigger: {id: "openURL", params: GOOGLE_TRANSLATE_PARAMS.open_urls},
   },
   {
     id: "YOUTUBE_ENHANCE_3",
     template: "cfr_doorhanger",
+    category: "cfrAddons",
     content: {
       bucket_id: "CFR_M1",
       notification_text: {string_id: "cfr-doorhanger-extension-notification"},
       heading_text: {string_id: "cfr-doorhanger-extension-heading"},
       info_icon: {
         label: {string_id: "cfr-doorhanger-extension-sumo-link"},
         sumo_path: YOUTUBE_ENHANCE_PARAMS.sumo_path,
       },
@@ -175,32 +178,33 @@ const CFR_MESSAGES = [
           label: {string_id: "cfr-doorhanger-extension-cancel-button"},
           action: {type: "CANCEL"},
         }, {
           label: {string_id: "cfr-doorhanger-extension-never-show-recommendation"},
         }, {
           label: {string_id: "cfr-doorhanger-extension-manage-settings-button"},
           action: {
             type: "OPEN_PREFERENCES_PAGE",
-            data: {category: "general-cfr", origin: "CFR"},
+            data: {category: "general-cfraddons", origin: "CFR"},
           },
         }],
       },
     },
     frequency: {lifetime: 3},
     targeting: `
       localeLanguageCode == "en" &&
       (xpinstallEnabled == true) &&
       (${JSON.stringify(YOUTUBE_ENHANCE_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
       (${JSON.stringify(YOUTUBE_ENHANCE_PARAMS.open_urls)} intersect topFrecentSites[.frecency >= ${YOUTUBE_ENHANCE_PARAMS.min_frecency}]|mapToProperty('host'))|length > 0`,
     trigger: {id: "openURL", params: YOUTUBE_ENHANCE_PARAMS.open_urls},
   },
   {
     id: "WIKIPEDIA_CONTEXT_MENU_SEARCH_3",
     template: "cfr_doorhanger",
+    category: "cfrAddons",
     exclude: true,
     content: {
       bucket_id: "CFR_M1",
       notification_text: {string_id: "cfr-doorhanger-extension-notification"},
       heading_text: {string_id: "cfr-doorhanger-extension-heading"},
       info_icon: {
         label: {string_id: "cfr-doorhanger-extension-sumo-link"},
         sumo_path: WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.sumo_path,
@@ -227,32 +231,33 @@ const CFR_MESSAGES = [
           label: {string_id: "cfr-doorhanger-extension-cancel-button"},
           action: {type: "CANCEL"},
         }, {
           label: {string_id: "cfr-doorhanger-extension-never-show-recommendation"},
         }, {
           label: {string_id: "cfr-doorhanger-extension-manage-settings-button"},
           action: {
             type: "OPEN_PREFERENCES_PAGE",
-            data: {category: "general-cfr", origin: "CFR"},
+            data: {category: "general-cfraddons", origin: "CFR"},
           },
         }],
       },
     },
     frequency: {lifetime: 3},
     targeting: `
       localeLanguageCode == "en" &&
       (xpinstallEnabled == true) &&
       (${JSON.stringify(WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
       (${JSON.stringify(WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.open_urls)} intersect topFrecentSites[.frecency >= ${WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.min_frecency}]|mapToProperty('host'))|length > 0`,
     trigger: {id: "openURL", params: WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.open_urls},
   },
   {
     id: "REDDIT_ENHANCEMENT_3",
     template: "cfr_doorhanger",
+    category: "cfrAddons",
     exclude: true,
     content: {
       bucket_id: "CFR_M1",
       notification_text: {string_id: "cfr-doorhanger-extension-notification"},
       heading_text: {string_id: "cfr-doorhanger-extension-heading"},
       info_icon: {
         label: {string_id: "cfr-doorhanger-extension-sumo-link"},
         sumo_path: REDDIT_ENHANCEMENT_PARAMS.sumo_path,
@@ -279,32 +284,33 @@ const CFR_MESSAGES = [
           label: {string_id: "cfr-doorhanger-extension-cancel-button"},
           action: {type: "CANCEL"},
         }, {
           label: {string_id: "cfr-doorhanger-extension-never-show-recommendation"},
         }, {
           label: {string_id: "cfr-doorhanger-extension-manage-settings-button"},
           action: {
             type: "OPEN_PREFERENCES_PAGE",
-            data: {category: "general-cfr", origin: "CFR"},
+            data: {category: "general-cfraddons", origin: "CFR"},
           },
         }],
       },
     },
     frequency: {lifetime: 3},
     targeting: `
       localeLanguageCode == "en" &&
       (xpinstallEnabled == true) &&
       (${JSON.stringify(REDDIT_ENHANCEMENT_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
       (${JSON.stringify(REDDIT_ENHANCEMENT_PARAMS.open_urls)} intersect topFrecentSites[.frecency >= ${REDDIT_ENHANCEMENT_PARAMS.min_frecency}]|mapToProperty('host'))|length > 0`,
     trigger: {id: "openURL", params: REDDIT_ENHANCEMENT_PARAMS.open_urls},
   },
   {
     id: "PIN_TAB",
     template: "cfr_doorhanger",
+    category: "cfrFeatures",
     exclude: true,
     content: {
       bucket_id: "CFR_PIN_TAB",
       notification_text: {string_id: "cfr-doorhanger-extension-notification"},
       heading_text: {string_id: "cfr-doorhanger-pintab-heading"},
       info_icon: {
         label: {string_id: "cfr-doorhanger-extension-sumo-link"},
         sumo_path: REDDIT_ENHANCEMENT_PARAMS.sumo_path,
@@ -321,17 +327,17 @@ const CFR_MESSAGES = [
           label: {string_id: "cfr-doorhanger-extension-cancel-button"},
           action: {type: "CANCEL"},
         }, {
           label: {string_id: "cfr-doorhanger-extension-never-show-recommendation"},
         }, {
           label: {string_id: "cfr-doorhanger-extension-manage-settings-button"},
           action: {
             type: "OPEN_PREFERENCES_PAGE",
-            data: {category: "general-cfr", origin: "CFR"},
+            data: {category: "general-cfrfeatures", origin: "CFR"},
           },
         }],
       },
     },
     targeting: `!hasPinnedTabs && recentVisits[.timestamp > (currentDate|date - 3600 * 1000 * 1)]|length >= 1`,
     trigger: {id: "frequentVisits", params: PINNED_TABS_TARGET_SITES},
   },
 ];
--- a/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
+++ b/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
@@ -377,16 +377,54 @@ describe("ASRouter", () => {
         {id: "foo", enabled: false, type: "remote", url: "https://www.foo.com/"},
         {id: "bar", enabled: true, type: "remote", url: "https://www.bar.com/"},
       ];
       setMessageProviderPref(providers);
       Router._updateMessageProviders();
       assert.equal(Router.state.providers.length, 1);
       assert.equal(Router.state.providers[0].id, providers[1].id);
     });
+    it("should return provider `foo` because both categories are enabled", () => {
+      const providers = [
+        {id: "foo", enabled: true, categories: ["cfrFeatures", "cfrAddons"], type: "remote", url: "https://www.foo.com/"},
+      ];
+      sandbox.stub(ASRouterPreferences, "providers").value(providers);
+      sandbox.stub(ASRouterPreferences, "getUserPreference")
+        .withArgs("cfrFeatures").returns(true)
+        .withArgs("cfrAddons")
+        .returns(true);
+      Router._updateMessageProviders();
+      assert.equal(Router.state.providers.length, 1);
+      assert.equal(Router.state.providers[0].id, providers[0].id);
+    });
+    it("should return provider `foo` because at least 1 category is enabled", () => {
+      const providers = [
+        {id: "foo", enabled: true, categories: ["cfrFeatures", "cfrAddons"], type: "remote", url: "https://www.foo.com/"},
+      ];
+      sandbox.stub(ASRouterPreferences, "providers").value(providers);
+      sandbox.stub(ASRouterPreferences, "getUserPreference")
+        .withArgs("cfrFeatures").returns(false)
+        .withArgs("cfrAddons")
+        .returns(true);
+      Router._updateMessageProviders();
+      assert.equal(Router.state.providers.length, 1);
+      assert.equal(Router.state.providers[0].id, providers[0].id);
+    });
+    it("should not return provider `foo` because no categories are enabled", () => {
+      const providers = [
+        {id: "foo", enabled: true, categories: ["cfrFeatures", "cfrAddons"], type: "remote", url: "https://www.foo.com/"},
+      ];
+      sandbox.stub(ASRouterPreferences, "providers").value(providers);
+      sandbox.stub(ASRouterPreferences, "getUserPreference")
+        .withArgs("cfrFeatures").returns(false)
+        .withArgs("cfrAddons")
+        .returns(false);
+      Router._updateMessageProviders();
+      assert.equal(Router.state.providers.length, 0);
+    });
   });
 
   describe("blocking", () => {
     it("should not return a blocked message", async () => {
       // Block all messages except the first
       await Router.setState(() => ({messageBlockList: ALL_MESSAGE_IDS.slice(1)}));
       const targetStub = {sendAsyncMessage: sandbox.stub()};
 
--- a/browser/components/newtab/test/unit/asrouter/ASRouterPreferences.test.js
+++ b/browser/components/newtab/test/unit/asrouter/ASRouterPreferences.test.js
@@ -1,23 +1,25 @@
 import {_ASRouterPreferences, ASRouterPreferences as ASRouterPreferencesSingleton, TEST_PROVIDER} from "lib/ASRouterPreferences.jsm";
 const FAKE_PROVIDERS = [{id: "foo"}, {id: "bar"}];
 
 const PROVIDER_PREF_BRANCH = "browser.newtabpage.activity-stream.asrouter.providers.";
 const DEVTOOLS_PREF = "browser.newtabpage.activity-stream.asrouter.devtoolsEnabled";
 const SNIPPETS_USER_PREF = "browser.newtabpage.activity-stream.feeds.snippets";
-const CFR_USER_PREF = "browser.newtabpage.activity-stream.asrouter.userprefs.cfr";
+const CFR_USER_PREF_ADDONS = "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons";
+const CFR_USER_PREF_FEATURES = "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features";
 
 /** NUMBER_OF_PREFS_TO_OBSERVE includes:
  *  1. asrouter.providers. pref branch
  *  2. asrouter.devtoolsEnabled
  *  3. browser.newtabpage.activity-stream.feeds.snippets (user preference - snippets)
- *  4. browser.newtabpage.activity-stream.asrouter.userprefs.cfr (user preference - cfr)
+ *  4. browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons (user preference - cfr)
+ *  4. browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features (user preference - cfr)
  */
-const NUMBER_OF_PREFS_TO_OBSERVE = 4;
+const NUMBER_OF_PREFS_TO_OBSERVE = 5;
 
 describe("ASRouterPreferences", () => {
   let ASRouterPreferences;
   let sandbox;
   let addObserverStub;
   let stringPrefStub;
   let boolPrefStub;
 
@@ -182,19 +184,20 @@ describe("ASRouterPreferences", () => {
     it("should return the user preference for snippets", () => {
       boolPrefStub.withArgs(SNIPPETS_USER_PREF).returns(true);
       assert.isTrue(ASRouterPreferences.getUserPreference("snippets"));
     });
   });
   describe("#getAllUserPreferences", () => {
     it("should return all user preferences", () => {
       boolPrefStub.withArgs(SNIPPETS_USER_PREF).returns(true);
-      boolPrefStub.withArgs(CFR_USER_PREF).returns(false);
+      boolPrefStub.withArgs(CFR_USER_PREF_ADDONS).returns(false);
+      boolPrefStub.withArgs(CFR_USER_PREF_FEATURES).returns(true);
       const result = ASRouterPreferences.getAllUserPreferences();
-      assert.deepEqual(result, {snippets: true, cfr: false});
+      assert.deepEqual(result, {snippets: true, cfrAddons: false, cfrFeatures: true});
     });
   });
   describe("#enableOrDisableProvider", () => {
     it("should enable an existing provider if second param is true", () => {
       const setStub = sandbox.stub(global.Services.prefs, "setStringPref");
       setPrefForProvider("foo", {id: "foo", enabled: false});
       assert.isFalse(ASRouterPreferences.providers[0].enabled);
 
@@ -237,17 +240,18 @@ describe("ASRouterPreferences", () => {
   describe("#resetProviderPref", () => {
     it("should reset the pref and user prefs", () => {
       const resetStub = sandbox.stub(global.Services.prefs, "clearUserPref");
       ASRouterPreferences.resetProviderPref();
       FAKE_PROVIDERS.forEach(provider => {
         assert.calledWith(resetStub, getPrefNameForProvider(provider.id));
       });
       assert.calledWith(resetStub, SNIPPETS_USER_PREF);
-      assert.calledWith(resetStub, CFR_USER_PREF);
+      assert.calledWith(resetStub, CFR_USER_PREF_ADDONS);
+      assert.calledWith(resetStub, CFR_USER_PREF_FEATURES);
     });
   });
   describe("observer, listeners", () => {
     it("should invalidate .providers when the pref is changed", () => {
       const testProvider = {id: "newstuff"};
       const newProviders = [...FAKE_PROVIDERS, testProvider];
 
       ASRouterPreferences.init();
@@ -298,9 +302,45 @@ describe("ASRouterPreferences", () => {
 
       callback.reset();
       ASRouterPreferences.removeListener(callback);
 
       ASRouterPreferences.observe(null, null, DEVTOOLS_PREF);
       assert.notCalled(callback);
     });
   });
+  describe("_migratePrefs", () => {
+    beforeEach(() => {
+      sandbox.stub(global.Services.prefs, "setBoolPref");
+      sandbox.stub(global.Services.prefs, "clearUserPref");
+    });
+    it("should not do anything if userpref was not modified", () => {
+      ASRouterPreferences.init();
+
+      assert.notCalled(global.Services.prefs.getBoolPref);
+      assert.notCalled(global.Services.prefs.setBoolPref);
+    });
+    it("should not do migration if newPref was modified", () => {
+      sandbox.stub(global.Services.prefs, "prefHasUserValue").returns(true);
+
+      ASRouterPreferences.init();
+
+      assert.notCalled(global.Services.prefs.getBoolPref);
+      assert.notCalled(global.Services.prefs.setBoolPref);
+      assert.calledOnce(global.Services.prefs.clearUserPref);
+      assert.calledWith(global.Services.prefs.clearUserPref, "browser.newtabpage.activity-stream.asrouter.userprefs.cfr");
+    });
+    it("should migrate userprefs.cfr", () => {
+      const hasUserValueStub = sandbox.stub(global.Services.prefs, "prefHasUserValue");
+      hasUserValueStub.onCall(0).returns(true);
+      hasUserValueStub.returns(false);
+
+      ASRouterPreferences.init();
+
+      assert.calledOnce(global.Services.prefs.getBoolPref);
+      assert.calledWith(global.Services.prefs.getBoolPref, "browser.newtabpage.activity-stream.asrouter.userprefs.cfr");
+      assert.calledOnce(global.Services.prefs.setBoolPref);
+      assert.calledWith(global.Services.prefs.setBoolPref, CFR_USER_PREF_ADDONS, false);
+      assert.calledOnce(global.Services.prefs.clearUserPref);
+      assert.calledWith(global.Services.prefs.clearUserPref, "browser.newtabpage.activity-stream.asrouter.userprefs.cfr");
+    });
+  });
 });
--- a/browser/components/newtab/test/unit/asrouter/CFRMessageProvider.test.js
+++ b/browser/components/newtab/test/unit/asrouter/CFRMessageProvider.test.js
@@ -6,17 +6,17 @@ const REGULAR_IDS = [
   "GOOGLE_TRANSLATE",
   "YOUTUBE_ENHANCE",
   // These are excluded for now.
   // "WIKIPEDIA_CONTEXT_MENU_SEARCH",
   // "REDDIT_ENHANCEMENT",
 ];
 
 describe("CFRMessageProvider", () => {
-  it("should have a total of 4 messages", () => {
+  it("should have a total of 3 messages", () => {
     assert.lengthOf(messages, 3);
   });
   it("should have one message each for the three regular addons", () => {
     for (const id of REGULAR_IDS) {
       const cohort3 = messages.find(msg => msg.id === `${id}_3`);
       assert.ok(cohort3, `contains three day cohort for ${id}`);
       assert.deepEqual(cohort3.frequency, {lifetime: 3}, "three day cohort has the right frequency cap");
       assert.notInclude(cohort3.targeting, `providerCohorts.cfr`);
--- a/browser/components/preferences/in-content/main.js
+++ b/browser/components/preferences/in-content/main.js
@@ -105,17 +105,18 @@ Preferences.addAll([
   { id: "browser.link.open_newwindow", type: "int" },
   { id: "browser.tabs.loadInBackground", type: "bool", inverted: true },
   { id: "browser.tabs.warnOnClose", type: "bool" },
   { id: "browser.tabs.warnOnOpen", type: "bool" },
   { id: "browser.sessionstore.restore_on_demand", type: "bool" },
   { id: "browser.ctrlTab.recentlyUsedOrder", type: "bool" },
 
   // CFR
-  {id: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr", type: "bool"},
+  {id: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons", type: "bool"},
+  {id: "browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features", type: "bool"},
 
   // Fonts
   { id: "font.language.group", type: "wstring" },
 
   // Languages
   { id: "browser.translation.detectLanguage", type: "bool" },
 
   // General tab
@@ -314,19 +315,21 @@ var gMainPane = {
     connectionSettingsLink.setAttribute("href", connectionSettingsUrl);
     this.updateProxySettingsUI();
     initializeProxyUI(gMainPane);
 
     if (Services.prefs.getBoolPref("intl.multilingual.enabled")) {
       gMainPane.initBrowserLocale();
     }
 
-    let cfrLearnMoreLink = document.getElementById("cfrLearnMore");
     let cfrLearnMoreUrl = Services.urlFormatter.formatURLPref("app.support.baseURL") + "extensionrecommendations";
-    cfrLearnMoreLink.setAttribute("href", cfrLearnMoreUrl);
+    for (const id of ["cfrLearnMore", "cfrFeaturesLearnMore"]) {
+      let link = document.getElementById(id);
+      link.setAttribute("href", cfrLearnMoreUrl);
+    }
 
     if (AppConstants.platform == "win") {
       // Functionality for "Show tabs in taskbar" on Windows 7 and up.
       try {
         let ver = parseFloat(Services.sysinfo.getProperty("version"));
         let showTabsInTaskbar = document.getElementById("showTabsInTaskbar");
         showTabsInTaskbar.hidden = ver < 6.1;
       } catch (ex) { }
--- a/browser/components/preferences/in-content/main.xul
+++ b/browser/components/preferences/in-content/main.xul
@@ -630,23 +630,30 @@
             preference="ui.osk.enabled"/>
 #endif
   <checkbox id="useCursorNavigation"
             data-l10n-id="browsing-use-cursor-navigation"
             preference="accessibility.browsewithcaret"/>
   <checkbox id="searchStartTyping"
             data-l10n-id="browsing-search-on-start-typing"
             preference="accessibility.typeaheadfind"/>
-  <hbox align="center" data-subcategory="cfr">
+  <hbox align="center" data-subcategory="cfraddons">
     <checkbox id="cfrRecommendations"
             class="tail-with-learn-more"
             data-l10n-id="browsing-cfr-recommendations"
-            preference="browser.newtabpage.activity-stream.asrouter.userprefs.cfr"/>
+            preference="browser.newtabpage.activity-stream.asrouter.userprefs.cfr.addons"/>
     <label id="cfrLearnMore" class="learnMore" data-l10n-id="browsing-cfr-recommendations-learn-more" is="text-link"/>
   </hbox>
+  <hbox align="center" data-subcategory="cfrfeatures">
+    <checkbox id="cfrRecommendations-features"
+            class="tail-with-learn-more"
+            data-l10n-id="browsing-cfr-features"
+            preference="browser.newtabpage.activity-stream.asrouter.userprefs.cfr.features"/>
+    <label id="cfrFeaturesLearnMore" class="learnMore" data-l10n-id="browsing-cfr-recommendations-learn-more" is="text-link"/>
+  </hbox>
 </groupbox>
 
 <hbox id="networkProxyCategory"
       class="subcategory"
       hidden="true"
       data-category="paneGeneral">
   <html:h1 data-l10n-id="network-settings-title"/>
 </hbox>