From 2ea8bcdabf19baa79625b73fec89c44a15642fd1 Mon Sep 17 00:00:00 2001 From: MuckT Date: Fri, 8 Jul 2022 01:11:43 -0700 Subject: [PATCH 01/28] build(deps): react-native-email-link, react-native-mail and react-native-fs --- ios/Podfile.lock | 8 ++++---- package.json | 6 +++--- yarn.lock | 23 ++++++++++++----------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index fea13afecf..3855b06491 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -452,7 +452,7 @@ PODS: - React-Core - react-native-keep-awake (4.0.0): - React-Core - - react-native-mail (6.0.0): + - react-native-mail (6.1.1): - React-Core - react-native-netinfo (5.8.0): - React-Core @@ -599,7 +599,7 @@ PODS: - Firebase/Storage (= 7.11.0) - React-Core - RNFBApp - - RNFS (2.16.6): + - RNFS (2.20.0): - React-Core - RNGestureHandler (1.9.0): - React-Core @@ -1092,7 +1092,7 @@ SPEC CHECKSUMS: react-native-fast-crypto: db61bcb2d1224a180449b886f27426a67d7628c0 react-native-flipper: b9e2e817604af8da0d5a9ba20a8516e780e30f3c react-native-keep-awake: 6c078705f3fb2586963b13588d31148adc6bd128 - react-native-mail: 88305252f4c3fb0157015ff8a7ac9c41b321a0dd + react-native-mail: 8fdcd3aef007c33a6877a18eb4cf7447a1d4ce4a react-native-netinfo: f3f52e6d92372003b78d5cafa3557797c7beee47 react-native-plaid-link-sdk: 33cb904b956dc3b56799ac3027de8aa6fbb4b0ab react-native-randombytes: b6677f7d495c27e9ee0dbd77ebc97b3c59173729 @@ -1132,7 +1132,7 @@ SPEC CHECKSUMS: RNFBMessaging: b05aa2a76ed2b9fd50f05f036c96b26189a1fce2 RNFBRemoteConfig: e8d462f46e1759a5921d0e4b76d0ab89aef9baee RNFBStorage: 48f869205eb5537749cdd4f53689a526728421f8 - RNFS: 7161a36db9bbdaab96d91fff660820f94e29ca58 + RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 9b7e605a741412e20e13c512738a31bd1611759b RNImageCropPicker: 9e0bf18cf4184a846fed55747c8e622208b39947 RNInAppBrowser: 48b95ba7a4eaff5cc223bca338d3e319561dbd1b diff --git a/package.json b/package.json index bf62935897..c106a66581 100644 --- a/package.json +++ b/package.json @@ -146,11 +146,11 @@ "react-native-config": "https://github.com/luggit/react-native-config#2f68b94", "react-native-contacts": "https://github.com/celo-org/react-native-contacts#9940121", "react-native-device-info": "^8.3.1", - "react-native-email-link": "^1.12.2", + "react-native-email-link": "^1.14.0", "react-native-exit-app": "https://github.com/wumke/react-native-exit-app#5a022a7", "react-native-fast-crypto": "^2.0.0", "react-native-flag-secure-android": "https://github.com/kristiansorens/react-native-flag-secure-android#e234251", - "react-native-fs": "git+https://github.com/celo-org/react-native-fs#04669ac", + "react-native-fs": "^2.20.0", "react-native-gesture-handler": "^1.9.0", "react-native-google-safetynet": "https://github.com/celo-org/react-native-google-safetynet#8a0355f", "react-native-image-crop-picker": "^0.35.1", @@ -158,7 +158,7 @@ "react-native-keep-awake": "^4.0.0", "react-native-keychain": "8.0.0", "react-native-localize": "^2.1.7", - "react-native-mail": "^6.0.0", + "react-native-mail": "^6.1.1", "react-native-modal": "^13.0.1", "react-native-ntp-client": "^1.0.3", "react-native-permissions": "^3.0.3", diff --git a/yarn.lock b/yarn.lock index fae59e2b65..fde3a19280 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16124,10 +16124,10 @@ react-native-dotenv@^3.2.0: dependencies: dotenv "^10.0.0" -react-native-email-link@^1.12.2: - version "1.12.2" - resolved "https://registry.yarnpkg.com/react-native-email-link/-/react-native-email-link-1.12.2.tgz#61e8718dca98b362b3a847d7d05aa2b930ad7206" - integrity sha512-tr/Md3J0YuR7RO7R7c/N6bmM5r/44ZEUnbeJVO69/X4kTfvTUbCaAwNChqSRLnr2nXAtdTbaap0lBnHu2uyy8w== +react-native-email-link@^1.14.0: + version "1.14.0" + resolved "https://registry.yarnpkg.com/react-native-email-link/-/react-native-email-link-1.14.0.tgz#e90e1abea8d170946a0cb973a2f93e9c0a841653" + integrity sha512-RecW/hNE2at7AlE2/4f3cO12J6YSalRL7jv+z8X2DVrRZNv2lMavcLEEXh3z5ikRFXkMc0OYs+h6SrsalpN1lg== "react-native-exit-app@https://github.com/wumke/react-native-exit-app#5a022a7": version "1.1.0" @@ -16157,9 +16157,10 @@ react-native-fs@*, react-native-fs@^2.14.1, "react-native-fs@git+https://github. base-64 "^0.1.0" utf8 "^3.0.0" -"react-native-fs@git+https://github.com/celo-org/react-native-fs#04669ac": - version "2.16.6" - resolved "git+https://github.com/celo-org/react-native-fs#04669ac9de33ff9e8c266cec4443f88229d740e0" +react-native-fs@^2.20.0: + version "2.20.0" + resolved "https://registry.yarnpkg.com/react-native-fs/-/react-native-fs-2.20.0.tgz#05a9362b473bfc0910772c0acbb73a78dbc810f6" + integrity sha512-VkTBzs7fIDUiy/XajOSNk0XazFE9l+QlMAce7lGuebZcag5CnjszB+u4BdqzwaQOdcYb5wsJIsqq4kxInIRpJQ== dependencies: base-64 "^0.1.0" utf8 "^3.0.0" @@ -16221,10 +16222,10 @@ react-native-localize@^2.1.7: resolved "https://registry.yarnpkg.com/react-native-localize/-/react-native-localize-2.1.7.tgz#f1dda0c6fd58ab06b5b119cb4a507ab8800b9823" integrity sha512-phimOUtDLiqY2ba7Rjk9KpuaVSo7iGMNnwr7rjBVqlhmtTF3ShQ1FPFPxOyUrzDU9jLtj1xMi7MXWYuiVg660Q== -react-native-mail@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/react-native-mail/-/react-native-mail-6.0.0.tgz#7e8014cccff794098f8de012fc822c57d28f7d72" - integrity sha512-aQjCxKmPIwwX+voC2oK5Lj54Gfuvkodhp9lYFpPMxYYCXXtADuuZMsNWN7jSeqVuQHQzJZvYhgRtYqx0UDoKoQ== +react-native-mail@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/react-native-mail/-/react-native-mail-6.1.1.tgz#f1b1f8034c84d2510a93e4a2a795f0db5a13595e" + integrity sha512-pTs180wwyh7hN/iyTC9SfOX579U4YhDlHOLxi47IGvhPJENqO/QFdBq+wWKxyhNqdQuVSy+LoeIxLreWnIeYmg== react-native-modal@^13.0.1: version "13.0.1" From c7d6d29680ef60b9c9c2d568180acfb77653930d Mon Sep 17 00:00:00 2001 From: MuckT Date: Fri, 8 Jul 2022 01:12:47 -0700 Subject: [PATCH 02/28] fix: delete log file if older than 28 days --- src/utils/Logger.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index fe9a68f729..dcdd31d4b6 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -171,6 +171,13 @@ class Logger { overrideConsoleLogs = () => { const logFilePath = this.getReactNativeLogsFilePath() console.debug('React Native logs will be piped to ' + logFilePath) + RNFS.stat(logFilePath).then((stat) => { + // If the creation time of the file is more than 28 days ago, delete it. + if (+stat.ctime < +new Date() - 4 * 7 * 24 * 60 * 60 * 1000) { + console.debug('Deleting React Native logs file older than 28 days') + RNFS.unlink(logFilePath) + } + }) const consoleFns: { [key: string]: (message?: any, ...optionalParams: any[]) => void } = { debug: console.debug, From 99bea78ef96e99e00c37aa4504c0b3366ab1cd9d Mon Sep 17 00:00:00 2001 From: MuckT Date: Fri, 8 Jul 2022 01:41:09 -0700 Subject: [PATCH 03/28] fix: lint errors --- src/utils/Logger.ts | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index dcdd31d4b6..6e0141b8a3 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -171,13 +171,20 @@ class Logger { overrideConsoleLogs = () => { const logFilePath = this.getReactNativeLogsFilePath() console.debug('React Native logs will be piped to ' + logFilePath) - RNFS.stat(logFilePath).then((stat) => { - // If the creation time of the file is more than 28 days ago, delete it. - if (+stat.ctime < +new Date() - 4 * 7 * 24 * 60 * 60 * 1000) { - console.debug('Deleting React Native logs file older than 28 days') - RNFS.unlink(logFilePath) - } - }) + + // If the creation time of the file is more than 28 days ago, delete it. + RNFS.stat(logFilePath) + .then((stat) => { + if (+stat.ctime < +new Date() - 4 * 7 * 24 * 60 * 60 * 1000) { + console.debug('Deleting React Native logs file older than 28 days') + RNFS.unlink(logFilePath).catch((err) => { + console.warn('Failed to delete React Native logs file: ' + err) + }) + } + }) + .catch((err) => { + console.warn('Failed get log file stat: ' + err) + }) const consoleFns: { [key: string]: (message?: any, ...optionalParams: any[]) => void } = { debug: console.debug, From 1512eacb689d26349cfae695155928ad3b7298b6 Mon Sep 17 00:00:00 2001 From: MuckT Date: Mon, 11 Jul 2022 18:57:16 -0700 Subject: [PATCH 04/28] feat: store logs by day and combine when sending to support --- index.js | 1 + src/account/SupportContact.tsx | 37 ++-------- src/app/Debug.tsx | 2 +- src/utils/Logger.ts | 119 +++++++++++++++++++++++++++------ 4 files changed, 105 insertions(+), 54 deletions(-) diff --git a/index.js b/index.js index de61f8d66c..9b0cb28afd 100644 --- a/index.js +++ b/index.js @@ -18,6 +18,7 @@ import { Text, TextInput } from 'react-native' const SENTRY_ENABLED = stringToBoolean(Config.SENTRY_ENABLED || 'false') +Logger.initializeLogs() Logger.overrideConsoleLogs() const defaultErrorHandler = ErrorUtils.getGlobalHandler() diff --git a/src/account/SupportContact.tsx b/src/account/SupportContact.tsx index cb0ee92bf8..57c09b9cb6 100644 --- a/src/account/SupportContact.tsx +++ b/src/account/SupportContact.tsx @@ -1,11 +1,9 @@ import { anonymizedPhone } from '@celo/utils/lib/phoneNumbers' import { StackScreenProps } from '@react-navigation/stack' -import path from 'path' import React, { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { ActivityIndicator, Platform, ScrollView, StyleSheet, Text, View } from 'react-native' +import { ActivityIndicator, ScrollView, StyleSheet, Text, View } from 'react-native' import DeviceInfo from 'react-native-device-info' -import * as RNFS from 'react-native-fs' import { useDispatch, useSelector } from 'react-redux' import { Email, sendEmail } from 'src/account/emailSender' import { e164NumberSelector } from 'src/account/selectors' @@ -17,7 +15,6 @@ import KeyboardSpacer from 'src/components/KeyboardSpacer' import Switch from 'src/components/Switch' import TextInput from 'src/components/TextInput' import { CELO_SUPPORT_EMAIL_ADDRESS, DEFAULT_TESTNET } from 'src/config' -import i18n from 'src/i18n' import { navigateBack } from 'src/navigator/NavigationService' import { Screens } from 'src/navigator/Screens' import { StackParamList } from 'src/navigator/types' @@ -28,28 +25,6 @@ import { currentAccountSelector } from 'src/web3/selectors' type Props = StackScreenProps -async function exportLogs() { - try { - Logger.showMessage(i18n.t('supportExportLogsStart')) - const logsPath = Logger.getReactNativeLogsFilePath() - - // For now we need to export to a world-readable directory on Android - // TODO: use the FileProvider approach so we don't need to do this. - // See https://developer.android.com/reference/androidx/core/content/FileProvider - // and https://github.com/chirag04/react-native-mail/blame/340618e4ef7f21a29d739d4180c2a267a14093d3/android/src/main/java/com/chirag/RNMail/RNMailModule.java#L106 - if (Platform.OS === 'android') { - const publicPath = `${RNFS.ExternalDirectoryPath}/${path.basename(logsPath)}` - await RNFS.copyFile(logsPath, publicPath) - return publicPath - } - - return logsPath - } catch (e) { - Logger.showError(i18n.t('supportExportLogsError', { error: e })) - return false - } -} - function SupportContact({ route }: Props) { const { t } = useTranslation() const [message, setMessage] = useState('') @@ -92,13 +67,13 @@ function SupportContact({ route }: Props) { body: `${message}

${JSON.stringify(deviceInfo)}`, isHTML: true, } - let logsPath: string | false = false + let combinedLogsPath: string | false = false if (attachLogs) { - logsPath = await exportLogs() - if (logsPath) { + combinedLogsPath = await Logger.createCombinedLogs() + if (combinedLogsPath) { email.attachments = [ { - path: logsPath, // The absolute path of the file from which to read data. + path: combinedLogsPath, // The absolute path of the file from which to read data. type: 'text', // Mime Type: jpg, png, doc, ppt, html, pdf, csv name: '', // Optional: Custom filename for attachment }, @@ -108,7 +83,7 @@ function SupportContact({ route }: Props) { } setInProgress(false) try { - await sendEmail(email, deviceInfo, logsPath) + await sendEmail(email, deviceInfo, combinedLogsPath) navigateBackAndToast() } catch (error) { Logger.error('SupportContact', 'Error while sending logs to support', error) diff --git a/src/app/Debug.tsx b/src/app/Debug.tsx index f4c6f3ddc4..48b9829002 100644 --- a/src/app/Debug.tsx +++ b/src/app/Debug.tsx @@ -33,7 +33,7 @@ export class Debug extends React.Component { } updateLogs = async () => { - const reactNativeLogs = await Logger.getLogs() + const reactNativeLogs = await Logger.getDailyLogs() this.setState({ reactNativeLogs: reactNativeLogs || 'Not Found', }) diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index 6e0141b8a3..aa0addbbe4 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -1,5 +1,7 @@ /* eslint-disable no-console */ import * as Sentry from '@sentry/react-native' +import { format } from 'date-fns' +import { Platform } from 'react-native' import * as RNFS from 'react-native-fs' import Toast from 'react-native-simple-toast' import { DEFAULT_SENTRY_NETWORK_ERRORS, LOGGER_LEVEL } from 'src/config' @@ -146,15 +148,71 @@ class Logger { return error } - getReactNativeLogsFilePath = () => { - return RNFS.CachesDirectoryPath + '/rn_logs.txt' + getReactNativeLogFilePath = () => { + return `${RNFS.CachesDirectoryPath}/rn_logs/${format(new Date(), 'yyyyMMdd')}.txt` } - getLogs = async () => { - // TODO(Rossy) Does this technique of passing logs back as a string - // fail when the logs get too big? + getReactNativeLogsDir = () => { + return `${RNFS.CachesDirectoryPath}/rn_logs` + } + + getCombinedLogsFilePath = () => { + // For now we need to export to a world-readable directory on Android + // TODO: use the FileProvider approach so we don't need to do this. + // See https://developer.android.com/reference/androidx/core/content/FileProvider + // and https://github.com/chirag04/react-native-mail/blame/340618e4ef7f21a29d739d4180c2a267a14093d3/android/src/main/java/com/chirag/RNMail/RNMailModule.java#L106 + const path = Platform.OS === 'ios' ? RNFS.TemporaryDirectoryPath : RNFS.ExternalDirectoryPath + return `${path}/rn_logs.txt` + } + + // Initialize the logs directories and files + initializeLogs = async () => { + const logFileCombinedPath = this.getCombinedLogsFilePath() + const logFilePath = this.getReactNativeLogFilePath() + const logDir = this.getReactNativeLogsDir() + console.debug('React Native logs will be piped to ' + logFilePath) + + // If log folder not present create it + if (!(await RNFS.exists(logDir))) { + await RNFS.mkdir(logDir) + } + + // If daily log file is not present create it + if (!(await RNFS.exists(logFilePath))) { + await RNFS.writeFile(logFilePath, '', 'utf8') + } + + // If legacy log file or combined logs exist, delete it + if (await RNFS.exists(logFileCombinedPath)) { + await RNFS.unlink(logFileCombinedPath) + } + + // Get the list of log files + let logFiles = await RNFS.readDir(logDir) + + // Delete log files older than 28 days + if (logFiles.length > 0) { + logFiles.forEach((file) => { + RNFS.stat(file.path) + .then((stat) => { + if (+stat.ctime < +new Date() - 4 * 7 * 24 * 60 * 60 * 1000) { + console.debug('Deleting React Native logs file older than 28 days') + RNFS.unlink(file.path).catch((err) => { + console.warn('Failed to delete React Native logs file: ' + err) + }) + } + }) + .catch((err) => { + console.warn('Failed get log file stat: ' + err) + }) + }) + } + } + + // Gets the logs for the current day + getDailyLogs = async () => { try { - const rnLogsSrc = this.getReactNativeLogsFilePath() + const rnLogsSrc = this.getReactNativeLogFilePath() let reactNativeLogs = null if (await RNFS.exists(rnLogsSrc)) { reactNativeLogs = await RNFS.readFile(rnLogsSrc) @@ -166,26 +224,43 @@ class Logger { } } + // Combines logs from the last n days into a single file + createCombinedLogs = async () => { + try { + this.showMessage('Creating combined log...') + const combinedLogsPath = this.getCombinedLogsFilePath() + const logDir = this.getReactNativeLogsDir() + + // Delete previous combined Logs if present + if (await RNFS.exists(combinedLogsPath)) { + await RNFS.unlink(combinedLogsPath) + } + + // Create empty file to combine log files into + await RNFS.writeFile(combinedLogsPath, '', 'utf8') + + // Get all daily log files and combine into one file + let logFiles = await RNFS.readDir(logDir) + if (logFiles.length > 0) { + logFiles.forEach(async (file) => { + if (await RNFS.exists(file.path)) { + await RNFS.appendFile(combinedLogsPath, await RNFS.readFile(file.path), 'utf8') + } + }) + } + return combinedLogsPath + } catch (e) { + this.showError('Failed to combine logs: ' + e) + return false + } + } + // Anything being sent to console.log, console.warn, or console.error is piped into // the logfile specified by getReactNativeLogsFilePath() - overrideConsoleLogs = () => { - const logFilePath = this.getReactNativeLogsFilePath() + overrideConsoleLogs = async () => { + const logFilePath = this.getReactNativeLogFilePath() console.debug('React Native logs will be piped to ' + logFilePath) - // If the creation time of the file is more than 28 days ago, delete it. - RNFS.stat(logFilePath) - .then((stat) => { - if (+stat.ctime < +new Date() - 4 * 7 * 24 * 60 * 60 * 1000) { - console.debug('Deleting React Native logs file older than 28 days') - RNFS.unlink(logFilePath).catch((err) => { - console.warn('Failed to delete React Native logs file: ' + err) - }) - } - }) - .catch((err) => { - console.warn('Failed get log file stat: ' + err) - }) - const consoleFns: { [key: string]: (message?: any, ...optionalParams: any[]) => void } = { debug: console.debug, log: console.log, From 242eccbe34f52c38c9ec9795cc618570e4a88d1c Mon Sep 17 00:00:00 2001 From: MuckT Date: Mon, 11 Jul 2022 19:05:29 -0700 Subject: [PATCH 05/28] test: update support contact tests to use mock combined logs --- src/account/SupportContact.test.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/account/SupportContact.test.tsx b/src/account/SupportContact.test.tsx index 1fa00b6e7b..5c5eff8010 100644 --- a/src/account/SupportContact.test.tsx +++ b/src/account/SupportContact.test.tsx @@ -7,6 +7,7 @@ import SupportContact from 'src/account/SupportContact' import { APP_NAME, CELO_SUPPORT_EMAIL_ADDRESS } from 'src/brandingConfig' import i18n from 'src/i18n' import { Screens } from 'src/navigator/Screens' +import Logger from 'src/utils/Logger' import { createMockStore, flushMicrotasksQueue, getMockStackScreenProps } from 'test/utils' const mockScreenProps = getMockStackScreenProps(Screens.SupportContact) @@ -22,6 +23,10 @@ describe('Contact', () => { }) it('submits email with logs', async () => { + const mockedCreateCombinedLogs = Logger.createCombinedLogs as jest.Mock + const combinedLogsPath = 'log_path' + mockedCreateCombinedLogs.mockResolvedValue(combinedLogsPath) + const { getByTestId } = render( @@ -42,7 +47,7 @@ describe('Contact', () => { subject: i18n.t('supportEmailSubject', { appName: APP_NAME, user: '+1415555XXXX' }), attachments: [ { - path: '__EXTERNAL_DIRECTORY_PATH__/rn_logs.txt', + path: combinedLogsPath, type: 'text', name: '', }, From 532124feb93037500c9c5cc7501d2a723004d459 Mon Sep 17 00:00:00 2001 From: MuckT Date: Mon, 11 Jul 2022 19:07:30 -0700 Subject: [PATCH 06/28] fix: lint errors use const instead of let --- src/utils/Logger.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index aa0addbbe4..5ee9eb2dc5 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -188,7 +188,7 @@ class Logger { } // Get the list of log files - let logFiles = await RNFS.readDir(logDir) + const logFiles = await RNFS.readDir(logDir) // Delete log files older than 28 days if (logFiles.length > 0) { @@ -240,7 +240,7 @@ class Logger { await RNFS.writeFile(combinedLogsPath, '', 'utf8') // Get all daily log files and combine into one file - let logFiles = await RNFS.readDir(logDir) + const logFiles = await RNFS.readDir(logDir) if (logFiles.length > 0) { logFiles.forEach(async (file) => { if (await RNFS.exists(file.path)) { From 80e8b8b85f8e5afd3fc2d341b334f3f972122a51 Mon Sep 17 00:00:00 2001 From: MuckT Date: Tue, 12 Jul 2022 22:43:14 -0700 Subject: [PATCH 07/28] feat: separate function for log cleanup and create log file in writeLog --- index.js | 2 +- src/utils/Logger.ts | 83 +++++++++++++++++++++++---------------------- 2 files changed, 43 insertions(+), 42 deletions(-) diff --git a/index.js b/index.js index 9b0cb28afd..39653660d7 100644 --- a/index.js +++ b/index.js @@ -18,8 +18,8 @@ import { Text, TextInput } from 'react-native' const SENTRY_ENABLED = stringToBoolean(Config.SENTRY_ENABLED || 'false') -Logger.initializeLogs() Logger.overrideConsoleLogs() +Logger.cleanupOldLogs() const defaultErrorHandler = ErrorUtils.getGlobalHandler() const customErrorHandler = (e, isFatal) => { diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index 5ee9eb2dc5..ecd6ea3282 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -149,7 +149,7 @@ class Logger { } getReactNativeLogFilePath = () => { - return `${RNFS.CachesDirectoryPath}/rn_logs/${format(new Date(), 'yyyyMMdd')}.txt` + return `${this.getReactNativeLogsDir()}/${format(new Date(), 'yyyyMMdd')}.txt` } getReactNativeLogsDir = () => { @@ -165,47 +165,38 @@ class Logger { return `${path}/rn_logs.txt` } - // Initialize the logs directories and files - initializeLogs = async () => { - const logFileCombinedPath = this.getCombinedLogsFilePath() - const logFilePath = this.getReactNativeLogFilePath() - const logDir = this.getReactNativeLogsDir() - console.debug('React Native logs will be piped to ' + logFilePath) - - // If log folder not present create it - if (!(await RNFS.exists(logDir))) { - await RNFS.mkdir(logDir) - } + cleanupOldLogs = async () => { + try { + const logFileCombinedPath = this.getCombinedLogsFilePath() + const logDir = this.getReactNativeLogsDir() - // If daily log file is not present create it - if (!(await RNFS.exists(logFilePath))) { - await RNFS.writeFile(logFilePath, '', 'utf8') - } + // If legacy log file or combined logs exist, delete it + if (await RNFS.exists(logFileCombinedPath)) { + await RNFS.unlink(logFileCombinedPath) + } - // If legacy log file or combined logs exist, delete it - if (await RNFS.exists(logFileCombinedPath)) { - await RNFS.unlink(logFileCombinedPath) - } + // Get the list of log files + const logFiles = await RNFS.readDir(logDir) - // Get the list of log files - const logFiles = await RNFS.readDir(logDir) - - // Delete log files older than 28 days - if (logFiles.length > 0) { - logFiles.forEach((file) => { - RNFS.stat(file.path) - .then((stat) => { - if (+stat.ctime < +new Date() - 4 * 7 * 24 * 60 * 60 * 1000) { - console.debug('Deleting React Native logs file older than 28 days') - RNFS.unlink(file.path).catch((err) => { - console.warn('Failed to delete React Native logs file: ' + err) - }) - } - }) - .catch((err) => { - console.warn('Failed get log file stat: ' + err) - }) - }) + // Delete log files older than 28 days + if (logFiles.length > 0) { + logFiles.forEach((file) => { + RNFS.stat(file.path) + .then((stat) => { + if (+stat.ctime < +new Date() - 4 * 7 * 24 * 60 * 60 * 1000) { + console.debug('Deleting React Native logs file older than 28 days') + RNFS.unlink(file.path).catch((err) => { + console.warn('Failed to delete React Native logs file: ' + err) + }) + } + }) + .catch((err) => { + console.warn('Failed get log file stat: ' + err) + }) + }) + } + } catch (e) { + console.warn('Failed to cleanup old React Native logs: ' + e) } } @@ -257,7 +248,7 @@ class Logger { // Anything being sent to console.log, console.warn, or console.error is piped into // the logfile specified by getReactNativeLogsFilePath() - overrideConsoleLogs = async () => { + overrideConsoleLogs = () => { const logFilePath = this.getReactNativeLogFilePath() console.debug('React Native logs will be piped to ' + logFilePath) @@ -271,7 +262,17 @@ class Logger { warn: console.info, } - const writeLog = (level: string, message: string) => { + const writeLog = async (level: string, message: string) => { + // If log folder not present create it + if (!(await RNFS.exists(this.getReactNativeLogsDir()))) { + await RNFS.mkdir(this.getReactNativeLogsDir()) + } + + // If daily log file is not present create it + if (!(await RNFS.exists(logFilePath))) { + await RNFS.writeFile(logFilePath, '', 'utf8') + } + const timestamp = new Date().toISOString() RNFS.appendFile(logFilePath, `${level} [${timestamp}] ${message}\n`, 'utf8').catch( (error) => { From 91d1e4a0ea2df14a6b72a4409e2e6dbee0c17c3a Mon Sep 17 00:00:00 2001 From: MuckT Date: Wed, 13 Jul 2022 09:19:46 -0700 Subject: [PATCH 08/28] fix: add try catch for writeLog --- src/utils/Logger.ts | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index ecd6ea3282..2d2a22d26a 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -263,22 +263,26 @@ class Logger { } const writeLog = async (level: string, message: string) => { - // If log folder not present create it - if (!(await RNFS.exists(this.getReactNativeLogsDir()))) { - await RNFS.mkdir(this.getReactNativeLogsDir()) - } - - // If daily log file is not present create it - if (!(await RNFS.exists(logFilePath))) { - await RNFS.writeFile(logFilePath, '', 'utf8') - } + try { + // If log folder not present create it + if (!(await RNFS.exists(this.getReactNativeLogsDir()))) { + await RNFS.mkdir(this.getReactNativeLogsDir()) + } - const timestamp = new Date().toISOString() - RNFS.appendFile(logFilePath, `${level} [${timestamp}] ${message}\n`, 'utf8').catch( - (error) => { - consoleFns.debug(`Failed to write to ${logFilePath}`, error) + // If daily log file is not present create it + if (!(await RNFS.exists(logFilePath))) { + await RNFS.writeFile(logFilePath, '', 'utf8') } - ) + + const timestamp = new Date().toISOString() + RNFS.appendFile(logFilePath, `${level} [${timestamp}] ${message}\n`, 'utf8').catch( + (error) => { + consoleFns.debug(`Failed to write to ${logFilePath}`, error) + } + ) + } catch (error) { + consoleFns.debug('writeLog Failed', error) + } } const log = (level: string, message?: any, ...optionalParams: any[]) => { From 854436be7a6c50e506f4093b1cf0ceb583a960dd Mon Sep 17 00:00:00 2001 From: MuckT Date: Wed, 13 Jul 2022 10:01:23 -0700 Subject: [PATCH 09/28] fix: lint error promises must be handled appropriately --- src/utils/Logger.ts | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index 2d2a22d26a..153e4a8847 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -263,26 +263,22 @@ class Logger { } const writeLog = async (level: string, message: string) => { - try { - // If log folder not present create it - if (!(await RNFS.exists(this.getReactNativeLogsDir()))) { - await RNFS.mkdir(this.getReactNativeLogsDir()) - } - - // If daily log file is not present create it - if (!(await RNFS.exists(logFilePath))) { - await RNFS.writeFile(logFilePath, '', 'utf8') - } + // If log folder not present create it + if (!(await RNFS.exists(this.getReactNativeLogsDir()))) { + await RNFS.mkdir(this.getReactNativeLogsDir()) + } - const timestamp = new Date().toISOString() - RNFS.appendFile(logFilePath, `${level} [${timestamp}] ${message}\n`, 'utf8').catch( - (error) => { - consoleFns.debug(`Failed to write to ${logFilePath}`, error) - } - ) - } catch (error) { - consoleFns.debug('writeLog Failed', error) + // If daily log file is not present create it + if (!(await RNFS.exists(logFilePath))) { + await RNFS.writeFile(logFilePath, '', 'utf8') } + + const timestamp = new Date().toISOString() + RNFS.appendFile(logFilePath, `${level} [${timestamp}] ${message}\n`, 'utf8').catch( + (error) => { + consoleFns.debug(`Failed to write to ${logFilePath}`, error) + } + ) } const log = (level: string, message?: any, ...optionalParams: any[]) => { @@ -291,7 +287,7 @@ class Logger { const consoleMessage = `[${timestamp}] ${message}` consoleFns[level](consoleMessage, ...optionalParams) - writeLog(level, message) + writeLog(level, message).catch((error) => consoleFns.debug('writeLog error', error)) } else { consoleFns[level](message, ...optionalParams) } From eb39621ad3ac581e21827afddfe374df120a0d29 Mon Sep 17 00:00:00 2001 From: MuckT Date: Wed, 13 Jul 2022 15:12:10 -0700 Subject: [PATCH 10/28] test(logger): add unit tests --- src/utils/Logger.test.tsx | 69 +++++++++++++++++++++++++++++++++++++++ src/utils/Logger.ts | 2 +- 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 src/utils/Logger.test.tsx diff --git a/src/utils/Logger.test.tsx b/src/utils/Logger.test.tsx new file mode 100644 index 0000000000..775a435848 --- /dev/null +++ b/src/utils/Logger.test.tsx @@ -0,0 +1,69 @@ +import 'react-native' +import { Platform } from 'react-native' +import { readDir, stat } from '../../__mocks__/react-native-fs' +import Logger from './Logger' + +describe('utils/Logger', () => { + it('Logger overrides console.debug', () => { + console.debug = jest.fn() + Logger.debug('Test/Debug', 'Test message #1', 'Test message #2') + expect(console.debug).toBeCalledTimes(1) + expect(console.debug).toHaveBeenCalledWith('Test/Debug/Test message #1, Test message #2') + }) + + it('Logger overrides console.info', () => { + console.info = jest.fn() + Logger.info('Test/Info', 'Test message #1', 'Test message #2') + expect(console.info).toBeCalledTimes(1) + expect(console.info).toHaveBeenCalledWith('Test/Info/Test message #1, Test message #2') + }) + + it('Logger.warn pipes to console.info', () => { + console.info = jest.fn() + Logger.warn('Test/Warn', 'Test message #1', 'Test message #2') + expect(console.info).toBeCalledTimes(1) + expect(console.info).toHaveBeenCalledWith('Test/Warn/Test message #1, Test message #2') + }) + + it('Returns combined logs file path iOS', () => { + // mock + Platform.OS = 'ios' + expect(Logger.getCombinedLogsFilePath()).toBe('__TEMPORARY_DIRECTORY_PATH__/rn_logs.txt') + }) + + it('Returns combined logs file path Android', () => { + // mock + Platform.OS = 'android' + expect(Logger.getCombinedLogsFilePath()).toBe('__EXTERNAL_DIRECTORY_PATH__/rn_logs.txt') + }) + + it('Cleans up old logs', async () => { + const mockData = [ + { + // ctime is 29 days ago + ctime: new Date(new Date().getTime() - 2505600000), + name: 'toDelete.txt', + path: '__TEMPORARY_DIRECTORY_PATH__/rn_logs/toDelete.txt', + size: 5318, + isFile: true, + isDirectory: false, + }, + { + ctime: new Date(), + name: 'toSave.txt', + path: '__TEMPORARY_DIRECTORY_PATH__/rn_logs/toSave.txt', + size: 5318, + isFile: true, + isDirectory: false, + }, + ] + console.debug = jest.fn() + readDir.mockImplementation(() => mockData) + stat.mockImplementation((filePath) => + Promise.resolve(mockData.find((file) => file.path === filePath)) + ) + await Logger.cleanupOldLogs() + expect(console.debug).toHaveBeenCalledTimes(1) + expect(console.debug).toHaveBeenCalledWith('Deleting React Native log file older than 28 days') + }) +}) diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index 153e4a8847..e822892a3e 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -184,7 +184,7 @@ class Logger { RNFS.stat(file.path) .then((stat) => { if (+stat.ctime < +new Date() - 4 * 7 * 24 * 60 * 60 * 1000) { - console.debug('Deleting React Native logs file older than 28 days') + console.debug('Deleting React Native log file older than 28 days') RNFS.unlink(file.path).catch((err) => { console.warn('Failed to delete React Native logs file: ' + err) }) From ddeaa9e41e4714787d4957fb1f882807788f0e54 Mon Sep 17 00:00:00 2001 From: MuckT Date: Wed, 13 Jul 2022 16:24:56 -0700 Subject: [PATCH 11/28] fix: lint errors --- src/utils/Logger.test.tsx | 62 ++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/src/utils/Logger.test.tsx b/src/utils/Logger.test.tsx index 775a435848..5525657032 100644 --- a/src/utils/Logger.test.tsx +++ b/src/utils/Logger.test.tsx @@ -1,8 +1,45 @@ +/* eslint no-console: 0 */ +// no-console disabled as we are testing console logs are overwritten by the logger import 'react-native' import { Platform } from 'react-native' -import { readDir, stat } from '../../__mocks__/react-native-fs' import Logger from './Logger' +const mockData = [ + { + // ctime is 29 days ago + ctime: new Date(new Date().getTime() - 2505600000), + name: 'toDelete.txt', + path: '__TEMPORARY_DIRECTORY_PATH__/rn_logs/toDelete.txt', + size: 5318, + isFile: true, + isDirectory: false, + }, + { + ctime: new Date(), + name: 'toSave.txt', + path: '__TEMPORARY_DIRECTORY_PATH__/rn_logs/toSave.txt', + size: 5318, + isFile: true, + isDirectory: false, + }, +] + +jest.mock('react-native-fs', () => { + return { + readDir: jest.fn(() => Promise.resolve(mockData)), + stat: jest + .fn() + .mockImplementation((filePath) => + Promise.resolve(mockData.find((file) => file.path === filePath)) + ), + exists: jest.fn(), + unlink: jest.fn(() => Promise.resolve()), + writeFile: jest.fn(() => Promise.resolve()), + ExternalDirectoryPath: '__EXTERNAL_DIRECTORY_PATH__', + TemporaryDirectoryPath: '__TEMPORARY_DIRECTORY_PATH__', + } +}) + describe('utils/Logger', () => { it('Logger overrides console.debug', () => { console.debug = jest.fn() @@ -38,30 +75,7 @@ describe('utils/Logger', () => { }) it('Cleans up old logs', async () => { - const mockData = [ - { - // ctime is 29 days ago - ctime: new Date(new Date().getTime() - 2505600000), - name: 'toDelete.txt', - path: '__TEMPORARY_DIRECTORY_PATH__/rn_logs/toDelete.txt', - size: 5318, - isFile: true, - isDirectory: false, - }, - { - ctime: new Date(), - name: 'toSave.txt', - path: '__TEMPORARY_DIRECTORY_PATH__/rn_logs/toSave.txt', - size: 5318, - isFile: true, - isDirectory: false, - }, - ] console.debug = jest.fn() - readDir.mockImplementation(() => mockData) - stat.mockImplementation((filePath) => - Promise.resolve(mockData.find((file) => file.path === filePath)) - ) await Logger.cleanupOldLogs() expect(console.debug).toHaveBeenCalledTimes(1) expect(console.debug).toHaveBeenCalledWith('Deleting React Native log file older than 28 days') From a86a7a7613ba7a029a9c89fad8917b8030bdbe45 Mon Sep 17 00:00:00 2001 From: MuckT Date: Thu, 14 Jul 2022 17:04:15 -0700 Subject: [PATCH 12/28] style: use error consistently --- src/utils/Logger.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index e822892a3e..2a75d89adf 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -185,18 +185,18 @@ class Logger { .then((stat) => { if (+stat.ctime < +new Date() - 4 * 7 * 24 * 60 * 60 * 1000) { console.debug('Deleting React Native log file older than 28 days') - RNFS.unlink(file.path).catch((err) => { - console.warn('Failed to delete React Native logs file: ' + err) + RNFS.unlink(file.path).catch((error) => { + console.warn('Failed to delete React Native logs file: ' + error) }) } }) - .catch((err) => { - console.warn('Failed get log file stat: ' + err) + .catch((error) => { + console.warn('Failed get log file stat: ' + error) }) }) } - } catch (e) { - console.warn('Failed to cleanup old React Native logs: ' + e) + } catch (error) { + console.warn('Failed to cleanup old React Native logs: ' + error) } } @@ -209,8 +209,8 @@ class Logger { reactNativeLogs = await RNFS.readFile(rnLogsSrc) } return reactNativeLogs - } catch (e) { - this.showError('Failed to read logs: ' + e) + } catch (error) { + this.showError('Failed to read logs: ' + error) return null } } @@ -240,8 +240,8 @@ class Logger { }) } return combinedLogsPath - } catch (e) { - this.showError('Failed to combine logs: ' + e) + } catch (error) { + this.showError('Failed to combine logs: ' + error) return false } } From f8b84262e339c90deeee4cbaad1d5bf7742f9960 Mon Sep 17 00:00:00 2001 From: MuckT Date: Thu, 14 Jul 2022 17:27:23 -0700 Subject: [PATCH 13/28] fix: use for...of instead of forEach for easier awaits --- src/utils/Logger.ts | 59 ++++++++++++++++++--------------------------- 1 file changed, 23 insertions(+), 36 deletions(-) diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index 2a75d89adf..e00f495bdd 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -179,21 +179,12 @@ class Logger { const logFiles = await RNFS.readDir(logDir) // Delete log files older than 28 days - if (logFiles.length > 0) { - logFiles.forEach((file) => { - RNFS.stat(file.path) - .then((stat) => { - if (+stat.ctime < +new Date() - 4 * 7 * 24 * 60 * 60 * 1000) { - console.debug('Deleting React Native log file older than 28 days') - RNFS.unlink(file.path).catch((error) => { - console.warn('Failed to delete React Native logs file: ' + error) - }) - } - }) - .catch((error) => { - console.warn('Failed get log file stat: ' + error) - }) - }) + for (const logFile of logFiles) { + const stat = await RNFS.stat(logFile.path) + if (+stat.ctime < +new Date() - 4 * 7 * 24 * 60 * 60 * 1000) { + console.debug('Deleting React Native log file older than 28 days') + await RNFS.unlink(logFile.path) + } } } catch (error) { console.warn('Failed to cleanup old React Native logs: ' + error) @@ -232,17 +223,14 @@ class Logger { // Get all daily log files and combine into one file const logFiles = await RNFS.readDir(logDir) - if (logFiles.length > 0) { - logFiles.forEach(async (file) => { - if (await RNFS.exists(file.path)) { - await RNFS.appendFile(combinedLogsPath, await RNFS.readFile(file.path), 'utf8') - } - }) + for (const logFile of logFiles) { + if (await RNFS.exists(logFile.path)) { + await RNFS.appendFile(combinedLogsPath, await RNFS.readFile(logFile.path), 'utf8') + } } return combinedLogsPath } catch (error) { this.showError('Failed to combine logs: ' + error) - return false } } @@ -263,22 +251,21 @@ class Logger { } const writeLog = async (level: string, message: string) => { - // If log folder not present create it - if (!(await RNFS.exists(this.getReactNativeLogsDir()))) { - await RNFS.mkdir(this.getReactNativeLogsDir()) - } - - // If daily log file is not present create it - if (!(await RNFS.exists(logFilePath))) { - await RNFS.writeFile(logFilePath, '', 'utf8') - } + try { + // If log folder not present create it + if (!(await RNFS.exists(this.getReactNativeLogsDir()))) { + await RNFS.mkdir(this.getReactNativeLogsDir()) + } - const timestamp = new Date().toISOString() - RNFS.appendFile(logFilePath, `${level} [${timestamp}] ${message}\n`, 'utf8').catch( - (error) => { - consoleFns.debug(`Failed to write to ${logFilePath}`, error) + // If daily log file is not present create it + if (!(await RNFS.exists(logFilePath))) { + await RNFS.writeFile(logFilePath, '', 'utf8') } - ) + const timestamp = new Date().toISOString() + await RNFS.appendFile(logFilePath, `${level} [${timestamp}] ${message}\n`, 'utf8') + } catch (error) { + consoleFns.debug(`Failed to write to ${logFilePath}`, error) + } } const log = (level: string, message?: any, ...optionalParams: any[]) => { From 76e1ea16e477ed3b7a7a4e5ca0ad3f274dc39bed Mon Sep 17 00:00:00 2001 From: MuckT Date: Thu, 14 Jul 2022 17:28:22 -0700 Subject: [PATCH 14/28] feat: add logFile path to console debug statement --- src/utils/{Logger.test.tsx => Logger.test.ts} | 5 ++++- src/utils/Logger.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) rename src/utils/{Logger.test.tsx => Logger.test.ts} (93%) diff --git a/src/utils/Logger.test.tsx b/src/utils/Logger.test.ts similarity index 93% rename from src/utils/Logger.test.tsx rename to src/utils/Logger.test.ts index 5525657032..e4b846e1b8 100644 --- a/src/utils/Logger.test.tsx +++ b/src/utils/Logger.test.ts @@ -78,6 +78,9 @@ describe('utils/Logger', () => { console.debug = jest.fn() await Logger.cleanupOldLogs() expect(console.debug).toHaveBeenCalledTimes(1) - expect(console.debug).toHaveBeenCalledWith('Deleting React Native log file older than 28 days') + expect(console.debug).toHaveBeenCalledWith( + 'Deleting React Native log file older than 28 days', + '__TEMPORARY_DIRECTORY_PATH__/rn_logs/toDelete.txt' + ) }) }) diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index e00f495bdd..ecf20d0ef0 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -182,7 +182,7 @@ class Logger { for (const logFile of logFiles) { const stat = await RNFS.stat(logFile.path) if (+stat.ctime < +new Date() - 4 * 7 * 24 * 60 * 60 * 1000) { - console.debug('Deleting React Native log file older than 28 days') + console.debug('Deleting React Native log file older than 28 days', logFile.path) await RNFS.unlink(logFile.path) } } From ae68c2171ed87805efa7dc66c7761af031af633f Mon Sep 17 00:00:00 2001 From: MuckT Date: Thu, 14 Jul 2022 17:34:12 -0700 Subject: [PATCH 15/28] fix: build error from createCombinedLogs --- src/utils/Logger.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index ecf20d0ef0..c61df89632 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -231,6 +231,7 @@ class Logger { return combinedLogsPath } catch (error) { this.showError('Failed to combine logs: ' + error) + return false } } From b8e398dc018b88752c9f3855393b4e00bd7211b2 Mon Sep 17 00:00:00 2001 From: MuckT Date: Fri, 15 Jul 2022 09:42:22 -0700 Subject: [PATCH 16/28] feat: create readFile util to read files in chunks for lower memory usage --- src/account/emailSender.ts | 5 ++--- src/utils/Logger.test.ts | 3 +-- src/utils/Logger.ts | 31 +++++++++++++++++++++++-------- src/utils/readFile.ts | 35 +++++++++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 13 deletions(-) create mode 100644 src/utils/readFile.ts diff --git a/src/account/emailSender.ts b/src/account/emailSender.ts index 6ecce4b35f..9f99460ea2 100644 --- a/src/account/emailSender.ts +++ b/src/account/emailSender.ts @@ -1,8 +1,7 @@ import { openComposer } from 'react-native-email-link' -import * as RNFS from 'react-native-fs' import Mailer from 'react-native-mail' import { CELO_SUPPORT_EMAIL_ADDRESS } from 'src/config' -import Logger from 'src/utils/Logger' +import { readFileChunked } from 'src/utils/readFile' export interface Email { subject: string @@ -24,7 +23,7 @@ export async function sendEmailWithNonNativeApp( ) { try { const supportLogsMessage = logsPath - ? `Support logs: ${!logsPath || (await RNFS.readFile(logsPath))}` + ? `Support logs: ${!logsPath || (await readFileChunked(logsPath))}` : '' await openComposer({ to: CELO_SUPPORT_EMAIL_ADDRESS, diff --git a/src/utils/Logger.test.ts b/src/utils/Logger.test.ts index e4b846e1b8..6419d40bdf 100644 --- a/src/utils/Logger.test.ts +++ b/src/utils/Logger.test.ts @@ -79,8 +79,7 @@ describe('utils/Logger', () => { await Logger.cleanupOldLogs() expect(console.debug).toHaveBeenCalledTimes(1) expect(console.debug).toHaveBeenCalledWith( - 'Deleting React Native log file older than 28 days', - '__TEMPORARY_DIRECTORY_PATH__/rn_logs/toDelete.txt' + 'Logger/cleanupOldLogs/Deleting React Native log file older than 14 days, __TEMPORARY_DIRECTORY_PATH__/rn_logs/toDelete.txt' ) }) }) diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index c61df89632..25271af6e4 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -6,6 +6,7 @@ import * as RNFS from 'react-native-fs' import Toast from 'react-native-simple-toast' import { DEFAULT_SENTRY_NETWORK_ERRORS, LOGGER_LEVEL } from 'src/config' import { LoggerLevel } from 'src/utils/LoggerLevels' +import { readFileChunked } from 'src/utils/readFile' class Logger { isNetworkConnected: boolean @@ -178,16 +179,20 @@ class Logger { // Get the list of log files const logFiles = await RNFS.readDir(logDir) - // Delete log files older than 28 days + // Delete log files older than 14 days for (const logFile of logFiles) { const stat = await RNFS.stat(logFile.path) - if (+stat.ctime < +new Date() - 4 * 7 * 24 * 60 * 60 * 1000) { - console.debug('Deleting React Native log file older than 28 days', logFile.path) + if (+stat.ctime < +new Date() - 2 * 7 * 24 * 60 * 60 * 1000) { + this.debug( + 'Logger/cleanupOldLogs', + 'Deleting React Native log file older than 14 days', + logFile.path + ) await RNFS.unlink(logFile.path) } } } catch (error) { - console.warn('Failed to cleanup old React Native logs: ' + error) + this.error('Logger@cleanupOldLogs', 'Failed to cleanupOldLogs', error as Error) } } @@ -197,7 +202,7 @@ class Logger { const rnLogsSrc = this.getReactNativeLogFilePath() let reactNativeLogs = null if (await RNFS.exists(rnLogsSrc)) { - reactNativeLogs = await RNFS.readFile(rnLogsSrc) + reactNativeLogs = await readFileChunked(rnLogsSrc) } return reactNativeLogs } catch (error) { @@ -225,7 +230,11 @@ class Logger { const logFiles = await RNFS.readDir(logDir) for (const logFile of logFiles) { if (await RNFS.exists(logFile.path)) { - await RNFS.appendFile(combinedLogsPath, await RNFS.readFile(logFile.path), 'utf8') + await RNFS.appendFile( + combinedLogsPath, + (await readFileChunked(logFile.path)) as string, + 'utf8' + ) } } return combinedLogsPath @@ -262,8 +271,14 @@ class Logger { if (!(await RNFS.exists(logFilePath))) { await RNFS.writeFile(logFilePath, '', 'utf8') } + const timestamp = new Date().toISOString() - await RNFS.appendFile(logFilePath, `${level} [${timestamp}] ${message}\n`, 'utf8') + // Ensure messages are converted to utf8 as some remote CTA's can have non utf8 characters + await RNFS.appendFile( + logFilePath, + `${level} [${timestamp}] ${Buffer.from(message, 'utf-8').toString()}\n`, + 'utf8' + ) } catch (error) { consoleFns.debug(`Failed to write to ${logFilePath}`, error) } @@ -275,7 +290,7 @@ class Logger { const consoleMessage = `[${timestamp}] ${message}` consoleFns[level](consoleMessage, ...optionalParams) - writeLog(level, message).catch((error) => consoleFns.debug('writeLog error', error)) + writeLog(level, message).catch((error) => consoleFns.debug(error)) } else { consoleFns[level](message, ...optionalParams) } diff --git a/src/utils/readFile.ts b/src/utils/readFile.ts new file mode 100644 index 0000000000..2e3982ad05 --- /dev/null +++ b/src/utils/readFile.ts @@ -0,0 +1,35 @@ +import * as RNFS from 'react-native-fs' +import Logger from 'src/utils/Logger' + +/** + * Reads and returns a portion of the input file. + */ +const readChunk = async (file: string, length: number, position: number) => { + try { + return await RNFS.read(file, length, position, 'utf8') + } catch (error) { + Logger.error('Logger@readChunk', 'readChunk error', error as Error) + return '' + } +} + +/** + * Reads the logs in chunks to a string and returns the string. + * [Credits]{@link: https://stackoverflow.com/a/56284734} + */ +export const readFileChunked = async (rnLogsSrc = Logger.getReactNativeLogFilePath()) => { + try { + const length = 4096 + let chunk = '' + let fileContents = '' + if (await RNFS.exists(rnLogsSrc)) { + do { + chunk = await readChunk(rnLogsSrc, length, fileContents.length) + fileContents += chunk + } while (chunk.length > 0) + } + return fileContents + } catch (error) { + Logger.error('Logger@readFileChunked', `Failed to readFileChunked ${rnLogsSrc}`, error as Error) + } +} From d728f24568943cc2546cfe0892d8c95833a8085f Mon Sep 17 00:00:00 2001 From: MuckT Date: Fri, 15 Jul 2022 10:00:54 -0700 Subject: [PATCH 17/28] fix: add missing logger import --- src/account/emailSender.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/account/emailSender.ts b/src/account/emailSender.ts index 9f99460ea2..8ff67d9c41 100644 --- a/src/account/emailSender.ts +++ b/src/account/emailSender.ts @@ -1,6 +1,7 @@ import { openComposer } from 'react-native-email-link' import Mailer from 'react-native-mail' import { CELO_SUPPORT_EMAIL_ADDRESS } from 'src/config' +import Logger from 'src/utils/Logger' import { readFileChunked } from 'src/utils/readFile' export interface Email { From 8bd70d0e8a85725f6c352f3bf85c615a63a7c78b Mon Sep 17 00:00:00 2001 From: MuckT Date: Sun, 17 Jul 2022 14:14:37 -0700 Subject: [PATCH 18/28] feat: upload multiple attachments and save logs up to 60 days --- __mocks__/src/utils/Logger.ts | 2 +- src/account/SupportContact.test.tsx | 25 ++++++---- src/account/SupportContact.tsx | 26 +++++----- src/app/Debug.tsx | 2 +- src/utils/Logger.test.ts | 7 +-- src/utils/Logger.ts | 74 ++++++++++++++--------------- 6 files changed, 69 insertions(+), 67 deletions(-) diff --git a/__mocks__/src/utils/Logger.ts b/__mocks__/src/utils/Logger.ts index 49d7ea151e..09245e0118 100644 --- a/__mocks__/src/utils/Logger.ts +++ b/__mocks__/src/utils/Logger.ts @@ -3,6 +3,6 @@ module.exports = { ...jest.requireActual('src/utils/Logger'), default: { ...jest.requireActual('src/utils/Logger').default, - createCombinedLogs: jest.fn(), + getLogsToAttach: jest.fn(), }, } diff --git a/src/account/SupportContact.test.tsx b/src/account/SupportContact.test.tsx index 5c5eff8010..6cd5a1f7d2 100644 --- a/src/account/SupportContact.test.tsx +++ b/src/account/SupportContact.test.tsx @@ -23,9 +23,20 @@ describe('Contact', () => { }) it('submits email with logs', async () => { - const mockedCreateCombinedLogs = Logger.createCombinedLogs as jest.Mock - const combinedLogsPath = 'log_path' - mockedCreateCombinedLogs.mockResolvedValue(combinedLogsPath) + const mockedLogAttachments = Logger.getLogsToAttach as jest.Mock + const logAttachments = [ + { + path: 'logs/log1.txt', + type: 'text', + name: 'log1.txt', + }, + { + path: 'logs/log2.txt', + type: 'text', + name: 'log2.txt', + }, + ] + mockedLogAttachments.mockResolvedValue(logAttachments) const { getByTestId } = render( @@ -45,13 +56,7 @@ describe('Contact', () => { 'Test Message

{"version":"0.0.1","buildNumber":"1","apiLevel":-1,"deviceId":"unknown","address":"0x0000000000000000000000000000000000007e57","sessionId":"","numberVerified":false,"network":"alfajores"}

Support logs are attached...', recipients: [CELO_SUPPORT_EMAIL_ADDRESS], subject: i18n.t('supportEmailSubject', { appName: APP_NAME, user: '+1415555XXXX' }), - attachments: [ - { - path: combinedLogsPath, - type: 'text', - name: '', - }, - ], + attachments: logAttachments, }), expect.any(Function) ) diff --git a/src/account/SupportContact.tsx b/src/account/SupportContact.tsx index 57c09b9cb6..5cf0c12ba8 100644 --- a/src/account/SupportContact.tsx +++ b/src/account/SupportContact.tsx @@ -48,6 +48,11 @@ function SupportContact({ route }: Props) { dispatch(showMessage(t('contactSuccess'))) } + // Used to prevent flickering of the activity indicator on quick uploads + const minSetInProgress = (setTo: boolean) => { + setTimeout(() => setInProgress(setTo), 1000) + } + const onPressSendEmail = useCallback(async () => { setInProgress(true) const deviceInfo = { @@ -67,26 +72,21 @@ function SupportContact({ route }: Props) { body: `${message}

${JSON.stringify(deviceInfo)}`, isHTML: true, } - let combinedLogsPath: string | false = false + // TODO(Tom): use correct typing + let attachments: Array | any if (attachLogs) { - combinedLogsPath = await Logger.createCombinedLogs() - if (combinedLogsPath) { - email.attachments = [ - { - path: combinedLogsPath, // The absolute path of the file from which to read data. - type: 'text', // Mime Type: jpg, png, doc, ppt, html, pdf, csv - name: '', // Optional: Custom filename for attachment - }, - ] + attachments = await Logger.getLogsToAttach() + if (attachments) { + email.attachments = attachments email.body += (email.body ? '

' : '') + 'Support logs are attached...' } } - setInProgress(false) + minSetInProgress(false) try { - await sendEmail(email, deviceInfo, combinedLogsPath) + await sendEmail(email, deviceInfo, attachments[0].path ?? false) navigateBackAndToast() } catch (error) { - Logger.error('SupportContact', 'Error while sending logs to support', error) + Logger.error('SupportContact', 'Error while sending logs to support', error as Error) } }, [message, attachLogs, e164PhoneNumber]) diff --git a/src/app/Debug.tsx b/src/app/Debug.tsx index 48b9829002..819da794bd 100644 --- a/src/app/Debug.tsx +++ b/src/app/Debug.tsx @@ -33,7 +33,7 @@ export class Debug extends React.Component { } updateLogs = async () => { - const reactNativeLogs = await Logger.getDailyLogs() + const reactNativeLogs = await Logger.getMonthLogs() this.setState({ reactNativeLogs: reactNativeLogs || 'Not Found', }) diff --git a/src/utils/Logger.test.ts b/src/utils/Logger.test.ts index 6419d40bdf..cde0ee6be6 100644 --- a/src/utils/Logger.test.ts +++ b/src/utils/Logger.test.ts @@ -2,12 +2,13 @@ // no-console disabled as we are testing console logs are overwritten by the logger import 'react-native' import { Platform } from 'react-native' +import { ONE_DAY_IN_MILLIS } from 'src/utils/time' import Logger from './Logger' const mockData = [ { - // ctime is 29 days ago - ctime: new Date(new Date().getTime() - 2505600000), + // ctime is 61 days ago + ctime: new Date(new Date().getTime() - ONE_DAY_IN_MILLIS * 61), name: 'toDelete.txt', path: '__TEMPORARY_DIRECTORY_PATH__/rn_logs/toDelete.txt', size: 5318, @@ -79,7 +80,7 @@ describe('utils/Logger', () => { await Logger.cleanupOldLogs() expect(console.debug).toHaveBeenCalledTimes(1) expect(console.debug).toHaveBeenCalledWith( - 'Logger/cleanupOldLogs/Deleting React Native log file older than 14 days, __TEMPORARY_DIRECTORY_PATH__/rn_logs/toDelete.txt' + 'Logger/cleanupOldLogs/Deleting React Native log file older than 60 days, __TEMPORARY_DIRECTORY_PATH__/rn_logs/toDelete.txt' ) }) }) diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index 25271af6e4..a56a70b0f2 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -4,9 +4,11 @@ import { format } from 'date-fns' import { Platform } from 'react-native' import * as RNFS from 'react-native-fs' import Toast from 'react-native-simple-toast' +import { Email } from 'src/account/emailSender' import { DEFAULT_SENTRY_NETWORK_ERRORS, LOGGER_LEVEL } from 'src/config' import { LoggerLevel } from 'src/utils/LoggerLevels' import { readFileChunked } from 'src/utils/readFile' +import { ONE_DAY_IN_MILLIS } from 'src/utils/time' class Logger { isNetworkConnected: boolean @@ -150,13 +152,40 @@ class Logger { } getReactNativeLogFilePath = () => { - return `${this.getReactNativeLogsDir()}/${format(new Date(), 'yyyyMMdd')}.txt` + return `${this.getReactNativeLogsDir()}/${format(new Date(), 'yyyy-MMM')}.txt` } getReactNativeLogsDir = () => { return `${RNFS.CachesDirectoryPath}/rn_logs` } + getLogsToAttach = async () => { + try { + const logDir = this.getReactNativeLogsDir() + const logFiles = await RNFS.readDir(logDir) + const path = Platform.OS === 'ios' ? RNFS.TemporaryDirectoryPath : RNFS.ExternalDirectoryPath + const toAttach = [] + for (const file of logFiles) { + // Always remove the logs from the current month and re-copy + // But not the logs from previous months if they exist + if (file.name === `${format(new Date(), 'yyyy-MMM')}.txt`) { + await RNFS.unlink(`${path}/${file.name}`) + await RNFS.copyFile(file.path, `${path}/${file.name}`) + } else if (!(await RNFS.exists(`${path}/${file.name}`))) { + await RNFS.copyFile(file.path, `${path}/${file.name}`) + } + toAttach.push({ + path: `${path}/${file.name}`, + name: file.name, + type: 'text', + }) + } + return toAttach as Email['attachments'] + } catch (error) { + this.error('Logger', 'Failed to move logs to share', error as Error) + } + } + getCombinedLogsFilePath = () => { // For now we need to export to a world-readable directory on Android // TODO: use the FileProvider approach so we don't need to do this. @@ -171,7 +200,7 @@ class Logger { const logFileCombinedPath = this.getCombinedLogsFilePath() const logDir = this.getReactNativeLogsDir() - // If legacy log file or combined logs exist, delete it + // If legacy log file exist, delete it if (await RNFS.exists(logFileCombinedPath)) { await RNFS.unlink(logFileCombinedPath) } @@ -179,13 +208,13 @@ class Logger { // Get the list of log files const logFiles = await RNFS.readDir(logDir) - // Delete log files older than 14 days + // Delete log files older than 60 days for (const logFile of logFiles) { const stat = await RNFS.stat(logFile.path) - if (+stat.ctime < +new Date() - 2 * 7 * 24 * 60 * 60 * 1000) { + if (+stat.ctime < +new Date() - 60 * ONE_DAY_IN_MILLIS) { this.debug( 'Logger/cleanupOldLogs', - 'Deleting React Native log file older than 14 days', + 'Deleting React Native log file older than 60 days', logFile.path ) await RNFS.unlink(logFile.path) @@ -197,7 +226,7 @@ class Logger { } // Gets the logs for the current day - getDailyLogs = async () => { + getMonthLogs = async () => { try { const rnLogsSrc = this.getReactNativeLogFilePath() let reactNativeLogs = null @@ -211,39 +240,6 @@ class Logger { } } - // Combines logs from the last n days into a single file - createCombinedLogs = async () => { - try { - this.showMessage('Creating combined log...') - const combinedLogsPath = this.getCombinedLogsFilePath() - const logDir = this.getReactNativeLogsDir() - - // Delete previous combined Logs if present - if (await RNFS.exists(combinedLogsPath)) { - await RNFS.unlink(combinedLogsPath) - } - - // Create empty file to combine log files into - await RNFS.writeFile(combinedLogsPath, '', 'utf8') - - // Get all daily log files and combine into one file - const logFiles = await RNFS.readDir(logDir) - for (const logFile of logFiles) { - if (await RNFS.exists(logFile.path)) { - await RNFS.appendFile( - combinedLogsPath, - (await readFileChunked(logFile.path)) as string, - 'utf8' - ) - } - } - return combinedLogsPath - } catch (error) { - this.showError('Failed to combine logs: ' + error) - return false - } - } - // Anything being sent to console.log, console.warn, or console.error is piped into // the logfile specified by getReactNativeLogsFilePath() overrideConsoleLogs = () => { From 82a3162bcb042760961e008c0132769d04f6ba48 Mon Sep 17 00:00:00 2001 From: MuckT Date: Mon, 18 Jul 2022 12:30:52 -0700 Subject: [PATCH 19/28] fix: error tag and bubble up errors from readFileChunked --- src/utils/Logger.ts | 2 +- src/utils/readFile.ts | 24 ++++++++++-------------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index a56a70b0f2..a0b2ac72fc 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -269,7 +269,7 @@ class Logger { } const timestamp = new Date().toISOString() - // Ensure messages are converted to utf8 as some remote CTA's can have non utf8 characters + // Ensure messages are converted to utf8 as some remote CTA's can have Non-ASCII characters await RNFS.appendFile( logFilePath, `${level} [${timestamp}] ${Buffer.from(message, 'utf-8').toString()}\n`, diff --git a/src/utils/readFile.ts b/src/utils/readFile.ts index 2e3982ad05..e59db93e99 100644 --- a/src/utils/readFile.ts +++ b/src/utils/readFile.ts @@ -8,7 +8,7 @@ const readChunk = async (file: string, length: number, position: number) => { try { return await RNFS.read(file, length, position, 'utf8') } catch (error) { - Logger.error('Logger@readChunk', 'readChunk error', error as Error) + Logger.error('readFile@readChunk', 'readChunk error', error as Error) return '' } } @@ -18,18 +18,14 @@ const readChunk = async (file: string, length: number, position: number) => { * [Credits]{@link: https://stackoverflow.com/a/56284734} */ export const readFileChunked = async (rnLogsSrc = Logger.getReactNativeLogFilePath()) => { - try { - const length = 4096 - let chunk = '' - let fileContents = '' - if (await RNFS.exists(rnLogsSrc)) { - do { - chunk = await readChunk(rnLogsSrc, length, fileContents.length) - fileContents += chunk - } while (chunk.length > 0) - } - return fileContents - } catch (error) { - Logger.error('Logger@readFileChunked', `Failed to readFileChunked ${rnLogsSrc}`, error as Error) + const length = 4096 + let chunk = '' + let fileContents = '' + if (await RNFS.exists(rnLogsSrc)) { + do { + chunk = await readChunk(rnLogsSrc, length, fileContents.length) + fileContents += chunk + } while (chunk.length > 0) } + return fileContents } From b29d0f5af6789860f661f7b026a2369d64c1863a Mon Sep 17 00:00:00 2001 From: MuckT Date: Wed, 20 Jul 2022 01:16:11 -0700 Subject: [PATCH 20/28] fix: remove type casting --- src/account/SupportContact.tsx | 3 +-- src/utils/Logger.ts | 6 +++--- src/utils/readFile.ts | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/account/SupportContact.tsx b/src/account/SupportContact.tsx index 5cf0c12ba8..d3d3c48a8c 100644 --- a/src/account/SupportContact.tsx +++ b/src/account/SupportContact.tsx @@ -72,7 +72,6 @@ function SupportContact({ route }: Props) { body: `${message}

${JSON.stringify(deviceInfo)}`, isHTML: true, } - // TODO(Tom): use correct typing let attachments: Array | any if (attachLogs) { attachments = await Logger.getLogsToAttach() @@ -86,7 +85,7 @@ function SupportContact({ route }: Props) { await sendEmail(email, deviceInfo, attachments[0].path ?? false) navigateBackAndToast() } catch (error) { - Logger.error('SupportContact', 'Error while sending logs to support', error as Error) + Logger.error('SupportContact', 'Error while sending logs to support', error) } }, [message, attachLogs, e164PhoneNumber]) diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index a0b2ac72fc..123f751aff 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -180,9 +180,9 @@ class Logger { type: 'text', }) } - return toAttach as Email['attachments'] + return toAttach } catch (error) { - this.error('Logger', 'Failed to move logs to share', error as Error) + this.error('Logger', 'Failed to move logs to share', error) } } @@ -221,7 +221,7 @@ class Logger { } } } catch (error) { - this.error('Logger@cleanupOldLogs', 'Failed to cleanupOldLogs', error as Error) + this.error('Logger@cleanupOldLogs', 'Failed to cleanupOldLogs', error) } } diff --git a/src/utils/readFile.ts b/src/utils/readFile.ts index e59db93e99..f76a09afb5 100644 --- a/src/utils/readFile.ts +++ b/src/utils/readFile.ts @@ -8,7 +8,7 @@ const readChunk = async (file: string, length: number, position: number) => { try { return await RNFS.read(file, length, position, 'utf8') } catch (error) { - Logger.error('readFile@readChunk', 'readChunk error', error as Error) + Logger.error('readFile@readChunk', 'readChunk error', error) return '' } } From bdc449dd1fb154fef1aec1600748d52dc88bde73 Mon Sep 17 00:00:00 2001 From: MuckT Date: Wed, 20 Jul 2022 01:19:48 -0700 Subject: [PATCH 21/28] feat: handle os specific paths and move date-fs formats to reusable function --- src/account/SupportContact.tsx | 9 ++++++++- src/utils/Logger.ts | 31 +++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/src/account/SupportContact.tsx b/src/account/SupportContact.tsx index d3d3c48a8c..48919eb132 100644 --- a/src/account/SupportContact.tsx +++ b/src/account/SupportContact.tsx @@ -82,7 +82,14 @@ function SupportContact({ route }: Props) { } minSetInProgress(false) try { - await sendEmail(email, deviceInfo, attachments[0].path ?? false) + await sendEmail( + email, + deviceInfo, + // Get the current months log file to attach as text if sendEmailWithNonNativeApp is used + attachments.find( + (attachment: { name: string }) => attachment.name === Logger.getCurrentLogFileName() + ) ?? false + ) navigateBackAndToast() } catch (error) { Logger.error('SupportContact', 'Error while sending logs to support', error) diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index 123f751aff..aafef9d716 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -4,7 +4,6 @@ import { format } from 'date-fns' import { Platform } from 'react-native' import * as RNFS from 'react-native-fs' import Toast from 'react-native-simple-toast' -import { Email } from 'src/account/emailSender' import { DEFAULT_SENTRY_NETWORK_ERRORS, LOGGER_LEVEL } from 'src/config' import { LoggerLevel } from 'src/utils/LoggerLevels' import { readFileChunked } from 'src/utils/readFile' @@ -151,8 +150,12 @@ class Logger { return error } + getCurrentLogFileName = () => { + return `${format(new Date(), 'yyyy-MMM')}.txt` + } + getReactNativeLogFilePath = () => { - return `${this.getReactNativeLogsDir()}/${format(new Date(), 'yyyy-MMM')}.txt` + return `${this.getReactNativeLogsDir()}/${this.getCurrentLogFileName()}` } getReactNativeLogsDir = () => { @@ -166,16 +169,24 @@ class Logger { const path = Platform.OS === 'ios' ? RNFS.TemporaryDirectoryPath : RNFS.ExternalDirectoryPath const toAttach = [] for (const file of logFiles) { - // Always remove the logs from the current month and re-copy - // But not the logs from previous months if they exist - if (file.name === `${format(new Date(), 'yyyy-MMM')}.txt`) { - await RNFS.unlink(`${path}/${file.name}`) - await RNFS.copyFile(file.path, `${path}/${file.name}`) - } else if (!(await RNFS.exists(`${path}/${file.name}`))) { - await RNFS.copyFile(file.path, `${path}/${file.name}`) + // RNFS.TemporaryDirectoryPath (iOS) has a trailing '/' + const osSpecificPath = + Platform.OS === 'android' ? `${path}/${file.name}` : `${path}${file.name}` + // If the file is the current months log file log file + if (file.name === this.getCurrentLogFileName()) { + // If this file exists delete it as the new one will have more logs + if (await RNFS.exists(osSpecificPath)) { + await RNFS.unlink(osSpecificPath) + } + // Then copy it to the folder we have access to + await RNFS.copyFile(file.path, osSpecificPath) + // Else if the a log file doesn't exist in the folder we have access to + // Then copy it to the folder we have access to + } else if (!(await RNFS.exists(osSpecificPath))) { + await RNFS.copyFile(file.path, osSpecificPath) } toAttach.push({ - path: `${path}/${file.name}`, + path: osSpecificPath, name: file.name, type: 'text', }) From 8290ca9532a158c39a5604fad1a66ff1ca3476b0 Mon Sep 17 00:00:00 2001 From: MuckT Date: Wed, 20 Jul 2022 02:23:18 -0700 Subject: [PATCH 22/28] feat: avoid log file copies and deletes to tmp directory on iOS --- src/utils/Logger.test.ts | 10 +++++----- src/utils/Logger.ts | 34 ++++++++++++++++++---------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/utils/Logger.test.ts b/src/utils/Logger.test.ts index cde0ee6be6..76e3e8101e 100644 --- a/src/utils/Logger.test.ts +++ b/src/utils/Logger.test.ts @@ -10,7 +10,7 @@ const mockData = [ // ctime is 61 days ago ctime: new Date(new Date().getTime() - ONE_DAY_IN_MILLIS * 61), name: 'toDelete.txt', - path: '__TEMPORARY_DIRECTORY_PATH__/rn_logs/toDelete.txt', + path: '__CACHES_DIRECTORY_PATH__/rn_logs/toDelete.txt', size: 5318, isFile: true, isDirectory: false, @@ -18,7 +18,7 @@ const mockData = [ { ctime: new Date(), name: 'toSave.txt', - path: '__TEMPORARY_DIRECTORY_PATH__/rn_logs/toSave.txt', + path: '__CACHES_DIRECTORY_PATH__/rn_logs/toSave.txt', size: 5318, isFile: true, isDirectory: false, @@ -37,7 +37,7 @@ jest.mock('react-native-fs', () => { unlink: jest.fn(() => Promise.resolve()), writeFile: jest.fn(() => Promise.resolve()), ExternalDirectoryPath: '__EXTERNAL_DIRECTORY_PATH__', - TemporaryDirectoryPath: '__TEMPORARY_DIRECTORY_PATH__', + TemporaryDirectoryPath: '__CACHES_DIRECTORY_PATH__', } }) @@ -66,7 +66,7 @@ describe('utils/Logger', () => { it('Returns combined logs file path iOS', () => { // mock Platform.OS = 'ios' - expect(Logger.getCombinedLogsFilePath()).toBe('__TEMPORARY_DIRECTORY_PATH__/rn_logs.txt') + expect(Logger.getCombinedLogsFilePath()).toBe('__CACHES_DIRECTORY_PATH__/rn_logs.txt') }) it('Returns combined logs file path Android', () => { @@ -80,7 +80,7 @@ describe('utils/Logger', () => { await Logger.cleanupOldLogs() expect(console.debug).toHaveBeenCalledTimes(1) expect(console.debug).toHaveBeenCalledWith( - 'Logger/cleanupOldLogs/Deleting React Native log file older than 60 days, __TEMPORARY_DIRECTORY_PATH__/rn_logs/toDelete.txt' + 'Logger/cleanupOldLogs/Deleting React Native log file older than 60 days, __CACHES_DIRECTORY_PATH__/rn_logs/toDelete.txt' ) }) }) diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index aafef9d716..ad2b438cc8 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -166,27 +166,29 @@ class Logger { try { const logDir = this.getReactNativeLogsDir() const logFiles = await RNFS.readDir(logDir) - const path = Platform.OS === 'ios' ? RNFS.TemporaryDirectoryPath : RNFS.ExternalDirectoryPath const toAttach = [] + // On Android we need to move the files to a directory we have access to + const path = Platform.OS === 'ios' ? logDir : RNFS.ExternalDirectoryPath for (const file of logFiles) { - // RNFS.TemporaryDirectoryPath (iOS) has a trailing '/' - const osSpecificPath = - Platform.OS === 'android' ? `${path}/${file.name}` : `${path}${file.name}` - // If the file is the current months log file log file - if (file.name === this.getCurrentLogFileName()) { - // If this file exists delete it as the new one will have more logs - if (await RNFS.exists(osSpecificPath)) { - await RNFS.unlink(osSpecificPath) + const filePath = `${path}/${file.name}` + // Android specific file deleting and copying + if (Platform.OS === 'android') { + // If the file is the current months log file log file, delete the previous copy + if (file.name === this.getCurrentLogFileName()) { + if (await RNFS.exists(filePath)) { + await RNFS.unlink(filePath) + } + // Then copy it to the folder we have access to + await RNFS.copyFile(file.path, filePath) + // Else if it is not the current months a log file + // And it doesn't exist in the folder we have access to + // Then copy it to the folder we have access to + } else if (!(await RNFS.exists(filePath))) { + await RNFS.copyFile(file.path, filePath) } - // Then copy it to the folder we have access to - await RNFS.copyFile(file.path, osSpecificPath) - // Else if the a log file doesn't exist in the folder we have access to - // Then copy it to the folder we have access to - } else if (!(await RNFS.exists(osSpecificPath))) { - await RNFS.copyFile(file.path, osSpecificPath) } toAttach.push({ - path: osSpecificPath, + path: filePath, name: file.name, type: 'text', }) From e56767daf37f142bb7700551dc0030371b1212ce Mon Sep 17 00:00:00 2001 From: MuckT Date: Wed, 20 Jul 2022 08:51:31 -0700 Subject: [PATCH 23/28] fix: remove minSetInProgress and use inline setTimeout --- src/account/SupportContact.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/account/SupportContact.tsx b/src/account/SupportContact.tsx index 48919eb132..daa04bdbf3 100644 --- a/src/account/SupportContact.tsx +++ b/src/account/SupportContact.tsx @@ -48,11 +48,6 @@ function SupportContact({ route }: Props) { dispatch(showMessage(t('contactSuccess'))) } - // Used to prevent flickering of the activity indicator on quick uploads - const minSetInProgress = (setTo: boolean) => { - setTimeout(() => setInProgress(setTo), 1000) - } - const onPressSendEmail = useCallback(async () => { setInProgress(true) const deviceInfo = { @@ -80,7 +75,8 @@ function SupportContact({ route }: Props) { email.body += (email.body ? '

' : '') + 'Support logs are attached...' } } - minSetInProgress(false) + // Used to prevent flickering of the activity indicator on quick uploads + setTimeout(() => setInProgress(false), 1000) try { await sendEmail( email, From b043de5460b72dacae2c5fbc7c9b2ea6441a0645 Mon Sep 17 00:00:00 2001 From: MuckT Date: Wed, 20 Jul 2022 08:52:02 -0700 Subject: [PATCH 24/28] fix: get attachment path to use with sendEmailWithNonNativeApp --- src/account/SupportContact.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/account/SupportContact.tsx b/src/account/SupportContact.tsx index daa04bdbf3..17238231a1 100644 --- a/src/account/SupportContact.tsx +++ b/src/account/SupportContact.tsx @@ -84,7 +84,7 @@ function SupportContact({ route }: Props) { // Get the current months log file to attach as text if sendEmailWithNonNativeApp is used attachments.find( (attachment: { name: string }) => attachment.name === Logger.getCurrentLogFileName() - ) ?? false + ).path ?? false ) navigateBackAndToast() } catch (error) { From ba6a33de8d3cfb5c38bf7e32c1f2a5e21c605e86 Mon Sep 17 00:00:00 2001 From: MuckT Date: Wed, 20 Jul 2022 09:17:02 -0700 Subject: [PATCH 25/28] fix: handle if attachments are undefined --- src/account/SupportContact.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/account/SupportContact.tsx b/src/account/SupportContact.tsx index 17238231a1..8a08c87086 100644 --- a/src/account/SupportContact.tsx +++ b/src/account/SupportContact.tsx @@ -82,9 +82,11 @@ function SupportContact({ route }: Props) { email, deviceInfo, // Get the current months log file to attach as text if sendEmailWithNonNativeApp is used - attachments.find( - (attachment: { name: string }) => attachment.name === Logger.getCurrentLogFileName() - ).path ?? false + attachments + ? attachments.find( + (attachment: { name: string }) => attachment.name === Logger.getCurrentLogFileName() + ).path ?? false + : false ) navigateBackAndToast() } catch (error) { From 6bd994f7f02ad5298b0f7a91206b41eb9df45e26 Mon Sep 17 00:00:00 2001 From: MuckT Date: Wed, 20 Jul 2022 16:09:37 -0700 Subject: [PATCH 26/28] test: mock Logger.getCurrentLogFileName --- src/account/SupportContact.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/account/SupportContact.test.tsx b/src/account/SupportContact.test.tsx index 6cd5a1f7d2..ca1f93a3dc 100644 --- a/src/account/SupportContact.test.tsx +++ b/src/account/SupportContact.test.tsx @@ -24,6 +24,7 @@ describe('Contact', () => { it('submits email with logs', async () => { const mockedLogAttachments = Logger.getLogsToAttach as jest.Mock + Logger.getCurrentLogFileName = jest.fn(() => 'log2.txt') const logAttachments = [ { path: 'logs/log1.txt', From ded87a8f61a5eeb9e9da0df52cc1691b823478c7 Mon Sep 17 00:00:00 2001 From: MuckT Date: Thu, 21 Jul 2022 09:29:28 -0700 Subject: [PATCH 27/28] fix: typing for attachments --- src/account/SupportContact.tsx | 4 ++-- src/utils/Logger.ts | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/account/SupportContact.tsx b/src/account/SupportContact.tsx index 8a08c87086..560f765e5a 100644 --- a/src/account/SupportContact.tsx +++ b/src/account/SupportContact.tsx @@ -67,7 +67,7 @@ function SupportContact({ route }: Props) { body: `${message}

${JSON.stringify(deviceInfo)}`, isHTML: true, } - let attachments: Array | any + let attachments: Email['attachments'] if (attachLogs) { attachments = await Logger.getLogsToAttach() if (attachments) { @@ -85,7 +85,7 @@ function SupportContact({ route }: Props) { attachments ? attachments.find( (attachment: { name: string }) => attachment.name === Logger.getCurrentLogFileName() - ).path ?? false + )?.path ?? false : false ) navigateBackAndToast() diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index ad2b438cc8..6d269c462a 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -4,6 +4,7 @@ import { format } from 'date-fns' import { Platform } from 'react-native' import * as RNFS from 'react-native-fs' import Toast from 'react-native-simple-toast' +import { Email } from 'src/account/emailSender' import { DEFAULT_SENTRY_NETWORK_ERRORS, LOGGER_LEVEL } from 'src/config' import { LoggerLevel } from 'src/utils/LoggerLevels' import { readFileChunked } from 'src/utils/readFile' @@ -163,10 +164,10 @@ class Logger { } getLogsToAttach = async () => { + const toAttach: Email['attachments'] = [] try { const logDir = this.getReactNativeLogsDir() const logFiles = await RNFS.readDir(logDir) - const toAttach = [] // On Android we need to move the files to a directory we have access to const path = Platform.OS === 'ios' ? logDir : RNFS.ExternalDirectoryPath for (const file of logFiles) { @@ -193,9 +194,10 @@ class Logger { type: 'text', }) } - return toAttach } catch (error) { this.error('Logger', 'Failed to move logs to share', error) + } finally { + return toAttach } } From c3b9650e91b5d0bd6fe3517364a55fe6128b6da3 Mon Sep 17 00:00:00 2001 From: MuckT Date: Thu, 21 Jul 2022 09:41:43 -0700 Subject: [PATCH 28/28] fix: lint no-unsafe-finally --- src/utils/Logger.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index 6d269c462a..69779037f9 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -196,9 +196,8 @@ class Logger { } } catch (error) { this.error('Logger', 'Failed to move logs to share', error) - } finally { - return toAttach } + return toAttach } getCombinedLogsFilePath = () => {