Bug 1640734 - Frequency capping should apply separately for production and experiment messages r=k88hudson
authorAndrei Oprea <andrei.br92@gmail.com>
Tue, 02 Jun 2020 14:56:57 +0000
changeset 597591 9e7dbd18dcbbd3e0aab361bcc392104133fffda9
parent 597590 c3b6e1ef06b46c0833752d414984957d6dc58f90
child 597592 a50daa9ee7697b5575b4cab128aa2ca035eb3ea7
push id13310
push userffxbld-merge
push dateMon, 29 Jun 2020 14:50:06 +0000
treeherdermozilla-beta@15a59a0afa5c [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
testing/profiles/common/user.js
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1290,17 +1290,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/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