Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: log network response and request with filter options in debug console #45769

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions assets/images/filter.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions src/components/HeaderWithBackButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ function HeaderWithBackButton({
horizontal: 0,
},
threeDotsMenuItems = [],
threeDotsMenuIcon,
threeDotsMenuIconFill,
shouldEnableDetailPageNavigation = false,
children = null,
shouldOverlayDots = false,
Expand Down Expand Up @@ -234,6 +236,8 @@ function HeaderWithBackButton({
{shouldShowPinButton && !!report && <PinButton report={report} />}
{shouldShowThreeDotsButton && (
<ThreeDotsMenu
icon={threeDotsMenuIcon}
iconFill={threeDotsMenuIconFill}
disabled={shouldDisableThreeDotsButton}
menuItems={threeDotsMenuItems}
onIconPress={onThreeDotsButtonPress}
Expand Down
6 changes: 6 additions & 0 deletions src/components/HeaderWithBackButton/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ type HeaderWithBackButtonProps = Partial<ChildrenProps> & {
/** 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;

Expand Down
2 changes: 2 additions & 0 deletions src/components/Icon/Expensicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -380,4 +381,5 @@ export {
QBOCircle,
Filters,
CalendarSolid,
Filter,
};
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
12 changes: 6 additions & 6 deletions src/libs/Console/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down Expand Up @@ -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: ''},
];
}
}
Expand Down
15 changes: 8 additions & 7 deletions src/libs/Log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
});
Expand Down
16 changes: 13 additions & 3 deletions src/libs/Middleware/Logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,25 @@ function logRequestDetails(message: string, request: Request, response?: Respons
logParams.requestID = response.requestID;
}

Log.info(message, false, logParams);
const extraData: Record<string, unknown> = {};
/**
* 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change raise regression issue #47509 when enable client log request data include has file can not store to indexdb, so we should serialize request data

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) => {
Expand Down
16 changes: 15 additions & 1 deletion src/libs/actions/Console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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};
1 change: 0 additions & 1 deletion src/libs/actions/OnyxUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
77 changes: 62 additions & 15 deletions src/pages/settings/AboutPage/ConsolePage.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -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>(filterBy.all);
const {translate} = useLocalize();
const styles = useThemeStyles();

const theme = useTheme();
const {windowWidth} = useWindowDimensions();
const route = useRoute<RouteProp<SettingsNavigatorParamList, typeof SCREENS.SETTINGS.CONSOLE>>();

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<OnyxEntry<CapturedLogs>>({});
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);

Expand All @@ -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}) => {
Expand Down Expand Up @@ -121,10 +163,15 @@ function ConsolePage({capturedLogs, shouldStoreLogs}: ConsolePageProps) {
<HeaderWithBackButton
title={translate('initialSettingsPage.troubleshoot.debugConsole')}
onBackButtonPress={() => Navigation.goBack(route.params?.backTo)}
shouldShowThreeDotsButton
threeDotsMenuItems={menuItems}
threeDotsAnchorPosition={styles.threeDotsPopoverOffset(windowWidth)}
threeDotsMenuIcon={Expensicons.Filter}
threeDotsMenuIconFill={theme.icon}
/>
<View style={[styles.border, styles.highlightBG, styles.borderNone, styles.mh5, styles.flex1]}>
<InvertedFlatList
data={logsList}
data={filteredLogsList}
renderItem={renderItem}
contentContainerStyle={styles.p5}
ListEmptyComponent={<Text>{translate('initialSettingsPage.debugConsole.noLogsAvailable')}</Text>}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/settings/Troubleshoot/TroubleshootPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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))),
};

Expand Down
3 changes: 3 additions & 0 deletions src/types/onyx/Console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ type Log = {

/** Log message */
message: string;

/** Additional data */
extraData: string | Record<string, unknown> | Array<Record<string, unknown>> | Error;
};

/** Record of captured logs */
Expand Down
Loading