Skip to content

Commit

Permalink
Merge pull request #38803 from callstack-internal/feat/four-finger-ta…
Browse files Browse the repository at this point in the history
…p-logging

feat: access client side logging from Test Tools Modal
  • Loading branch information
amyevans authored Apr 2, 2024
2 parents 06e3b84 + 155d7ed commit 2bcdd75
Show file tree
Hide file tree
Showing 14 changed files with 273 additions and 61 deletions.
2 changes: 1 addition & 1 deletion src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ 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.LOGS]: OnyxTypes.CapturedLogs;
[ONYXKEYS.SHOULD_STORE_LOGS]: boolean;
[ONYXKEYS.CACHED_PDF_PATHS]: Record<string, string>;
[ONYXKEYS.POLICY_OWNERSHIP_CHANGE_CHECKS]: Record<string, OnyxTypes.PolicyOwnershipChangeChecks>;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React from 'react';
import {Alert} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import Button from '@components/Button';
import Switch from '@components/Switch';
import TestToolRow from '@components/TestToolRow';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Console from '@libs/actions/Console';
import {parseStringifiedMessages} from '@libs/Console';
import ONYXKEYS from '@src/ONYXKEYS';
import type {CapturedLogs, Log} from '@src/types/onyx';

type BaseClientSideLoggingToolMenuOnyxProps = {
/** Logs captured on the current device */
capturedLogs: OnyxEntry<CapturedLogs>;

/** Whether or not logs should be stored */
shouldStoreLogs: OnyxEntry<boolean>;
};

type BaseClientSideLoggingToolProps = {
/** Locally created file */
file?: {path: string; newFileName: string; size: number};
/** Action to run when pressing Share button */
onShareLogs?: () => void;
/** Action to run when disabling the switch */
onDisableLogging: (logs: Log[]) => void;
/** Action to run when enabling logging */
onEnableLogging?: () => void;
} & BaseClientSideLoggingToolMenuOnyxProps;

function BaseClientSideLoggingToolMenu({shouldStoreLogs, capturedLogs, file, onShareLogs, onDisableLogging, onEnableLogging}: BaseClientSideLoggingToolProps) {
const {translate} = useLocalize();

const onToggle = () => {
if (!shouldStoreLogs) {
Console.setShouldStoreLogs(true);

if (onEnableLogging) {
onEnableLogging();
}

return;
}

if (!capturedLogs) {
Alert.alert(translate('initialSettingsPage.troubleshoot.noLogsToShare'));
Console.disableLoggingAndFlushLogs();
return;
}

const logs = Object.values(capturedLogs);
const logsWithParsedMessages = parseStringifiedMessages(logs);

onDisableLogging(logsWithParsedMessages);
Console.disableLoggingAndFlushLogs();
};
const styles = useThemeStyles();
return (
<>
<TestToolRow title={translate('initialSettingsPage.troubleshoot.clientSideLogging')}>
<Switch
accessibilityLabel={translate('initialSettingsPage.troubleshoot.clientSideLogging')}
isOn={!!shouldStoreLogs}
onToggle={onToggle}
/>
</TestToolRow>
{!!file && (
<>
<Text style={[styles.textLabelSupporting, styles.mb4]}>{`path: ${file.path}`}</Text>
<TestToolRow title={translate('initialSettingsPage.debugConsole.logs')}>
<Button
small
text={translate('common.share')}
onPress={onShareLogs}
/>
</TestToolRow>
</>
)}
</>
);
}

BaseClientSideLoggingToolMenu.displayName = 'BaseClientSideLoggingToolMenu';

export default withOnyx<BaseClientSideLoggingToolProps, BaseClientSideLoggingToolMenuOnyxProps>({
capturedLogs: {
key: ONYXKEYS.LOGS,
},
shouldStoreLogs: {
key: ONYXKEYS.SHOULD_STORE_LOGS,
},
})(BaseClientSideLoggingToolMenu);
47 changes: 47 additions & 0 deletions src/components/ClientSideLoggingToolMenu/index.android.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React, {useState} from 'react';
import RNFetchBlob from 'react-native-blob-util';
import Share from 'react-native-share';
import type {Log} from '@libs/Console';
import localFileCreate from '@libs/localFileCreate';
import BaseClientSideLoggingToolMenu from './BaseClientSideLoggingToolMenu';

function ClientSideLoggingToolMenu() {
const [file, setFile] = useState<{path: string; newFileName: string; size: number}>();

const createAndSaveFile = (logs: Log[]) => {
localFileCreate('logs', JSON.stringify(logs, null, 2)).then((localFile) => {
RNFetchBlob.MediaCollection.copyToMediaStore(
{
name: localFile.newFileName,
parentFolder: '',
mimeType: 'text/plain',
},
'Download',
localFile.path,
);
setFile(localFile);
});
};

const shareLogs = () => {
if (!file) {
return;
}
Share.open({
url: `file://${file.path}`,
});
};

return (
<BaseClientSideLoggingToolMenu
file={file}
onEnableLogging={() => setFile(undefined)}
onDisableLogging={createAndSaveFile}
onShareLogs={shareLogs}
/>
);
}

ClientSideLoggingToolMenu.displayName = 'ClientSideLoggingToolMenu';

export default ClientSideLoggingToolMenu;
37 changes: 37 additions & 0 deletions src/components/ClientSideLoggingToolMenu/index.ios.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, {useState} from 'react';
import Share from 'react-native-share';
import type {Log} from '@libs/Console';
import localFileCreate from '@libs/localFileCreate';
import BaseClientSideLoggingToolMenu from './BaseClientSideLoggingToolMenu';

function ClientSideLoggingToolMenu() {
const [file, setFile] = useState<{path: string; newFileName: string; size: number}>();

const createFile = (logs: Log[]) => {
localFileCreate('logs', JSON.stringify(logs, null, 2)).then((localFile) => {
setFile(localFile);
});
};

const shareLogs = () => {
if (!file) {
return;
}
Share.open({
url: `file://${file.path}`,
});
};

return (
<BaseClientSideLoggingToolMenu
file={file}
onEnableLogging={() => setFile(undefined)}
onDisableLogging={createFile}
onShareLogs={shareLogs}
/>
);
}

ClientSideLoggingToolMenu.displayName = 'ClientSideLoggingToolMenu';

export default ClientSideLoggingToolMenu;
16 changes: 16 additions & 0 deletions src/components/ClientSideLoggingToolMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import type {Log} from '@libs/Console';
import localFileDownload from '@libs/localFileDownload';
import BaseClientSideLoggingToolMenu from './BaseClientSideLoggingToolMenu';

function ClientSideLoggingToolMenu() {
const downloadFile = (logs: Log[]) => {
localFileDownload('logs', JSON.stringify(logs, null, 2));
};

return <BaseClientSideLoggingToolMenu onDisableLogging={downloadFile} />;
}

ClientSideLoggingToolMenu.displayName = 'ClientSideLoggingToolMenu';

export default ClientSideLoggingToolMenu;
31 changes: 14 additions & 17 deletions src/components/ProfilingToolMenu/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Button from '@components/Button';
import Switch from '@components/Switch';
import TestToolRow from '@components/TestToolRow';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import toggleProfileTool from '@libs/actions/ProfilingTool';
import getPlatform from '@libs/getPlatform';
Expand Down Expand Up @@ -44,6 +45,7 @@ function ProfilingToolMenu({isProfilingInProgress = false}: ProfilingToolMenuPro
const [sharePath, setSharePath] = useState('');
const [totalMemory, setTotalMemory] = useState(0);
const [usedMemory, setUsedMemory] = useState(0);
const {translate} = useLocalize();

// eslint-disable-next-line @lwc/lwc/no-async-await
const stop = useCallback(async () => {
Expand Down Expand Up @@ -142,29 +144,24 @@ function ProfilingToolMenu({isProfilingInProgress = false}: ProfilingToolMenuPro

return (
<>
<Text
style={[styles.textLabelSupporting, styles.mt4, styles.mb3]}
numberOfLines={1}
>
Release options
</Text>

<TestToolRow title="Use Profiling">
<TestToolRow title={translate('initialSettingsPage.troubleshoot.useProfiling')}>
<Switch
accessibilityLabel="Use Profiling"
accessibilityLabel={translate('initialSettingsPage.troubleshoot.useProfiling')}
isOn={!!isProfilingInProgress}
onToggle={onToggleProfiling}
/>
</TestToolRow>
<Text style={[styles.textLabelSupporting, styles.mb4]}>{!!pathIOS && `path: ${pathIOS}`}</Text>
{!!pathIOS && (
<TestToolRow title="Profile trace">
<Button
small
text="Share"
onPress={onDownloadProfiling}
/>
</TestToolRow>
<>
<Text style={[styles.textLabelSupporting, styles.mb4]}>{`path: ${pathIOS}`}</Text>
<TestToolRow title={translate('initialSettingsPage.troubleshoot.profileTrace')}>
<Button
small
text={translate('common.share')}
onPress={onDownloadProfiling}
/>
</TestToolRow>
</>
)}
</>
);
Expand Down
13 changes: 13 additions & 0 deletions src/components/TestToolsModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import toggleTestToolsModal from '@userActions/TestTool';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ClientSideLoggingToolMenu from './ClientSideLoggingToolMenu';
import Modal from './Modal';
import ProfilingToolMenu from './ProfilingToolMenu';
import TestToolMenu from './TestToolMenu';
import Text from './Text';

type TestToolsModalOnyxProps = {
/** Whether the test tools modal is open */
Expand All @@ -23,6 +27,8 @@ function TestToolsModal({isTestToolsModalOpen = false}: TestToolsModalProps) {
const {isDevelopment} = useEnvironment();
const {windowWidth} = useWindowDimensions();
const StyleUtils = useStyleUtils();
const styles = useThemeStyles();
const {translate} = useLocalize();

return (
<Modal
Expand All @@ -32,7 +38,14 @@ function TestToolsModal({isTestToolsModalOpen = false}: TestToolsModalProps) {
>
<View style={[StyleUtils.getTestToolsModalStyle(windowWidth)]}>
{isDevelopment && <TestToolMenu />}
<Text
style={[styles.textLabelSupporting, styles.mt4, styles.mb3]}
numberOfLines={1}
>
{translate('initialSettingsPage.troubleshoot.releaseOptions')}
</Text>
<ProfilingToolMenu />
<ClientSideLoggingToolMenu />
</View>
</Modal>
);
Expand Down
6 changes: 6 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,11 @@ export default {
submitBug: 'submit a bug',
confirmResetDescription: 'All unsent draft messages will be lost, but the rest of your data is safe.',
resetAndRefresh: 'Reset and refresh',
clientSideLogging: 'Client side logging',
noLogsToShare: 'No logs to share',
useProfiling: 'Use profiling',
profileTrace: 'Profile trace',
releaseOptions: 'Release options',
},
debugConsole: {
saveLog: 'Save log',
Expand All @@ -899,6 +904,7 @@ export default {
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.`,
logs: 'Logs',
},
security: 'Security',
signOut: 'Sign out',
Expand Down
6 changes: 6 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -889,6 +889,11 @@ export default {
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',
clientSideLogging: 'Logs del cliente',
noLogsToShare: 'No hay logs que compartir',
useProfiling: 'Usar el trazado',
profileTrace: 'Traza de ejecución',
releaseOptions: 'Opciones de publicación',
},
debugConsole: {
saveLog: 'Guardar registro',
Expand All @@ -897,6 +902,7 @@ export default {
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.`,
logs: 'Logs',
},
security: 'Seguridad',
restoreStashed: 'Restablecer login guardado',
Expand Down
27 changes: 26 additions & 1 deletion src/libs/Console/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/naming-convention */
import isEmpty from 'lodash/isEmpty';
import Onyx from 'react-native-onyx';
import {addLog} from '@libs/actions/Console';
import CONFIG from '@src/CONFIG';
Expand Down Expand Up @@ -118,5 +119,29 @@ function createLog(text: string) {
}
}

export {sanitizeConsoleInput, createLog, shouldAttachLog};
/**
* Loops through all the logs and parses the message if it's a stringified JSON
* @param logs Logs captured on the current device
* @returns CapturedLogs with parsed messages
*/
function parseStringifiedMessages(logs: Log[]): Log[] {
if (isEmpty(logs)) {
return logs;
}

return logs.map((log) => {
try {
const parsedMessage = JSON.parse(log.message);
return {
...log,
message: parsedMessage,
};
} catch {
// If the message can't be parsed, just return the original log
return log;
}
});
}

export {sanitizeConsoleInput, createLog, shouldAttachLog, parseStringifiedMessages};
export type {Log};
Loading

0 comments on commit 2bcdd75

Please sign in to comment.