tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

messages.js (53502B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 "use strict";
      5 
      6 const {
      7  isGroupType,
      8  isMessageNetworkError,
      9  l10n,
     10 } = require("resource://devtools/client/webconsole/utils/messages.js");
     11 
     12 const constants = require("resource://devtools/client/webconsole/constants.js");
     13 const { DEFAULT_FILTERS, FILTERS, MESSAGE_TYPE, MESSAGE_SOURCE } = constants;
     14 
     15 const { getGripPreviewItems } = ChromeUtils.importESModule(
     16  "resource://devtools/client/shared/components/reps/index.mjs",
     17  { global: "current" }
     18 );
     19 loader.lazyRequireGetter(
     20  this,
     21  "getUnicodeUrlPath",
     22  "resource://devtools/client/shared/unicode-url.js",
     23  true
     24 );
     25 loader.lazyRequireGetter(
     26  this,
     27  "getSourceNames",
     28  "resource://devtools/client/shared/source-utils.js",
     29  true
     30 );
     31 loader.lazyRequireGetter(
     32  this,
     33  [
     34    "areMessagesSimilar",
     35    "createWarningGroupMessage",
     36    "isWarningGroup",
     37    "getWarningGroupType",
     38    "getDescriptorValue",
     39    "getParentWarningGroupMessageId",
     40    "getNaturalOrder",
     41  ],
     42  "resource://devtools/client/webconsole/utils/messages.js",
     43  true
     44 );
     45 
     46 const {
     47  UPDATE_REQUEST,
     48 } = require("resource://devtools/client/netmonitor/src/constants.js");
     49 
     50 const {
     51  processNetworkUpdates,
     52 } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js");
     53 
     54 const MessageState = overrides =>
     55  Object.freeze(
     56    Object.assign(
     57      {
     58        // List of all the messages added to the console. Unlike other properties, this Map
     59        // will be mutated on state changes for performance reasons.
     60        mutableMessagesById: new Map(),
     61        // Array of message ids, in chronological order. We use a dedicated property to store
     62        // the order (instead of relying on the order of insertion in mutableMessagesById)
     63        // as we might receive messages that need to be inserted at a specific index. Doing
     64        // so on the Map can be costly, especially when the Map holds lots of messages.
     65        mutableMessagesOrder: [],
     66        // List of elements matching the selector of CSS Warning messages(populated
     67        // on-demand via the UI).
     68        cssMessagesMatchingElements: new Map(),
     69        // Array of the visible messages.
     70        visibleMessages: [],
     71        // Object for the filtered messages.
     72        filteredMessagesCount: getDefaultFiltersCounter(),
     73        // List of the message ids which are opened.
     74        messagesUiById: [],
     75        // Map of the form {groupMessageId : groupArray},
     76        // where groupArray is the list of of all the parent groups' ids of the groupMessageId.
     77        // This handles console API groups.
     78        groupsById: new Map(),
     79        // Message id of the current console API group (no corresponding console.groupEnd yet).
     80        currentGroup: null,
     81        // This group handles "warning groups" (Content Blocking, CORS, CSP, …)
     82        warningGroupsById: new Map(),
     83        // Array of fronts to release (i.e. fronts logged in removed messages).
     84        // This array *should not* be consumed by any UI component.
     85        frontsToRelease: [],
     86        // Map of the form {messageId : numberOfRepeat}
     87        repeatById: {},
     88        // Map of the form {messageId : networkInformation}
     89        // `networkInformation` holds request, response, totalTime, ...
     90        networkMessagesUpdateById: {},
     91        // Id of the last messages that was added.
     92        lastMessageId: null,
     93        // List of the message ids which are disabled
     94        disabledMessagesById: [],
     95      },
     96      overrides
     97    )
     98  );
     99 
    100 function cloneState(state) {
    101  return {
    102    visibleMessages: [...state.visibleMessages],
    103    filteredMessagesCount: { ...state.filteredMessagesCount },
    104    messagesUiById: [...state.messagesUiById],
    105    cssMessagesMatchingElements: new Map(state.cssMessagesMatchingElements),
    106    groupsById: new Map(state.groupsById),
    107    frontsToRelease: [...state.frontsToRelease],
    108    repeatById: { ...state.repeatById },
    109    networkMessagesUpdateById: { ...state.networkMessagesUpdateById },
    110    warningGroupsById: new Map(state.warningGroupsById),
    111    // no need to mutate the properties below as they're not directly triggering re-render
    112    mutableMessagesById: state.mutableMessagesById,
    113    mutableMessagesOrder: state.mutableMessagesOrder,
    114    currentGroup: state.currentGroup,
    115    lastMessageId: state.lastMessageId,
    116    disabledMessagesById: [...state.disabledMessagesById],
    117  };
    118 }
    119 
    120 /**
    121 * Add a console message to the state.
    122 *
    123 * @param {ConsoleMessage} newMessage: The message to add to the state.
    124 * @param {MessageState} state: The message state ( = managed by this reducer).
    125 * @param {FiltersState} filtersState: The filters state.
    126 * @param {PrefsState} prefsState: The preferences state.
    127 * @param {UiState} uiState: The ui state.
    128 * @returns {MessageState} a new messages state.
    129 */
    130 // eslint-disable-next-line complexity
    131 function addMessage(newMessage, state, filtersState, prefsState, uiState) {
    132  const { mutableMessagesById, groupsById, repeatById } = state;
    133 
    134  if (newMessage.type === constants.MESSAGE_TYPE.NAVIGATION_MARKER) {
    135    // We set the state's currentGroup property to null after navigating
    136    state.currentGroup = null;
    137  }
    138  const { currentGroup } = state;
    139 
    140  if (newMessage.type === constants.MESSAGE_TYPE.NULL_MESSAGE) {
    141    // When the message has a NULL type, we don't add it.
    142    return state;
    143  }
    144 
    145  if (newMessage.type === constants.MESSAGE_TYPE.END_GROUP) {
    146    // Compute the new current group.
    147    state.currentGroup = getNewCurrentGroup(currentGroup, groupsById);
    148    return state;
    149  }
    150 
    151  const lastMessage = mutableMessagesById.get(state.lastMessageId);
    152  // It can happen that the new message was actually emitted earlier than the last message,
    153  // which means we need to insert it at the right position.
    154  const isUnsorted =
    155    lastMessage && lastMessage.timeStamp > newMessage.timeStamp;
    156 
    157  if (lastMessage && mutableMessagesById.size > 0) {
    158    if (
    159      // only repeat messages if the group similar messages pref is enabled
    160      prefsState.groupSimilar &&
    161      lastMessage.groupId === currentGroup &&
    162      areMessagesSimilar(lastMessage, newMessage)
    163    ) {
    164      state.repeatById[lastMessage.id] = (repeatById[lastMessage.id] || 1) + 1;
    165      return state;
    166    }
    167  }
    168 
    169  // Store the id of the message as being the last one being added.
    170  if (!isUnsorted) {
    171    state.lastMessageId = newMessage.id;
    172  }
    173 
    174  // Add the new message with a reference to the parent group.
    175  const parentGroups = getParentGroups(currentGroup, groupsById);
    176  if (!isWarningGroup(newMessage)) {
    177    newMessage.groupId = currentGroup;
    178    newMessage.indent = parentGroups.length;
    179  }
    180 
    181  // Check if the current message could be placed in a Warning Group.
    182  // This needs to be done before setting the new message in mutableMessagesById so we have a
    183  // proper message.
    184  const warningGroupType = getWarningGroupType(newMessage);
    185 
    186  // If the preference for grouping is true, and the new message could be in a warning group.
    187  if (prefsState.groupSimilar && warningGroupType !== null) {
    188    const warningGroupMessageId = getParentWarningGroupMessageId(newMessage);
    189 
    190    // If there's no warning group for the type/innerWindowID yet
    191    if (!state.mutableMessagesById.has(warningGroupMessageId)) {
    192      // We create it and add it to the store.
    193      const groupMessage = createWarningGroupMessage(
    194        warningGroupMessageId,
    195        warningGroupType,
    196        newMessage
    197      );
    198      state = addMessage(
    199        groupMessage,
    200        state,
    201        filtersState,
    202        prefsState,
    203        uiState
    204      );
    205    }
    206 
    207    // We add the new message to the appropriate warningGroup.
    208    state.warningGroupsById.get(warningGroupMessageId).push(newMessage.id);
    209 
    210    // If the warningGroup message is not visible yet, but should be.
    211    if (
    212      !state.visibleMessages.includes(warningGroupMessageId) &&
    213      getMessageVisibility(
    214        state.mutableMessagesById.get(warningGroupMessageId),
    215        {
    216          messagesState: state,
    217          filtersState,
    218          prefsState,
    219          uiState,
    220        }
    221      ).visible
    222    ) {
    223      // Then we put it in the visibleMessages properties, at the position of the first
    224      // warning message inside the warningGroup.
    225      // If that first warning message is in a console.group, we place it before the
    226      // outermost console.group message.
    227      const firstWarningMessageId = state.warningGroupsById.get(
    228        warningGroupMessageId
    229      )[0];
    230      const firstWarningMessage = state.mutableMessagesById.get(
    231        firstWarningMessageId
    232      );
    233      const outermostGroupId = getOutermostGroup(
    234        firstWarningMessage,
    235        groupsById
    236      );
    237      const groupIndex = state.visibleMessages.indexOf(outermostGroupId);
    238      const warningMessageIndex = state.visibleMessages.indexOf(
    239        firstWarningMessageId
    240      );
    241 
    242      if (groupIndex > -1) {
    243        // We remove the warning message
    244        if (warningMessageIndex > -1) {
    245          state.visibleMessages.splice(warningMessageIndex, 1);
    246        }
    247 
    248        // And we put the warning group before the console.group
    249        state.visibleMessages.splice(groupIndex, 0, warningGroupMessageId);
    250      } else {
    251        // If the warning message is not in a console.group, we replace it by the
    252        // warning group message.
    253        state.visibleMessages.splice(
    254          warningMessageIndex,
    255          1,
    256          warningGroupMessageId
    257        );
    258      }
    259    }
    260  }
    261 
    262  // If we're creating a warningGroup, we init the array for its children.
    263  if (isWarningGroup(newMessage)) {
    264    state.warningGroupsById.set(newMessage.id, []);
    265  }
    266 
    267  const addedMessage = Object.freeze(newMessage);
    268 
    269  // If the new message isn't the "oldest" one, then we need to insert it at the right
    270  // position in the message map.
    271  if (isUnsorted) {
    272    let newMessageIndex = 0;
    273    // This is can be on a hot path, so we're not using `findIndex`, which could be slow.
    274    // Furthermore, there's a high chance the message beed to be inserted somewhere at the
    275    // end of the list, so we loop through mutableMessagesOrder in reverse order.
    276    for (let i = state.mutableMessagesOrder.length - 1; i >= 0; i--) {
    277      const message = state.mutableMessagesById.get(
    278        state.mutableMessagesOrder[i]
    279      );
    280      if (message.timeStamp <= addedMessage.timeStamp) {
    281        newMessageIndex = i + 1;
    282        break;
    283      }
    284    }
    285 
    286    state.mutableMessagesOrder.splice(newMessageIndex, 0, addedMessage.id);
    287  } else {
    288    state.mutableMessagesOrder.push(addedMessage.id);
    289  }
    290  state.mutableMessagesById.set(addedMessage.id, addedMessage);
    291 
    292  if (newMessage.type === "trace" || newMessage.type === "logPoint") {
    293    // We want the stacktrace to be open by default.
    294    state.messagesUiById.push(newMessage.id);
    295  } else if (isGroupType(newMessage.type)) {
    296    state.currentGroup = newMessage.id;
    297    state.groupsById.set(newMessage.id, parentGroups);
    298 
    299    if (newMessage.type === constants.MESSAGE_TYPE.START_GROUP) {
    300      // We want the group to be open by default.
    301      state.messagesUiById.push(newMessage.id);
    302    }
    303  }
    304 
    305  const { visible, cause } = getMessageVisibility(addedMessage, {
    306    messagesState: state,
    307    filtersState,
    308    prefsState,
    309    uiState,
    310  });
    311 
    312  if (visible) {
    313    // If the message is part of a visible warning group, we want to add it after the last
    314    // visible message of the group.
    315    const warningGroupId = getParentWarningGroupMessageId(newMessage);
    316    if (warningGroupId && state.visibleMessages.includes(warningGroupId)) {
    317      // Defaults to the warning group message.
    318      let index = state.visibleMessages.indexOf(warningGroupId);
    319 
    320      // We loop backward through the warning group's messages to get the latest visible
    321      // messages in it.
    322      const messagesInWarningGroup =
    323        state.warningGroupsById.get(warningGroupId);
    324      for (let i = messagesInWarningGroup.length - 1; i >= 0; i--) {
    325        const idx = state.visibleMessages.indexOf(messagesInWarningGroup[i]);
    326        if (idx > -1) {
    327          index = idx;
    328          break;
    329        }
    330      }
    331      // Inserts the new warning message at the wanted location "in" the warning group.
    332      state.visibleMessages.splice(index + 1, 0, newMessage.id);
    333    } else if (isUnsorted) {
    334      // If the new message wasn't the "oldest" one, then we need to insert its id at
    335      // the right position in the array.
    336      // This is can be on a hot path, so we're not using `findIndex`, which could be slow.
    337      // Furthermore, there's a high chance the message beed to be inserted somewhere at the
    338      // end of the list, so we loop through visibleMessages in reverse order.
    339      let index = 0;
    340      for (let i = state.visibleMessages.length - 1; i >= 0; i--) {
    341        const id = state.visibleMessages[i];
    342        if (
    343          state.mutableMessagesById.get(id).timeStamp <= newMessage.timeStamp
    344        ) {
    345          index = i + 1;
    346          break;
    347        }
    348      }
    349      state.visibleMessages.splice(index, 0, newMessage.id);
    350    } else {
    351      state.visibleMessages.push(newMessage.id);
    352    }
    353    maybeSortVisibleMessages(state, false);
    354  } else if (DEFAULT_FILTERS.includes(cause)) {
    355    state.filteredMessagesCount.global++;
    356    state.filteredMessagesCount[cause]++;
    357  }
    358 
    359  // Append received network-data also into networkMessagesUpdateById
    360  // that is responsible for collecting (lazy loaded) HTTP payload data.
    361  if (newMessage.source == "network") {
    362    state.networkMessagesUpdateById[newMessage.actor] = newMessage;
    363  }
    364 
    365  return state;
    366 }
    367 
    368 // eslint-disable-next-line complexity
    369 function messages(
    370  state = MessageState(),
    371  action,
    372  filtersState,
    373  prefsState,
    374  uiState
    375 ) {
    376  const {
    377    mutableMessagesById,
    378    cssMessagesMatchingElements,
    379    messagesUiById,
    380    networkMessagesUpdateById,
    381    groupsById,
    382    visibleMessages,
    383    disabledMessagesById,
    384  } = state;
    385 
    386  const { logLimit } = prefsState;
    387 
    388  let newState;
    389  switch (action.type) {
    390    case constants.MESSAGES_ADD: {
    391      // If the action holds more messages than the log limit, we can preemptively remove
    392      // messages that will never be rendered.
    393      const batchHasMoreMessagesThanLogLimit =
    394        action.messages.length > logLimit;
    395      const list = batchHasMoreMessagesThanLogLimit ? [] : action.messages;
    396      if (batchHasMoreMessagesThanLogLimit) {
    397        let prunableCount = 0;
    398        let lastMessage = null;
    399        for (let i = action.messages.length - 1; i >= 0; i--) {
    400          const message = action.messages[i];
    401          if (
    402            !message.groupId &&
    403            !isGroupType(message.type) &&
    404            message.type !== MESSAGE_TYPE.END_GROUP
    405          ) {
    406            const messagesSimilar = areMessagesSimilar(lastMessage, message);
    407            if (!messagesSimilar) {
    408              prunableCount++;
    409            }
    410            // Once we've added the max number of messages that can be added, stop.
    411            // Except for repeated messages, where we keep adding over the limit.
    412            if (prunableCount <= logLimit || messagesSimilar) {
    413              list.unshift(action.messages[i]);
    414            } else {
    415              break;
    416            }
    417          } else {
    418            list.unshift(message);
    419          }
    420          lastMessage = message;
    421        }
    422      }
    423 
    424      newState = cloneState(state);
    425      for (const message of list) {
    426        newState = addMessage(
    427          message,
    428          newState,
    429          filtersState,
    430          prefsState,
    431          uiState
    432        );
    433      }
    434 
    435      return limitTopLevelMessageCount(newState, logLimit);
    436    }
    437 
    438    case constants.MESSAGES_CLEAR: {
    439      const frontsToRelease = [];
    440      for (const message of state.mutableMessagesById.values()) {
    441        // We want to minimize time spent in reducer as much as we can, so we're using
    442        // prototype.push.apply here as it seems faster than other solutions (e.g. the
    443        // spread operator, Array#concat, …)
    444        Array.prototype.push.apply(
    445          frontsToRelease,
    446          getAllFrontsInMessage(message)
    447        );
    448      }
    449      return MessageState({
    450        // Store all actors from removed messages. This array is used by
    451        // `releaseActorsEnhancer` to release all of those backend actors.
    452        frontsToRelease,
    453      });
    454    }
    455 
    456    case constants.PRIVATE_MESSAGES_CLEAR: {
    457      const removedIds = new Set();
    458      for (const [id, message] of mutableMessagesById) {
    459        if (message.private === true) {
    460          removedIds.add(id);
    461        }
    462      }
    463 
    464      // If there's no private messages, there's no need to change the state.
    465      if (removedIds.size === 0) {
    466        return state;
    467      }
    468 
    469      return removeMessagesFromState(
    470        {
    471          ...state,
    472        },
    473        removedIds
    474      );
    475    }
    476 
    477    case constants.TARGET_MESSAGES_REMOVE: {
    478      const removedIds = new Set();
    479      for (const [id, message] of mutableMessagesById) {
    480        // Remove message from the target but not evaluations and their results, so
    481        // 1. we're consistent with the filtering behavior, i.e. we never hide those
    482        // 2. when switching mode from multiprocess to parent process and back to multi,
    483        //    if we'd clear evaluations we wouldn't have a way to get them back, unlike
    484        //    log messages and errors, which are still available in the server caches).
    485        if (
    486          message.targetFront == action.targetFront &&
    487          message.type !== MESSAGE_TYPE.COMMAND &&
    488          message.type !== MESSAGE_TYPE.RESULT
    489        ) {
    490          removedIds.add(id);
    491        }
    492      }
    493 
    494      return removeMessagesFromState(
    495        {
    496          ...state,
    497        },
    498        removedIds
    499      );
    500    }
    501 
    502    case constants.MESSAGES_DISABLE:
    503      return {
    504        ...state,
    505        disabledMessagesById: [...disabledMessagesById, ...action.ids],
    506      };
    507 
    508    case constants.MESSAGE_OPEN: {
    509      const openState = { ...state };
    510      openState.messagesUiById = [...messagesUiById, action.id];
    511      const currMessage = mutableMessagesById.get(action.id);
    512 
    513      // If the message is a console.group/groupCollapsed or a warning group.
    514      if (isGroupType(currMessage.type) || isWarningGroup(currMessage)) {
    515        // We want to make its children visible
    516        const messagesToShow = [];
    517        for (const id of state.mutableMessagesOrder) {
    518          const message = mutableMessagesById.get(id);
    519          if (
    520            !visibleMessages.includes(message.id) &&
    521            ((isWarningGroup(currMessage) && !!getWarningGroupType(message)) ||
    522              (isGroupType(currMessage.type) &&
    523                getParentGroups(message.groupId, groupsById).includes(
    524                  action.id
    525                ))) &&
    526            getMessageVisibility(message, {
    527              messagesState: openState,
    528              filtersState,
    529              prefsState,
    530              uiState,
    531              // We want to check if the message is in an open group
    532              // only if it is not a direct child of the group we're opening.
    533              checkGroup: message.groupId !== action.id,
    534            }).visible
    535          ) {
    536            messagesToShow.push(id);
    537          }
    538        }
    539 
    540        // We can then insert the messages ids right after the one of the group.
    541        const insertIndex = visibleMessages.indexOf(action.id) + 1;
    542        openState.visibleMessages = [
    543          ...visibleMessages.slice(0, insertIndex),
    544          ...messagesToShow,
    545          ...visibleMessages.slice(insertIndex),
    546        ];
    547      }
    548      return openState;
    549    }
    550 
    551    case constants.MESSAGE_CLOSE: {
    552      const closeState = { ...state };
    553      const messageId = action.id;
    554      const index = closeState.messagesUiById.indexOf(messageId);
    555      closeState.messagesUiById.splice(index, 1);
    556      closeState.messagesUiById = [...closeState.messagesUiById];
    557 
    558      // If the message is a group
    559      if (isGroupType(mutableMessagesById.get(messageId).type)) {
    560        // Hide all its children, unless they're in a warningGroup.
    561        closeState.visibleMessages = visibleMessages.filter((id, i, arr) => {
    562          const message = mutableMessagesById.get(id);
    563          const warningGroupMessage = mutableMessagesById.get(
    564            getParentWarningGroupMessageId(message)
    565          );
    566 
    567          // If the message is in a warning group, then we return its current visibility.
    568          if (
    569            shouldGroupWarningMessages(
    570              warningGroupMessage,
    571              closeState,
    572              prefsState
    573            )
    574          ) {
    575            return arr.includes(id);
    576          }
    577 
    578          const parentGroups = getParentGroups(message.groupId, groupsById);
    579          return parentGroups.includes(messageId) === false;
    580        });
    581      } else if (isWarningGroup(mutableMessagesById.get(messageId))) {
    582        // If the message was a warningGroup, we hide all the messages in the group.
    583        const groupMessages = closeState.warningGroupsById.get(messageId);
    584        closeState.visibleMessages = visibleMessages.filter(
    585          id => !groupMessages.includes(id)
    586        );
    587      }
    588      return closeState;
    589    }
    590 
    591    case constants.CSS_MESSAGE_ADD_MATCHING_ELEMENTS:
    592      return {
    593        ...state,
    594        cssMessagesMatchingElements: new Map(cssMessagesMatchingElements).set(
    595          action.id,
    596          action.elements
    597        ),
    598      };
    599 
    600    case constants.NETWORK_MESSAGES_UPDATE: {
    601      const updatedState = {
    602        ...state,
    603        networkMessagesUpdateById: {
    604          ...networkMessagesUpdateById,
    605        },
    606      };
    607      let hasNetworkError = null;
    608      for (const message of action.messages) {
    609        const { id } = message;
    610        updatedState.mutableMessagesById.set(id, message);
    611        updatedState.networkMessagesUpdateById[id] = {
    612          ...(updatedState.networkMessagesUpdateById[id] || {}),
    613          ...message,
    614        };
    615 
    616        if (isMessageNetworkError(message)) {
    617          hasNetworkError = true;
    618        }
    619      }
    620 
    621      // If the message updates contained a network error, then we may have to display it.
    622      if (hasNetworkError) {
    623        return setVisibleMessages({
    624          messagesState: updatedState,
    625          filtersState,
    626          prefsState,
    627          uiState,
    628        });
    629      }
    630 
    631      return updatedState;
    632    }
    633 
    634    case UPDATE_REQUEST:
    635    case constants.NETWORK_UPDATES_REQUEST: {
    636      newState = {
    637        ...state,
    638        networkMessagesUpdateById: {
    639          ...networkMessagesUpdateById,
    640        },
    641      };
    642 
    643      // Netmonitor's UPDATE_REQUEST action comes for only one request
    644      const updates =
    645        action.type == UPDATE_REQUEST
    646          ? [{ id: action.id, data: action.data }]
    647          : action.updates;
    648      for (const { id, data } of updates) {
    649        const request = newState.networkMessagesUpdateById[id];
    650        if (!request) {
    651          continue;
    652        }
    653        newState.networkMessagesUpdateById[id] = {
    654          ...request,
    655          ...processNetworkUpdates(data),
    656        };
    657      }
    658      return newState;
    659    }
    660 
    661    case constants.FRONTS_TO_RELEASE_CLEAR:
    662      return {
    663        ...state,
    664        frontsToRelease: [],
    665      };
    666 
    667    case constants.GROUP_SIMILAR_MESSAGES_TOGGLE: {
    668      // There's no warningGroups, and the pref was set to false,
    669      // we don't need to do anything.
    670      if (!prefsState.groupSimilar && state.warningGroupsById.size === 0) {
    671        return state;
    672      }
    673 
    674      let needSort = false;
    675      for (const msgId of state.mutableMessagesOrder) {
    676        const message = state.mutableMessagesById.get(msgId);
    677        const warningGroupType = getWarningGroupType(message);
    678        if (warningGroupType) {
    679          const warningGroupMessageId = getParentWarningGroupMessageId(message);
    680 
    681          // If there's no warning group for the type/innerWindowID yet.
    682          if (!state.mutableMessagesById.has(warningGroupMessageId)) {
    683            // We create it and add it to the store.
    684            const groupMessage = createWarningGroupMessage(
    685              warningGroupMessageId,
    686              warningGroupType,
    687              message
    688            );
    689            state = addMessage(
    690              groupMessage,
    691              state,
    692              filtersState,
    693              prefsState,
    694              uiState
    695            );
    696          }
    697 
    698          // We add the new message to the appropriate warningGroup.
    699          const warningGroup = state.warningGroupsById.get(
    700            warningGroupMessageId
    701          );
    702          if (warningGroup && !warningGroup.includes(msgId)) {
    703            warningGroup.push(msgId);
    704          }
    705 
    706          needSort = true;
    707        }
    708      }
    709 
    710      // If we don't have any warning messages that could be in a group, we don't do
    711      // anything.
    712      if (!needSort) {
    713        return state;
    714      }
    715 
    716      return setVisibleMessages({
    717        messagesState: state,
    718        filtersState,
    719        prefsState,
    720        uiState,
    721        // If the user disabled warning groups, we want the messages to be sorted by their
    722        // timestamps.
    723        forceTimestampSort: !prefsState.groupSimilar,
    724      });
    725    }
    726 
    727    case constants.MESSAGE_REMOVE:
    728      return removeMessagesFromState(
    729        {
    730          ...state,
    731        },
    732        new Set([action.id])
    733      );
    734 
    735    case constants.FILTER_TOGGLE:
    736    case constants.FILTER_TEXT_SET:
    737    case constants.FILTERS_CLEAR:
    738    case constants.DEFAULT_FILTERS_RESET:
    739      return setVisibleMessages({
    740        messagesState: state,
    741        filtersState,
    742        prefsState,
    743        uiState,
    744      });
    745  }
    746 
    747  return state;
    748 }
    749 
    750 function setVisibleMessages({
    751  messagesState,
    752  filtersState,
    753  prefsState,
    754  uiState,
    755  forceTimestampSort = false,
    756 }) {
    757  const {
    758    mutableMessagesById,
    759    mutableMessagesOrder,
    760    visibleMessages,
    761    messagesUiById,
    762  } = messagesState;
    763 
    764  const messagesToShow = new Set();
    765  const matchedGroups = new Set();
    766  const filtered = getDefaultFiltersCounter();
    767 
    768  mutableMessagesOrder.forEach(msgId => {
    769    const message = mutableMessagesById.get(msgId);
    770    const groupParentId = message.groupId;
    771    let hasMatchedAncestor = false;
    772    const ancestors = [];
    773 
    774    if (groupParentId) {
    775      let ancestorId = groupParentId;
    776 
    777      // we track the message's ancestors and their state
    778      while (ancestorId) {
    779        ancestors.push({
    780          ancestorId,
    781          matchedFilters: matchedGroups.has(ancestorId),
    782          isOpen: messagesUiById.includes(ancestorId),
    783          isCurrentlyVisible: visibleMessages.includes(ancestorId),
    784        });
    785        if (!hasMatchedAncestor && matchedGroups.has(ancestorId)) {
    786          hasMatchedAncestor = true;
    787        }
    788        ancestorId = mutableMessagesById.get(ancestorId).groupId;
    789      }
    790    }
    791 
    792    const { visible, cause } = getMessageVisibility(message, {
    793      messagesState,
    794      filtersState,
    795      prefsState,
    796      uiState,
    797      hasMatchedAncestor,
    798    });
    799 
    800    // if the message is not visible but passes the search filters, we show its visible ancestors
    801    if (!visible && passSearchFilters(message, filtersState)) {
    802      const tmp = [];
    803      ancestors.forEach(msg => {
    804        if (msg.isCurrentlyVisible) {
    805          tmp.push(msg.ancestorId);
    806        }
    807      });
    808      tmp.reverse().forEach(id => {
    809        messagesToShow.add(id);
    810      });
    811    }
    812    if (visible) {
    813      // if the visible message is a child of a group, we add its ancestors to the visible messages
    814      if (groupParentId) {
    815        // We need to reverse the visibleAncestors array to show the groups in the correct order
    816        ancestors.reverse().forEach(msg => {
    817          messagesToShow.add(msg.ancestorId);
    818        });
    819      }
    820 
    821      // we keep track of matched startGroup and startGroupCollapsed messages so we don't filter their children
    822      if (
    823        message.type === "startGroup" ||
    824        message.type === "startGroupCollapsed"
    825      ) {
    826        matchedGroups.add(msgId);
    827      }
    828 
    829      messagesToShow.add(msgId);
    830    } else if (DEFAULT_FILTERS.includes(cause)) {
    831      filtered.global = filtered.global + 1;
    832      filtered[cause] = filtered[cause] + 1;
    833    }
    834  });
    835 
    836  const newState = {
    837    ...messagesState,
    838    visibleMessages: Array.from(messagesToShow),
    839    filteredMessagesCount: filtered,
    840  };
    841 
    842  maybeSortVisibleMessages(
    843    newState,
    844    // Only sort for warningGroups if the feature is enabled
    845    prefsState.groupSimilar,
    846    forceTimestampSort
    847  );
    848 
    849  return newState;
    850 }
    851 
    852 /**
    853 * Returns the new current group id given the previous current group and the groupsById
    854 * state property.
    855 *
    856 * @param {string} currentGroup: id of the current group
    857 * @param {Map} groupsById
    858 * @param {Array} ignoredIds: An array of ids which can't be the new current group.
    859 * @returns {string | null} The new current group id, or null if there isn't one.
    860 */
    861 function getNewCurrentGroup(currentGroup, groupsById, ignoredIds = new Set()) {
    862  if (!currentGroup) {
    863    return null;
    864  }
    865 
    866  // Retrieve the parent groups of the current group.
    867  const parents = groupsById.get(currentGroup);
    868 
    869  // If there's at least one parent, make the first one the new currentGroup.
    870  if (Array.isArray(parents) && parents.length) {
    871    // If the found group must be ignored, let's search for its parent.
    872    if (ignoredIds.has(parents[0])) {
    873      return getNewCurrentGroup(parents[0], groupsById, ignoredIds);
    874    }
    875 
    876    return parents[0];
    877  }
    878 
    879  return null;
    880 }
    881 
    882 function getParentGroups(currentGroup, groupsById) {
    883  let groups = [];
    884  if (currentGroup) {
    885    // If there is a current group, we add it as a parent
    886    groups = [currentGroup];
    887 
    888    // As well as all its parents, if it has some.
    889    const parentGroups = groupsById.get(currentGroup);
    890    if (Array.isArray(parentGroups) && parentGroups.length) {
    891      groups = groups.concat(parentGroups);
    892    }
    893  }
    894 
    895  return groups;
    896 }
    897 
    898 function getOutermostGroup(message, groupsById) {
    899  const groups = getParentGroups(message.groupId, groupsById);
    900  if (groups.length === 0) {
    901    return null;
    902  }
    903  return groups[groups.length - 1];
    904 }
    905 
    906 /**
    907 * Remove all top level messages that exceeds message limit.
    908 * Also populate an array of all backend actors associated with these
    909 * messages so they can be released.
    910 */
    911 function limitTopLevelMessageCount(newState, logLimit) {
    912  let topLevelCount =
    913    newState.groupsById.size === 0
    914      ? newState.mutableMessagesById.size
    915      : getToplevelMessageCount(newState);
    916 
    917  if (topLevelCount <= logLimit) {
    918    return newState;
    919  }
    920 
    921  const removedMessagesId = new Set();
    922 
    923  let cleaningGroup = false;
    924  for (const id of newState.mutableMessagesOrder) {
    925    const message = newState.mutableMessagesById.get(id);
    926    // If we were cleaning a group and the current message does not have
    927    // a groupId, we're done cleaning.
    928    if (cleaningGroup === true && !message.groupId) {
    929      cleaningGroup = false;
    930    }
    931 
    932    // If we're not cleaning a group and the message count is below the logLimit,
    933    // we exit the loop.
    934    if (cleaningGroup === false && topLevelCount <= logLimit) {
    935      break;
    936    }
    937 
    938    // If we're not currently cleaning a group, and the current message is identified
    939    // as a group, set the cleaning flag to true.
    940    if (cleaningGroup === false && newState.groupsById.has(id)) {
    941      cleaningGroup = true;
    942    }
    943 
    944    if (!message.groupId) {
    945      topLevelCount--;
    946    }
    947 
    948    removedMessagesId.add(id);
    949  }
    950 
    951  return removeMessagesFromState(newState, removedMessagesId);
    952 }
    953 
    954 /**
    955 * Clean the properties for a given state object and an array of removed messages ids.
    956 * Be aware that this function MUTATE the `state` argument.
    957 *
    958 * @param {MessageState} state
    959 * @param {Set} removedMessagesIds
    960 * @returns {MessageState}
    961 */
    962 function removeMessagesFromState(state, removedMessagesIds) {
    963  if (removedMessagesIds.size === 0) {
    964    return state;
    965  }
    966 
    967  const frontsToRelease = [];
    968  const visibleMessages = [...state.visibleMessages];
    969  removedMessagesIds.forEach(id => {
    970    const index = visibleMessages.indexOf(id);
    971    if (index > -1) {
    972      visibleMessages.splice(index, 1);
    973    }
    974 
    975    // We want to minimize time spent in reducer as much as we can, so we're using
    976    // prototype.push.apply here as it seems faster than other solutions (e.g. the
    977    // spread operator, Array#concat, …)
    978    Array.prototype.push.apply(
    979      frontsToRelease,
    980      getAllFrontsInMessage(state.mutableMessagesById.get(id))
    981    );
    982  });
    983 
    984  if (state.visibleMessages.length > visibleMessages.length) {
    985    state.visibleMessages = visibleMessages;
    986  }
    987 
    988  if (frontsToRelease.length) {
    989    state.frontsToRelease = state.frontsToRelease.concat(frontsToRelease);
    990  }
    991 
    992  const isInRemovedId = id => removedMessagesIds.has(id);
    993  const mapHasRemovedIdKey = map => {
    994    for (const id of removedMessagesIds) {
    995      if (map.has(id)) {
    996        return true;
    997      }
    998    }
    999    return false;
   1000  };
   1001  const objectHasRemovedIdKey = obj =>
   1002    Object.keys(obj).findIndex(isInRemovedId) !== -1;
   1003 
   1004  const cleanUpMap = map => {
   1005    const clonedMap = new Map(map);
   1006    removedMessagesIds.forEach(id => clonedMap.delete(id));
   1007    return clonedMap;
   1008  };
   1009  const cleanUpObject = object =>
   1010    [...Object.entries(object)].reduce((res, [id, value]) => {
   1011      if (!isInRemovedId(id)) {
   1012        res[id] = value;
   1013      }
   1014      return res;
   1015    }, {});
   1016 
   1017  removedMessagesIds.forEach(id => {
   1018    state.mutableMessagesById.delete(id);
   1019 
   1020    state.mutableMessagesOrder.splice(
   1021      state.mutableMessagesOrder.indexOf(id),
   1022      1
   1023    );
   1024  });
   1025 
   1026  if (state.disabledMessagesById.find(isInRemovedId)) {
   1027    state.disabledMessagesById = state.disabledMessagesById.filter(
   1028      id => !isInRemovedId(id)
   1029    );
   1030  }
   1031 
   1032  if (state.messagesUiById.find(isInRemovedId)) {
   1033    state.messagesUiById = state.messagesUiById.filter(
   1034      id => !isInRemovedId(id)
   1035    );
   1036  }
   1037 
   1038  if (isInRemovedId(state.currentGroup)) {
   1039    state.currentGroup = getNewCurrentGroup(
   1040      state.currentGroup,
   1041      state.groupsById,
   1042      removedMessagesIds
   1043    );
   1044  }
   1045 
   1046  if (mapHasRemovedIdKey(state.cssMessagesMatchingElements)) {
   1047    state.cssMessagesMatchingElements = cleanUpMap(
   1048      state.cssMessagesMatchingElements
   1049    );
   1050  }
   1051  if (mapHasRemovedIdKey(state.groupsById)) {
   1052    state.groupsById = cleanUpMap(state.groupsById);
   1053  }
   1054 
   1055  if (objectHasRemovedIdKey(state.repeatById)) {
   1056    state.repeatById = cleanUpObject(state.repeatById);
   1057  }
   1058 
   1059  if (objectHasRemovedIdKey(state.networkMessagesUpdateById)) {
   1060    state.networkMessagesUpdateById = cleanUpObject(
   1061      state.networkMessagesUpdateById
   1062    );
   1063  }
   1064 
   1065  return state;
   1066 }
   1067 
   1068 /**
   1069 * Get an array of all the fronts logged in a specific message.
   1070 *
   1071 * @param {Message} message: The message to get actors from.
   1072 * @return {Array<ObjectFront|LongStringFront>} An array containing all the fronts logged
   1073 *                                              in a message.
   1074 */
   1075 function getAllFrontsInMessage(message) {
   1076  const { parameters, messageText } = message;
   1077 
   1078  const fronts = [];
   1079  const isFront = p => p && typeof p.release === "function";
   1080 
   1081  if (Array.isArray(parameters)) {
   1082    message.parameters.forEach(parameter => {
   1083      if (isFront(parameter)) {
   1084        fronts.push(parameter);
   1085      }
   1086    });
   1087  }
   1088 
   1089  if (isFront(messageText)) {
   1090    fronts.push(messageText);
   1091  }
   1092 
   1093  return fronts;
   1094 }
   1095 
   1096 /**
   1097 * Returns total count of top level messages (those which are not
   1098 * within a group).
   1099 */
   1100 function getToplevelMessageCount(state) {
   1101  let count = 0;
   1102  state.mutableMessagesById.forEach(message => {
   1103    if (!message.groupId) {
   1104      count++;
   1105    }
   1106  });
   1107  return count;
   1108 }
   1109 
   1110 /**
   1111 * Check if a message should be visible in the console output, and if not, what
   1112 * causes it to be hidden.
   1113 *
   1114 * @param {Message} message: The message to check
   1115 * @param {object} option: An option object of the following shape:
   1116 *                   - {MessageState} messagesState: The current messages state
   1117 *                   - {FilterState} filtersState: The current filters state
   1118 *                   - {PrefsState} prefsState: The current preferences state
   1119 *                   - {UiState} uiState: The current ui state
   1120 *                   - {Boolean} checkGroup: Set to false to not check if a message should
   1121 *                                 be visible because it is in a console.group.
   1122 *                   - {Boolean} checkParentWarningGroupVisibility: Set to false to not
   1123 *                                 check if a message should be visible because it is in a
   1124 *                                 warningGroup and the warningGroup is visible.
   1125 *                   - {Boolean} hasMatchedAncestor: Set to true if message is part of a
   1126 *                                 group that has been set to visible
   1127 *
   1128 * @return {object} An object of the following form:
   1129 *         - visible {Boolean}: true if the message should be visible
   1130 *         - cause {String}: if visible is false, what causes the message to be hidden.
   1131 */
   1132 // eslint-disable-next-line complexity
   1133 function getMessageVisibility(
   1134  message,
   1135  {
   1136    messagesState,
   1137    filtersState,
   1138    prefsState,
   1139    uiState,
   1140    checkGroup = true,
   1141    checkParentWarningGroupVisibility = true,
   1142    hasMatchedAncestor = false,
   1143  }
   1144 ) {
   1145  const warningGroupMessageId = getParentWarningGroupMessageId(message);
   1146  const parentWarningGroupMessage = messagesState.mutableMessagesById.get(
   1147    warningGroupMessageId
   1148  );
   1149 
   1150  // Do not display the message if it's in closed group and not in a warning group.
   1151  if (
   1152    checkGroup &&
   1153    !isInOpenedGroup(
   1154      message,
   1155      messagesState.groupsById,
   1156      messagesState.messagesUiById
   1157    ) &&
   1158    !shouldGroupWarningMessages(
   1159      parentWarningGroupMessage,
   1160      messagesState,
   1161      prefsState
   1162    )
   1163  ) {
   1164    return {
   1165      visible: false,
   1166      cause: "closedGroup",
   1167    };
   1168  }
   1169 
   1170  // If the message is a warningGroup, check if it should be displayed.
   1171  if (isWarningGroup(message)) {
   1172    if (!shouldGroupWarningMessages(message, messagesState, prefsState)) {
   1173      return {
   1174        visible: false,
   1175        cause: "warningGroupHeuristicNotMet",
   1176      };
   1177    }
   1178 
   1179    // Hide a warningGroup if the warning filter is off.
   1180    if (!filtersState[FILTERS.WARN]) {
   1181      // We don't include any cause as we don't want that message to be reflected in the
   1182      // message count.
   1183      return {
   1184        visible: false,
   1185      };
   1186    }
   1187 
   1188    // Display a warningGroup if at least one of its message will be visible.
   1189    const childrenMessages = messagesState.warningGroupsById.get(message.id);
   1190    const hasVisibleChild =
   1191      childrenMessages &&
   1192      childrenMessages.some(id => {
   1193        const child = messagesState.mutableMessagesById.get(id);
   1194        if (!child) {
   1195          return false;
   1196        }
   1197 
   1198        const { visible, cause } = getMessageVisibility(child, {
   1199          messagesState,
   1200          filtersState,
   1201          prefsState,
   1202          uiState,
   1203          checkParentWarningGroupVisibility: false,
   1204        });
   1205        return visible && cause !== "visibleWarningGroup";
   1206      });
   1207 
   1208    if (hasVisibleChild) {
   1209      return {
   1210        visible: true,
   1211        cause: "visibleChild",
   1212      };
   1213    }
   1214  }
   1215 
   1216  // Do not display the message if it can be in a warningGroup, and the group is
   1217  // displayed but collapsed.
   1218  if (
   1219    parentWarningGroupMessage &&
   1220    shouldGroupWarningMessages(
   1221      parentWarningGroupMessage,
   1222      messagesState,
   1223      prefsState
   1224    ) &&
   1225    !messagesState.messagesUiById.includes(warningGroupMessageId)
   1226  ) {
   1227    return {
   1228      visible: false,
   1229      cause: "closedWarningGroup",
   1230    };
   1231  }
   1232 
   1233  // Display a message if it is in a warningGroup that is visible. We don't check the
   1234  // warningGroup visibility if `checkParentWarningGroupVisibility` is false, because
   1235  // it means we're checking the warningGroup visibility based on the visibility of its
   1236  // children, which would cause an infinite loop.
   1237  const parentVisibility =
   1238    parentWarningGroupMessage && checkParentWarningGroupVisibility
   1239      ? getMessageVisibility(parentWarningGroupMessage, {
   1240          messagesState,
   1241          filtersState,
   1242          prefsState,
   1243          uiState,
   1244          checkGroup,
   1245          checkParentWarningGroupVisibility,
   1246        })
   1247      : null;
   1248  if (
   1249    parentVisibility &&
   1250    parentVisibility.visible &&
   1251    parentVisibility.cause !== "visibleChild"
   1252  ) {
   1253    return {
   1254      visible: true,
   1255      cause: "visibleWarningGroup",
   1256    };
   1257  }
   1258 
   1259  // Some messages can't be filtered out (e.g. groups).
   1260  // So, always return visible: true for those.
   1261  if (isUnfilterable(message)) {
   1262    return {
   1263      visible: true,
   1264    };
   1265  }
   1266 
   1267  // Let's check all level filters (error, warn, log, …) and return visible: false
   1268  // and the message level as a cause if the function returns false.
   1269  if (!passLevelFilters(message, filtersState)) {
   1270    return {
   1271      visible: false,
   1272      cause: message.level,
   1273    };
   1274  }
   1275 
   1276  if (!passCssFilters(message, filtersState)) {
   1277    return {
   1278      visible: false,
   1279      cause: FILTERS.CSS,
   1280    };
   1281  }
   1282 
   1283  if (!passNetworkFilter(message, filtersState)) {
   1284    return {
   1285      visible: false,
   1286      cause: FILTERS.NET,
   1287    };
   1288  }
   1289 
   1290  if (!passXhrFilter(message, filtersState)) {
   1291    return {
   1292      visible: false,
   1293      cause: FILTERS.NETXHR,
   1294    };
   1295  }
   1296 
   1297  // This should always be the last check, or we might report that a message was hidden
   1298  // because of text search, while it may be hidden because its category is disabled.
   1299  // Do not check for search filters if it is part of a group and one of its ancestor
   1300  // has matched the current search filters and set to visible
   1301  if (!hasMatchedAncestor && !passSearchFilters(message, filtersState)) {
   1302    return {
   1303      visible: false,
   1304      cause: FILTERS.TEXT,
   1305    };
   1306  }
   1307 
   1308  return {
   1309    visible: true,
   1310  };
   1311 }
   1312 
   1313 function isUnfilterable(message) {
   1314  return [
   1315    MESSAGE_TYPE.COMMAND,
   1316    MESSAGE_TYPE.RESULT,
   1317    MESSAGE_TYPE.NAVIGATION_MARKER,
   1318  ].includes(message.type);
   1319 }
   1320 
   1321 function isInOpenedGroup(message, groupsById, messagesUI) {
   1322  return (
   1323    !message.groupId ||
   1324    (!isGroupClosed(message.groupId, messagesUI) &&
   1325      !hasClosedParentGroup(groupsById.get(message.groupId), messagesUI))
   1326  );
   1327 }
   1328 
   1329 function hasClosedParentGroup(group, messagesUI) {
   1330  return group.some(groupId => isGroupClosed(groupId, messagesUI));
   1331 }
   1332 
   1333 function isGroupClosed(groupId, messagesUI) {
   1334  return messagesUI.includes(groupId) === false;
   1335 }
   1336 
   1337 /**
   1338 * Returns true if the message shouldn't be hidden because of the network filter state.
   1339 *
   1340 * @param {object} message - The message to check the filter against.
   1341 * @param {FilterState} filters - redux "filters" state.
   1342 * @returns {boolean}
   1343 */
   1344 function passNetworkFilter(message, filters) {
   1345  // The message passes the filter if it is not a network message,
   1346  // or if it is an xhr one,
   1347  // or if the network filter is on.
   1348  return (
   1349    message.source !== MESSAGE_SOURCE.NETWORK ||
   1350    message.isXHR === true ||
   1351    filters[FILTERS.NET] === true ||
   1352    (filters[FILTERS.ERROR] && isMessageNetworkError(message))
   1353  );
   1354 }
   1355 
   1356 /**
   1357 * Returns true if the message shouldn't be hidden because of the xhr filter state.
   1358 *
   1359 * @param {object} message - The message to check the filter against.
   1360 * @param {FilterState} filters - redux "filters" state.
   1361 * @returns {boolean}
   1362 */
   1363 function passXhrFilter(message, filters) {
   1364  // The message passes the filter if it is not a network message,
   1365  // or if it is a non-xhr one,
   1366  // or if the xhr filter is on.
   1367  return (
   1368    message.source !== MESSAGE_SOURCE.NETWORK ||
   1369    message.isXHR === false ||
   1370    filters[FILTERS.NETXHR] === true ||
   1371    (filters[FILTERS.ERROR] && isMessageNetworkError(message))
   1372  );
   1373 }
   1374 
   1375 /**
   1376 * Returns true if the message shouldn't be hidden because of levels filter state.
   1377 *
   1378 * @param {object} message - The message to check the filter against.
   1379 * @param {FilterState} filters - redux "filters" state.
   1380 * @returns {boolean}
   1381 */
   1382 function passLevelFilters(message, filters) {
   1383  // The message passes the filter if it is not a console call,
   1384  // or if its level matches the state of the corresponding filter.
   1385  return (
   1386    (message.source !== MESSAGE_SOURCE.CONSOLE_API &&
   1387      message.source !== MESSAGE_SOURCE.JAVASCRIPT) ||
   1388    filters[message.level] === true ||
   1389    (filters[FILTERS.ERROR] && isMessageNetworkError(message))
   1390  );
   1391 }
   1392 
   1393 /**
   1394 * Returns true if the message shouldn't be hidden because of the CSS filter state.
   1395 *
   1396 * @param {object} message - The message to check the filter against.
   1397 * @param {FilterState} filters - redux "filters" state.
   1398 * @returns {boolean}
   1399 */
   1400 function passCssFilters(message, filters) {
   1401  // The message passes the filter if it is not a CSS message,
   1402  // or if the CSS filter is on.
   1403  return message.source !== MESSAGE_SOURCE.CSS || filters.css === true;
   1404 }
   1405 
   1406 /**
   1407 * Returns true if the message shouldn't be hidden because of search filter state.
   1408 *
   1409 * @param {object} message - The message to check the filter against.
   1410 * @param {FilterState} filters - redux "filters" state.
   1411 * @returns {boolean}
   1412 */
   1413 function passSearchFilters(message, filters) {
   1414  const trimmed = (filters.text || "").trim();
   1415 
   1416  // "-"-prefix switched to exclude mode
   1417  const exclude = trimmed.startsWith("-");
   1418  const term = exclude ? trimmed.slice(1) : trimmed;
   1419 
   1420  // This regex matches a very basic regex with an optional i flag
   1421  const regexMatch = /^\/(?<search>.+)\/(?<flags>i)?$/.exec(term);
   1422  let regex;
   1423  if (regexMatch !== null) {
   1424    const flags = "m" + (regexMatch.groups.flags || "");
   1425    try {
   1426      regex = new RegExp(regexMatch.groups.search, flags);
   1427    } catch (e) {}
   1428  }
   1429  const matchStr = regex
   1430    ? str => regex.test(str)
   1431    : str => str.toLocaleLowerCase().includes(term.toLocaleLowerCase());
   1432 
   1433  // If there is no search, the message passes the filter.
   1434  if (!term) {
   1435    return true;
   1436  }
   1437 
   1438  const matched =
   1439    // Look for a match in parameters.
   1440    isTextInParameters(matchStr, message.parameters) ||
   1441    // Look for a match in location.
   1442    isTextInFrame(matchStr, message.frame) ||
   1443    // Look for a match in net events.
   1444    isTextInNetEvent(matchStr, message) ||
   1445    // Look for a match in stack-trace.
   1446    isTextInStackTrace(matchStr, message.stacktrace) ||
   1447    // Look for a match in messageText.
   1448    isTextInMessageText(matchStr, message.messageText) ||
   1449    // Look for a match in notes.
   1450    isTextInNotes(matchStr, message.notes) ||
   1451    // Look for a match in prefix.
   1452    isTextInPrefix(matchStr, message.prefix) ||
   1453    // Look for a match in displayName.
   1454    isTextInDisplayName(matchStr, message.displayName);
   1455 
   1456  return matched ? !exclude : exclude;
   1457 }
   1458 
   1459 /**
   1460 * Returns true if given text is included in provided stack frame.
   1461 */
   1462 function isTextInFrame(matchStr, frame) {
   1463  if (!frame) {
   1464    return false;
   1465  }
   1466 
   1467  const { functionName, line, column, source } = frame;
   1468  const { short } = getSourceNames(source);
   1469  const unicodeShort = getUnicodeUrlPath(short);
   1470 
   1471  const str = `${
   1472    functionName ? functionName + " " : ""
   1473  }${unicodeShort}:${line}:${column}`;
   1474  return matchStr(str);
   1475 }
   1476 
   1477 /**
   1478 * Returns true if given text is included in provided parameters.
   1479 */
   1480 function isTextInParameters(matchStr, parameters) {
   1481  if (!parameters) {
   1482    return false;
   1483  }
   1484 
   1485  return parameters.some(parameter => isTextInParameter(matchStr, parameter));
   1486 }
   1487 
   1488 /**
   1489 * Returns true if given text is included in provided parameter.
   1490 */
   1491 function isTextInParameter(matchStr, parameter) {
   1492  const paramGrip =
   1493    parameter && parameter.getGrip ? parameter.getGrip() : parameter;
   1494 
   1495  if (paramGrip && paramGrip.class && matchStr(paramGrip.class)) {
   1496    return true;
   1497  }
   1498 
   1499  const parameterType = typeof parameter;
   1500  if (parameterType !== "object" && parameterType !== "undefined") {
   1501    const str = paramGrip + "";
   1502    if (matchStr(str)) {
   1503      return true;
   1504    }
   1505  }
   1506 
   1507  const previewItems = getGripPreviewItems(paramGrip);
   1508  for (const item of previewItems) {
   1509    if (isTextInParameter(matchStr, item)) {
   1510      return true;
   1511    }
   1512  }
   1513 
   1514  if (paramGrip && paramGrip.ownProperties) {
   1515    for (const [key, desc] of Object.entries(paramGrip.ownProperties)) {
   1516      if (matchStr(key)) {
   1517        return true;
   1518      }
   1519 
   1520      if (isTextInParameter(matchStr, getDescriptorValue(desc))) {
   1521        return true;
   1522      }
   1523    }
   1524  }
   1525 
   1526  return false;
   1527 }
   1528 
   1529 /**
   1530 * Returns true if given text is included in provided net event grip.
   1531 */
   1532 function isTextInNetEvent(matchStr, { method, url } = {}) {
   1533  if (!method && !url) {
   1534    return false;
   1535  }
   1536  return matchStr(method) || matchStr(url);
   1537 }
   1538 
   1539 /**
   1540 * Returns true if given text is included in provided stack trace.
   1541 */
   1542 function isTextInStackTrace(matchStr, stacktrace) {
   1543  if (!Array.isArray(stacktrace)) {
   1544    return false;
   1545  }
   1546 
   1547  // isTextInFrame expect the properties of the frame object to be in the same
   1548  // order they are rendered in the Frame component.
   1549  return stacktrace.some(frame =>
   1550    isTextInFrame(matchStr, {
   1551      functionName:
   1552        frame.functionName || l10n.getStr("stacktrace.anonymousFunction"),
   1553      source: frame.filename,
   1554      lineNumber: frame.lineNumber,
   1555      columnNumber: frame.columnNumber,
   1556    })
   1557  );
   1558 }
   1559 
   1560 /**
   1561 * Returns true if given text is included in `messageText` field.
   1562 */
   1563 function isTextInMessageText(matchStr, messageText) {
   1564  if (!messageText) {
   1565    return false;
   1566  }
   1567 
   1568  if (typeof messageText === "string") {
   1569    return matchStr(messageText);
   1570  }
   1571 
   1572  const grip =
   1573    messageText && messageText.getGrip ? messageText.getGrip() : messageText;
   1574  if (grip && grip.type === "longString") {
   1575    return matchStr(grip.initial);
   1576  }
   1577 
   1578  return true;
   1579 }
   1580 
   1581 /**
   1582 * Returns true if given text is included in JS Trace display name.
   1583 */
   1584 function isTextInDisplayName(matchStr, displayName) {
   1585  return displayName && matchStr(displayName);
   1586 }
   1587 
   1588 /**
   1589 * Returns true if given text is included in notes.
   1590 */
   1591 function isTextInNotes(matchStr, notes) {
   1592  if (!Array.isArray(notes)) {
   1593    return false;
   1594  }
   1595 
   1596  return notes.some(
   1597    note =>
   1598      // Look for a match in location.
   1599      isTextInFrame(matchStr, note.frame) ||
   1600      // Look for a match in messageBody.
   1601      (note.messageBody && matchStr(note.messageBody))
   1602  );
   1603 }
   1604 
   1605 /**
   1606 * Returns true if given text is included in prefix.
   1607 */
   1608 function isTextInPrefix(matchStr, prefix) {
   1609  if (!prefix) {
   1610    return false;
   1611  }
   1612 
   1613  return matchStr(`${prefix}: `);
   1614 }
   1615 
   1616 function getDefaultFiltersCounter() {
   1617  const count = DEFAULT_FILTERS.reduce((res, filter) => {
   1618    res[filter] = 0;
   1619    return res;
   1620  }, {});
   1621  count.global = 0;
   1622  return count;
   1623 }
   1624 
   1625 /**
   1626 * Sort state.visibleMessages if needed.
   1627 *
   1628 * @param {MessageState} state
   1629 * @param {boolean} sortWarningGroupMessage: set to true to sort warningGroup
   1630 *                                           messages. Default to false, as in some
   1631 *                                           situations we already take care of putting
   1632 *                                           the ids at the right position.
   1633 * @param {boolean} timeStampSort: set to true to sort messages by their timestamps.
   1634 */
   1635 function maybeSortVisibleMessages(
   1636  state,
   1637  sortWarningGroupMessage = false,
   1638  timeStampSort = false
   1639 ) {
   1640  if (state.warningGroupsById.size > 0 && sortWarningGroupMessage) {
   1641    state.visibleMessages.sort((a, b) => {
   1642      const messageA = state.mutableMessagesById.get(a);
   1643      const messageB = state.mutableMessagesById.get(b);
   1644 
   1645      const warningGroupIdA = getParentWarningGroupMessageId(messageA);
   1646      const warningGroupIdB = getParentWarningGroupMessageId(messageB);
   1647 
   1648      const warningGroupA = state.mutableMessagesById.get(warningGroupIdA);
   1649      const warningGroupB = state.mutableMessagesById.get(warningGroupIdB);
   1650 
   1651      const aFirst = -1;
   1652      const bFirst = 1;
   1653 
   1654      // If both messages are in a warningGroup, or if both are not in warningGroups.
   1655      if (
   1656        (warningGroupA && warningGroupB) ||
   1657        (!warningGroupA && !warningGroupB)
   1658      ) {
   1659        return getNaturalOrder(messageA, messageB);
   1660      }
   1661 
   1662      // If `a` is in a warningGroup (and `b` isn't).
   1663      if (warningGroupA) {
   1664        // If `b` is the warningGroup of `a`, `a` should be after `b`.
   1665        if (warningGroupIdA === messageB.id) {
   1666          return bFirst;
   1667        }
   1668        // `b` is a regular message, we place `a` before `b` if `b` came after `a`'s
   1669        // warningGroup.
   1670        return getNaturalOrder(warningGroupA, messageB);
   1671      }
   1672 
   1673      // If `b` is in a warningGroup (and `a` isn't).
   1674      if (warningGroupB) {
   1675        // If `a` is the warningGroup of `b`, `a` should be before `b`.
   1676        if (warningGroupIdB === messageA.id) {
   1677          return aFirst;
   1678        }
   1679        // `a` is a regular message, we place `a` after `b` if `a` came after `b`'s
   1680        // warningGroup.
   1681        return getNaturalOrder(messageA, warningGroupB);
   1682      }
   1683 
   1684      return 0;
   1685    });
   1686  }
   1687 
   1688  if (timeStampSort) {
   1689    state.visibleMessages.sort((a, b) => {
   1690      const messageA = state.mutableMessagesById.get(a);
   1691      const messageB = state.mutableMessagesById.get(b);
   1692      return getNaturalOrder(messageA, messageB);
   1693    });
   1694  }
   1695 }
   1696 
   1697 /**
   1698 * Returns if a given type of warning message should be grouped.
   1699 *
   1700 * @param {ConsoleMessage} warningGroupMessage
   1701 * @param {MessageState} messagesState
   1702 * @param {PrefsState} prefsState
   1703 */
   1704 function shouldGroupWarningMessages(
   1705  warningGroupMessage,
   1706  messagesState,
   1707  prefsState
   1708 ) {
   1709  if (!warningGroupMessage) {
   1710    return false;
   1711  }
   1712 
   1713  // Only group if the preference is ON.
   1714  if (!prefsState.groupSimilar) {
   1715    return false;
   1716  }
   1717 
   1718  // We group warning messages if there are at least 2 messages that could go in it.
   1719  const warningGroup = messagesState.warningGroupsById.get(
   1720    warningGroupMessage.id
   1721  );
   1722  if (!warningGroup || !Array.isArray(warningGroup)) {
   1723    return false;
   1724  }
   1725 
   1726  return warningGroup.length > 1;
   1727 }
   1728 
   1729 exports.messages = messages;