add debug logging

This commit is contained in:
ammarahm-ed
2022-07-19 13:36:45 +05:00
parent eb952dec67
commit 0b6aa7a762
7 changed files with 307 additions and 172 deletions

View File

@@ -17,7 +17,7 @@ import { useNoteStore } from '../../stores/use-notes-store';
import { useSettingStore } from '../../stores/use-setting-store'; import { useSettingStore } from '../../stores/use-setting-store';
import { useThemeStore } from '../../stores/use-theme-store'; import { useThemeStore } from '../../stores/use-theme-store';
import { useUserStore } from '../../stores/use-user-store'; import { useUserStore } from '../../stores/use-user-store';
import { db, loadDatabase } from '../../utils/database'; import { DatabaseLogger, db, loadDatabase } from '../../utils/database';
import { MMKV } from '../../utils/database/mmkv'; import { MMKV } from '../../utils/database/mmkv';
import { eCloseProgressDialog, eOpenAnnouncementDialog } from '../../utils/events'; import { eCloseProgressDialog, eOpenAnnouncementDialog } from '../../utils/events';
import { tabBarRef } from '../../utils/global-refs'; import { tabBarRef } from '../../utils/global-refs';
@@ -85,6 +85,7 @@ const Launcher = React.memo(
await loadDatabase(); await loadDatabase();
db?.eventManager?.subscribe(EVENTS.databaseMigrating, onDatabaseMigratingProgess); db?.eventManager?.subscribe(EVENTS.databaseMigrating, onDatabaseMigratingProgess);
db?.eventManager?.subscribe(EVENTS.databaseMigrated, onMigrationCompleted); db?.eventManager?.subscribe(EVENTS.databaseMigrated, onMigrationCompleted);
DatabaseLogger.info('Initializing database');
await db.init(); await db.init();
dbInitCompleted.current = true; dbInitCompleted.current = true;
} }
@@ -139,6 +140,7 @@ const Launcher = React.memo(
}; };
const checkAppUpdateAvailable = async () => { const checkAppUpdateAvailable = async () => {
if (__DEV__) return;
try { try {
const version = await checkVersion(); const version = await checkVersion();
if (!version.needsUpdate) return false; if (!version.needsUpdate) return false;

View File

@@ -6,18 +6,28 @@ import { Header } from '../components/header';
import { Toast } from '../components/toast'; import { Toast } from '../components/toast';
import { useNoteStore } from '../stores/use-notes-store'; import { useNoteStore } from '../stores/use-notes-store';
import { useSettingStore } from '../stores/use-setting-store'; import { useSettingStore } from '../stores/use-setting-store';
import { useThemeStore } from '../stores/use-theme-store';
import { TabsHolder } from './tabs-holder'; import { TabsHolder } from './tabs-holder';
export const ApplicationHolder = React.memo( export const ApplicationHolder = React.memo(
() => { () => {
const loading = useNoteStore(state => state.loading); const loading = useNoteStore(state => state.loading);
const introCompleted = useSettingStore(state => state.settings.introCompleted); const introCompleted = useSettingStore(state => state.settings.introCompleted);
const colors = useThemeStore(state => state.colors);
return ( return (
<> <>
{loading && introCompleted ? ( {loading && introCompleted ? (
<> <>
<Header /> <SafeAreaView
<DelayLayout animated={false} wait={loading} /> style={{
flex: 1,
backgroundColor: colors.bg
}}
>
<Header />
<DelayLayout animated={false} wait={loading} />
</SafeAreaView>
</> </>
) : ( ) : (
<> <>

View File

@@ -139,7 +139,7 @@ const Editor = React.memo(
allowUniversalAccessFromFileURLs={true} allowUniversalAccessFromFileURLs={true}
originWhitelist={['*']} originWhitelist={['*']}
source={{ source={{
uri: __DEV__ ? 'http://localhost:3000/index.html' : EDITOR_URI uri: __DEV__ ? 'http://192.168.10.6:3000/index.html' : EDITOR_URI
}} }}
style={style} style={style}
autoManageStatusBarEnabled={false} autoManageStatusBarEnabled={false}

View File

@@ -1,5 +1,5 @@
import Clipboard from '@react-native-clipboard/clipboard'; import Clipboard from '@react-native-clipboard/clipboard';
import React from 'react'; import React, { useState } from 'react';
import { FlatList, Platform, TouchableOpacity, View } from 'react-native'; import { FlatList, Platform, TouchableOpacity, View } from 'react-native';
import * as ScopedStorage from 'react-native-scoped-storage'; import * as ScopedStorage from 'react-native-scoped-storage';
import RNFetchBlob from 'rn-fetch-blob'; import RNFetchBlob from 'rn-fetch-blob';
@@ -12,28 +12,71 @@ import { useThemeStore } from '../../stores/use-theme-store';
import { hexToRGBA } from '../../utils/color-scheme/utils'; import { hexToRGBA } from '../../utils/color-scheme/utils';
import Storage from '../../utils/database/storage'; import Storage from '../../utils/database/storage';
import { sanitizeFilename } from '../../utils/sanitizer'; import { sanitizeFilename } from '../../utils/sanitizer';
import { DatabaseLogger } from '../../utils/database/index';
import { useEffect } from 'react';
import useTimer from '../../utils/hooks/use-timer';
import { LogLevel } from '@streetwriters/notesnook-core/logger';
function getLevelString(level: number) {
switch (level) {
case LogLevel.Debug:
return 'DEBUG';
case LogLevel.Info:
return 'INFO';
case LogLevel.Log:
return 'LOG';
case LogLevel.Error:
return 'ERROR';
case LogLevel.Warn:
return 'WARN';
case LogLevel.Fatal:
return 'FATAL';
}
}
export default function DebugLogs() { export default function DebugLogs() {
const colors = useThemeStore(state => state.colors); const colors = useThemeStore(state => state.colors);
const { seconds, start } = useTimer('debug_logs_timer');
const [logs, setLogs] = useState<
{
key: string;
logs: any[];
}[]
>([]);
const [currentLog, setCurrentLog] = useState<{
key: string;
logs: any[];
}>();
useEffect(() => {
(async () => {
if (seconds === 0) {
start(5, 'debug_logs_timer');
let logs = await DatabaseLogger.get();
if (logs.length > 0 && !currentLog) {
setCurrentLog(logs[0]);
} else {
setCurrentLog(logs[logs.findIndex(l => l.key === currentLog?.key)]);
}
setLogs(logs);
}
})();
}, [seconds]);
const renderItem = ({ item, index }) => { const renderItem = ({ item, index }) => {
const background = const background =
item.type === 'error' item.level === LogLevel.Error || item.level === LogLevel.Fatal
? hexToRGBA(colors.red, 0.2) ? hexToRGBA(colors.red, 0.2)
: item.type === 'success' : item.level === LogLevel.Warn
? hexToRGBA(colors.green, 0.2)
: item.type === 'warn'
? hexToRGBA(colors.orange, 0.2) ? hexToRGBA(colors.orange, 0.2)
: 'transparent'; : 'transparent';
const color = const color =
item.type === 'error' item.level === LogLevel.Error || item.level === LogLevel.Fatal
? colors.red ? colors.red
: item.type === 'success' : item.level === LogLevel.Warn
? colors.green
: item.type === 'warn'
? colors.orange ? colors.orange
: 'transparent'; : colors.pri;
return ( return (
<TouchableOpacity <TouchableOpacity
@@ -50,24 +93,31 @@ export default function DebugLogs() {
paddingHorizontal: 12, paddingHorizontal: 12,
paddingVertical: 12, paddingVertical: 12,
backgroundColor: background, backgroundColor: background,
flexShrink: 1 flexShrink: 1,
borderBottomWidth: 1,
borderBottomColor: colors.nav
}} }}
> >
<Paragraph <Paragraph
style={{ style={{
flexShrink: 1, flexShrink: 1,
flexWrap: 'wrap', flexWrap: 'wrap',
fontFamily: 'monospace' fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace'
}} }}
size={12}
color={color} color={color}
> >
{item.error} [{getLevelString(item.level)}] {item.message}
</Paragraph> </Paragraph>
</TouchableOpacity> </TouchableOpacity>
); );
}; };
return ( return (
<View> <View
style={{
flex: 1
}}
>
<View <View
style={{ style={{
padding: 12 padding: 12
@@ -77,104 +127,154 @@ export default function DebugLogs() {
text="If you are facing an issue in the app. You can send us the logs from here to help us investigate the issue." text="If you are facing an issue in the app. You can send us the logs from here to help us investigate the issue."
type="information" type="information"
/> />
<View
style={{
flexDirection: 'row',
width: '100%',
alignItems: 'flex-end',
justifyContent: 'flex-end',
marginTop: 12
}}
>
<IconButton
onPress={() => {
Clipboard.setString('All logs copied');
ToastEvent.show({
heading: 'Debug log copied!',
context: 'global',
type: 'success'
});
}}
size={20}
name="clipboard"
color={colors.gray}
/>
<IconButton
onPress={async () => {
try {
let path = null;
const fileName = sanitizeFilename(`notesnook_logs_${Date.now()}`);
if (Platform.OS === 'android') {
let file = await ScopedStorage.createDocument(
fileName,
'text/plain',
'save data',
'utf8'
);
if (!file) return;
path = file.uri;
} else {
path = await Storage.checkAndCreateDir('/');
await RNFetchBlob.fs.writeFile(path + fileName, 'save data', 'utf8');
path = path + fileName;
}
if (path) {
ToastEvent.show({
heading: 'Debug logs downloaded',
context: 'global',
type: 'success'
});
}
} catch (e) {
console.log(e);
}
}}
size={20}
name="download"
color={colors.gray}
/>
<IconButton
onPress={() => {
presentDialog({
title: 'Clear all logs',
paragraph: 'Are you sure you want to delete alll logs?',
negativeText: 'Cancel',
positiveText: 'Clear',
positivePress: () => {}
});
}}
size={20}
name="delete"
color={colors.gray}
/>
</View>
</View> </View>
<FlatList {currentLog && (
inverted <FlatList
data={[ ListHeaderComponent={
{ <View
type: 'error', style={{
error: 'Error: file not found and jt aksdj adksj kdajkd jsakdjs kjda kdjsak djaks dkas.' paddingHorizontal: 12,
}, marginBottom: 10,
{ flexDirection: 'row',
type: 'warn', alignItems: 'center',
error: 'Warning: file found with invalid name' backgroundColor: colors.bg,
}, justifyContent: 'space-between'
{ }}
type: 'success', >
error: 'Success: file found' <View
}, style={{
{ flexDirection: 'row',
type: 'error', alignItems: 'center'
error: 'Error: file not found' }}
>
<Paragraph>{currentLog.key}</Paragraph>
<IconButton
customStyle={{
width: 30,
height: 30,
marginHorizontal: 5
}}
onPress={() => {
let index = logs.findIndex(l => l.key === currentLog.key);
if (index === 0) return;
setCurrentLog(logs[index - 1]);
}}
size={20}
name="chevron-left"
color={colors.icon}
/>
<IconButton
customStyle={{
width: 30,
height: 30
}}
onPress={() => {
let index = logs.findIndex(l => l.key === currentLog.key);
if (index === logs.length - 1) return;
setCurrentLog(logs[index + 1]);
}}
size={20}
name="chevron-right"
color={colors.icon}
/>
</View>
<View
style={{
flexDirection: 'row'
}}
>
<IconButton
onPress={() => {
Clipboard.setString('All logs copied');
ToastEvent.show({
heading: 'Debug log copied!',
context: 'global',
type: 'success'
});
}}
size={20}
customStyle={{
width: 30,
height: 30,
marginRight: 5
}}
name="content-copy"
color={colors.gray}
/>
<IconButton
onPress={async () => {
try {
let path = null;
const fileName = sanitizeFilename(`notesnook_logs_${Date.now()}`);
const data = currentLog?.logs
.map(
log =>
`${new Date(log.timestamp).toUTCString()}: [${getLevelString(
log.level
)}] ${log.message || log.error?.message}${
log.error?.stack ? '\n' + log.error?.stack : ''
}`
)
.join(`\n`);
if (!data) return;
if (Platform.OS === 'android') {
let file = await ScopedStorage.createDocument(
fileName + '.txt',
'text/plain',
data,
'utf8'
);
if (!file) return;
path = file.uri;
} else {
path = await Storage.checkAndCreateDir('/');
await RNFetchBlob.fs.writeFile(path + fileName + '.txt', data, 'utf8');
path = path + fileName;
}
if (path) {
ToastEvent.show({
heading: 'Debug logs downloaded',
context: 'global',
type: 'success'
});
}
} catch (e) {
console.log(e);
}
}}
customStyle={{
width: 30,
height: 30,
marginRight: 5
}}
size={20}
name="download"
color={colors.gray}
/>
</View>
</View>
} }
]} style={{
renderItem={renderItem} flex: 1,
/> width: '100%'
}}
stickyHeaderIndices={[0]}
ListFooterComponent={
<View
style={{
height: 200
}}
/>
}
data={currentLog.logs}
renderItem={renderItem}
/>
)}
</View> </View>
); );
} }

View File

@@ -749,14 +749,14 @@ export const settingsGroups: SettingSection[] = [
name: 'Debug mode', name: 'Debug mode',
description: 'Show debug options on items', description: 'Show debug options on items',
property: 'devMode' property: 'devMode'
},
{
id: 'debug-logs',
type: 'screen',
name: 'Debug logs',
description: 'View debug logs from the app',
component: 'debug-logs'
} }
// {
// id: 'debug-logs',
// type: 'screen',
// name: 'Debug logs',
// description: 'View debug logs from the app',
// component: 'debug-logs'
// }
] ]
}, },
{ {

View File

@@ -12,53 +12,71 @@ import {
} from './encryption'; } from './encryption';
import { MMKV } from './mmkv'; import { MMKV } from './mmkv';
async function read(key) { export class KV {
if (!key) return null; storage = null;
let data = MMKV.getString(key); constructor(storage) {
if (!data) return null; this.storage = storage;
try { }
let parse = JSON.parse(data); async read(key) {
return parse; if (!key) return null;
} catch (e) { let data = this.storage.getString(key);
return data; if (!data) return null;
try {
let parse = JSON.parse(data);
return parse;
} catch (e) {
return data;
}
}
async write(key, data) {
this.storage.setString(key, typeof data === 'string' ? data : JSON.stringify(data));
return true;
}
async readMulti(keys) {
if (keys.length <= 0) {
return [];
} else {
let data = await this.storage.getMultipleItemsAsync(keys.slice());
return data.map(([key, value]) => {
let obj;
try {
obj = JSON.parse(value);
} catch (e) {
obj = value;
}
return [key, obj];
});
}
}
async remove(key) {
return await this.storage.removeItem(key);
}
async clear() {
return await this.storage.clearStore();
}
async getAllKeys() {
let keys = (await this.storage.indexer.getKeys()) || [];
keys = keys.filter(
k =>
k !== 'stringIndex' &&
k !== 'boolIndex' &&
k !== 'mapIndex' &&
k !== 'arrayIndex' &&
k !== 'numberIndex' &&
k !== this.storage.instanceID
);
return keys;
} }
} }
async function write(key, data) { const DefaultStorage = new KV(MMKV);
MMKV.setString(key, typeof data === 'string' ? data : JSON.stringify(data));
return true;
}
async function readMulti(keys) {
if (keys.length <= 0) {
return [];
} else {
let data = await MMKV.getMultipleItemsAsync(keys.slice());
return data.map(([key, value]) => {
let obj;
try {
obj = JSON.parse(value);
} catch (e) {
obj = value;
}
return [key, obj];
});
}
}
async function remove(key) {
return await MMKV.removeItem(key);
}
async function clear() {
return await MMKV.clearStore();
}
async function getAllKeys() {
return await MMKV.indexer.getKeys();
}
async function requestPermission() { async function requestPermission() {
if (Platform.OS === 'ios') return true; if (Platform.OS === 'ios') return true;
@@ -84,14 +102,14 @@ async function checkAndCreateDir(path) {
} }
export default { export default {
read, read: key => DefaultStorage.read(key),
write, write: (key, value) => DefaultStorage.write(key, value),
readMulti, readMulti: keys => DefaultStorage.readMulti(keys),
remove, remove: key => DefaultStorage.remove(key),
clear, clear: () => DefaultStorage.clear(),
getAllKeys: () => DefaultStorage.getAllKeys(),
encrypt, encrypt,
decrypt, decrypt,
getAllKeys,
getRandomBytes, getRandomBytes,
checkAndCreateDir, checkAndCreateDir,
requestPermission, requestPermission,

View File

@@ -1,9 +1,14 @@
import Database from '@streetwriters/notesnook-core/api/index';
import { initalize, logger as dbLogger } from '@streetwriters/notesnook-core/logger';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import { MMKVLoader } from 'react-native-mmkv-storage';
import filesystem from '../filesystem'; import filesystem from '../filesystem';
import EventSource from '../sse/even-source-ios'; import EventSource from '../sse/even-source-ios';
import AndroidEventSource from '../sse/event-source'; import AndroidEventSource from '../sse/event-source';
import Storage from './storage'; import Storage, { KV } from './storage';
import Database from '@streetwriters/notesnook-core/api/index'; const LoggerStorage = new MMKVLoader().withInstanceID('notesnook_logs').initialize();
initalize(new KV(LoggerStorage));
export const DatabaseLogger = dbLogger;
/** /**
* @type {import("@streetwriters/notesnook-core/api/index").default} * @type {import("@streetwriters/notesnook-core/api/index").default}