/* 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/. */ "use strict"; const { isGroupType, isMessageNetworkError, l10n, } = require("resource://devtools/client/webconsole/utils/messages.js"); const constants = require("resource://devtools/client/webconsole/constants.js"); const { DEFAULT_FILTERS, FILTERS, MESSAGE_TYPE, MESSAGE_SOURCE } = constants; loader.lazyRequireGetter( this, "getGripPreviewItems", "resource://devtools/client/shared/components/reps/index.js", true ); loader.lazyRequireGetter( this, "getUnicodeUrlPath", "resource://devtools/client/shared/unicode-url.js", true ); loader.lazyRequireGetter( this, "getSourceNames", "resource://devtools/client/shared/source-utils.js", true ); loader.lazyRequireGetter( this, [ "areMessagesSimilar", "createWarningGroupMessage", "isWarningGroup", "getWarningGroupType", "getDescriptorValue", "getParentWarningGroupMessageId", "getNaturalOrder", ], "resource://devtools/client/webconsole/utils/messages.js", true ); const { UPDATE_REQUEST, } = require("resource://devtools/client/netmonitor/src/constants.js"); const { processNetworkUpdates, } = require("resource://devtools/client/netmonitor/src/utils/request-utils.js"); const MessageState = overrides => Object.freeze( Object.assign( { // List of all the messages added to the console. Unlike other properties, this Map // will be mutated on state changes for performance reasons. mutableMessagesById: new Map(), // Array of message ids, in chronological order. We use a dedicated property to store // the order (instead of relying on the order of insertion in mutableMessagesById) // as we might receive messages that need to be inserted at a specific index. Doing // so on the Map can be costly, especially when the Map holds lots of messages. mutableMessagesOrder: [], // List of elements matching the selector of CSS Warning messages(populated // on-demand via the UI). cssMessagesMatchingElements: new Map(), // Array of the visible messages. visibleMessages: [], // Object for the filtered messages. filteredMessagesCount: getDefaultFiltersCounter(), // List of the message ids which are opened. messagesUiById: [], // Map of the form {groupMessageId : groupArray}, // where groupArray is the list of of all the parent groups' ids of the groupMessageId. // This handles console API groups. groupsById: new Map(), // Message id of the current console API group (no corresponding console.groupEnd yet). currentGroup: null, // This group handles "warning groups" (Content Blocking, CORS, CSP, …) warningGroupsById: new Map(), // Array of fronts to release (i.e. fronts logged in removed messages). // This array *should not* be consumed by any UI component. frontsToRelease: [], // Map of the form {messageId : numberOfRepeat} repeatById: {}, // Map of the form {messageId : networkInformation} // `networkInformation` holds request, response, totalTime, ... networkMessagesUpdateById: {}, // Id of the last messages that was added. lastMessageId: null, // List of the message ids which are disabled disabledMessagesById: [], }, overrides ) ); function cloneState(state) { return { visibleMessages: [...state.visibleMessages], filteredMessagesCount: { ...state.filteredMessagesCount }, messagesUiById: [...state.messagesUiById], cssMessagesMatchingElements: new Map(state.cssMessagesMatchingElements), groupsById: new Map(state.groupsById), frontsToRelease: [...state.frontsToRelease], repeatById: { ...state.repeatById }, networkMessagesUpdateById: { ...state.networkMessagesUpdateById }, warningGroupsById: new Map(state.warningGroupsById), // no need to mutate the properties below as they're not directly triggering re-render mutableMessagesById: state.mutableMessagesById, mutableMessagesOrder: state.mutableMessagesOrder, currentGroup: state.currentGroup, lastMessageId: state.lastMessageId, disabledMessagesById: [...state.disabledMessagesById], }; } /** * 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. */ // eslint-disable-next-line complexity function addMessage(newMessage, state, filtersState, prefsState, uiState) { const { mutableMessagesById, groupsById, repeatById } = state; if (newMessage.type === constants.MESSAGE_TYPE.NAVIGATION_MARKER) { // We set the state's currentGroup property to null after navigating state.currentGroup = null; } const { currentGroup } = state; if (newMessage.type === constants.MESSAGE_TYPE.NULL_MESSAGE) { // When the message has a NULL type, we don't add it. return state; } if (newMessage.type === constants.MESSAGE_TYPE.END_GROUP) { // Compute the new current group. state.currentGroup = getNewCurrentGroup(currentGroup, groupsById); return state; } const lastMessage = mutableMessagesById.get(state.lastMessageId); // It can happen that the new message was actually emitted earlier than the last message, // which means we need to insert it at the right position. const isUnsorted = lastMessage && lastMessage.timeStamp > newMessage.timeStamp; if (lastMessage && mutableMessagesById.size > 0) { if ( lastMessage.groupId === currentGroup && areMessagesSimilar(lastMessage, newMessage) ) { state.repeatById[lastMessage.id] = (repeatById[lastMessage.id] || 1) + 1; return state; } } // Store the id of the message as being the last one being added. if (!isUnsorted) { state.lastMessageId = newMessage.id; } // Add the new message with a reference to the parent group. const parentGroups = getParentGroups(currentGroup, groupsById); if (!isWarningGroup(newMessage)) { newMessage.groupId = currentGroup; newMessage.indent = parentGroups.length; } // Check if the current message could be placed in a Warning Group. // This needs to be done before setting the new message in mutableMessagesById so we have a // proper message. const warningGroupType = getWarningGroupType(newMessage); // If the preference for warning grouping is true, and the new message could be in a // warning group. if (prefsState.groupWarnings && warningGroupType !== null) { const warningGroupMessageId = getParentWarningGroupMessageId(newMessage); // If there's no warning group for the type/innerWindowID yet if (!state.mutableMessagesById.has(warningGroupMessageId)) { // We create it and add it to the store. const groupMessage = createWarningGroupMessage( warningGroupMessageId, warningGroupType, newMessage ); state = addMessage( groupMessage, state, filtersState, prefsState, uiState ); } // 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.mutableMessagesById.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.mutableMessagesById.get( firstWarningMessageId ); const outermostGroupId = getOutermostGroup( firstWarningMessage, groupsById ); const groupIndex = state.visibleMessages.indexOf(outermostGroupId); const warningMessageIndex = state.visibleMessages.indexOf( firstWarningMessageId ); if (groupIndex > -1) { // We remove the warning message if (warningMessageIndex > -1) { state.visibleMessages.splice(warningMessageIndex, 1); } // And we put the warning group before the console.group state.visibleMessages.splice(groupIndex, 0, warningGroupMessageId); } else { // If the warning message is not in a console.group, we replace it by the // warning group message. state.visibleMessages.splice( warningMessageIndex, 1, warningGroupMessageId ); } } } // If we're creating a warningGroup, we init the array for its children. if (isWarningGroup(newMessage)) { state.warningGroupsById.set(newMessage.id, []); } const addedMessage = Object.freeze(newMessage); // If the new message isn't the "oldest" one, then we need to insert it at the right // position in the message map. if (isUnsorted) { let newMessageIndex = 0; // This is can be on a hot path, so we're not using `findIndex`, which could be slow. // Furthermore, there's a high chance the message beed to be inserted somewhere at the // end of the list, so we loop through mutableMessagesOrder in reverse order. for (let i = state.mutableMessagesOrder.length - 1; i >= 0; i--) { const message = state.mutableMessagesById.get( state.mutableMessagesOrder[i] ); if (message.timeStamp <= addedMessage.timeStamp) { newMessageIndex = i + 1; break; } } state.mutableMessagesOrder.splice(newMessageIndex, 0, addedMessage.id); } else { state.mutableMessagesOrder.push(addedMessage.id); } state.mutableMessagesById.set(addedMessage.id, addedMessage); if (newMessage.type === "trace") { // We want the stacktrace to be open by default. state.messagesUiById.push(newMessage.id); } else if (isGroupType(newMessage.type)) { state.currentGroup = newMessage.id; state.groupsById.set(newMessage.id, parentGroups); if (newMessage.type === constants.MESSAGE_TYPE.START_GROUP) { // We want the group to be open by default. 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. let index = state.visibleMessages.indexOf(warningGroupId); // We loop backward through the warning group's messages to get the latest visible // messages in it. const messagesInWarningGroup = state.warningGroupsById.get(warningGroupId); for (let i = messagesInWarningGroup.length - 1; i >= 0; i--) { const idx = state.visibleMessages.indexOf(messagesInWarningGroup[i]); if (idx > -1) { index = idx; break; } } // Inserts the new warning message at the wanted location "in" the warning group. state.visibleMessages.splice(index + 1, 0, newMessage.id); } else if (isUnsorted) { // If the new message wasn't the "oldest" one, then we need to insert its id at // the right position in the array. // This is can be on a hot path, so we're not using `findIndex`, which could be slow. // Furthermore, there's a high chance the message beed to be inserted somewhere at the // end of the list, so we loop through visibleMessages in reverse order. let index = 0; for (let i = state.visibleMessages.length - 1; i >= 0; i--) { const id = state.visibleMessages[i]; if ( state.mutableMessagesById.get(id).timeStamp <= newMessage.timeStamp ) { index = i + 1; break; } } state.visibleMessages.splice(index, 0, newMessage.id); } else { state.visibleMessages.push(newMessage.id); } maybeSortVisibleMessages(state, false); } else if (DEFAULT_FILTERS.includes(cause)) { state.filteredMessagesCount.global++; state.filteredMessagesCount[cause]++; } // Append received network-data also into networkMessagesUpdateById // that is responsible for collecting (lazy loaded) HTTP payload data. if (newMessage.source == "network") { state.networkMessagesUpdateById[newMessage.actor] = newMessage; } return state; } // eslint-disable-next-line complexity function messages( state = MessageState(), action, filtersState, prefsState, uiState ) { const { mutableMessagesById, cssMessagesMatchingElements, messagesUiById, networkMessagesUpdateById, groupsById, visibleMessages, disabledMessagesById, } = state; const { logLimit } = prefsState; let newState; switch (action.type) { case constants.MESSAGES_ADD: // If the action holds more messages than the log limit, we can preemptively remove // messages that will never be rendered. const batchHasMoreMessagesThanLogLimit = action.messages.length > logLimit; const list = batchHasMoreMessagesThanLogLimit ? [] : action.messages; if (batchHasMoreMessagesThanLogLimit) { let prunableCount = 0; let lastMessage = null; for (let i = action.messages.length - 1; i >= 0; i--) { const message = action.messages[i]; if ( !message.groupId && !isGroupType(message.type) && message.type !== MESSAGE_TYPE.END_GROUP ) { const messagesSimilar = areMessagesSimilar(lastMessage, message); if (!messagesSimilar) { prunableCount++; } // Once we've added the max number of messages that can be added, stop. // Except for repeated messages, where we keep adding over the limit. if (prunableCount <= logLimit || messagesSimilar) { list.unshift(action.messages[i]); } else { break; } } else { list.unshift(message); } lastMessage = message; } } newState = cloneState(state); for (const message of list) { newState = addMessage( message, newState, filtersState, prefsState, uiState ); } return limitTopLevelMessageCount(newState, logLimit); case constants.MESSAGES_CLEAR: const frontsToRelease = []; for (const message of state.mutableMessagesById.values()) { // We want to minimize time spent in reducer as much as we can, so we're using // prototype.push.apply here as it seems faster than other solutions (e.g. the // spread operator, Array#concat, …) Array.prototype.push.apply( frontsToRelease, getAllFrontsInMessage(message) ); } return MessageState({ // Store all actors from removed messages. This array is used by // `releaseActorsEnhancer` to release all of those backend actors. frontsToRelease, }); case constants.PRIVATE_MESSAGES_CLEAR: { const removedIds = []; for (const [id, message] of mutableMessagesById) { if (message.private === true) { removedIds.push(id); } } // If there's no private messages, there's no need to change the state. if (removedIds.length === 0) { return state; } return removeMessagesFromState( { ...state, }, removedIds ); } case constants.TARGET_MESSAGES_REMOVE: { const removedIds = []; for (const [id, message] of mutableMessagesById) { // Remove message from the target but not evaluations and their results, so // 1. we're consistent with the filtering behavior, i.e. we never hide those // 2. when switching mode from multiprocess to parent process and back to multi, // if we'd clear evaluations we wouldn't have a way to get them back, unlike // log messages and errors, which are still available in the server caches). if ( message.targetFront == action.targetFront && message.type !== MESSAGE_TYPE.COMMAND && message.type !== MESSAGE_TYPE.RESULT ) { removedIds.push(id); } } return removeMessagesFromState( { ...state, }, removedIds ); } case constants.MESSAGES_DISABLE: return { ...state, disabledMessagesById: [...disabledMessagesById, ...action.ids], }; case constants.MESSAGE_OPEN: const openState = { ...state }; openState.messagesUiById = [...messagesUiById, action.id]; const currMessage = mutableMessagesById.get(action.id); // If the message is a console.group/groupCollapsed or a warning group. if (isGroupType(currMessage.type) || isWarningGroup(currMessage)) { // We want to make its children visible const messagesToShow = []; for (const id of state.mutableMessagesOrder) { const message = mutableMessagesById.get(id); if ( !visibleMessages.includes(message.id) && ((isWarningGroup(currMessage) && !!getWarningGroupType(message)) || (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 ) { messagesToShow.push(id); } } // We can then insert the messages ids right after the one of the group. const insertIndex = visibleMessages.indexOf(action.id) + 1; openState.visibleMessages = [ ...visibleMessages.slice(0, insertIndex), ...messagesToShow, ...visibleMessages.slice(insertIndex), ]; } return openState; case constants.MESSAGE_CLOSE: const closeState = { ...state }; const messageId = action.id; const index = closeState.messagesUiById.indexOf(messageId); closeState.messagesUiById.splice(index, 1); closeState.messagesUiById = [...closeState.messagesUiById]; // If the message is a group if (isGroupType(mutableMessagesById.get(messageId).type)) { // Hide all its children, unless they're in a warningGroup. closeState.visibleMessages = visibleMessages.filter((id, i, arr) => { const message = mutableMessagesById.get(id); const warningGroupMessage = mutableMessagesById.get( getParentWarningGroupMessageId(message) ); // If the message is in a warning group, then we return its current visibility. if ( shouldGroupWarningMessages( warningGroupMessage, closeState, prefsState ) ) { return arr.includes(id); } const parentGroups = getParentGroups(message.groupId, groupsById); return parentGroups.includes(messageId) === false; }); } else if (isWarningGroup(mutableMessagesById.get(messageId))) { // If the message was a warningGroup, we hide all the messages in the group. const groupMessages = closeState.warningGroupsById.get(messageId); closeState.visibleMessages = visibleMessages.filter( id => !groupMessages.includes(id) ); } return closeState; case constants.CSS_MESSAGE_ADD_MATCHING_ELEMENTS: return { ...state, cssMessagesMatchingElements: new Map(cssMessagesMatchingElements).set( action.id, action.elements ), }; case constants.NETWORK_MESSAGES_UPDATE: const updatedState = { ...state, networkMessagesUpdateById: { ...networkMessagesUpdateById, }, }; let hasNetworkError = null; for (const message of action.messages) { const { id } = message; updatedState.mutableMessagesById.set(id, message); updatedState.networkMessagesUpdateById[id] = { ...(updatedState.networkMessagesUpdateById[id] || {}), ...message, }; if (isMessageNetworkError(message)) { hasNetworkError = true; } } // If the message updates contained a network error, then we may have to display it. if (hasNetworkError) { return setVisibleMessages({ messagesState: updatedState, filtersState, prefsState, uiState, }); } return updatedState; case UPDATE_REQUEST: case constants.NETWORK_UPDATES_REQUEST: { newState = { ...state, networkMessagesUpdateById: { ...networkMessagesUpdateById, }, }; // Netmonitor's UPDATE_REQUEST action comes for only one request const updates = action.type == UPDATE_REQUEST ? [{ id: action.id, data: action.data }] : action.updates; for (const { id, data } of updates) { const request = newState.networkMessagesUpdateById[id]; if (!request) { continue; } newState.networkMessagesUpdateById[id] = { ...request, ...processNetworkUpdates(data), }; } return newState; } case constants.FRONTS_TO_RELEASE_CLEAR: return { ...state, frontsToRelease: [], }; case constants.WARNING_GROUPS_TOGGLE: // There's no warningGroups, and the pref was set to false, // we don't need to do anything. if (!prefsState.groupWarnings && state.warningGroupsById.size === 0) { return state; } let needSort = false; for (const msgId of state.mutableMessagesOrder) { const message = state.mutableMessagesById.get(msgId); const warningGroupType = getWarningGroupType(message); if (warningGroupType) { const warningGroupMessageId = getParentWarningGroupMessageId(message); // If there's no warning group for the type/innerWindowID yet. if (!state.mutableMessagesById.has(warningGroupMessageId)) { // We create it and add it to the store. const groupMessage = createWarningGroupMessage( warningGroupMessageId, warningGroupType, message ); state = addMessage( groupMessage, state, filtersState, prefsState, uiState ); } // We add the new message to the appropriate warningGroup. const warningGroup = state.warningGroupsById.get( warningGroupMessageId ); if (warningGroup && !warningGroup.includes(msgId)) { warningGroup.push(msgId); } needSort = true; } } // If we don't have any warning messages that could be in a group, we don't do // anything. if (!needSort) { return state; } return setVisibleMessages({ messagesState: state, filtersState, prefsState, uiState, // If the user disabled warning groups, we want the messages to be sorted by their // timestamps. forceTimestampSort: !prefsState.groupWarnings, }); case constants.MESSAGE_REMOVE: return removeMessagesFromState( { ...state, }, [action.id] ); case constants.FILTER_TOGGLE: case constants.FILTER_TEXT_SET: case constants.FILTERS_CLEAR: case constants.DEFAULT_FILTERS_RESET: return setVisibleMessages({ messagesState: state, filtersState, prefsState, uiState, }); } return state; } function setVisibleMessages({ messagesState, filtersState, prefsState, uiState, forceTimestampSort = false, }) { const { mutableMessagesById, mutableMessagesOrder, visibleMessages, messagesUiById, } = messagesState; const messagesToShow = new Set(); const matchedGroups = new Set(); const filtered = getDefaultFiltersCounter(); mutableMessagesOrder.forEach(msgId => { const message = mutableMessagesById.get(msgId); const groupParentId = message.groupId; let hasMatchedAncestor = false; const ancestors = []; if (groupParentId) { let ancestorId = groupParentId; // we track the message's ancestors and their state while (ancestorId) { ancestors.push({ ancestorId, matchedFilters: matchedGroups.has(ancestorId), isOpen: messagesUiById.includes(ancestorId), isCurrentlyVisible: visibleMessages.includes(ancestorId), }); if (!hasMatchedAncestor && matchedGroups.has(ancestorId)) { hasMatchedAncestor = true; } ancestorId = mutableMessagesById.get(ancestorId).groupId; } } const { visible, cause } = getMessageVisibility(message, { messagesState, filtersState, prefsState, uiState, hasMatchedAncestor, }); // if the message is not visible but passes the search filters, we show its visible ancestors if (!visible && passSearchFilters(message, filtersState)) { const tmp = []; ancestors.forEach(msg => { if (msg.isCurrentlyVisible) { tmp.push(msg.ancestorId); } }); tmp.reverse().forEach(id => { messagesToShow.add(id); }); } if (visible) { // if the visible message is a child of a group, we add its ancestors to the visible messages if (groupParentId) { // We need to reverse the visibleAncestors array to show the groups in the correct order ancestors.reverse().forEach(msg => { messagesToShow.add(msg.ancestorId); }); } // we keep track of matched startGroup and startGroupCollapsed messages so we don't filter their children if ( message.type === "startGroup" || message.type === "startGroupCollapsed" ) { matchedGroups.add(msgId); } messagesToShow.add(msgId); } else if (DEFAULT_FILTERS.includes(cause)) { filtered.global = filtered.global + 1; filtered[cause] = filtered[cause] + 1; } }); const newState = { ...messagesState, visibleMessages: Array.from(messagesToShow), filteredMessagesCount: filtered, }; maybeSortVisibleMessages( newState, // Only sort for warningGroups if the feature is enabled prefsState.groupWarnings, forceTimestampSort ); return newState; } /** * Returns the new current group id given the previous current group and the groupsById * state property. * * @param {String} currentGroup: id of the current group * @param {Map} groupsById * @param {Array} ignoredIds: An array of ids which can't be the new current group. * @returns {String|null} The new current group id, or null if there isn't one. */ function getNewCurrentGroup(currentGroup, groupsById, ignoredIds = []) { if (!currentGroup) { return null; } // Retrieve the parent groups of the current group. const parents = groupsById.get(currentGroup); // If there's at least one parent, make the first one the new currentGroup. if (Array.isArray(parents) && parents.length) { // If the found group must be ignored, let's search for its parent. if (ignoredIds.includes(parents[0])) { return getNewCurrentGroup(parents[0], groupsById, ignoredIds); } return parents[0]; } return null; } function getParentGroups(currentGroup, groupsById) { let groups = []; if (currentGroup) { // If there is a current group, we add it as a parent groups = [currentGroup]; // As well as all its parents, if it has some. const parentGroups = groupsById.get(currentGroup); if (Array.isArray(parentGroups) && parentGroups.length) { groups = groups.concat(parentGroups); } } return groups; } function getOutermostGroup(message, groupsById) { const groups = getParentGroups(message.groupId, groupsById); if (groups.length === 0) { return null; } return groups[groups.length - 1]; } /** * Remove all top level messages that exceeds message limit. * Also populate an array of all backend actors associated with these * messages so they can be released. */ function limitTopLevelMessageCount(newState, logLimit) { let topLevelCount = newState.groupsById.size === 0 ? newState.mutableMessagesById.size : getToplevelMessageCount(newState); if (topLevelCount <= logLimit) { return newState; } const removedMessagesId = []; let cleaningGroup = false; for (const id of newState.mutableMessagesOrder) { const message = newState.mutableMessagesById.get(id); // If we were cleaning a group and the current message does not have // a groupId, we're done cleaning. if (cleaningGroup === true && !message.groupId) { cleaningGroup = false; } // If we're not cleaning a group and the message count is below the logLimit, // we exit the loop. if (cleaningGroup === false && topLevelCount <= logLimit) { break; } // If we're not currently cleaning a group, and the current message is identified // as a group, set the cleaning flag to true. if (cleaningGroup === false && newState.groupsById.has(id)) { cleaningGroup = true; } if (!message.groupId) { topLevelCount--; } removedMessagesId.push(id); } return removeMessagesFromState(newState, removedMessagesId); } /** * Clean the properties for a given state object and an array of removed messages ids. * Be aware that this function MUTATE the `state` argument. * * @param {MessageState} state * @param {Array} removedMessagesIds * @returns {MessageState} */ function removeMessagesFromState(state, removedMessagesIds) { if (!Array.isArray(removedMessagesIds) || removedMessagesIds.length === 0) { return state; } const frontsToRelease = []; const visibleMessages = [...state.visibleMessages]; removedMessagesIds.forEach(id => { const index = visibleMessages.indexOf(id); if (index > -1) { visibleMessages.splice(index, 1); } // We want to minimize time spent in reducer as much as we can, so we're using // prototype.push.apply here as it seems faster than other solutions (e.g. the // spread operator, Array#concat, …) Array.prototype.push.apply( frontsToRelease, getAllFrontsInMessage(state.mutableMessagesById.get(id)) ); }); if (state.visibleMessages.length > visibleMessages.length) { state.visibleMessages = visibleMessages; } if (frontsToRelease.length) { state.frontsToRelease = state.frontsToRelease.concat(frontsToRelease); } const isInRemovedId = id => removedMessagesIds.includes(id); const mapHasRemovedIdKey = map => removedMessagesIds.some(id => map.has(id)); const objectHasRemovedIdKey = obj => Object.keys(obj).findIndex(isInRemovedId) !== -1; const cleanUpMap = map => { const clonedMap = new Map(map); removedMessagesIds.forEach(id => clonedMap.delete(id)); return clonedMap; }; const cleanUpObject = object => [...Object.entries(object)].reduce((res, [id, value]) => { if (!isInRemovedId(id)) { res[id] = value; } return res; }, {}); removedMessagesIds.forEach(id => { state.mutableMessagesById.delete(id); state.mutableMessagesOrder.splice( state.mutableMessagesOrder.indexOf(id), 1 ); }); if (state.disabledMessagesById.find(isInRemovedId)) { state.disabledMessagesById = state.disabledMessagesById.filter( id => !isInRemovedId(id) ); } if (state.messagesUiById.find(isInRemovedId)) { state.messagesUiById = state.messagesUiById.filter( id => !isInRemovedId(id) ); } if (isInRemovedId(state.currentGroup)) { state.currentGroup = getNewCurrentGroup( state.currentGroup, state.groupsById, removedMessagesIds ); } if (mapHasRemovedIdKey(state.cssMessagesMatchingElements)) { state.cssMessagesMatchingElements = cleanUpMap( state.cssMessagesMatchingElements ); } if (mapHasRemovedIdKey(state.groupsById)) { state.groupsById = cleanUpMap(state.groupsById); } if (objectHasRemovedIdKey(state.repeatById)) { state.repeatById = cleanUpObject(state.repeatById); } if (objectHasRemovedIdKey(state.networkMessagesUpdateById)) { state.networkMessagesUpdateById = cleanUpObject( state.networkMessagesUpdateById ); } return state; } /** * Get an array of all the fronts logged in a specific message. * * @param {Message} message: The message to get actors from. * @return {Array} An array containing all the fronts logged * in a message. */ function getAllFrontsInMessage(message) { const { parameters, messageText } = message; const fronts = []; const isFront = p => p && typeof p.release === "function"; if (Array.isArray(parameters)) { message.parameters.forEach(parameter => { if (isFront(parameter)) { fronts.push(parameter); } }); } if (isFront(messageText)) { fronts.push(messageText); } return fronts; } /** * Returns total count of top level messages (those which are not * within a group). */ function getToplevelMessageCount(state) { let count = 0; state.mutableMessagesById.forEach(message => { if (!message.groupId) { count++; } }); return count; } /** * Check if a message should be visible in the console output, and if not, what * causes it to be hidden. * @param {Message} message: The message to check * @param {Object} option: An option object of the following shape: * - {MessageState} messagesState: The current messages state * - {FilterState} filtersState: The current filters state * - {PrefsState} prefsState: The current preferences state * - {UiState} uiState: The current ui state * - {Boolean} checkGroup: Set to false to not check if a message should * be visible because it is in a console.group. * - {Boolean} checkParentWarningGroupVisibility: Set to false to not * check if a message should be visible because it is in a * warningGroup and the warningGroup is visible. * - {Boolean} hasMatchedAncestor: Set to true if message is part of a * group that has been set to visible * * @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. */ // eslint-disable-next-line complexity function getMessageVisibility( message, { messagesState, filtersState, prefsState, uiState, checkGroup = true, checkParentWarningGroupVisibility = true, hasMatchedAncestor = false, } ) { const warningGroupMessageId = getParentWarningGroupMessageId(message); const parentWarningGroupMessage = messagesState.mutableMessagesById.get( warningGroupMessageId ); // 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( parentWarningGroupMessage, messagesState, prefsState ) ) { return { visible: false, cause: "closedGroup", }; } // If the message is a warningGroup, check if it should be displayed. if (isWarningGroup(message)) { if (!shouldGroupWarningMessages(message, messagesState, prefsState)) { return { visible: false, cause: "warningGroupHeuristicNotMet", }; } // Hide a warningGroup if the warning filter is off. if (!filtersState[FILTERS.WARN]) { // We don't include any cause as we don't want that message to be reflected in the // message count. return { visible: false, }; } // Display a warningGroup if at least one of its message will be visible. const childrenMessages = messagesState.warningGroupsById.get(message.id); const hasVisibleChild = childrenMessages && childrenMessages.some(id => { const child = messagesState.mutableMessagesById.get(id); if (!child) { return false; } const { visible, cause } = getMessageVisibility(child, { messagesState, filtersState, prefsState, uiState, checkParentWarningGroupVisibility: false, }); return visible && cause !== "visibleWarningGroup"; }); if (hasVisibleChild) { return { visible: true, cause: "visibleChild", }; } } // Do not display the message if it can be in a warningGroup, and the group is // displayed but collapsed. if ( parentWarningGroupMessage && shouldGroupWarningMessages( parentWarningGroupMessage, messagesState, prefsState ) && !messagesState.messagesUiById.includes(warningGroupMessageId) ) { return { visible: false, cause: "closedWarningGroup", }; } // Display a message if it is in a warningGroup that is visible. We don't check the // warningGroup visibility if `checkParentWarningGroupVisibility` is false, because // it means we're checking the warningGroup visibility based on the visibility of its // children, which would cause an infinite loop. const parentVisibility = parentWarningGroupMessage && checkParentWarningGroupVisibility ? getMessageVisibility(parentWarningGroupMessage, { messagesState, filtersState, prefsState, uiState, checkGroup, checkParentWarningGroupVisibility, }) : null; if ( parentVisibility && parentVisibility.visible && parentVisibility.cause !== "visibleChild" ) { return { visible: true, cause: "visibleWarningGroup", }; } // Some messages can't be filtered out (e.g. groups). // So, always return visible: true for those. if (isUnfilterable(message)) { return { visible: true, }; } // Let's check all level filters (error, warn, log, …) and return visible: false // and the message level as a cause if the function returns false. if (!passLevelFilters(message, filtersState)) { return { visible: false, cause: message.level, }; } if (!passCssFilters(message, filtersState)) { return { visible: false, cause: FILTERS.CSS, }; } if (!passNetworkFilter(message, filtersState)) { return { visible: false, cause: FILTERS.NET, }; } if (!passXhrFilter(message, filtersState)) { return { visible: false, cause: FILTERS.NETXHR, }; } // This should always be the last check, or we might report that a message was hidden // because of text search, while it may be hidden because its category is disabled. // Do not check for search filters if it is part of a group and one of its ancestor // has matched the current search filters and set to visible if (!hasMatchedAncestor && !passSearchFilters(message, filtersState)) { return { visible: false, cause: FILTERS.TEXT, }; } return { visible: true, }; } function isUnfilterable(message) { return [ MESSAGE_TYPE.COMMAND, MESSAGE_TYPE.RESULT, MESSAGE_TYPE.NAVIGATION_MARKER, ].includes(message.type); } function isInOpenedGroup(message, groupsById, messagesUI) { return ( !message.groupId || (!isGroupClosed(message.groupId, messagesUI) && !hasClosedParentGroup(groupsById.get(message.groupId), messagesUI)) ); } function hasClosedParentGroup(group, messagesUI) { return group.some(groupId => isGroupClosed(groupId, messagesUI)); } function isGroupClosed(groupId, messagesUI) { return messagesUI.includes(groupId) === false; } /** * Returns true if the message shouldn't be hidden because of the network filter state. * * @param {Object} message - The message to check the filter against. * @param {FilterState} filters - redux "filters" state. * @returns {Boolean} */ function passNetworkFilter(message, filters) { // The message passes the filter if it is not a network message, // or if it is an xhr one, // or if the network filter is on. return ( message.source !== MESSAGE_SOURCE.NETWORK || message.isXHR === true || filters[FILTERS.NET] === true || (filters[FILTERS.ERROR] && isMessageNetworkError(message)) ); } /** * Returns true if the message shouldn't be hidden because of the xhr filter state. * * @param {Object} message - The message to check the filter against. * @param {FilterState} filters - redux "filters" state. * @returns {Boolean} */ function passXhrFilter(message, filters) { // The message passes the filter if it is not a network message, // or if it is a non-xhr one, // or if the xhr filter is on. return ( message.source !== MESSAGE_SOURCE.NETWORK || message.isXHR === false || filters[FILTERS.NETXHR] === true || (filters[FILTERS.ERROR] && isMessageNetworkError(message)) ); } /** * Returns true if the message shouldn't be hidden because of levels filter state. * * @param {Object} message - The message to check the filter against. * @param {FilterState} filters - redux "filters" state. * @returns {Boolean} */ function passLevelFilters(message, filters) { // The message passes the filter if it is not a console call, // or if its level matches the state of the corresponding filter. return ( (message.source !== MESSAGE_SOURCE.CONSOLE_API && message.source !== MESSAGE_SOURCE.JAVASCRIPT) || filters[message.level] === true || (filters[FILTERS.ERROR] && isMessageNetworkError(message)) ); } /** * Returns true if the message shouldn't be hidden because of the CSS filter state. * * @param {Object} message - The message to check the filter against. * @param {FilterState} filters - redux "filters" state. * @returns {Boolean} */ function passCssFilters(message, filters) { // The message passes the filter if it is not a CSS message, // or if the CSS filter is on. return message.source !== MESSAGE_SOURCE.CSS || filters.css === true; } /** * Returns true if the message shouldn't be hidden because of search filter state. * * @param {Object} message - The message to check the filter against. * @param {FilterState} filters - redux "filters" state. * @returns {Boolean} */ function passSearchFilters(message, filters) { const trimmed = (filters.text || "").trim(); // "-"-prefix switched to exclude mode const exclude = trimmed.startsWith("-"); const term = exclude ? trimmed.slice(1) : trimmed; // This regex matches a very basic regex with an optional i flag const regexMatch = /^\/(?.+)\/(?i)?$/.exec(term); let regex; if (regexMatch !== null) { const flags = "m" + (regexMatch.groups.flags || ""); try { regex = new RegExp(regexMatch.groups.search, flags); } catch (e) {} } const matchStr = regex ? str => regex.test(str) : str => str.toLocaleLowerCase().includes(term.toLocaleLowerCase()); // If there is no search, the message passes the filter. if (!term) { return true; } const matched = // Look for a match in parameters. isTextInParameters(matchStr, message.parameters) || // Look for a match in location. isTextInFrame(matchStr, message.frame) || // Look for a match in net events. isTextInNetEvent(matchStr, message) || // Look for a match in stack-trace. isTextInStackTrace(matchStr, message.stacktrace) || // Look for a match in messageText. isTextInMessageText(matchStr, message.messageText) || // Look for a match in notes. isTextInNotes(matchStr, message.notes) || // Look for a match in prefix. isTextInPrefix(matchStr, message.prefix); return matched ? !exclude : exclude; } /** * Returns true if given text is included in provided stack frame. */ function isTextInFrame(matchStr, frame) { if (!frame) { return false; } const { functionName, line, column, source } = frame; const { short } = getSourceNames(source); const unicodeShort = getUnicodeUrlPath(short); const str = `${ functionName ? functionName + " " : "" }${unicodeShort}:${line}:${column}`; return matchStr(str); } /** * Returns true if given text is included in provided parameters. */ function isTextInParameters(matchStr, parameters) { if (!parameters) { return false; } return parameters.some(parameter => isTextInParameter(matchStr, parameter)); } /** * Returns true if given text is included in provided parameter. */ function isTextInParameter(matchStr, parameter) { const paramGrip = parameter && parameter.getGrip ? parameter.getGrip() : parameter; if (paramGrip && paramGrip.class && matchStr(paramGrip.class)) { return true; } const parameterType = typeof parameter; if (parameterType !== "object" && parameterType !== "undefined") { const str = paramGrip + ""; if (matchStr(str)) { return true; } } const previewItems = getGripPreviewItems(paramGrip); for (const item of previewItems) { if (isTextInParameter(matchStr, item)) { return true; } } if (paramGrip && paramGrip.ownProperties) { for (const [key, desc] of Object.entries(paramGrip.ownProperties)) { if (matchStr(key)) { return true; } if (isTextInParameter(matchStr, getDescriptorValue(desc))) { return true; } } } return false; } /** * Returns true if given text is included in provided net event grip. */ function isTextInNetEvent(matchStr, { method, url } = {}) { if (!method && !url) { return false; } return matchStr(method) || matchStr(url); } /** * Returns true if given text is included in provided stack trace. */ function isTextInStackTrace(matchStr, stacktrace) { if (!Array.isArray(stacktrace)) { return false; } // isTextInFrame expect the properties of the frame object to be in the same // order they are rendered in the Frame component. return stacktrace.some(frame => isTextInFrame(matchStr, { functionName: frame.functionName || l10n.getStr("stacktrace.anonymousFunction"), source: frame.filename, lineNumber: frame.lineNumber, columnNumber: frame.columnNumber, }) ); } /** * Returns true if given text is included in `messageText` field. */ function isTextInMessageText(matchStr, messageText) { if (!messageText) { return false; } if (typeof messageText === "string") { return matchStr(messageText); } const grip = messageText && messageText.getGrip ? messageText.getGrip() : messageText; if (grip && grip.type === "longString") { return matchStr(grip.initial); } return true; } /** * Returns true if given text is included in notes. */ function isTextInNotes(matchStr, notes) { if (!Array.isArray(notes)) { return false; } return notes.some( note => // Look for a match in location. isTextInFrame(matchStr, note.frame) || // Look for a match in messageBody. (note.messageBody && matchStr(note.messageBody)) ); } /** * Returns true if given text is included in prefix. */ function isTextInPrefix(matchStr, prefix) { if (!prefix) { return false; } return matchStr(`${prefix}: `); } function getDefaultFiltersCounter() { const count = DEFAULT_FILTERS.reduce((res, filter) => { res[filter] = 0; return res; }, {}); count.global = 0; return count; } /** * Sort state.visibleMessages if needed. * * @param {MessageState} state * @param {Boolean} sortWarningGroupMessage: set to true to sort warningGroup * messages. Default to false, as in some * situations we already take care of putting * the ids at the right position. * @param {Boolean} timeStampSort: set to true to sort messages by their timestamps. */ function maybeSortVisibleMessages( state, sortWarningGroupMessage = false, timeStampSort = false ) { if (state.warningGroupsById.size > 0 && sortWarningGroupMessage) { state.visibleMessages.sort((a, b) => { const messageA = state.mutableMessagesById.get(a); const messageB = state.mutableMessagesById.get(b); const warningGroupIdA = getParentWarningGroupMessageId(messageA); const warningGroupIdB = getParentWarningGroupMessageId(messageB); const warningGroupA = state.mutableMessagesById.get(warningGroupIdA); const warningGroupB = state.mutableMessagesById.get(warningGroupIdB); const aFirst = -1; const bFirst = 1; // If both messages are in a warningGroup, or if both are not in warningGroups. if ( (warningGroupA && warningGroupB) || (!warningGroupA && !warningGroupB) ) { return getNaturalOrder(messageA, messageB); } // If `a` is in a warningGroup (and `b` isn't). if (warningGroupA) { // If `b` is the warningGroup of `a`, `a` should be after `b`. if (warningGroupIdA === messageB.id) { return bFirst; } // `b` is a regular message, we place `a` before `b` if `b` came after `a`'s // warningGroup. return getNaturalOrder(warningGroupA, messageB); } // If `b` is in a warningGroup (and `a` isn't). if (warningGroupB) { // If `a` is the warningGroup of `b`, `a` should be before `b`. if (warningGroupIdB === messageA.id) { return aFirst; } // `a` is a regular message, we place `a` after `b` if `a` came after `b`'s // warningGroup. return getNaturalOrder(messageA, warningGroupB); } return 0; }); } if (timeStampSort) { state.visibleMessages.sort((a, b) => { const messageA = state.mutableMessagesById.get(a); const messageB = state.mutableMessagesById.get(b); return getNaturalOrder(messageA, messageB); }); } } /** * Returns if a given type of warning message should be grouped. * * @param {ConsoleMessage} warningGroupMessage * @param {MessageState} messagesState * @param {PrefsState} prefsState */ function shouldGroupWarningMessages( warningGroupMessage, messagesState, prefsState ) { if (!warningGroupMessage) { return false; } // Only group if the preference is ON. if (!prefsState.groupWarnings) { return false; } // We group warning messages if there are at least 2 messages that could go in it. const warningGroup = messagesState.warningGroupsById.get( warningGroupMessage.id ); if (!warningGroup || !Array.isArray(warningGroup)) { return false; } return warningGroup.length > 1; } exports.messages = messages;