Skip to content

Commit

Permalink
Merge pull request #36337 from callstack-internal/feat/console
Browse files Browse the repository at this point in the history
feat: Debug console view
  • Loading branch information
techievivek authored Feb 14, 2024
2 parents b0b651b + 96b08ec commit 51c0ecc
Show file tree
Hide file tree
Showing 30 changed files with 786 additions and 31 deletions.
8 changes: 8 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3223,6 +3223,14 @@ const CONST = {

REPORT_FIELD_TITLE_FIELD_ID: 'text_title',

DEBUG_CONSOLE: {
LEVELS: {
INFO: 'INFO',
ERROR: 'ERROR',
RESULT: 'RESULT',
DEBUG: 'DEBUG',
},
},
REIMBURSEMENT_ACCOUNT_SUBSTEP_INDEX: {
BANK_ACCOUNT: {
ACCOUNT_NUMBERS: 0,
Expand Down
8 changes: 8 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,12 @@ const ONYXKEYS = {
/** Indicates whether an forced upgrade is required */
UPDATE_REQUIRED: 'updateRequired',

/** Stores the logs of the app for debugging purposes */
LOGS: 'logs',

/** Indicates whether we should store logs or not */
SHOULD_STORE_LOGS: 'shouldStoreLogs',

/** Collection Keys */
COLLECTION: {
DOWNLOAD: 'download_',
Expand Down Expand Up @@ -541,6 +547,8 @@ type OnyxValuesMapping = {
[ONYXKEYS.RECENTLY_USED_REPORT_FIELDS]: OnyxTypes.RecentlyUsedReportFields;
[ONYXKEYS.UPDATE_REQUIRED]: boolean;
[ONYXKEYS.PLAID_CURRENT_EVENT]: string;
[ONYXKEYS.LOGS]: Record<number, OnyxTypes.Log>;
[ONYXKEYS.SHOULD_STORE_LOGS]: boolean;
};

type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping;
Expand Down
5 changes: 5 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,11 @@ const ROUTES = {
SETTINGS_STATUS_CLEAR_AFTER_DATE: 'settings/profile/status/clear-after/date',
SETTINGS_STATUS_CLEAR_AFTER_TIME: 'settings/profile/status/clear-after/time',
SETTINGS_TROUBLESHOOT: 'settings/troubleshoot',
SETTINGS_CONSOLE: 'settings/troubleshoot/console',
SETTINGS_SHARE_LOG: {
route: 'settings/troubleshoot/console/share-log',
getRoute: (source: string) => `settings/troubleshoot/console/share-log?source=${encodeURI(source)}` as const,
},

KEYBOARD_SHORTCUTS: 'keyboard-shortcuts',

Expand Down
2 changes: 2 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const SCREENS = {
TWO_FACTOR_AUTH: 'Settings_TwoFactorAuth',
REPORT_CARD_LOST_OR_DAMAGED: 'Settings_ReportCardLostOrDamaged',
TROUBLESHOOT: 'Settings_Troubleshoot',
CONSOLE: 'Settings_Console',
SHARE_LOG: 'Share_Log',

PROFILE: {
ROOT: 'Settings_Profile',
Expand Down
2 changes: 1 addition & 1 deletion src/components/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ function Button(
shouldRemoveRightBorderRadius ? styles.noRightBorderRadius : undefined,
shouldRemoveLeftBorderRadius ? styles.noLeftBorderRadius : undefined,
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
'text' in rest && (rest?.icon || rest?.shouldShowRightIcon) ? styles.alignItemsStretch : undefined,
'text' in rest && rest?.shouldShowRightIcon ? styles.alignItemsStretch : undefined,
innerStyles,
]}
hoverStyle={[
Expand Down
10 changes: 10 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
InstantSummaryParams,
LocalTimeParams,
LoggedInAsParams,
LogSizeParams,
ManagerApprovedAmountParams,
ManagerApprovedParams,
MaxParticipantsReachedParams,
Expand Down Expand Up @@ -826,11 +827,20 @@ export default {
troubleshoot: {
clearCacheAndRestart: 'Clear cache and restart',
viewConsole: 'View debug console',
debugConsole: 'Debug console',
description: 'Use the tools below to help troubleshoot the Expensify experience. If you encounter any issues, please',
submitBug: 'submit a bug',
confirmResetDescription: 'All unsent draft messages will be lost, but the rest of your data is safe.',
resetAndRefresh: 'Reset and refresh',
},
debugConsole: {
saveLog: 'Save log',
shareLog: 'Share log',
enterCommand: 'Enter command',
execute: 'Execute',
noLogsAvailable: 'No logs available',
logSizeTooLarge: ({size}: LogSizeParams) => `Log size exceeds the limit of ${size} MB. Please use "Save log" to download the log file instead.`,
},
goToExpensifyClassic: 'Go to Expensify Classic',
security: 'Security',
signOut: 'Sign out',
Expand Down
10 changes: 10 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type {
InstantSummaryParams,
LocalTimeParams,
LoggedInAsParams,
LogSizeParams,
ManagerApprovedAmountParams,
ManagerApprovedParams,
MaxParticipantsReachedParams,
Expand Down Expand Up @@ -821,11 +822,20 @@ export default {
troubleshoot: {
clearCacheAndRestart: 'Borrar caché y reiniciar',
viewConsole: 'Ver la consola de depuración',
debugConsole: 'Consola de depuración',
description: 'Utilice las herramientas que aparecen a continuación para solucionar los problemas de Expensify. Si tiene algún problema, por favor',
submitBug: 'envíe un error',
confirmResetDescription: 'Todos los borradores no enviados se perderán, pero el resto de tus datos estarán a salvo.',
resetAndRefresh: 'Restablecer y actualizar',
},
debugConsole: {
saveLog: 'Guardar registro',
shareLog: 'Compartir registro',
enterCommand: 'Introducir comando',
execute: 'Ejecutar',
noLogsAvailable: 'No hay registros disponibles',
logSizeTooLarge: ({size}: LogSizeParams) => `El tamaño del registro excede el límite de ${size} MB. Utilice "Guardar registro" para descargar el archivo de registro.`,
},
security: 'Seguridad',
signOut: 'Desconectar',
signOutConfirmationText: 'Si cierras sesión perderás los cambios hechos mientras estabas desconectado',
Expand Down
3 changes: 3 additions & 0 deletions src/languages/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,8 @@ type TermsParams = {amount: string};

type ElectronicFundsParams = {percentage: string; amount: string};

type LogSizeParams = {size: number};

export type {
ApprovedAmountParams,
AddressLineParams,
Expand Down Expand Up @@ -389,4 +391,5 @@ export type {
WelcomeNoteParams,
WelcomeToRoomParams,
ZipCodeExampleFormatParams,
LogSizeParams,
};
104 changes: 104 additions & 0 deletions src/libs/Console/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/* eslint-disable @typescript-eslint/naming-convention */
import {addLog} from '@libs/actions/Console';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Log} from '@src/types/onyx';

/* store the original console.log function so we can call it */
// eslint-disable-next-line no-console
const originalConsoleLog = console.log;

/* List of patterns to ignore in logs. "logs" key always needs to be ignored because otherwise it will cause infinite loop */
const logPatternsToIgnore = [`merge() called for key: ${ONYXKEYS.LOGS}`];

/**
* Check if the log should be attached to the console
* @param message the message to check
* @returns true if the log should be attached to the console
*/
function shouldAttachLog(message: string) {
return !logPatternsToIgnore.some((pattern) => message.includes(pattern));
}

/**
* Goes through all the arguments passed the console, parses them to a string and adds them to the logs
* @param args the arguments to log
*/
function logMessage(args: unknown[]) {
const message = args
.map((arg) => {
if (typeof arg === 'object') {
try {
return JSON.stringify(arg, null, 2); // Indent for better readability
} catch (e) {
return 'Unserializable Object';
}
}

return String(arg);
})
.join(' ');
const newLog = {time: new Date(), level: CONST.DEBUG_CONSOLE.LEVELS.INFO, message};
addLog(newLog);
}

/**
* Override the console.log function to add logs to the store
* @param args arguments passed to the console.log function
*/
// eslint-disable-next-line no-console
console.log = (...args) => {
logMessage(args);
originalConsoleLog.apply(console, args);
};

const charsToSanitize = /[\u2018\u2019\u201C\u201D\u201E\u2026]/g;

const charMap: Record<string, string> = {
'\u2018': "'",
'\u2019': "'",
'\u201C': '"',
'\u201D': '"',
'\u201E': '"',
'\u2026': '...',
};

/**
* Sanitize the input to the console
* @param text the text to sanitize
* @returns the sanitized text
*/
function sanitizeConsoleInput(text: string) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return text.replace(charsToSanitize, (match) => charMap[match]);
}

/**
* Run an arbitrary JS code and create a log from the output
* @param text the JS code to run
* @returns an array of logs created by eval call
*/
function createLog(text: string) {
const time = new Date();
try {
// @ts-expect-error Any code inside `sanitizedInput` that gets evaluated by `eval()` will be executed in the context of the current this value.
// eslint-disable-next-line no-eval, no-invalid-this
const result = eval.call(this, text);

if (result !== undefined) {
return [
{time, level: CONST.DEBUG_CONSOLE.LEVELS.INFO, message: `> ${text}`},
{time, level: CONST.DEBUG_CONSOLE.LEVELS.RESULT, message: String(result)},
];
}
return [{time, level: CONST.DEBUG_CONSOLE.LEVELS.INFO, message: `> ${text}`}];
} 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}`},
];
}
}

export {sanitizeConsoleInput, createLog, shouldAttachLog};
export type {Log};
25 changes: 25 additions & 0 deletions src/libs/Log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,30 @@

/* eslint-disable rulesdir/no-api-in-views */
import Logger from 'expensify-common/lib/Logger';
import Onyx from 'react-native-onyx';
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 {shouldAttachLog} from './Console';
import getPlatform from './getPlatform';
import * as Network from './Network';
import requireParameters from './requireParameters';

let timeout: NodeJS.Timeout;
let shouldCollectLogs = false;

Onyx.connect({
key: ONYXKEYS.SHOULD_STORE_LOGS,
callback: (val) => {
if (!val) {
shouldCollectLogs = false;
}

shouldCollectLogs = Boolean(val);
},
});

type LogCommandParameters = {
expensifyCashAppVersion: string;
Expand Down Expand Up @@ -50,7 +67,15 @@ function serverLoggingCallback(logger: Logger, params: ServerLoggingCallbackOpti
const Log = new Logger({
serverLoggingCallback,
clientLoggingCallback: (message) => {
if (!shouldAttachLog(message)) {
return;
}

console.debug(message);

if (shouldCollectLogs) {
addLog({time: new Date(), level: CONST.DEBUG_CONSOLE.LEVELS.DEBUG, message});
}
},
isDebug: true,
});
Expand Down
2 changes: 2 additions & 0 deletions src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.SETTINGS.APP_DOWNLOAD_LINKS]: () => require('../../../pages/settings/AppDownloadLinks').default as React.ComponentType,
[SCREENS.SETTINGS.LOUNGE_ACCESS]: () => require('../../../pages/settings/Profile/LoungeAccessPage').default as React.ComponentType,
[SCREENS.SETTINGS.TROUBLESHOOT]: () => require('../../../pages/settings/AboutPage/TroubleshootPage').default as React.ComponentType,
[SCREENS.SETTINGS.CONSOLE]: () => require('../../../pages/settings/AboutPage/ConsolePage').default as React.ComponentType,
[SCREENS.SETTINGS.SHARE_LOG]: () => require('../../../pages/settings/AboutPage/ShareLogPage').default as React.ComponentType,
[SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS]: () => require('../../../pages/settings/Profile/PersonalDetails/AddressPage').default as React.ComponentType,
[SCREENS.SETTINGS.WALLET.DOMAIN_CARD]: () => require('../../../pages/settings/Wallet/ExpensifyCardPage').default as React.ComponentType,
[SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD]: () => require('../../../pages/settings/Wallet/ReportVirtualCardFraudPage').default as React.ComponentType,
Expand Down
5 changes: 5 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
path: ROUTES.SETTINGS_TROUBLESHOOT,
exact: true,
},
[SCREENS.SETTINGS.CONSOLE]: {
path: ROUTES.SETTINGS_CONSOLE,
exact: true,
},
[SCREENS.SETTINGS.SHARE_LOG]: ROUTES.SETTINGS_SHARE_LOG.route,
[SCREENS.SETTINGS.PROFILE.CONTACT_METHODS]: {
path: ROUTES.SETTINGS_CONTACT_METHODS.route,
exact: true,
Expand Down
5 changes: 5 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ type SettingsNavigatorParamList = {
[SCREENS.SETTINGS.ABOUT]: undefined;
[SCREENS.SETTINGS.APP_DOWNLOAD_LINKS]: undefined;
[SCREENS.SETTINGS.TROUBLESHOOT]: undefined;
[SCREENS.SETTINGS.CONSOLE]: undefined;
[SCREENS.SETTINGS.SHARE_LOG]: {
/** URL of the generated file to share logs in a report */
source: string;
};
[SCREENS.SETTINGS.LOUNGE_ACCESS]: undefined;
[SCREENS.SETTINGS.WALLET.ROOT]: undefined;
[SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS]: undefined;
Expand Down
16 changes: 15 additions & 1 deletion src/libs/OptionsListUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1718,6 +1718,19 @@ function getSearchOptions(reports: Record<string, Report>, personalDetails: Onyx
return options;
}

function getShareLogOptions(reports: OnyxCollection<Report>, personalDetails: OnyxEntry<PersonalDetailsList>, searchValue = '', betas: Beta[] = []): GetOptions {
return getOptions(reports, personalDetails, {
betas,
searchInputValue: searchValue.trim(),
includeRecentReports: true,
includeMultipleParticipantReports: true,
sortByReportTypeInSearch: true,
includePersonalDetails: true,
forcePolicyNamePreview: true,
includeOwnedWorkspaceChats: true,
});
}

/**
* Build the IOUConfirmation options for showing the payee personalDetail
*/
Expand Down Expand Up @@ -2015,6 +2028,7 @@ export {
formatMemberForList,
formatSectionsFromSearchTerm,
transformedTaxRates,
getShareLogOptions,
};

export type {MemberForList, CategorySection};
export type {MemberForList, CategorySection, GetOptions};
31 changes: 31 additions & 0 deletions src/libs/actions/Console.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Onyx from 'react-native-onyx';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Log} from '@src/types/onyx';

/**
* Merge the new log into the existing logs in Onyx
* @param log the log to add
*/
function addLog(log: Log) {
Onyx.merge(ONYXKEYS.LOGS, {
[log.time.getTime()]: log,
});
}

/**
* Set whether or not to store logs in Onyx
* @param store whether or not to store logs
*/
function setShouldStoreLogs(store: boolean) {
Onyx.set(ONYXKEYS.SHOULD_STORE_LOGS, store);
}

/**
* Disable logging and flush the logs from Onyx
*/
function disableLoggingAndFlushLogs() {
setShouldStoreLogs(false);
Onyx.set(ONYXKEYS.LOGS, null);
}

export {addLog, setShouldStoreLogs, disableLoggingAndFlushLogs};
Loading

0 comments on commit 51c0ecc

Please sign in to comment.