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 */