Skip to content

Commit a2313d1

Browse files
authored
Merge pull request #76649 from callstack-internal/fix/troubleshoot
feat: hide troubleshoot on prod, Implement auto-off timeout for troub…
2 parents 0d9c788 + 371ff8b commit a2313d1

10 files changed

Lines changed: 410 additions & 155 deletions

File tree

src/ONYXKEYS.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,9 @@ const ONYXKEYS = {
447447
/** Indicates whether we should record troubleshoot data or not */
448448
SHOULD_RECORD_TROUBLESHOOT_DATA: 'shouldRecordTroubleshootData',
449449

450+
/** Timestamp when troubleshoot recording was started (for auto-off after 10 minutes) */
451+
TROUBLESHOOT_RECORDING_START_TIME: 'troubleshootRecordingStartTime',
452+
450453
/** Indicates whether we should mask fragile user data while exporting onyx state or not */
451454
SHOULD_MASK_ONYX_STATE: 'shouldMaskOnyxState',
452455

@@ -1283,6 +1286,7 @@ type OnyxValuesMapping = {
12831286
[ONYXKEYS.LOGS]: OnyxTypes.CapturedLogs;
12841287
[ONYXKEYS.SHOULD_STORE_LOGS]: boolean;
12851288
[ONYXKEYS.SHOULD_RECORD_TROUBLESHOOT_DATA]: boolean;
1289+
[ONYXKEYS.TROUBLESHOOT_RECORDING_START_TIME]: number | null;
12861290
[ONYXKEYS.SHOULD_MASK_ONYX_STATE]: boolean;
12871291
[ONYXKEYS.SHOULD_USE_STAGING_SERVER]: boolean;
12881292
[ONYXKEYS.IS_DEBUG_MODE_ENABLED]: boolean;

src/components/RecordTroubleshootDataToolMenu/BaseRecordTroubleshootDataToolMenu.tsx

Lines changed: 45 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,25 @@
11
import type JSZip from 'jszip';
22
import type {RefObject} from 'react';
3-
import React, {useCallback, useEffect, useMemo, useState} from 'react';
3+
import React, {useEffect, useState} from 'react';
44
import {Alert} from 'react-native';
5-
import RNFetchBlob from 'react-native-blob-util';
65
import DeviceInfo from 'react-native-device-info';
7-
import {startProfiling, stopProfiling} from 'react-native-release-profiler';
86
import Button from '@components/Button';
97
import Switch from '@components/Switch';
108
import TestToolRow from '@components/TestToolRow';
119
import Text from '@components/Text';
1210
import useLocalize from '@hooks/useLocalize';
1311
import useOnyx from '@hooks/useOnyx';
1412
import useThemeStyles from '@hooks/useThemeStyles';
15-
import {disableLoggingAndFlushLogs, setShouldStoreLogs} from '@libs/actions/Console';
16-
import toggleProfileTool from '@libs/actions/ProfilingTool';
17-
import {setShouldRecordTroubleshootData} from '@libs/actions/Troubleshoot';
13+
import {cleanupAfterDisable, disableRecording, enableRecording, stopProfilingAndGetData} from '@libs/actions/Troubleshoot';
14+
import type {ProfilingData} from '@libs/actions/Troubleshoot';
1815
import {parseStringifiedMessages} from '@libs/Console';
1916
import getPlatform from '@libs/getPlatform';
20-
import Log from '@libs/Log';
21-
import {Memoize} from '@libs/memoize';
22-
import Performance from '@libs/Performance';
23-
import {shouldShowProfileTool as shouldShowProfileToolUtil} from '@userActions/TestTool';
2417
import CONFIG from '@src/CONFIG';
25-
import CONST from '@src/CONST';
2618
import ONYXKEYS from '@src/ONYXKEYS';
2719
import type {Log as OnyxLog} from '@src/types/onyx';
2820
import pkg from '../../../package.json';
29-
import RNFS from './RNFS';
21+
import handleStopRecording from './handleStopRecording';
22+
import type StopRecordingParams from './handleStopRecording.types';
3023
import Share from './Share';
3124

3225
type File = {
@@ -88,52 +81,26 @@ function BaseRecordTroubleshootDataToolMenu({
8881
const styles = useThemeStyles();
8982
const [shouldRecordTroubleshootData] = useOnyx(ONYXKEYS.SHOULD_RECORD_TROUBLESHOOT_DATA, {canBeMissing: true});
9083
const [capturedLogs] = useOnyx(ONYXKEYS.LOGS, {canBeMissing: true});
91-
const [isProfilingInProgress] = useOnyx(ONYXKEYS.APP_PROFILING_IN_PROGRESS, {canBeMissing: true});
9284
const [shareUrls, setShareUrls] = useState<string[]>();
9385
const [isDisabled, setIsDisabled] = useState<boolean>(false);
9486
const [profileTracePath, setProfileTracePath] = useState<string>();
9587

96-
const shouldShowProfileTool = useMemo(() => shouldShowProfileToolUtil(), []);
97-
98-
const onToggleProfiling = useCallback(() => {
99-
const shouldProfiling = !isProfilingInProgress;
100-
if (shouldProfiling) {
101-
setShareUrls(undefined);
102-
Memoize.startMonitoring();
103-
Performance.enableMonitoring();
104-
startProfiling();
105-
} else {
106-
Performance.disableMonitoring();
107-
}
108-
toggleProfileTool(shouldProfiling);
109-
return () => {
110-
Performance.disableMonitoring();
111-
};
112-
}, [isProfilingInProgress]);
113-
114-
const getAppInfo = useCallback(() => {
115-
return Promise.all([DeviceInfo.getTotalMemory(), DeviceInfo.getUsedMemory()]).then(([totalMemory, usedMemory]) => {
116-
return JSON.stringify({
117-
appVersion: pkg.version,
118-
environment: CONFIG.ENVIRONMENT,
119-
platform: getPlatform(),
120-
totalMemory: formatBytes(totalMemory, 2),
121-
usedMemory: formatBytes(usedMemory, 2),
122-
memoizeStats: Memoize.stopMonitoring(),
123-
performance: shouldShowProfileTool ? Performance.getPerformanceMeasures() : undefined,
124-
});
88+
const getAppInfo = async (profilingData: ProfilingData) => {
89+
const [totalMemory, usedMemory] = await Promise.all([DeviceInfo.getTotalMemory(), DeviceInfo.getUsedMemory()]);
90+
return JSON.stringify({
91+
appVersion: pkg.version,
92+
environment: CONFIG.ENVIRONMENT,
93+
platform: getPlatform(),
94+
totalMemory: formatBytes(totalMemory, 2),
95+
usedMemory: formatBytes(usedMemory, 2),
96+
memoizeStats: profilingData.memoizeStats,
97+
performance: profilingData.performanceMeasures,
12598
});
126-
}, [shouldShowProfileTool]);
127-
128-
const onStopProfiling = useMemo(() => (shouldShowProfileTool ? stopProfiling : () => Promise.resolve()), [shouldShowProfileTool]);
99+
};
129100

130-
const onToggle = () => {
131-
if (shouldShowProfileTool) {
132-
onToggleProfiling();
133-
}
101+
const onToggle = async () => {
134102
if (!shouldRecordTroubleshootData) {
135-
setShouldStoreLogs(true);
136-
setShouldRecordTroubleshootData(true);
103+
enableRecording();
137104

138105
if (onEnableLogging) {
139106
onEnableLogging();
@@ -146,8 +113,7 @@ function BaseRecordTroubleshootDataToolMenu({
146113

147114
if (!capturedLogs) {
148115
Alert.alert(translate('initialSettingsPage.troubleshoot.noLogsToShare'));
149-
disableLoggingAndFlushLogs();
150-
setShouldRecordTroubleshootData(false);
116+
disableRecording();
151117
return;
152118
}
153119

@@ -156,103 +122,30 @@ function BaseRecordTroubleshootDataToolMenu({
156122

157123
const infoFileName = `App_Info_${pkg.version}.json`;
158124

159-
if (getPlatform() === CONST.PLATFORM.WEB) {
160-
onStopProfiling(true, newFileName).then(() => {
161-
getAppInfo().then((appInfo) => {
162-
zipRef.current?.file(infoFileName, appInfo);
163-
164-
onDisableLogging(logsWithParsedMessages).then(() => {
165-
disableLoggingAndFlushLogs();
166-
setShouldRecordTroubleshootData(false);
167-
setIsDisabled(false);
168-
onDownloadZip?.();
169-
});
170-
});
171-
});
172-
} else if (getPlatform() === CONST.PLATFORM.IOS) {
173-
onStopProfiling(true, newFileName).then((path) => {
174-
if (!path) {
175-
return;
176-
}
177-
178-
const newFilePath = `${pathToBeUsed}/${newFileName}`;
179-
180-
RNFS.exists(newFilePath)
181-
.then((fileExists) => {
182-
if (!fileExists) {
183-
return;
184-
}
185-
186-
return RNFS.unlink(newFilePath).then(() => {
187-
Log.hmmm('[ProfilingToolMenu] existing file deleted successfully');
188-
});
189-
})
190-
.catch((error) => {
191-
const typedError = error as Error;
192-
Log.hmmm('[ProfilingToolMenu] error checking/deleting existing file: ', typedError.message);
193-
})
194-
.then(() => {
195-
RNFS.copyFile(path, newFilePath)
196-
.then(() => {
197-
getAppInfo().then((appInfo) => {
198-
zipRef.current?.file(infoFileName, appInfo);
199-
200-
onDisableLogging(logsWithParsedMessages).then(() => {
201-
disableLoggingAndFlushLogs();
202-
setShouldRecordTroubleshootData(false);
203-
setIsDisabled(false);
204-
onDownloadZip?.();
205-
});
206-
});
207-
Log.hmmm('[ProfilingToolMenu] file copied successfully');
208-
209-
setProfileTracePath(newFilePath);
210-
})
211-
.catch((err) => {
212-
console.error('[ProfilingToolMenu] error copying file: ', err);
213-
});
214-
})
215-
.catch((error: Record<string, unknown>) => {
216-
console.error('[ProfilingToolMenu] error copying file: ', error);
217-
Log.hmmm('[ProfilingToolMenu] error copying file: ', error);
218-
});
219-
});
220-
} else if (getPlatform() === CONST.PLATFORM.ANDROID) {
221-
onStopProfiling(true, newFileName).then((path) => {
222-
if (!path) {
223-
return;
224-
}
225-
226-
RNFetchBlob.fs
227-
// Check if it is an internal path of `DownloadManager` then append content://media to create a valid url
228-
.stat(!path.startsWith('content://media/') && path.match(/\/downloads\/\d+$/) ? `content://media/${path}` : path)
229-
.then(({path: realPath}) => setProfileTracePath(realPath))
230-
.catch(() => setProfileTracePath(path));
231-
232-
getAppInfo().then((appInfo) => {
233-
zipRef.current?.file(infoFileName, appInfo);
234-
235-
onDisableLogging(logsWithParsedMessages).then(() => {
236-
disableLoggingAndFlushLogs();
237-
setShouldRecordTroubleshootData(false);
238-
setIsDisabled(false);
239-
});
240-
});
241-
});
242-
} else {
243-
// Desktop
244-
onStopProfiling(true, newFileName).then(() => {
245-
getAppInfo().then((appInfo) => {
246-
zipRef.current?.file(infoFileName, appInfo);
247-
248-
onDisableLogging(logsWithParsedMessages).then(() => {
249-
disableLoggingAndFlushLogs();
250-
setShouldRecordTroubleshootData(false);
251-
setIsDisabled(false);
252-
});
253-
});
254-
});
125+
try {
126+
const profilingData = await stopProfilingAndGetData(newFileName);
127+
const appInfo = await getAppInfo(profilingData);
128+
129+
const params: StopRecordingParams = {
130+
profilingData,
131+
infoFileName,
132+
profileFileName: newFileName,
133+
appInfo,
134+
logsWithParsedMessages,
135+
onDisableLogging,
136+
cleanupAfterDisable,
137+
zipRef,
138+
pathToBeUsed,
139+
onDownloadZip,
140+
setProfileTracePath,
141+
};
142+
143+
await handleStopRecording(params);
144+
} catch (error) {
145+
console.error('[ProfilingToolMenu] error handling stop recording', error);
255146
}
147+
148+
setIsDisabled(false);
256149
};
257150

258151
useEffect(() => {
@@ -275,7 +168,9 @@ function BaseRecordTroubleshootDataToolMenu({
275168
<Switch
276169
accessibilityLabel={translate('initialSettingsPage.troubleshoot.recordTroubleshootData')}
277170
isOn={!!shouldRecordTroubleshootData}
278-
onToggle={onToggle}
171+
onToggle={() => {
172+
onToggle().catch((error) => console.error('[ProfilingToolMenu] toggle failed', error));
173+
}}
279174
disabled={isDisabled}
280175
/>
281176
</TestToolRow>
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type StopRecordingParams from './handleStopRecording.types';
2+
3+
type FinalizeStopRecordingParams = Pick<StopRecordingParams, 'infoFileName' | 'appInfo' | 'logsWithParsedMessages' | 'onDisableLogging' | 'cleanupAfterDisable' | 'zipRef' | 'onDownloadZip'>;
4+
5+
export default async function finalizeStopRecording({
6+
infoFileName,
7+
appInfo,
8+
logsWithParsedMessages,
9+
onDisableLogging,
10+
cleanupAfterDisable,
11+
zipRef,
12+
onDownloadZip,
13+
}: FinalizeStopRecordingParams): Promise<void> {
14+
zipRef.current?.file(infoFileName, appInfo);
15+
16+
await onDisableLogging(logsWithParsedMessages);
17+
cleanupAfterDisable();
18+
onDownloadZip?.();
19+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import RNFetchBlob from 'react-native-blob-util';
2+
import finalizeStopRecording from './finalizeStopRecording';
3+
import type StopRecordingParams from './handleStopRecording.types';
4+
5+
export default async function handleStopRecording({
6+
profilingData,
7+
infoFileName,
8+
appInfo,
9+
logsWithParsedMessages,
10+
onDisableLogging,
11+
cleanupAfterDisable,
12+
zipRef,
13+
onDownloadZip,
14+
setProfileTracePath,
15+
}: StopRecordingParams): Promise<void> {
16+
const {profilePath} = profilingData;
17+
18+
if (profilePath) {
19+
try {
20+
// Check if it is an internal path of `DownloadManager` then append content://media to create a valid url
21+
const {path} = await RNFetchBlob.fs.stat(!profilePath.startsWith('content://media/') && profilePath.match(/\/downloads\/\d+$/) ? `content://media/${profilePath}` : profilePath);
22+
setProfileTracePath?.(path);
23+
} catch {
24+
setProfileTracePath?.(profilePath);
25+
}
26+
}
27+
28+
await finalizeStopRecording({
29+
infoFileName,
30+
appInfo,
31+
logsWithParsedMessages,
32+
onDisableLogging,
33+
cleanupAfterDisable,
34+
zipRef,
35+
onDownloadZip,
36+
});
37+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import RNFS from 'react-native-fs';
2+
import Log from '@libs/Log';
3+
import finalizeStopRecording from './finalizeStopRecording';
4+
import type StopRecordingParams from './handleStopRecording.types';
5+
6+
export default async function handleStopRecording({
7+
profilingData,
8+
infoFileName,
9+
profileFileName,
10+
appInfo,
11+
logsWithParsedMessages,
12+
onDisableLogging,
13+
cleanupAfterDisable,
14+
zipRef,
15+
pathToBeUsed,
16+
onDownloadZip,
17+
setProfileTracePath,
18+
}: StopRecordingParams): Promise<void> {
19+
const {profilePath} = profilingData;
20+
21+
if (!profilePath) {
22+
cleanupAfterDisable();
23+
return;
24+
}
25+
26+
const newFilePath = `${pathToBeUsed}/${profileFileName}`;
27+
28+
try {
29+
const fileExists = await RNFS.exists(newFilePath);
30+
if (fileExists) {
31+
await RNFS.unlink(newFilePath);
32+
Log.hmmm('[ProfilingToolMenu] existing file deleted successfully');
33+
}
34+
} catch (error) {
35+
const message = error instanceof Error ? error.message : String(error);
36+
Log.hmmm('[ProfilingToolMenu] error checking/deleting existing file: ', message);
37+
}
38+
39+
try {
40+
await RNFS.copyFile(profilePath, newFilePath);
41+
await finalizeStopRecording({
42+
infoFileName,
43+
appInfo,
44+
logsWithParsedMessages,
45+
onDisableLogging,
46+
cleanupAfterDisable,
47+
zipRef,
48+
onDownloadZip,
49+
});
50+
51+
setProfileTracePath?.(newFilePath);
52+
Log.hmmm('[ProfilingToolMenu] file copied successfully');
53+
} catch (error) {
54+
const message = error instanceof Error ? error.message : String(error);
55+
Log.hmmm('[ProfilingToolMenu] error copying file: ', message);
56+
}
57+
}

0 commit comments

Comments
 (0)