Bug 1640734 - Frequency capping should apply separately for production and experiment messages r=k88hudson
authorAndrei Oprea <andrei.br92@gmail.com>
Wed, 03 Jun 2020 12:46:19 +0000
changeset 533697 952bcd54fdff7117e941b7d222cbd4817de47a66
parent 533696 d03d90267a78db61c33d1b76d5635099afa7c8c9
child 533698 a2078a4cae22765e5930511ec0b74148a8151668
push id37476
push userccoroiu@mozilla.com
push dateWed, 03 Jun 2020 21:49:22 +0000
treeherdermozilla-central@66d3efe9fc7a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersk88hudson
bugs1640734
milestone79.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 1640734 - Frequency capping should apply separately for production and experiment messages r=k88hudson Differential Revision: https://phabricator.services.mozilla.com/D77010
browser/app/profile/firefox.js
browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx
browser/components/newtab/data/content/activity-stream.bundle.js
browser/components/newtab/lib/ASRouter.jsm
browser/components/newtab/test/browser/browser.ini
browser/components/newtab/test/browser/browser_asrouter_group_frequency.js
browser/components/newtab/test/browser/browser_asrouter_group_userprefs.js
browser/components/newtab/test/unit/asrouter/ASRouter.test.js
testing/profiles/common/user.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1288,17 +1288,17 @@ pref("browser.library.activity-stream.en
 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\":\"remote-settings\",\"bucket\":\"cfr\",\"frequency\":{\"custom\":[{\"period\":\"daily\",\"cap\":1}]},\"categories\":[\"cfrAddons\",\"cfrFeatures\"],\"updateCycleInMs\":3600000}");
 pref("browser.newtabpage.activity-stream.asrouter.providers.whats-new-panel", "{\"id\":\"whats-new-panel\",\"enabled\":true,\"type\":\"remote-settings\",\"bucket\":\"whats-new-panel\",\"updateCycleInMs\":3600000}");
-pref("browser.newtabpage.activity-stream.asrouter.providers.message-groups", "{\"id\":\"message-groups\",\"enabled\":false,\"type\":\"remote-settings\",\"bucket\":\"message-groups\",\"updateCycleInMs\":3600000}");
+pref("browser.newtabpage.activity-stream.asrouter.providers.message-groups", "{\"id\":\"message-groups\",\"enabled\":true,\"type\":\"remote-settings\",\"bucket\":\"message-groups\",\"updateCycleInMs\":3600000}");
 // This url, if changed, MUST continue to point to an https url. Pulling arbitrary content to inject into
 // this page over http opens us up to a man-in-the-middle attack that we'd rather not face. If you are a downstream
 // repackager of this code using an alternate snippet url, please keep your users safe
 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}");
 pref("browser.newtabpage.activity-stream.asrouter.providers.messaging-experiments", "{\"id\":\"messaging-experiments\",\"enabled\":true,\"type\":\"remote-experiments\",\"messageGroups\":[\"cfr\",\"whats-new-panel\",\"moments-page\",\"snippets\",\"cfr-fxa\",\"aboutwelcome\"],\"updateCycleInMs\":3600000}");
 
 // The pref that controls if ASRouter uses the remote fluent files.
 // It's enabled by default, but could be disabled to force ASRouter to use the local files.
--- a/browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx
+++ b/browser/components/newtab/content-src/components/ASRouterAdmin/ASRouterAdmin.jsx
@@ -1615,32 +1615,36 @@ export class ASRouterAdminInner extends 
           <React.Fragment>
             <h2>Message Groups</h2>
             <table>
               <thead>
                 <tr className="message-item">
                   <td>Enabled</td>
                   <td>Impressions count</td>
                   <td>Custom frequency</td>
+                  <td>User preferences</td>
                 </tr>
               </thead>
               {this.state.groups &&
-                this.state.groups.map(({ id, enabled, frequency }, index) => (
-                  <Row key={id}>
-                    <td>
-                      <TogglePrefCheckbox
-                        checked={enabled}
-                        pref={id}
-                        onChange={this.toggleGroups}
-                      />
-                    </td>
-                    <td>{this._getGroupImpressionsCount(id, frequency)}</td>
-                    <td>{JSON.stringify(frequency, null, 2)}</td>
-                  </Row>
-                ))}
+                this.state.groups.map(
+                  ({ id, enabled, frequency, userPreferences = [] }, index) => (
+                    <Row key={id}>
+                      <td>
+                        <TogglePrefCheckbox
+                          checked={enabled}
+                          pref={id}
+                          onChange={this.toggleGroups}
+                        />
+                      </td>
+                      <td>{this._getGroupImpressionsCount(id, frequency)}</td>
+                      <td>{JSON.stringify(frequency, null, 2)}</td>
+                      <td>{userPreferences.join(", ")}</td>
+                    </Row>
+                  )
+                )}
             </table>
           </React.Fragment>
         );
       case "ds":
         return (
           <React.Fragment>
             <h2>Discovery Stream</h2>
             <DiscoveryStreamAdmin
--- a/browser/components/newtab/data/content/activity-stream.bundle.js
+++ b/browser/components/newtab/data/content/activity-stream.bundle.js
@@ -1982,27 +1982,28 @@ class ASRouterAdminInner extends react__
         return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(react__WEBPACK_IMPORTED_MODULE_4___default.a.Fragment, null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("h2", null, "Targeting Utilities"), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("button", {
           className: "button",
           onClick: this.expireCache
         }, "Expire Cache"), " ", "(This expires the cache in ASR Targeting for bookmarks and top sites)", this.renderTargetingParameters(), this.renderAttributionParamers());
 
       case "groups":
         return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(react__WEBPACK_IMPORTED_MODULE_4___default.a.Fragment, null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("h2", null, "Message Groups"), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("table", null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("thead", null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("tr", {
           className: "message-item"
-        }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", null, "Enabled"), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", null, "Impressions count"), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", null, "Custom frequency"))), this.state.groups && this.state.groups.map(({
+        }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", null, "Enabled"), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", null, "Impressions count"), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", null, "Custom frequency"), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", null, "User preferences"))), this.state.groups && this.state.groups.map(({
           id,
           enabled,
-          frequency
+          frequency,
+          userPreferences = []
         }, index) => react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(Row, {
           key: id
         }, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(TogglePrefCheckbox, {
           checked: enabled,
           pref: id,
           onChange: this.toggleGroups
-        })), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", null, this._getGroupImpressionsCount(id, frequency)), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", null, JSON.stringify(frequency, null, 2))))));
+        })), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", null, this._getGroupImpressionsCount(id, frequency)), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", null, JSON.stringify(frequency, null, 2)), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("td", null, userPreferences.join(", "))))));
 
       case "ds":
         return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(react__WEBPACK_IMPORTED_MODULE_4___default.a.Fragment, null, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("h2", null, "Discovery Stream"), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(DiscoveryStreamAdmin, {
           state: {
             DiscoveryStream: this.props.DiscoveryStream,
             Personalization: this.props.Personalization
           },
           otherPrefs: this.props.Prefs.values,
--- a/browser/components/newtab/lib/ASRouter.jsm
+++ b/browser/components/newtab/lib/ASRouter.jsm
@@ -967,27 +967,16 @@ class _ASRouter {
       (await this._storage.get("messageImpressions")) || {};
     const groupImpressions =
       (await this._storage.get("groupImpressions")) || {};
     // Combine the existing providersBlockList into the groupBlockList
     const groupBlockList = (
       (await this._storage.get("groupBlockList")) || []
     ).concat(providerBlockList);
 
-    // Merge any existing provider impressions into the corresponding group
-    // Don't keep providerImpressions in state anymore
-    const providerImpressions =
-      (await this._storage.get("providerImpressions")) || {};
-    for (const provider of Object.keys(providerImpressions)) {
-      groupImpressions[provider] = [
-        ...(groupImpressions[provider] || []),
-        ...providerImpressions[provider],
-      ];
-    }
-
     const previousSessionEnd =
       (await this._storage.get("previousSessionEnd")) || 0;
     await this.setState({
       messageBlockList,
       groupBlockList,
       providerBlockList,
       groupImpressions,
       messageImpressions,
@@ -1524,21 +1513,16 @@ class _ASRouter {
         state.messages,
         "messageImpressions"
       );
       const groupImpressions = this._cleanupImpressionsForItems(
         state,
         state.groups,
         "groupImpressions"
       );
-      this._cleanupImpressionsForItems(
-        state,
-        state.providers,
-        "providerImpressions"
-      );
       return { messageImpressions, groupImpressions };
     });
   }
 
   /** _cleanupImpressionsForItems - Helper for cleanupImpressions - calculate the updated
   /*                                impressions object for the given items, then store it and return it
    *
    * @param {obj} state Reference to ASRouter internal state
@@ -1721,21 +1705,18 @@ class _ASRouter {
     return this.setGroupState({ id, value: true });
   }
 
   async blockProviderById(idOrIds) {
     const idsToBlock = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
 
     await this.setState(state => {
       const providerBlockList = [...state.providerBlockList, ...idsToBlock];
-      // When a provider is blocked, its impressions should be cleared as well
-      const providerImpressions = { ...state.providerImpressions };
-      idsToBlock.forEach(id => delete providerImpressions[id]);
       this._storage.set("providerBlockList", providerBlockList);
-      return { providerBlockList, providerImpressions };
+      return { providerBlockList };
     });
   }
 
   setGroupState({ id, value }) {
     const newGroupState = {
       ...this.state.groups.find(group => group.id === id),
       enabled: value,
     };
--- a/browser/components/newtab/test/browser/browser.ini
+++ b/browser/components/newtab/test/browser/browser.ini
@@ -38,9 +38,11 @@ skip-if = (os == "linux") # Test setup o
 [browser_asrouter_cfr.js]
 [browser_asrouter_bookmarkpanel.js]
 [browser_asrouter_toolbarbadge.js]
 [browser_asrouter_whatsnewpanel.js]
 tags = remote-settings
 [browser_asrouter_momentspagehub.js]
 tags = remote-settings
 [browser_asrouter_experimentsAPILoader.js]
+[browser_asrouter_group_frequency.js]
+[browser_asrouter_group_userprefs.js]
 [browser_asrouter_trigger_docs.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/test/browser/browser_asrouter_group_frequency.js
@@ -0,0 +1,163 @@
+const { ASRouter } = ChromeUtils.import(
+  "resource://activity-stream/lib/ASRouter.jsm"
+);
+const { RemoteSettings } = ChromeUtils.import(
+  "resource://services-settings/remote-settings.js"
+);
+const { CFRMessageProvider } = ChromeUtils.import(
+  "resource://activity-stream/lib/CFRMessageProvider.jsm"
+);
+
+/**
+ * Load and modify a message for the test.
+ */
+add_task(async function setup() {
+  // Force the WNPanel provider cache to 0 by modifying updateCycleInMs
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      [
+        "browser.newtabpage.activity-stream.asrouter.providers.cfr",
+        `{"id":"cfr","enabled":true,"type":"remote-settings","bucket":"cfr","categories":["cfrAddons","cfrFeatures"],"updateCycleInMs":0}`,
+      ],
+    ],
+  });
+
+  const initialMsgCount = ASRouter.state.messages.length;
+
+  const heartbeatMsg = CFRMessageProvider.getMessages().find(
+    m => m.id === "HEARTBEAT_TACTIC_2"
+  );
+  const testMessage = {
+    ...heartbeatMsg,
+    groups: ["messaging-experiments"],
+    targeting: "true",
+    // Ensure no overlap due to frequency capping with other tests
+    id: `HEARTBEAT_MESSAGE_${Date.now()}`,
+  };
+  const client = RemoteSettings("cfr");
+  await client.db.clear();
+  await client.db.create(testMessage);
+  await client.db.saveLastModified(42); // Prevent from loading JSON dump.
+
+  // Reload the providers
+  await BrowserTestUtils.waitForCondition(async () => {
+    await ASRouter._updateMessageProviders();
+    await ASRouter.loadMessagesFromAllProviders();
+    return ASRouter.state.messages.length > initialMsgCount;
+  }, "Should load the extra heartbeat message");
+
+  BrowserTestUtils.waitForCondition(
+    () => ASRouter.state.messages.find(m => m.id === testMessage.id),
+    "Wait to load the message"
+  );
+
+  const msg = ASRouter.state.messages.find(m => m.id === testMessage.id);
+  Assert.equal(msg.targeting, "true");
+  Assert.equal(msg.groups[0], "messaging-experiments");
+
+  registerCleanupFunction(async () => {
+    await client.db.clear();
+    // Reload the providers
+    await BrowserTestUtils.waitForCondition(async () => {
+      await ASRouter._updateMessageProviders();
+      await ASRouter.loadMessagesFromAllProviders();
+      return ASRouter.state.messages.length === initialMsgCount;
+    }, "Should reset messages");
+    await SpecialPowers.popPrefEnv();
+  });
+});
+
+/**
+ * Test group frequency capping.
+ * Message has a lifetime frequency of 3 but it's group has a lifetime frequency
+ * of 2. It should only show up twice.
+ * We update the provider to remove any daily limitations so it should show up
+ * on every new tab load.
+ */
+add_task(async function test_heartbeat_tactic_2() {
+  const TEST_URL = "http://example.com";
+
+  // Force the WNPanel provider cache to 0 by modifying updateCycleInMs
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      [
+        "browser.newtabpage.activity-stream.asrouter.providers.message-groups",
+        `{"id":"message-groups","enabled":true,"type":"remote-settings","bucket":"message-groups","updateCycleInMs":0}`,
+      ],
+    ],
+  });
+  const groupConfiguration = {
+    id: "messaging-experiments",
+    enabled: true,
+    userPreferences: [],
+    frequency: { lifetime: 2 },
+  };
+  const client = RemoteSettings("message-groups");
+  await client.db.clear();
+  await client.db.create(groupConfiguration);
+  await client.db.saveLastModified(42); // Prevent from loading JSON dump.
+
+  // Reload the providers
+  await ASRouter._updateMessageProviders();
+  await ASRouter.loadMessagesFromAllProviders();
+
+  await BrowserTestUtils.waitForCondition(
+    () => ASRouter.state.groups.find(g => g.id === groupConfiguration.id),
+    "Wait for group config to load"
+  );
+  let groupState = ASRouter.state.groups.find(
+    g => g.id === groupConfiguration.id
+  );
+  Assert.ok(groupState, "Group config found");
+  Assert.ok(groupState.enabled, "Group is enabled");
+
+  let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+  await BrowserTestUtils.loadURI(tab1.linkedBrowser, TEST_URL);
+
+  let chiclet = document.getElementById("contextual-feature-recommendation");
+  Assert.ok(chiclet, "CFR chiclet element found (tab1)");
+  await BrowserTestUtils.waitForCondition(
+    () => !chiclet.hidden,
+    "Chiclet should be visible (tab1)"
+  );
+
+  let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+  await BrowserTestUtils.loadURI(tab2.linkedBrowser, TEST_URL);
+
+  Assert.ok(chiclet, "CFR chiclet element found (tab2)");
+  await BrowserTestUtils.waitForCondition(
+    () => !chiclet.hidden,
+    "Chiclet should be visible (tab2)"
+  );
+
+  // Wait for the message to become blocked (frequency limit reached)
+  const msg = ASRouter.state.messages.find(m =>
+    m.groups.includes("messaging-experiments")
+  );
+  Assert.ok(msg, "Message found");
+  await BrowserTestUtils.waitForCondition(
+    () => !ASRouter.isBelowFrequencyCaps(msg),
+    "Message frequency limit should be reached"
+  );
+
+  let tab3 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+  await BrowserTestUtils.loadURI(tab3.linkedBrowser, TEST_URL);
+
+  await BrowserTestUtils.waitForCondition(
+    () => chiclet.hidden,
+    "Heartbeat button should be hidden"
+  );
+
+  info("Cleanup");
+  BrowserTestUtils.removeTab(tab1);
+  BrowserTestUtils.removeTab(tab2);
+  BrowserTestUtils.removeTab(tab3);
+  await client.db.clear();
+  // Reset group impressions
+  await ASRouter.setGroupState({ id: "messaging-experiments", value: true });
+  await ASRouter.setGroupState({ id: "cfr", value: true });
+  // Reload the providers
+  await ASRouter._updateMessageProviders();
+  await ASRouter.loadMessagesFromAllProviders();
+  await SpecialPowers.popPrefEnv();
+});
new file mode 100644
--- /dev/null
+++ b/browser/components/newtab/test/browser/browser_asrouter_group_userprefs.js
@@ -0,0 +1,143 @@
+const { ASRouter } = ChromeUtils.import(
+  "resource://activity-stream/lib/ASRouter.jsm"
+);
+const { RemoteSettings } = ChromeUtils.import(
+  "resource://services-settings/remote-settings.js"
+);
+const { CFRMessageProvider } = ChromeUtils.import(
+  "resource://activity-stream/lib/CFRMessageProvider.jsm"
+);
+
+/**
+ * Load and modify a message for the test.
+ */
+add_task(async function setup() {
+  // Force the WNPanel provider cache to 0 by modifying updateCycleInMs
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      [
+        "browser.newtabpage.activity-stream.asrouter.providers.cfr",
+        `{"id":"cfr","enabled":true,"type":"remote-settings","bucket":"cfr","updateCycleInMs":0}`,
+      ],
+    ],
+  });
+
+  const initialMsgCount = ASRouter.state.messages.length;
+
+  const heartbeatMsg = CFRMessageProvider.getMessages().find(
+    m => m.id === "HEARTBEAT_TACTIC_2"
+  );
+  const testMessage = {
+    ...heartbeatMsg,
+    groups: ["messaging-experiments"],
+    targeting: "true",
+    // Ensure no overlap due to frequency capping with other tests
+    id: `HEARTBEAT_MESSAGE_${Date.now()}`,
+  };
+  const client = RemoteSettings("cfr");
+  await client.db.clear();
+  await client.db.create(testMessage);
+  await client.db.saveLastModified(42); // Prevent from loading JSON dump.
+
+  // Reload the providers
+  await BrowserTestUtils.waitForCondition(async () => {
+    await ASRouter._updateMessageProviders();
+    await ASRouter.loadMessagesFromAllProviders();
+    return ASRouter.state.messages.length > initialMsgCount;
+  }, "Should load the extra heartbeat message");
+
+  BrowserTestUtils.waitForCondition(
+    () => ASRouter.state.messages.find(m => m.id === testMessage.id),
+    "Wait to load the message"
+  );
+
+  const msg = ASRouter.state.messages.find(m => m.id === testMessage.id);
+  Assert.equal(msg.targeting, "true");
+  Assert.equal(msg.groups[0], "messaging-experiments");
+  Assert.ok(ASRouter.isUnblockedMessage(msg), "Message is unblocked");
+
+  registerCleanupFunction(async () => {
+    await client.db.clear();
+    // Reload the providers
+    await BrowserTestUtils.waitForCondition(async () => {
+      await ASRouter._updateMessageProviders();
+      await ASRouter.loadMessagesFromAllProviders();
+      return ASRouter.state.messages.length === initialMsgCount;
+    }, "Should reset messages");
+    await SpecialPowers.popPrefEnv();
+  });
+});
+
+/**
+ * Test group user preferences.
+ * Group is enabled if both user preferences are enabled.
+ */
+add_task(async function test_heartbeat_tactic_2() {
+  const TEST_URL = "http://example.com";
+
+  await SpecialPowers.pushPrefEnv({
+    set: [
+      [
+        "browser.newtabpage.activity-stream.asrouter.providers.message-groups",
+        `{"id":"message-groups","enabled":true,"type":"remote-settings","bucket":"message-groups","updateCycleInMs":0}`,
+      ],
+    ],
+  });
+  const groupConfiguration = {
+    id: "messaging-experiments",
+    enabled: true,
+    userPreferences: ["browser.userPreference.messaging-experiments"],
+  };
+  const client = RemoteSettings("message-groups");
+  await client.db.clear();
+  await client.db.create(groupConfiguration);
+  await client.db.saveLastModified(42); // Prevent from loading JSON dump.
+
+  // Reload the providers
+  await ASRouter._updateMessageProviders();
+  await ASRouter.loadMessagesFromAllProviders();
+
+  await BrowserTestUtils.waitForCondition(
+    () => ASRouter.state.groups.find(g => g.id === groupConfiguration.id),
+    "Wait for group config to load"
+  );
+  let groupState = ASRouter.state.groups.find(
+    g => g.id === groupConfiguration.id
+  );
+  Assert.ok(groupState, "Group config found");
+  Assert.ok(groupState.enabled, "Group is enabled");
+
+  let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+  await BrowserTestUtils.loadURI(tab1.linkedBrowser, TEST_URL);
+
+  let chiclet = document.getElementById("contextual-feature-recommendation");
+  Assert.ok(chiclet, "CFR chiclet element found");
+  await BrowserTestUtils.waitForCondition(
+    () => !chiclet.hidden,
+    "Chiclet should be visible (userprefs enabled)"
+  );
+
+  await SpecialPowers.pushPrefEnv({
+    set: [["browser.userPreference.messaging-experiments", false]],
+  });
+
+  let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_URL);
+  await BrowserTestUtils.loadURI(tab2.linkedBrowser, TEST_URL);
+
+  await BrowserTestUtils.waitForCondition(
+    () => chiclet.hidden,
+    "Heartbeat button should not be visible (userprefs disabled)"
+  );
+
+  info("Cleanup");
+  BrowserTestUtils.removeTab(tab1);
+  BrowserTestUtils.removeTab(tab2);
+  await client.db.clear();
+  // Reset group impressions
+  await ASRouter.setGroupState({ id: "messaging-experiments", value: true });
+  await ASRouter.setGroupState({ id: "cfr", value: true });
+  // Reload the providers
+  await ASRouter._updateMessageProviders();
+  await ASRouter.loadMessagesFromAllProviders();
+  await SpecialPowers.popPrefEnv();
+});
--- a/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
+++ b/browser/components/newtab/test/unit/asrouter/ASRouter.test.js
@@ -54,17 +54,16 @@ function fakeExecuteUserAction(action) {
 describe("ASRouter", () => {
   let Router;
   let globals;
   let channel;
   let sandbox;
   let messageBlockList;
   let providerBlockList;
   let messageImpressions;
-  let providerImpressions;
   let groupImpressions;
   let previousSessionEnd;
   let fetchStub;
   let clock;
   let getStringPrefStub;
   let dispatchStub;
   let fakeAttributionCode;
   let FakeBookmarkPanelHub;
@@ -82,19 +81,16 @@ describe("ASRouter", () => {
     getStub
       .withArgs("providerBlockList")
       .returns(Promise.resolve(providerBlockList));
     getStub
       .withArgs("messageImpressions")
       .returns(Promise.resolve(messageImpressions));
     getStub.withArgs("groupImpressions").resolves(groupImpressions);
     getStub
-      .withArgs("providerImpressions")
-      .returns(Promise.resolve(providerImpressions));
-    getStub
       .withArgs("previousSessionEnd")
       .returns(Promise.resolve(previousSessionEnd));
     return {
       get: getStub,
       set: sandbox.stub().returns(Promise.resolve()),
     };
   }
 
@@ -111,17 +107,16 @@ describe("ASRouter", () => {
     await Router.init(channel, createFakeStorage(), dispatchStub);
   }
 
   beforeEach(async () => {
     globals = new GlobalOverrider();
     messageBlockList = [];
     providerBlockList = [];
     messageImpressions = {};
-    providerImpressions = {};
     groupImpressions = {};
     previousSessionEnd = 100;
     sandbox = sinon.createSandbox();
     personalizedCfrScores = {};
 
     sandbox.spy(ASRouterPreferences, "init");
     sandbox.spy(ASRouterPreferences, "uninit");
     sandbox.spy(ASRouterPreferences, "addListener");
@@ -243,30 +238,16 @@ describe("ASRouter", () => {
     });
     it("should set state.messageBlockList to the block list in persistent storage", async () => {
       messageBlockList = ["foo"];
       Router = new _ASRouter();
       await Router.init(channel, createFakeStorage(), dispatchStub);
 
       assert.deepEqual(Router.state.messageBlockList, ["foo"]);
     });
-    it("should migrate provider impressions to group impressions", async () => {
-      setMessageProviderPref([
-        { id: "onboarding", type: "local", messages: [] },
-      ]);
-      providerImpressions = { onboarding: [1, 2, 3] };
-      Router = new _ASRouter();
-      await Router.init(channel, createFakeStorage(), dispatchStub);
-
-      assert.property(Router.state.groupImpressions, "onboarding");
-      assert.deepEqual(
-        Router.state.groupImpressions.onboarding,
-        providerImpressions.onboarding
-      );
-    });
     it("should initialize all the hub providers", async () => {
       // ASRouter init called in `beforeEach` block above
 
       assert.calledOnce(FakeToolbarBadgeHub.init);
       assert.calledOnce(FakeToolbarPanelHub.init);
       assert.calledOnce(FakeBookmarkPanelHub.init);
       assert.calledOnce(FakeMomentsPageHub.init);
 
@@ -3181,17 +3162,17 @@ describe("ASRouter", () => {
           // Add provider
           const providers = [...state.providers];
           // Add fooMessageImpressions
           // eslint-disable-next-line no-shadow
           const messageImpressions = Object.assign(
             {},
             state.messageImpressions
           );
-          const gImpressions = Object.assign({}, state.providerImpressions);
+          let gImpressions = {};
           gImpressions.bar = barGroupImpressions;
           messageImpressions.foo = fooMessageImpressions;
           return {
             providers,
             messageImpressions,
             groups,
             groupImpressions: gImpressions,
           };
--- a/testing/profiles/common/user.js
+++ b/testing/profiles/common/user.js
@@ -4,16 +4,19 @@ user_pref("app.update.checkInstallTime",
 user_pref("app.update.disabledForTesting", true);
 user_pref("browser.chrome.guess_favicon", false);
 user_pref("browser.dom.window.dump.enabled", true);
 user_pref("devtools.console.stdout.chrome", true);
 // Use a python-eval-able empty JSON array even though asrouter expects plain object
 user_pref("browser.newtabpage.activity-stream.asrouter.providers.cfr", "[]");
 user_pref("browser.newtabpage.activity-stream.asrouter.providers.cfr-fxa", "[]");
 user_pref("browser.newtabpage.activity-stream.asrouter.providers.snippets", "[]");
+user_pref("browser.newtabpage.activity-stream.asrouter.providers.message-groups", "[]");
+user_pref("browser.newtabpage.activity-stream.asrouter.providers.whats-new-panel", "[]");
+user_pref("browser.newtabpage.activity-stream.asrouter.providers.messaging-experiments", "[]");
 user_pref("browser.newtabpage.activity-stream.feeds.section.topstories", false);
 user_pref("browser.newtabpage.activity-stream.feeds.snippets", false);
 user_pref("browser.newtabpage.activity-stream.tippyTop.service.endpoint", "");
 user_pref("browser.newtabpage.activity-stream.discoverystream.config", "[]");
 
 // For Activity Stream firstrun page, use an empty string to avoid fetching.
 user_pref("browser.newtabpage.activity-stream.fxaccounts.endpoint", "");
 // Background thumbnails in particular cause grief, and disabling thumbnails