diff --git a/assets/images/filter.svg b/assets/images/filter.svg new file mode 100644 index 000000000000..9323573df12c --- /dev/null +++ b/assets/images/filter.svg @@ -0,0 +1,11 @@ + + + + + diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index 2d73e3c2dd24..f1e715bface8 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -53,6 +53,8 @@ function HeaderWithBackButton({ horizontal: 0, }, threeDotsMenuItems = [], + threeDotsMenuIcon, + threeDotsMenuIconFill, shouldEnableDetailPageNavigation = false, children = null, shouldOverlayDots = false, @@ -234,6 +236,8 @@ function HeaderWithBackButton({ {shouldShowPinButton && !!report && } {shouldShowThreeDotsButton && ( & { /** The anchor position of the menu */ threeDotsAnchorPosition?: AnchorPosition; + /** Icon displayed on the right of the title */ + threeDotsMenuIcon?: IconAsset; + + /** The fill color to pass into the icon. */ + threeDotsMenuIconFill?: string; + /** Whether we should show a close button */ shouldShowCloseButton?: boolean; diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index c03ccfd80e3b..a3fd1c93a261 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -82,6 +82,7 @@ import ExpensifyLogoNew from '@assets/images/expensify-logo-new.svg'; import ExpensifyWordmark from '@assets/images/expensify-wordmark.svg'; import EyeDisabled from '@assets/images/eye-disabled.svg'; import Eye from '@assets/images/eye.svg'; +import Filter from '@assets/images/filter.svg'; import Filters from '@assets/images/filters.svg'; import Flag from '@assets/images/flag.svg'; import FlagLevelOne from '@assets/images/flag_level_01.svg'; @@ -380,4 +381,5 @@ export { QBOCircle, Filters, CalendarSolid, + Filter, }; diff --git a/src/languages/en.ts b/src/languages/en.ts index fe3b51a660dc..1b6aa540f0e5 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -370,6 +370,8 @@ export default { value: 'Value', downloadFailedTitle: 'Download failed', downloadFailedDescription: "Your download couldn't be completed. Please try again later.", + filterLogs: 'Filter Logs', + network: 'Network', reportID: 'Report ID', }, location: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 8d12a35d831d..3575a9a00429 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -360,6 +360,8 @@ export default { value: 'Valor', downloadFailedTitle: 'Error en la descarga', downloadFailedDescription: 'No se pudo completar la descarga. Por favor, inténtalo más tarde.', + filterLogs: 'Registros de filtrado', + network: 'La red', reportID: 'ID del informe', }, connectionComplete: { diff --git a/src/libs/Console/index.ts b/src/libs/Console/index.ts index 9bbdb173e61b..153008e2b785 100644 --- a/src/libs/Console/index.ts +++ b/src/libs/Console/index.ts @@ -53,7 +53,7 @@ function logMessage(args: unknown[]) { return String(arg); }) .join(' '); - const newLog = {time: new Date(), level: CONST.DEBUG_CONSOLE.LEVELS.INFO, message}; + const newLog = {time: new Date(), level: CONST.DEBUG_CONSOLE.LEVELS.INFO, message, extraData: ''}; addLog(newLog); } @@ -105,15 +105,15 @@ function createLog(text: string) { if (result !== undefined) { return [ - {time, level: CONST.DEBUG_CONSOLE.LEVELS.INFO, message: `> ${text}`}, - {time, level: CONST.DEBUG_CONSOLE.LEVELS.RESULT, message: String(result)}, + {time, level: CONST.DEBUG_CONSOLE.LEVELS.INFO, message: `> ${text}`, extraData: ''}, + {time, level: CONST.DEBUG_CONSOLE.LEVELS.RESULT, message: String(result), extraData: ''}, ]; } - return [{time, level: CONST.DEBUG_CONSOLE.LEVELS.INFO, message: `> ${text}`}]; + return [{time, level: CONST.DEBUG_CONSOLE.LEVELS.INFO, message: `> ${text}`, extraData: ''}]; } catch (error) { return [ - {time, level: CONST.DEBUG_CONSOLE.LEVELS.ERROR, message: `> ${text}`}, - {time, level: CONST.DEBUG_CONSOLE.LEVELS.ERROR, message: `Error: ${(error as Error).message}`}, + {time, level: CONST.DEBUG_CONSOLE.LEVELS.ERROR, message: `> ${text}`, extraData: ''}, + {time, level: CONST.DEBUG_CONSOLE.LEVELS.ERROR, message: `Error: ${(error as Error).message}`, extraData: ''}, ]; } } diff --git a/src/libs/Log.ts b/src/libs/Log.ts index 83965807263a..72673b8d3f79 100644 --- a/src/libs/Log.ts +++ b/src/libs/Log.ts @@ -8,7 +8,7 @@ import type {Merge} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import pkg from '../../package.json'; -import {addLog} from './actions/Console'; +import {addLog, flushAllLogsOnAppLaunch} from './actions/Console'; import {shouldAttachLog} from './Console'; import getPlatform from './getPlatform'; import * as Network from './Network'; @@ -66,16 +66,17 @@ function serverLoggingCallback(logger: Logger, params: ServerLoggingCallbackOpti // callback methods are passed in here so we can decouple the logging library from the logging methods. const Log = new Logger({ serverLoggingCallback, - clientLoggingCallback: (message) => { + clientLoggingCallback: (message, extraData) => { if (!shouldAttachLog(message)) { return; } - console.debug(message); - - if (shouldCollectLogs) { - addLog({time: new Date(), level: CONST.DEBUG_CONSOLE.LEVELS.DEBUG, message}); - } + flushAllLogsOnAppLaunch().then(() => { + console.debug(message, extraData); + if (shouldCollectLogs) { + addLog({time: new Date(), level: CONST.DEBUG_CONSOLE.LEVELS.DEBUG, message, extraData}); + } + }); }, isDebug: true, }); diff --git a/src/libs/Middleware/Logging.ts b/src/libs/Middleware/Logging.ts index f10e8d2f5120..d327dd06becc 100644 --- a/src/libs/Middleware/Logging.ts +++ b/src/libs/Middleware/Logging.ts @@ -32,15 +32,25 @@ function logRequestDetails(message: string, request: Request, response?: Respons logParams.requestID = response.requestID; } - Log.info(message, false, logParams); + const extraData: Record = {}; + /** + * We don't want to log the request and response data for AuthenticatePusher + * requests because they contain sensitive information. + */ + if (request.command !== 'AuthenticatePusher') { + extraData.request = request; + extraData.response = response; + } + + Log.info(message, false, logParams, false, extraData); } const Logging: Middleware = (response, request) => { const startTime = Date.now(); - logRequestDetails('Making API request', request); + logRequestDetails('[Network] Making API request', request); return response .then((data) => { - logRequestDetails(`Finished API request in ${Date.now() - startTime}ms`, request, data); + logRequestDetails(`[Network] Finished API request in ${Date.now() - startTime}ms`, request, data); return data; }) .catch((error: HttpsError) => { diff --git a/src/libs/actions/Console.ts b/src/libs/actions/Console.ts index 79276d3307ac..6e585c6ad12d 100644 --- a/src/libs/actions/Console.ts +++ b/src/libs/actions/Console.ts @@ -2,6 +2,7 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Log} from '@src/types/onyx'; +let isNewAppLaunch = true; /** * Merge the new log into the existing logs in Onyx * @param log the log to add @@ -28,4 +29,17 @@ function disableLoggingAndFlushLogs() { Onyx.set(ONYXKEYS.LOGS, null); } -export {addLog, setShouldStoreLogs, disableLoggingAndFlushLogs}; +/** + * Clears the persisted logs on app launch, + * so that we have fresh logs for the new app session. + */ +function flushAllLogsOnAppLaunch() { + if (!isNewAppLaunch) { + return Promise.resolve(); + } + + isNewAppLaunch = false; + return Onyx.set(ONYXKEYS.LOGS, {}); +} + +export {addLog, setShouldStoreLogs, disableLoggingAndFlushLogs, flushAllLogsOnAppLaunch}; diff --git a/src/libs/actions/OnyxUpdates.ts b/src/libs/actions/OnyxUpdates.ts index 73c8b3d592bf..672f325be58a 100644 --- a/src/libs/actions/OnyxUpdates.ts +++ b/src/libs/actions/OnyxUpdates.ts @@ -35,7 +35,6 @@ function applyHTTPSOnyxUpdates(request: Request, response: Response) { // apply successData or failureData. This ensures that we do not update any pending, loading, or other UI states contained // in successData/failureData until after the component has received and API data. const onyxDataUpdatePromise = response.onyxData ? updateHandler(response.onyxData) : Promise.resolve(); - return onyxDataUpdatePromise .then(() => { // Handle the request's success/failure data (client-side data) diff --git a/src/pages/settings/AboutPage/ConsolePage.tsx b/src/pages/settings/AboutPage/ConsolePage.tsx index aee11c89f22c..eb9b13608039 100644 --- a/src/pages/settings/AboutPage/ConsolePage.tsx +++ b/src/pages/settings/AboutPage/ConsolePage.tsx @@ -1,7 +1,7 @@ import type {RouteProp} from '@react-navigation/native'; import {useRoute} from '@react-navigation/native'; import {format} from 'date-fns'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useCallback, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; import type {ListRenderItem, ListRenderItemInfo} from 'react-native'; import {withOnyx} from 'react-native-onyx'; @@ -11,12 +11,15 @@ import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import InvertedFlatList from '@components/InvertedFlatList'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import {addLog} from '@libs/actions/Console'; import {createLog, parseStringifiedMessages, sanitizeConsoleInput} from '@libs/Console'; import type {Log} from '@libs/Console'; @@ -40,32 +43,71 @@ type ConsolePageOnyxProps = { type ConsolePageProps = ConsolePageOnyxProps; +const filterBy = { + all: '', + network: '[Network]', +} as const; +type FilterBy = (typeof filterBy)[keyof typeof filterBy]; + function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { const [input, setInput] = useState(''); - const [logs, setLogs] = useState(capturedLogs); const [isGeneratingLogsFile, setIsGeneratingLogsFile] = useState(false); const [isLimitModalVisible, setIsLimitModalVisible] = useState(false); + const [activeFilterIndex, setActiveFilterIndex] = useState(filterBy.all); const {translate} = useLocalize(); const styles = useThemeStyles(); - + const theme = useTheme(); + const {windowWidth} = useWindowDimensions(); const route = useRoute>(); - const logsList = useMemo( - () => - Object.entries(logs ?? {}) - .map(([key, value]) => ({key, ...value})) - .reverse(), - [logs], + const menuItems: PopoverMenuItem[] = useMemo( + () => [ + { + text: translate('common.filterLogs'), + disabled: true, + }, + { + icon: Expensicons.All, + text: translate('common.all'), + iconFill: activeFilterIndex === filterBy.all ? theme.iconSuccessFill : theme.icon, + iconRight: Expensicons.Checkmark, + shouldShowRightIcon: activeFilterIndex === filterBy.all, + success: activeFilterIndex === filterBy.all, + onSelected: () => { + setActiveFilterIndex(filterBy.all); + }, + }, + { + icon: Expensicons.Globe, + text: translate('common.network'), + iconFill: activeFilterIndex === filterBy.network ? theme.iconSuccessFill : theme.icon, + iconRight: Expensicons.CheckCircle, + shouldShowRightIcon: activeFilterIndex === filterBy.network, + success: activeFilterIndex === filterBy.network, + onSelected: () => { + setActiveFilterIndex(filterBy.network); + }, + }, + ], + [activeFilterIndex, theme.icon, theme.iconSuccessFill, translate], ); - useEffect(() => { + const prevLogs = useRef>({}); + const getLogs = useCallback(() => { if (!shouldStoreLogs) { - return; + return []; } - setLogs((prevLogs) => ({...prevLogs, ...capturedLogs})); + prevLogs.current = {...prevLogs.current, ...capturedLogs}; + return Object.entries(prevLogs.current ?? {}) + .map(([key, value]) => ({key, ...value})) + .reverse(); }, [capturedLogs, shouldStoreLogs]); + const logsList = useMemo(() => getLogs(), [getLogs]); + + const filteredLogsList = useMemo(() => logsList.filter((log) => log.message.includes(activeFilterIndex)), [activeFilterIndex, logsList]); + const executeArbitraryCode = () => { const sanitizedInput = sanitizeConsoleInput(input); @@ -77,14 +119,14 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, executeArbitraryCode); const saveLogs = () => { - const logsWithParsedMessages = parseStringifiedMessages(logsList); + const logsWithParsedMessages = parseStringifiedMessages(filteredLogsList); localFileDownload('logs', JSON.stringify(logsWithParsedMessages, null, 2)); }; const shareLogs = () => { setIsGeneratingLogsFile(true); - const logsWithParsedMessages = parseStringifiedMessages(logsList); + const logsWithParsedMessages = parseStringifiedMessages(filteredLogsList); // Generate a file with the logs and pass its path to the list of reports to share it with localFileCreate('logs', JSON.stringify(logsWithParsedMessages, null, 2)).then(({path, size}) => { @@ -121,10 +163,15 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) { Navigation.goBack(route.params?.backTo)} + shouldShowThreeDotsButton + threeDotsMenuItems={menuItems} + threeDotsAnchorPosition={styles.threeDotsPopoverOffset(windowWidth)} + threeDotsMenuIcon={Expensicons.Filter} + threeDotsMenuIconFill={theme.icon} /> {translate('initialSettingsPage.debugConsole.noLogsAvailable')}} diff --git a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx index 2ec8ac1e54a0..8396595f5ca9 100644 --- a/src/pages/settings/Troubleshoot/TroubleshootPage.tsx +++ b/src/pages/settings/Troubleshoot/TroubleshootPage.tsx @@ -69,7 +69,7 @@ function TroubleshootPage({shouldStoreLogs, shouldMaskOnyxState}: TroubleshootPa const menuItems = useMemo(() => { const debugConsoleItem: BaseMenuItem = { translationKey: 'initialSettingsPage.troubleshoot.viewConsole', - icon: Expensicons.Gear, + icon: Expensicons.Bug, action: waitForNavigate(() => Navigation.navigate(ROUTES.SETTINGS_CONSOLE.getRoute(ROUTES.SETTINGS_TROUBLESHOOT))), }; diff --git a/src/types/onyx/Console.ts b/src/types/onyx/Console.ts index c8d2b714ae2b..03c76b6912b5 100644 --- a/src/types/onyx/Console.ts +++ b/src/types/onyx/Console.ts @@ -10,6 +10,9 @@ type Log = { /** Log message */ message: string; + + /** Additional data */ + extraData: string | Record | Array> | Error; }; /** Record of captured logs */