Bug 1260877 - Display a Show content messages checkbox in Browser Console. r=bgrins.
authorNicolas Chevobbe <nchevobbe@mozilla.com>
Tue, 16 Apr 2019 09:09:01 +0000
changeset 469650 8186ed6d36ff
parent 469649 b5164ff07d12
child 469651 0fab2cc0f44d
push id35878
push userapavel@mozilla.com
push dateTue, 16 Apr 2019 15:43:40 +0000
treeherdermozilla-central@258af4e91151 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbgrins
bugs1260877
milestone68.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 1260877 - Display a Show content messages checkbox in Browser Console. r=bgrins. A preference is added to enable this feature, another one to store the last value of the checkbox. When unchecked, the console hides all the messages that don't originate from a chrome contect, via the `chromeContext` property on the message. Since we already have a test to check that content messages are displayed in the console output, we use it to assert the effects of the "Show content messages" checkbox. Differential Revision: https://phabricator.services.mozilla.com/D26337
devtools/client/locales/en-US/webconsole.properties
devtools/client/preferences/devtools-client.js
devtools/client/webconsole/actions/ui.js
devtools/client/webconsole/components/App.js
devtools/client/webconsole/components/FilterBar.js
devtools/client/webconsole/constants.js
devtools/client/webconsole/reducers/messages.js
devtools/client/webconsole/reducers/prefs.js
devtools/client/webconsole/reducers/ui.js
devtools/client/webconsole/store.js
devtools/client/webconsole/test/mocha-test-setup.js
devtools/client/webconsole/test/mochitest/browser_console_webconsole_console_api_calls.js
devtools/client/webconsole/webconsole-wrapper.js
--- a/devtools/client/locales/en-US/webconsole.properties
+++ b/devtools/client/locales/en-US/webconsole.properties
@@ -282,16 +282,25 @@ webconsole.filteredMessages.label=#1 ite
 # It resets the default filters of the console to their original values.
 webconsole.resetFiltersButton.label=Reset filters
 
 # LOCALIZATION NOTE (webconsole.enablePersistentLogs.label)
 webconsole.enablePersistentLogs.label=Persist Logs
 # LOCALIZATION NOTE (webconsole.enablePersistentLogs.tooltip)
 webconsole.enablePersistentLogs.tooltip=If you enable this option the output will not be cleared each time you navigate to a new page
 
+# LOCALIZATION NOTE (browserconsole.contentMessagesCheckbox.label)
+# Label used in the browser console filter bar. This label is used for a checkbox that
+# allows the user to show or hide console messages from the content process in the browser
+# console.
+browserconsole.contentMessagesCheckbox.label=Show Content Messages
+# LOCALIZATION NOTE (browserconsole.contentMessagesCheckbox.tooltip)
+# Tooltip for the "Show content messages" checkbox in the Browser Console filter bar.
+browserconsole.contentMessagesCheckbox.tooltip=Enable this to display messages from the content process in the output
+
 # LOCALIZATION NOTE (webconsole.navigated): this string is used in the console when the
 # current inspected page is navigated to a new location.
 # Parameters: %S is the new URL.
 webconsole.navigated=Navigated to %S
 
 # LOCALIZATION NOTE (webconsole.closeSplitConsoleButton.tooltip): This is the tooltip for
 # the close button of the split console.
 webconsole.closeSplitConsoleButton.tooltip=Close Split Console (Esc)
--- a/devtools/client/preferences/devtools-client.js
+++ b/devtools/client/preferences/devtools-client.js
@@ -281,16 +281,21 @@ pref("devtools.webconsole.jsterm.codeMir
 pref("devtools.webconsole.input.editor", false);
 
 // Disable the new performance recording panel by default
 pref("devtools.performance.new-panel-enabled", false);
 
 // Enable message grouping in the console, false by default
 pref("devtools.webconsole.groupWarningMessages", false);
 
+// Enable Content messages filtering in the browser console.
+pref("devtools.browserconsole.filterContentMessages", false);
+// Saved state of the Display content messages checkbox in the browser console.
+pref("devtools.browserconsole.contentMessages", false);
+
 // Enable client-side mapping service for source maps
 pref("devtools.source-map.client-service.enabled", true);
 
 // The number of lines that are displayed in the web console.
 pref("devtools.hud.loglimit", 10000);
 
 // The developer tools editor configuration:
 // - tabsize: how many spaces to use when a Tab character is displayed.
--- a/devtools/client/webconsole/actions/ui.js
+++ b/devtools/client/webconsole/actions/ui.js
@@ -10,32 +10,43 @@ const { getAllUi } = require("devtools/c
 const { getMessage } = require("devtools/client/webconsole/selectors/messages");
 
 const {
   INITIALIZE,
   PERSIST_TOGGLE,
   PREFS,
   REVERSE_SEARCH_INPUT_TOGGLE,
   SELECT_NETWORK_MESSAGE_TAB,
+  SHOW_CONTENT_MESSAGES_TOGGLE,
   SHOW_OBJECT_IN_SIDEBAR,
   SIDEBAR_CLOSE,
   SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE,
   TIMESTAMPS_TOGGLE,
 } = require("devtools/client/webconsole/constants");
 
 function persistToggle() {
   return ({dispatch, getState, prefsService}) => {
     dispatch({
       type: PERSIST_TOGGLE,
     });
     const uiState = getAllUi(getState());
     prefsService.setBoolPref(PREFS.UI.PERSIST, uiState.persistLogs);
   };
 }
 
+function contentMessagesToggle() {
+  return ({dispatch, getState, prefsService}) => {
+    dispatch({
+      type: SHOW_CONTENT_MESSAGES_TOGGLE,
+    });
+    const uiState = getAllUi(getState());
+    prefsService.setBoolPref(PREFS.UI.CONTENT_MESSAGES, uiState.showContentMessages);
+  };
+}
+
 function timestampsToggle(visible) {
   return {
     type: TIMESTAMPS_TOGGLE,
     visible,
   };
 }
 
 function selectNetworkMessageTab(id) {
@@ -95,16 +106,17 @@ function showObjectInSidebar(grip) {
 function reverseSearchInputToggle({initialValue} = {}) {
   return {
     type: REVERSE_SEARCH_INPUT_TOGGLE,
     initialValue,
   };
 }
 
 module.exports = {
+  contentMessagesToggle,
   initialize,
   persistToggle,
   reverseSearchInputToggle,
   selectNetworkMessageTab,
   showMessageObjectInSidebar,
   showObjectInSidebar,
   sidebarClose,
   splitConsoleCloseButtonToggle,
--- a/devtools/client/webconsole/components/App.js
+++ b/devtools/client/webconsole/components/App.js
@@ -47,16 +47,17 @@ class App extends Component {
       serviceContainer: PropTypes.object.isRequired,
       closeSplitConsole: PropTypes.func.isRequired,
       jstermCodeMirror: PropTypes.bool,
       autocomplete: PropTypes.bool,
       currentReverseSearchEntry: PropTypes.string,
       reverseSearchInputVisible: PropTypes.bool,
       reverseSearchInitialValue: PropTypes.string,
       editorMode: PropTypes.bool,
+      hideShowContentMessagesCheckbox: PropTypes.bool,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.onClick = this.onClick.bind(this);
     this.onPaste = this.onPaste.bind(this);
@@ -199,16 +200,17 @@ class App extends Component {
       notifications,
       onFirstMeaningfulPaint,
       serviceContainer,
       closeSplitConsole,
       jstermCodeMirror,
       autocomplete,
       reverseSearchInitialValue,
       editorMode,
+      hideShowContentMessagesCheckbox,
     } = this.props;
 
     const classNames = ["webconsole-app"];
     if (jstermCodeMirror) {
       classNames.push("jsterm-cm");
     }
     if (editorMode) {
       classNames.push("jsterm-editor");
@@ -228,16 +230,17 @@ class App extends Component {
         onKeyDown: this.onKeyDown,
         onClick: this.onClick,
         ref: node => {
           this.node = node;
         }},
         div({className: "webconsole-flex-wrapper"},
           FilterBar({
             hidePersistLogsCheckbox: webConsoleUI.isBrowserConsole,
+            hideShowContentMessagesCheckbox,
             attachRefToWebConsoleUI,
             closeSplitConsole,
           }),
           ConsoleOutput({
             serviceContainer,
             onFirstMeaningfulPaint,
           }),
           NotificationBox({
--- a/devtools/client/webconsole/components/FilterBar.js
+++ b/devtools/client/webconsole/components/FilterBar.js
@@ -25,35 +25,39 @@ loader.lazyRequireGetter(this, "PropType
 class FilterBar extends Component {
   static get propTypes() {
     return {
       dispatch: PropTypes.func.isRequired,
       filter: PropTypes.object.isRequired,
       attachRefToWebConsoleUI: PropTypes.func.isRequired,
       persistLogs: PropTypes.bool.isRequired,
       hidePersistLogsCheckbox: PropTypes.bool.isRequired,
+      showContentMessages: PropTypes.bool.isRequired,
+      hideShowContentMessagesCheckbox: PropTypes.bool.isRequired,
       filteredMessagesCount: PropTypes.object.isRequired,
       closeButtonVisible: PropTypes.bool,
       closeSplitConsole: PropTypes.func,
     };
   }
 
   static get defaultProps() {
     return {
       hidePersistLogsCheckbox: false,
+      hideShowContentMessagesCheckbox: true,
     };
   }
 
   constructor(props) {
     super(props);
     this.onClickMessagesClear = this.onClickMessagesClear.bind(this);
     this.onClickRemoveAllFilters = this.onClickRemoveAllFilters.bind(this);
     this.onClickRemoveTextFilter = this.onClickRemoveTextFilter.bind(this);
     this.onSearchInput = this.onSearchInput.bind(this);
     this.onChangePersistToggle = this.onChangePersistToggle.bind(this);
+    this.onChangeShowContent = this.onChangeShowContent.bind(this);
     this.renderFiltersConfigBar = this.renderFiltersConfigBar.bind(this);
     this.renderFilteredMessagesBar = this.renderFilteredMessagesBar.bind(this);
   }
 
   componentDidMount() {
     this.props.attachRefToWebConsoleUI(
       "filterBox",
       this.wrapperNode.querySelector(".text-filter")
@@ -63,28 +67,33 @@ class FilterBar extends Component {
       this.wrapperNode.querySelector(".clear-button")
     );
   }
 
   shouldComponentUpdate(nextProps, nextState) {
     const {
       filter,
       persistLogs,
+      showContentMessages,
       filteredMessagesCount,
       closeButtonVisible,
     } = this.props;
 
     if (nextProps.filter !== filter) {
       return true;
     }
 
     if (nextProps.persistLogs !== persistLogs) {
       return true;
     }
 
+    if (nextProps.showContentMessages !== showContentMessages) {
+      return true;
+    }
+
     if (
       JSON.stringify(nextProps.filteredMessagesCount) !==
         JSON.stringify(filteredMessagesCount)
     ) {
       return true;
     }
 
     if (nextProps.closeButtonVisible != closeButtonVisible) {
@@ -109,16 +118,20 @@ class FilterBar extends Component {
   onSearchInput(e) {
     this.props.dispatch(actions.filterTextSet(e.target.value));
   }
 
   onChangePersistToggle() {
     this.props.dispatch(actions.persistToggle());
   }
 
+  onChangeShowContent() {
+    this.props.dispatch(actions.contentMessagesToggle());
+  }
+
   renderFiltersConfigBar() {
     const {
       dispatch,
       filter,
       filteredMessagesCount,
     } = this.props;
 
     const getLabel = (baseLabel, filterKey) => {
@@ -228,17 +241,19 @@ class FilterBar extends Component {
   }
 
   render() {
     const {
       filter,
       persistLogs,
       filteredMessagesCount,
       hidePersistLogsCheckbox,
+      hideShowContentMessagesCheckbox,
       closeSplitConsole,
+      showContentMessages,
     } = this.props;
 
     const children = [
       dom.div({
         className: "devtools-toolbar webconsole-filterbar-primary",
         key: "primary-bar",
       },
         dom.button({
@@ -265,16 +280,22 @@ class FilterBar extends Component {
           }),
         ),
         !hidePersistLogsCheckbox && FilterCheckbox({
           label: l10n.getStr("webconsole.enablePersistentLogs.label"),
           title: l10n.getStr("webconsole.enablePersistentLogs.tooltip"),
           onChange: this.onChangePersistToggle,
           checked: persistLogs,
         }),
+        !hideShowContentMessagesCheckbox && FilterCheckbox({
+          label: l10n.getStr("browserconsole.contentMessagesCheckbox.label"),
+          title: l10n.getStr("browserconsole.contentMessagesCheckbox.tooltip"),
+          onChange: this.onChangeShowContent,
+          checked: showContentMessages,
+        }),
       ),
     ];
 
     if (filteredMessagesCount.global > 0) {
       children.push(this.renderFilteredMessagesBar());
     }
 
     if (this.props.closeButtonVisible) {
@@ -308,14 +329,15 @@ class FilterBar extends Component {
   }
 }
 
 function mapStateToProps(state) {
   const uiState = getAllUi(state);
   return {
     filter: getAllFilters(state),
     persistLogs: uiState.persistLogs,
+    showContentMessages: uiState.showContentMessages,
     filteredMessagesCount: getFilteredMessagesCount(state),
     closeButtonVisible: uiState.closeButtonVisible,
   };
 }
 
 module.exports = connect(mapStateToProps)(FilterBar);
--- a/devtools/client/webconsole/constants.js
+++ b/devtools/client/webconsole/constants.js
@@ -30,16 +30,17 @@ const actionTypes = {
   NETWORK_UPDATE_REQUEST: "NETWORK_UPDATE_REQUEST",
   PERSIST_TOGGLE: "PERSIST_TOGGLE",
   PRIVATE_MESSAGES_CLEAR: "PRIVATE_MESSAGES_CLEAR",
   REMOVE_NOTIFICATION: "REMOVE_NOTIFICATION",
   REMOVED_ACTORS_CLEAR: "REMOVED_ACTORS_CLEAR",
   REVERSE_SEARCH_INPUT_TOGGLE: "REVERSE_SEARCH_INPUT_TOGGLE",
   SELECT_NETWORK_MESSAGE_TAB: "SELECT_NETWORK_MESSAGE_TAB",
   SHOW_OBJECT_IN_SIDEBAR: "SHOW_OBJECT_IN_SIDEBAR",
+  SHOW_CONTENT_MESSAGES_TOGGLE: "SHOW_CONTENT_MESSAGES_TOGGLE",
   SIDEBAR_CLOSE: "SIDEBAR_CLOSE",
   SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE: "SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE",
   TIMESTAMPS_TOGGLE: "TIMESTAMPS_TOGGLE",
   UPDATE_HISTORY_POSITION: "UPDATE_HISTORY_POSITION",
   REVERSE_SEARCH_INPUT_CHANGE: "REVERSE_SEARCH_INPUT_CHANGE",
   REVERSE_SEARCH_NEXT: "REVERSE_SEARCH_NEXT",
   REVERSE_SEARCH_BACK: "REVERSE_SEARCH_BACK",
   PAUSED_EXCECUTION_POINT: "PAUSED_EXCECUTION_POINT",
@@ -62,23 +63,26 @@ const prefs = {
     },
     UI: {
       // Persist is only used by the webconsole.
       PERSIST: "devtools.webconsole.persistlog",
       // Max number of entries in history list.
       INPUT_HISTORY_COUNT: "devtools.webconsole.inputHistoryCount",
       // Is editor mode enabled.
       EDITOR: "devtools.webconsole.input.editor",
+      // Display content messages in the browser console
+      CONTENT_MESSAGES: "devtools.browserconsole.contentMessages",
     },
     FEATURES: {
       // We use the same pref to enable the sidebar on webconsole and browser console.
       SIDEBAR_TOGGLE: "devtools.webconsole.sidebarToggle",
       JSTERM_CODE_MIRROR: "devtools.webconsole.jsterm.codeMirror",
       AUTOCOMPLETE: "devtools.webconsole.input.autocomplete",
       GROUP_WARNINGS: "devtools.webconsole.groupWarningMessages",
+      FILTER_CONTENT_MESSAGES: "devtools.browserconsole.filterContentMessages",
     },
   },
 };
 
 const FILTERS = {
   CSS: "css",
   DEBUG: "debug",
   ERROR: "error",
--- a/devtools/client/webconsole/reducers/messages.js
+++ b/devtools/client/webconsole/reducers/messages.js
@@ -91,19 +91,20 @@ function cloneState(state) {
 
 /**
  * Add a console message to the state.
  *
  * @param {ConsoleMessage} newMessage: The message to add to the state.
  * @param {MessageState} state: The message state ( = managed by this reducer).
  * @param {FiltersState} filtersState: The filters state.
  * @param {PrefsState} prefsState: The preferences state.
+ * @param {UiState} uiState: The ui state.
  * @returns {MessageState} a new messages state.
  */
-function addMessage(newMessage, state, filtersState, prefsState) {
+function addMessage(newMessage, state, filtersState, prefsState, uiState) {
   const {
     messagesById,
     replayProgressMessages,
     groupsById,
     currentGroup,
     repeatById,
   } = state;
 
@@ -168,29 +169,30 @@ function addMessage(newMessage, state, f
   if (prefsState.groupWarnings && warningGroupType !== null) {
     const warningGroupMessageId = getParentWarningGroupMessageId(newMessage);
 
     // If there's no warning group for the type/innerWindowID yet
     if (!state.messagesById.has(warningGroupMessageId)) {
       // We create it and add it to the store.
       const groupMessage = createWarningGroupMessage(
         warningGroupMessageId, warningGroupType, newMessage);
-      state = addMessage(groupMessage, state, filtersState, prefsState);
+      state = addMessage(groupMessage, state, filtersState, prefsState, uiState);
       state.warningGroupsById.set(warningGroupMessageId, []);
     }
 
     // We add the new message to the appropriate warningGroup.
     state.warningGroupsById.get(warningGroupMessageId).push(newMessage.id);
 
     // If the warningGroup message is not visible yet, but should be.
     if (!state.visibleMessages.includes(warningGroupMessageId)
       && getMessageVisibility(state.messagesById.get(warningGroupMessageId), {
         messagesState: state,
         filtersState,
         prefsState,
+        uiState,
       }).visible
     ) {
       // Then we put it in the visibleMessages properties, at the position of the first
       // warning message inside the warningGroup.
       // If that first warning message is in a console.group, we place it before the
       // outermost console.group message.
       const firstWarningMessageId = state.warningGroupsById.get(warningGroupMessageId)[0];
       const firstWarningMessage = state.messagesById.get(firstWarningMessageId);
@@ -229,16 +231,17 @@ function addMessage(newMessage, state, f
       state.messagesUiById.push(newMessage.id);
     }
   }
 
   const { visible, cause } = getMessageVisibility(addedMessage, {
     messagesState: state,
     filtersState,
     prefsState,
+    uiState,
   });
 
   if (visible) {
     // If the message is part of a visible warning group, we want to add it after the last
     // visible message of the group.
     const warningGroupId = getParentWarningGroupMessageId(newMessage);
     if (warningGroupId && state.visibleMessages.includes(warningGroupId)) {
       // Defaults to the warning group message.
@@ -269,17 +272,17 @@ function addMessage(newMessage, state, f
   // that is responsible for collecting (lazy loaded) HTTP payload data.
   if (newMessage.source == "network") {
     state.networkMessagesUpdateById[newMessage.actor] = newMessage;
   }
 
   return state;
 }
 
-function messages(state = MessageState(), action, filtersState, prefsState) {
+function messages(state = MessageState(), action, filtersState, prefsState, uiState) {
   const {
     messagesById,
     messagesUiById,
     messagesTableDataById,
     networkMessagesUpdateById,
     groupsById,
     visibleMessages,
   } = state;
@@ -314,17 +317,17 @@ function messages(state = MessageState()
         } else {
           list.unshift(message);
         }
         lastMessageRepeatId = message.repeatId;
       }
 
       newState = cloneState(state);
       list.forEach(message => {
-        newState = addMessage(message, newState, filtersState, prefsState);
+        newState = addMessage(message, newState, filtersState, prefsState, uiState);
       });
 
       return limitTopLevelMessageCount(newState, logLimit);
 
     case constants.MESSAGES_CLEAR:
       return MessageState({
         // Store all actors from removed messages. This array is used by
         // `releaseActorsEnhancer` to release all of those backend actors.
@@ -387,16 +390,17 @@ function messages(state = MessageState()
                 isGroupType(currMessage.type)
                 && getParentGroups(message.groupId, groupsById).includes(action.id)
               )
             )
             && getMessageVisibility(message, {
               messagesState: openState,
               filtersState,
               prefsState,
+              uiState,
             // We want to check if the message is in an open group
             // only if it is not a direct child of the group we're opening.
               checkGroup: message.groupId !== action.id,
             }).visible
           ) {
             res.push(id);
           }
           return res;
@@ -495,24 +499,26 @@ function messages(state = MessageState()
         ...state,
         removedActors: [],
       };
 
     case constants.FILTER_TOGGLE:
     case constants.FILTER_TEXT_SET:
     case constants.FILTERS_CLEAR:
     case constants.DEFAULT_FILTERS_RESET:
+    case constants.SHOW_CONTENT_MESSAGES_TOGGLE:
       const messagesToShow = [];
       const filtered = getDefaultFiltersCounter();
 
       messagesById.forEach((message, msgId) => {
         const { visible, cause } = getMessageVisibility(message, {
           messagesState: state,
           filtersState,
           prefsState,
+          uiState,
         });
 
         if (visible) {
           messagesToShow.push(msgId);
         } else if (DEFAULT_FILTERS.includes(cause)) {
           filtered.global = filtered.global + 1;
           filtered[cause] = filtered[cause] + 1;
         }
@@ -761,20 +767,35 @@ function getToplevelMessageCount(state) 
  * @return {Object} An object of the following form:
  *         - visible {Boolean}: true if the message should be visible
  *         - cause {String}: if visible is false, what causes the message to be hidden.
  */
 function getMessageVisibility(message, {
     messagesState,
     filtersState,
     prefsState,
+    uiState,
     checkGroup = true,
 }) {
+  // Do not display the message if it's not from chromeContext and we don't show content
+  // messages.
+  if (
+    !uiState.showContentMessages &&
+    message.chromeContext === false &&
+    message.type !== MESSAGE_TYPE.COMMAND &&
+    message.type !== MESSAGE_TYPE.RESULT
+  ) {
+    return {
+      visible: false,
+      cause: "contentMessage",
+    };
+  }
+
   const warningGroupMessage =
-    messagesState.messagesById.get(getParentWarningGroupMessageId(message));
+  messagesState.messagesById.get(getParentWarningGroupMessageId(message));
 
   // Do not display the message if it's in closed group and not in a warning group.
   if (
     checkGroup
     && !isInOpenedGroup(message, messagesState.groupsById, messagesState.messagesUiById)
     && !shouldGroupWarningMessages(warningGroupMessage, messagesState, prefsState)
   ) {
     return {
--- a/devtools/client/webconsole/reducers/prefs.js
+++ b/devtools/client/webconsole/reducers/prefs.js
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 "use strict";
 
 const PrefState = (overrides) => Object.freeze(Object.assign({
   logLimit: 1000,
   sidebarToggle: false,
   jstermCodeMirror: false,
   groupWarnings: false,
+  filterContentMessages: false,
   historyCount: 50,
 }, overrides));
 
 function prefs(state = PrefState(), action) {
   return state;
 }
 
 exports.PrefState = PrefState;
--- a/devtools/client/webconsole/reducers/ui.js
+++ b/devtools/client/webconsole/reducers/ui.js
@@ -6,63 +6,68 @@
 "use strict";
 
 const {
   INITIALIZE,
   MESSAGES_CLEAR,
   PERSIST_TOGGLE,
   REVERSE_SEARCH_INPUT_TOGGLE,
   SELECT_NETWORK_MESSAGE_TAB,
+  SHOW_CONTENT_MESSAGES_TOGGLE,
   SHOW_OBJECT_IN_SIDEBAR,
   SIDEBAR_CLOSE,
   SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE,
   TIMESTAMPS_TOGGLE,
 } = require("devtools/client/webconsole/constants");
 
 const {
   PANELS,
 } = require("devtools/client/netmonitor/src/constants");
 
 const UiState = (overrides) => Object.freeze(Object.assign({
   initialized: false,
   networkMessageActiveTabId: PANELS.HEADERS,
   persistLogs: false,
+  showContentMessages: false,
   sidebarVisible: false,
   timestampsVisible: true,
   gripInSidebar: null,
   closeButtonVisible: false,
   reverseSearchInputVisible: false,
   reverseSearchInitialValue: "",
   editor: false,
 }, overrides));
 
 function ui(state = UiState(), action) {
   switch (action.type) {
     case PERSIST_TOGGLE:
-      return Object.assign({}, state, {persistLogs: !state.persistLogs});
+      return {...state, persistLogs: !state.persistLogs};
+    case SHOW_CONTENT_MESSAGES_TOGGLE:
+      return {...state, showContentMessages: !state.showContentMessages};
     case TIMESTAMPS_TOGGLE:
-      return Object.assign({}, state, {timestampsVisible: action.visible});
+      return {...state, timestampsVisible: action.visible};
     case SELECT_NETWORK_MESSAGE_TAB:
-      return Object.assign({}, state, {networkMessageActiveTabId: action.id});
+      return {...state, networkMessageActiveTabId: action.id};
     case SIDEBAR_CLOSE:
-      return Object.assign({}, state, {
+      return {
+        ...state,
         sidebarVisible: false,
         gripInSidebar: null,
-      });
+      };
     case INITIALIZE:
-      return Object.assign({}, state, {initialized: true});
+      return {...state, initialized: true};
     case MESSAGES_CLEAR:
-      return Object.assign({}, state, {sidebarVisible: false, gripInSidebar: null});
+      return {...state, sidebarVisible: false, gripInSidebar: null};
     case SHOW_OBJECT_IN_SIDEBAR:
       if (action.grip === state.gripInSidebar) {
         return state;
       }
-      return Object.assign({}, state, {sidebarVisible: true, gripInSidebar: action.grip});
+      return {...state, sidebarVisible: true, gripInSidebar: action.grip};
     case SPLIT_CONSOLE_CLOSE_BUTTON_TOGGLE:
-      return Object.assign({}, state, {closeButtonVisible: action.shouldDisplayButton});
+      return {...state, closeButtonVisible: action.shouldDisplayButton};
     case REVERSE_SEARCH_INPUT_TOGGLE:
       return {
         ...state,
         reverseSearchInputVisible: !state.reverseSearchInputVisible,
         reverseSearchInitialValue: action.initialValue || "",
       };
   }
 
--- a/devtools/client/webconsole/store.js
+++ b/devtools/client/webconsole/store.js
@@ -47,39 +47,44 @@ function configureStore(webConsoleUI, op
 
   const logLimit = options.logLimit
     || Math.max(getIntPref("devtools.hud.loglimit"), 1);
   const sidebarToggle = getBoolPref(PREFS.FEATURES.SIDEBAR_TOGGLE);
   const jstermCodeMirror = getBoolPref(PREFS.FEATURES.JSTERM_CODE_MIRROR);
   const autocomplete = getBoolPref(PREFS.FEATURES.AUTOCOMPLETE);
   const groupWarnings = getBoolPref(PREFS.FEATURES.GROUP_WARNINGS);
   const historyCount = getIntPref(PREFS.UI.INPUT_HISTORY_COUNT);
+  const filterContentMessages = getBoolPref(PREFS.FEATURES.FILTER_CONTENT_MESSAGES);
 
   const initialState = {
     prefs: PrefState({
       logLimit,
       sidebarToggle,
       jstermCodeMirror,
       autocomplete,
       historyCount,
       groupWarnings,
+      filterContentMessages,
     }),
     filters: FilterState({
       error: getBoolPref(PREFS.FILTER.ERROR),
       warn: getBoolPref(PREFS.FILTER.WARN),
       info: getBoolPref(PREFS.FILTER.INFO),
       debug: getBoolPref(PREFS.FILTER.DEBUG),
       log: getBoolPref(PREFS.FILTER.LOG),
       css: getBoolPref(PREFS.FILTER.CSS),
       net: getBoolPref(PREFS.FILTER.NET),
       netxhr: getBoolPref(PREFS.FILTER.NETXHR),
     }),
     ui: UiState({
       networkMessageActiveTabId: "headers",
       persistLogs: getBoolPref(PREFS.UI.PERSIST),
+      showContentMessages: webConsoleUI.isBrowserConsole && filterContentMessages
+        ? getBoolPref(PREFS.UI.CONTENT_MESSAGES)
+        : true,
       editor: getBoolPref(PREFS.UI.EDITOR),
     }),
   };
 
   // Prepare middleware.
   const services = (options.services || {});
 
   const middleware = applyMiddleware(
@@ -111,33 +116,34 @@ function configureStore(webConsoleUI, op
   );
 }
 
 function createRootReducer() {
   return function rootReducer(state, action) {
     // We want to compute the new state for all properties except
     // "messages" and "history". These two reducers are handled
     // separately since they are receiving additional arguments.
-    const newState = [...Object.entries(reducers)].reduce((res, [key, reducer]) => {
+    const newState = Object.entries(reducers).reduce((res, [key, reducer]) => {
       if (key !== "messages" && key !== "history") {
         res[key] = reducer(state[key], action);
       }
       return res;
     }, {});
 
     // Pass prefs state as additional argument to the history reducer.
     newState.history = reducers.history(state.history, action, newState.prefs);
 
-    return Object.assign(newState, {
-      // specifically pass the updated filters and prefs state as additional arguments.
-      messages: reducers.messages(
-        state.messages,
-        action,
-        newState.filters,
-        newState.prefs,
-      ),
-    });
+    // Specifically pass the updated filters, prefs and ui states as additional arguments.
+    newState.messages = reducers.messages(
+      state.messages,
+      action,
+      newState.filters,
+      newState.prefs,
+      newState.ui,
+    );
+
+    return newState;
   };
 }
 
 // Provide the store factory for test code so that each test is working with
 // its own instance.
 module.exports.configureStore = configureStore;
--- a/devtools/client/webconsole/test/mocha-test-setup.js
+++ b/devtools/client/webconsole/test/mocha-test-setup.js
@@ -22,16 +22,18 @@ pref("devtools.webconsole.filter.netxhr"
 pref("devtools.webconsole.inputHistoryCount", 300);
 pref("devtools.webconsole.persistlog", false);
 pref("devtools.webconsole.timestampMessages", false);
 pref("devtools.webconsole.sidebarToggle", true);
 pref("devtools.webconsole.jsterm.codeMirror", true);
 pref("devtools.webconsole.groupWarningMessages", false);
 pref("devtools.webconsole.input.editor", false);
 pref("devtools.webconsole.input.autocomplete", true);
+pref("devtools.browserconsole.contentMessages", true);
+pref("devtools.browserconsole.filterContentMessages", false);
 
 global.loader = {
   lazyServiceGetter: () => {},
   lazyGetter: (context, name, fn) => {
 
   },
   lazyRequireGetter: (context, name, path, destruct) => {
     if (path === "devtools/shared/async-storage") {
--- a/devtools/client/webconsole/test/mochitest/browser_console_webconsole_console_api_calls.js
+++ b/devtools/client/webconsole/test/mochitest/browser_console_webconsole_console_api_calls.js
@@ -1,54 +1,82 @@
 /* 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/. */
 
 // Test that console API calls in the content page appear in the browser console.
 
 "use strict";
 
-const TEST_URI = `data:text/html,<meta charset=utf8>console API calls`;
+const contentArgs = {
+  log: "MyLog",
+  warn: "MyWarn",
+  error: "MyError",
+  info: "MyInfo",
+  debug: "MyDebug",
+  counterName: "MyCounter",
+  timerName: "MyTimer",
+};
+
+const TEST_URI = `data:text/html,<meta charset=utf8>console API calls<script>
+  console.log("${contentArgs.log}");
+  console.warn("${contentArgs.warn}");
+  console.error("${contentArgs.error}");
+  console.info("${contentArgs.info}");
+  console.debug("${contentArgs.debug}");
+  console.count("${contentArgs.counterName}");
+  console.time("${contentArgs.timerName}");
+  console.timeLog("${contentArgs.timerName}");
+  console.timeEnd("${contentArgs.timerName}");
+  console.trace();
+  console.assert(false, "err");
+</script>`;
 
 add_task(async function() {
-  await addTab(TEST_URI);
-  const hud = await HUDService.toggleBrowserConsole();
+  // Enable the checkbox
+  await pushPref("devtools.browserconsole.filterContentMessages", true);
 
-  const contentArgs = {
-    log: "MyLog",
-    warn: "MyWarn",
-    error: "MyError",
-    info: "MyInfo",
-    debug: "MyDebug",
-    counterName: "MyCounter",
-    timerName: "MyTimer",
-  };
+  // Show the content messages
+  await pushPref("devtools.browserconsole.contentMessages", true);
+
+  const hud = await HUDService.toggleBrowserConsole();
+  hud.ui.clearOutput();
+
+  await addTab(TEST_URI);
 
   const expectedMessages = [
     contentArgs.log,
     contentArgs.warn,
     contentArgs.error,
     contentArgs.info,
     contentArgs.debug,
     `${contentArgs.counterName}: 1`,
     `${contentArgs.timerName}:`,
+    `timer ended`,
     `console.trace`,
     `Assertion failed`,
   ];
-  const onAllMessages = Promise.all(expectedMessages.map(m => waitForMessage(hud, m)));
-
-  ContentTask.spawn(gBrowser.selectedBrowser, contentArgs, function(args) {
-    content.console.log(args.log);
-    content.console.warn(args.warn);
-    content.console.error(args.error);
-    content.console.info(args.info);
-    content.console.debug(args.debug);
-    content.console.count(args.counterName);
-    content.console.time(args.timerName);
-    content.console.timeEnd(args.timerName);
-    content.console.trace();
-    content.console.assert(false, "err");
-  });
-
-  await onAllMessages;
+  await waitFor(() =>
+    expectedMessages.every(expectedMessage => findMessage(hud, expectedMessage)));
 
   ok(true, "Expected messages are displayed in the browser console");
+
+  info("Uncheck the Show content messages checkbox");
+  const onContentMessagesHidden = waitFor(() => !findMessage(hud, contentArgs.log));
+  const checkbox =
+    hud.ui.outputNode.querySelector(".webconsole-filterbar-primary .filter-checkbox");
+  checkbox.click();
+  await onContentMessagesHidden;
+
+  for (const expectedMessage of expectedMessages) {
+    ok(!findMessage(hud, expectedMessage), `"${expectedMessage}" is hidden`);
+  }
+
+  info("Check the Show content messages checkbox");
+  const onContentMessagesDisplayed = waitFor(() =>
+    expectedMessages.every(expectedMessage => findMessage(hud, expectedMessage)));
+  checkbox.click();
+  await onContentMessagesDisplayed;
+
+  for (const expectedMessage of expectedMessages) {
+    ok(findMessage(hud, expectedMessage), `"${expectedMessage}" is visible`);
+  }
 });
--- a/devtools/client/webconsole/webconsole-wrapper.js
+++ b/devtools/client/webconsole/webconsole-wrapper.js
@@ -349,16 +349,18 @@ class WebConsoleWrapper {
       const app = App({
         attachRefToWebConsoleUI,
         serviceContainer,
         webConsoleUI,
         onFirstMeaningfulPaint: resolve,
         closeSplitConsole: this.closeSplitConsole.bind(this),
         jstermCodeMirror,
         autocomplete,
+        hideShowContentMessagesCheckbox: !webConsoleUI.isBrowserConsole ||
+          !prefs.filterContentMessages,
       });
 
       // Render the root Application component.
       if (this.parentNode) {
         const provider = createElement(Provider, { store }, app);
         this.body = ReactDOM.render(provider, this.parentNode);
       } else {
         // If there's no parentNode, we are in a test. So we can resolve immediately.