about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-01-02 17:38:13 -0600
committerPaul Frazee <pfrazee@gmail.com>2023-01-02 17:38:13 -0600
commitf6a0e634d78eb97d1d877033bf620ea982038731 (patch)
tree5d6b7ccbc8f6a587c3910c760bceeefda0c0a291 /src
parent99cec71ed798b29079eb474acd6f7cc799b51a51 (diff)
downloadvoidsky-f6a0e634d78eb97d1d877033bf620ea982038731.tar.zst
Implement logging system
Diffstat (limited to 'src')
-rw-r--r--src/state/index.ts12
-rw-r--r--src/state/lib/api.ts7
-rw-r--r--src/state/models/feed-view.ts1
-rw-r--r--src/state/models/log.ts94
-rw-r--r--src/state/models/me.ts15
-rw-r--r--src/state/models/notifications-view.ts17
-rw-r--r--src/state/models/profile-ui.ts16
-rw-r--r--src/state/models/profile-view.ts1
-rw-r--r--src/state/models/root-store.ts11
-rw-r--r--src/state/models/session.ts26
-rw-r--r--src/state/models/suggested-invites-view.ts14
-rw-r--r--src/view/com/composer/PhotoCarouselPicker.tsx15
-rw-r--r--src/view/com/discover/SuggestedFollows.tsx19
-rw-r--r--src/view/com/login/CreateAccount.tsx10
-rw-r--r--src/view/com/login/Signin.tsx14
-rw-r--r--src/view/com/modals/CreateScene.tsx18
-rw-r--r--src/view/com/modals/InviteToScene.tsx12
-rw-r--r--src/view/com/notifications/Feed.tsx18
-rw-r--r--src/view/com/post-thread/PostRepostedBy.tsx6
-rw-r--r--src/view/com/post-thread/PostThread.tsx9
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx6
-rw-r--r--src/view/com/post-thread/PostVotedBy.tsx6
-rw-r--r--src/view/com/post/Post.tsx10
-rw-r--r--src/view/com/post/PostText.tsx4
-rw-r--r--src/view/com/posts/Feed.tsx15
-rw-r--r--src/view/com/posts/FeedItem.tsx6
-rw-r--r--src/view/com/profile/ProfileFollowers.tsx6
-rw-r--r--src/view/com/profile/ProfileFollows.tsx6
-rw-r--r--src/view/com/profile/ProfileHeader.tsx6
-rw-r--r--src/view/com/profile/ProfileMembers.tsx6
-rw-r--r--src/view/com/util/ViewHeader.tsx3
-rw-r--r--src/view/index.ts4
-rw-r--r--src/view/routes.ts4
-rw-r--r--src/view/screens/Home.tsx8
-rw-r--r--src/view/screens/Log.tsx100
-rw-r--r--src/view/screens/Notifications.tsx4
-rw-r--r--src/view/screens/PostThread.tsx5
-rw-r--r--src/view/screens/Profile.tsx13
-rw-r--r--src/view/screens/Settings.tsx20
39 files changed, 442 insertions, 125 deletions
diff --git a/src/state/index.ts b/src/state/index.ts
index aa24eb27e..a8b95cc65 100644
--- a/src/state/index.ts
+++ b/src/state/index.ts
@@ -24,19 +24,19 @@ export async function setupState() {
   try {
     data = (await storage.load(ROOT_STATE_STORAGE_KEY)) || {}
     rootStore.hydrate(data)
-  } catch (e) {
-    console.error('Failed to load state from storage', e)
+  } catch (e: any) {
+    rootStore.log.error('Failed to load state from storage', e.toString())
   }
 
-  console.log('Initial hydrate', rootStore.me)
+  rootStore.log.debug('Initial hydrate')
   rootStore.session
     .connect()
     .then(() => {
-      console.log('Session connected', rootStore.me)
+      rootStore.log.debug('Session connected')
       return rootStore.fetchStateUpdate()
     })
-    .catch(e => {
-      console.log('Failed initial connect', e)
+    .catch((e: any) => {
+      rootStore.log.warn('Failed initial connect', e.toString())
     })
   // @ts-ignore .on() is correct -prf
   api.sessionManager.on('session', () => {
diff --git a/src/state/lib/api.ts b/src/state/lib/api.ts
index 31df20468..6bbc43271 100644
--- a/src/state/lib/api.ts
+++ b/src/state/lib/api.ts
@@ -99,7 +99,7 @@ export async function post(
             ) {
               encoding = 'image/jpeg'
             } else {
-              console.error(
+              store.log.warn(
                 'Unexpected image format for thumbnail, skipping',
                 thumbLocal.uri,
               )
@@ -126,7 +126,10 @@ export async function post(
           },
         } as AppBskyEmbedExternal.Main
       } catch (e: any) {
-        console.error('Failed to fetch link meta', link.value, e)
+        store.log.warn(
+          `Failed to fetch link meta for ${link.value}`,
+          e.toString(),
+        )
       }
     }
   }
diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts
index b5a3b29d7..c8827b1fb 100644
--- a/src/state/models/feed-view.ts
+++ b/src/state/models/feed-view.ts
@@ -405,7 +405,6 @@ export class FeedModel {
         cursor = this.feed[res.data.feed.length - 1]
           ? ts(this.feed[res.data.feed.length - 1])
           : undefined
-        console.log(numToFetch, cursor, res.data.feed.length)
       } while (numToFetch > 0)
       this._xIdle()
     } catch (e: any) {
diff --git a/src/state/models/log.ts b/src/state/models/log.ts
new file mode 100644
index 000000000..42172a3b1
--- /dev/null
+++ b/src/state/models/log.ts
@@ -0,0 +1,94 @@
+import {makeAutoObservable} from 'mobx'
+import {isObj, hasProp} from '../lib/type-guards'
+
+interface LogEntry {
+  id: string
+  type?: string
+  summary?: string
+  details?: string
+  ts?: number
+}
+
+let _lastTs: string
+let _lastId: string
+function genId(): string {
+  let candidate = String(Date.now())
+  if (_lastTs === candidate) {
+    const id = _lastId + 'x'
+    _lastId = id
+    return id
+  }
+  _lastTs = candidate
+  _lastId = candidate
+  return candidate
+}
+
+export class LogModel {
+  entries: LogEntry[] = []
+
+  constructor() {
+    makeAutoObservable(this, {serialize: false, hydrate: false})
+  }
+
+  serialize(): unknown {
+    return {
+      entries: this.entries.slice(-100),
+    }
+  }
+
+  hydrate(v: unknown) {
+    if (isObj(v)) {
+      if (hasProp(v, 'entries') && Array.isArray(v.entries)) {
+        this.entries = v.entries.filter(
+          e => isObj(e) && hasProp(e, 'id') && typeof e.id === 'string',
+        )
+      }
+    }
+  }
+
+  private add(entry: LogEntry) {
+    this.entries.push(entry)
+  }
+
+  debug(summary: string, details?: any) {
+    if (details && typeof details !== 'string') {
+      details = JSON.stringify(details, null, 2)
+    }
+    console.debug(summary, details || '')
+    this.add({
+      id: genId(),
+      type: 'debug',
+      summary,
+      details,
+      ts: Date.now(),
+    })
+  }
+
+  warn(summary: string, details?: any) {
+    if (details && typeof details !== 'string') {
+      details = JSON.stringify(details, null, 2)
+    }
+    console.warn(summary, details || '')
+    this.add({
+      id: genId(),
+      type: 'warn',
+      summary,
+      details,
+      ts: Date.now(),
+    })
+  }
+
+  error(summary: string, details?: any) {
+    if (details && typeof details !== 'string') {
+      details = JSON.stringify(details, null, 2)
+    }
+    console.error(summary, details || '')
+    this.add({
+      id: genId(),
+      type: 'error',
+      summary,
+      details,
+      ts: Date.now(),
+    })
+  }
+}
diff --git a/src/state/models/me.ts b/src/state/models/me.ts
index 9591acb80..ae1e6aed2 100644
--- a/src/state/models/me.ts
+++ b/src/state/models/me.ts
@@ -104,13 +104,22 @@ export class MeModel {
       })
       await Promise.all([
         this.memberships?.setup().catch(e => {
-          console.error('Failed to setup memberships model', e)
+          this.rootStore.log.error(
+            'Failed to setup memberships model',
+            e.toString(),
+          )
         }),
         this.mainFeed.setup().catch(e => {
-          console.error('Failed to setup main feed model', e)
+          this.rootStore.log.error(
+            'Failed to setup main feed model',
+            e.toString(),
+          )
         }),
         this.notifications.setup().catch(e => {
-          console.error('Failed to setup notifications model', e)
+          this.rootStore.log.error(
+            'Failed to setup notifications model',
+            e.toString(),
+          )
         }),
       ])
     } else {
diff --git a/src/state/models/notifications-view.ts b/src/state/models/notifications-view.ts
index c1ee78d41..38a8ca133 100644
--- a/src/state/models/notifications-view.ts
+++ b/src/state/models/notifications-view.ts
@@ -149,7 +149,10 @@ export class NotificationsViewItemModel implements GroupedNotification {
         depth: 0,
       })
       await this.additionalPost.setup().catch(e => {
-        console.error('Failed to load post needed by notification', e)
+        this.rootStore.log.error(
+          'Failed to load post needed by notification',
+          e.toString(),
+        )
       })
     }
   }
@@ -262,8 +265,11 @@ export class NotificationsViewModel {
         seenAt: new Date().toISOString(),
       })
       this.rootStore.me.clearNotificationCount()
-    } catch (e) {
-      console.log('Failed to update notifications read state', e)
+    } catch (e: any) {
+      this.rootStore.log.warn(
+        'Failed to update notifications read state',
+        e.toString(),
+      )
     }
   }
 
@@ -350,7 +356,6 @@ export class NotificationsViewModel {
         this._updateAll(res)
         numToFetch -= res.data.notifications.length
         cursor = this.notifications[res.data.notifications.length - 1].indexedAt
-        console.log(numToFetch, cursor, res.data.notifications.length)
       } while (numToFetch > 0)
       this._xIdle()
     } catch (e: any) {
@@ -379,9 +384,9 @@ export class NotificationsViewModel {
       itemModels.push(itemModel)
     }
     await Promise.all(promises).catch(e => {
-      console.error(
+      this.rootStore.log.error(
         'Uncaught failure during notifications-view _appendAll()',
-        e,
+        e.toString(),
       )
     })
     runInAction(() => {
diff --git a/src/state/models/profile-ui.ts b/src/state/models/profile-ui.ts
index d0eb1f858..081160e65 100644
--- a/src/state/models/profile-ui.ts
+++ b/src/state/models/profile-ui.ts
@@ -114,20 +114,28 @@ export class ProfileUiModel {
     await Promise.all([
       this.profile
         .setup()
-        .catch(err => console.error('Failed to fetch profile', err)),
+        .catch(err =>
+          this.rootStore.log.error('Failed to fetch profile', err.toString()),
+        ),
       this.feed
         .setup()
-        .catch(err => console.error('Failed to fetch feed', err)),
+        .catch(err =>
+          this.rootStore.log.error('Failed to fetch feed', err.toString()),
+        ),
     ])
     if (this.isUser) {
       await this.memberships
         .setup()
-        .catch(err => console.error('Failed to fetch members', err))
+        .catch(err =>
+          this.rootStore.log.error('Failed to fetch members', err.toString()),
+        )
     }
     if (this.isScene) {
       await this.members
         .setup()
-        .catch(err => console.error('Failed to fetch members', err))
+        .catch(err =>
+          this.rootStore.log.error('Failed to fetch members', err.toString()),
+        )
     }
   }
 
diff --git a/src/state/models/profile-view.ts b/src/state/models/profile-view.ts
index 2a69a1345..1c825a482 100644
--- a/src/state/models/profile-view.ts
+++ b/src/state/models/profile-view.ts
@@ -203,7 +203,6 @@ export class ProfileViewModel {
   }
 
   private _replaceAll(res: GetProfile.Response) {
-    console.log(res.data)
     this.did = res.data.did
     this.handle = res.data.handle
     Object.assign(this.declaration, res.data.declaration)
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index 54578b4a5..0166b67e6 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -6,6 +6,7 @@ import {makeAutoObservable} from 'mobx'
 import {sessionClient as AtpApi, SessionServiceClient} from '@atproto/api'
 import {createContext, useContext} from 'react'
 import {isObj, hasProp} from '../lib/type-guards'
+import {LogModel} from './log'
 import {SessionModel} from './session'
 import {NavigationModel} from './navigation'
 import {ShellUiModel} from './shell-ui'
@@ -16,6 +17,7 @@ import {OnboardModel} from './onboard'
 import {isNetworkError} from '../../lib/errors'
 
 export class RootStoreModel {
+  log = new LogModel()
   session = new SessionModel(this)
   nav = new NavigationModel()
   shell = new ShellUiModel()
@@ -53,16 +55,17 @@ export class RootStoreModel {
         await this.session.connect()
       }
       await this.me.fetchStateUpdate()
-    } catch (e: unknown) {
+    } catch (e: any) {
       if (isNetworkError(e)) {
         this.session.setOnline(false) // connection lost
       }
-      console.error('Failed to fetch latest state', e)
+      this.log.error('Failed to fetch latest state', e.toString())
     }
   }
 
   serialize(): unknown {
     return {
+      log: this.log.serialize(),
       session: this.session.serialize(),
       me: this.me.serialize(),
       nav: this.nav.serialize(),
@@ -73,8 +76,8 @@ export class RootStoreModel {
 
   hydrate(v: unknown) {
     if (isObj(v)) {
-      if (hasProp(v, 'session')) {
-        this.session.hydrate(v.session)
+      if (hasProp(v, 'log')) {
+        this.log.hydrate(v.log)
       }
       if (hasProp(v, 'me')) {
         this.me.hydrate(v.me)
diff --git a/src/state/models/session.ts b/src/state/models/session.ts
index febffcec3..3efb5d2a6 100644
--- a/src/state/models/session.ts
+++ b/src/state/models/session.ts
@@ -121,11 +121,11 @@ export class SessionModel {
     try {
       const serviceUri = new URL(this.data.service)
       this.rootStore.api.xrpc.uri = serviceUri
-    } catch (e) {
-      console.error(
+    } catch (e: any) {
+      this.rootStore.log.error(
         `Invalid service URL: ${this.data.service}. Resetting session.`,
+        e.toString(),
       )
-      console.error(e)
       this.clear()
       return false
     }
@@ -160,7 +160,10 @@ export class SessionModel {
           this.rootStore.me.clear()
         }
         this.rootStore.me.load().catch(e => {
-          console.error('Failed to fetch local user information', e)
+          this.rootStore.log.error(
+            'Failed to fetch local user information',
+            e.toString(),
+          )
         })
         return // success
       }
@@ -204,7 +207,10 @@ export class SessionModel {
       this.configureApi()
       this.setOnline(true, false)
       this.rootStore.me.load().catch(e => {
-        console.error('Failed to fetch local user information', e)
+        this.rootStore.log.error(
+          'Failed to fetch local user information',
+          e.toString(),
+        )
       })
     }
   }
@@ -240,7 +246,10 @@ export class SessionModel {
       this.rootStore.onboard.start()
       this.configureApi()
       this.rootStore.me.load().catch(e => {
-        console.error('Failed to fetch local user information', e)
+        this.rootStore.log.error(
+          'Failed to fetch local user information',
+          e.toString(),
+        )
       })
     }
   }
@@ -248,7 +257,10 @@ export class SessionModel {
   async logout() {
     if (this.hasSession) {
       this.rootStore.api.com.atproto.session.delete().catch((e: any) => {
-        console.error('(Minor issue) Failed to delete session on the server', e)
+        this.rootStore.log.warn(
+          '(Minor issue) Failed to delete session on the server',
+          e,
+        )
       })
     }
     this.rootStore.clearAll()
diff --git a/src/state/models/suggested-invites-view.ts b/src/state/models/suggested-invites-view.ts
index 33ca7396e..eb0665bca 100644
--- a/src/state/models/suggested-invites-view.ts
+++ b/src/state/models/suggested-invites-view.ts
@@ -98,8 +98,11 @@ export class SuggestedInvitesView {
     try {
       // TODO need to fetch all!
       await this.sceneAssertionsView.setup()
-    } catch (e) {
-      console.error(e)
+    } catch (e: any) {
+      this.rootStore.log.error(
+        'Failed to fetch current scene members in suggested invites',
+        e.toString(),
+      )
       this._xIdle(
         'Failed to fetch the current scene members. Check your internet connection and try again.',
       )
@@ -107,8 +110,11 @@ export class SuggestedInvitesView {
     }
     try {
       await this.myFollowsView.setup()
-    } catch (e) {
-      console.error(e)
+    } catch (e: any) {
+      this.rootStore.log.error(
+        'Failed to fetch current followers in suggested invites',
+        e.toString(),
+      )
       this._xIdle(
         'Failed to fetch the your current followers. Check your internet connection and try again.',
       )
diff --git a/src/view/com/composer/PhotoCarouselPicker.tsx b/src/view/com/composer/PhotoCarouselPicker.tsx
index 47c4c3746..f7a3d7987 100644
--- a/src/view/com/composer/PhotoCarouselPicker.tsx
+++ b/src/view/com/composer/PhotoCarouselPicker.tsx
@@ -1,7 +1,6 @@
 import React, {useCallback} from 'react'
 import {Image, StyleSheet, TouchableOpacity, ScrollView} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
-import {colors} from '../../lib/styles'
 import {
   openPicker,
   openCamera,
@@ -9,6 +8,7 @@ import {
 } from 'react-native-image-crop-picker'
 import {compressIfNeeded} from '../../../lib/images'
 import {usePalette} from '../../lib/hooks/usePalette'
+import {useStores} from '../../../state'
 
 const IMAGE_PARAMS = {
   width: 1000,
@@ -28,6 +28,7 @@ export const PhotoCarouselPicker = ({
   localPhotos: any
 }) => {
   const pal = usePalette('default')
+  const store = useStores()
   const handleOpenCamera = useCallback(async () => {
     try {
       const cameraRes = await openCamera({
@@ -37,11 +38,11 @@ export const PhotoCarouselPicker = ({
       })
       const uri = await compressIfNeeded(cameraRes, 300000)
       onSelectPhotos([uri, ...selectedPhotos])
-    } catch (err) {
+    } catch (err: any) {
       // ignore
-      console.log('Error using camera', err)
+      store.log.warn('Error using camera', err.toString())
     }
-  }, [selectedPhotos, onSelectPhotos])
+  }, [store.log, selectedPhotos, onSelectPhotos])
 
   const handleSelectPhoto = useCallback(
     async (uri: string) => {
@@ -53,12 +54,12 @@ export const PhotoCarouselPicker = ({
         })
         const finalUri = await compressIfNeeded(cropperRes, 300000)
         onSelectPhotos([finalUri, ...selectedPhotos])
-      } catch (err) {
+      } catch (err: any) {
         // ignore
-        console.log('Error selecting photo', err)
+        store.log.warn('Error selecting photo', err.toString())
       }
     },
-    [selectedPhotos, onSelectPhotos],
+    [store.log, selectedPhotos, onSelectPhotos],
   )
 
   const handleOpenGallery = useCallback(() => {
diff --git a/src/view/com/discover/SuggestedFollows.tsx b/src/view/com/discover/SuggestedFollows.tsx
index 24926df69..936dcd6db 100644
--- a/src/view/com/discover/SuggestedFollows.tsx
+++ b/src/view/com/discover/SuggestedFollows.tsx
@@ -42,11 +42,12 @@ export const SuggestedFollows = observer(
     )
 
     useEffect(() => {
-      console.log('Fetching suggested actors')
       view
         .setup()
-        .catch((err: any) => console.error('Failed to fetch suggestions', err))
-    }, [view])
+        .catch((err: any) =>
+          store.log.error('Failed to fetch suggestions', err.toString()),
+        )
+    }, [view, store.log])
 
     useEffect(() => {
       if (!view.isLoading && !view.hasError && !view.hasContent) {
@@ -57,14 +58,16 @@ export const SuggestedFollows = observer(
     const onPressTryAgain = () =>
       view
         .setup()
-        .catch((err: any) => console.error('Failed to fetch suggestions', err))
+        .catch((err: any) =>
+          store.log.error('Failed to fetch suggestions', err.toString()),
+        )
 
     const onPressFollow = async (item: SuggestedActor) => {
       try {
         const res = await apilib.follow(store, item.did, item.declaration.cid)
         setFollows({[item.did]: res.uri, ...follows})
-      } catch (e) {
-        console.log(e)
+      } catch (e: any) {
+        store.log.error('Failed fo create follow', {error: e.toString(), item})
         Toast.show('An issue occurred, please try again.')
       }
     }
@@ -72,8 +75,8 @@ export const SuggestedFollows = observer(
       try {
         await apilib.unfollow(store, follows[item.did])
         setFollows(_omit(follows, [item.did]))
-      } catch (e) {
-        console.log(e)
+      } catch (e: any) {
+        store.log.error('Failed fo delete follow', {error: e.toString(), item})
         Toast.show('An issue occurred, please try again.')
       }
     }
diff --git a/src/view/com/login/CreateAccount.tsx b/src/view/com/login/CreateAccount.tsx
index e6ce2efa6..f07ad7071 100644
--- a/src/view/com/login/CreateAccount.tsx
+++ b/src/view/com/login/CreateAccount.tsx
@@ -44,7 +44,6 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
     let aborted = false
     setError('')
     setServiceDescription(undefined)
-    console.log('Fetching service description', serviceUrl)
     store.session.describeService(serviceUrl).then(
       desc => {
         if (aborted) return
@@ -53,7 +52,10 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
       },
       err => {
         if (aborted) return
-        console.error(err)
+        store.log.warn(
+          `Failed to fetch service description for ${serviceUrl}`,
+          err.toString(),
+        )
         setError(
           'Unable to contact your service. Please check your Internet connection.',
         )
@@ -62,7 +64,7 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
     return () => {
       aborted = true
     }
-  }, [serviceUrl, store.session])
+  }, [serviceUrl, store.session, store.log])
 
   const onPressSelectService = () => {
     store.shell.openModal(new ServerInputModal(serviceUrl, setServiceUrl))
@@ -98,7 +100,7 @@ export const CreateAccount = ({onPressBack}: {onPressBack: () => void}) => {
         errMsg =
           'Invite code not accepted. Check that you input it correctly and try again.'
       }
-      console.log(e)
+      store.log.warn('Failed to create account', e.toString())
       setIsProcessing(false)
       setError(errMsg.replace(/^Error:/, ''))
     }
diff --git a/src/view/com/login/Signin.tsx b/src/view/com/login/Signin.tsx
index f76507d71..0a78b6401 100644
--- a/src/view/com/login/Signin.tsx
+++ b/src/view/com/login/Signin.tsx
@@ -44,7 +44,6 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
   useEffect(() => {
     let aborted = false
     setError('')
-    console.log('Fetching service description', serviceUrl)
     store.session.describeService(serviceUrl).then(
       desc => {
         if (aborted) return
@@ -52,7 +51,10 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
       },
       err => {
         if (aborted) return
-        console.error(err)
+        store.log.warn(
+          `Failed to fetch service description for ${serviceUrl}`,
+          err.toString(),
+        )
         setError(
           'Unable to contact your service. Please check your Internet connection.',
         )
@@ -61,7 +63,7 @@ export const Signin = ({onPressBack}: {onPressBack: () => void}) => {
     return () => {
       aborted = true
     }
-  }, [store.session, serviceUrl])
+  }, [store.session, store.log, serviceUrl])
 
   return (
     <KeyboardAvoidingView behavior="padding" style={{flex: 1}}>
@@ -169,7 +171,7 @@ const LoginForm = ({
       })
     } catch (e: any) {
       const errMsg = e.toString()
-      console.log(e)
+      store.log.warn('Failed to login', e.toString())
       setIsProcessing(false)
       if (errMsg.includes('Authentication Required')) {
         setError('Invalid username or password')
@@ -305,7 +307,7 @@ const ForgotPasswordForm = ({
       onEmailSent()
     } catch (e: any) {
       const errMsg = e.toString()
-      console.log(e)
+      store.log.warn('Failed to request password reset', e.toString())
       setIsProcessing(false)
       if (isNetworkError(e)) {
         setError(
@@ -417,7 +419,7 @@ const SetNewPasswordForm = ({
       onPasswordSet()
     } catch (e: any) {
       const errMsg = e.toString()
-      console.log(e)
+      store.log.warn('Failed to set new password', e.toString())
       setIsProcessing(false)
       if (isNetworkError(e)) {
         setError(
diff --git a/src/view/com/modals/CreateScene.tsx b/src/view/com/modals/CreateScene.tsx
index 60c240546..c26087850 100644
--- a/src/view/com/modals/CreateScene.tsx
+++ b/src/view/com/modals/CreateScene.tsx
@@ -55,7 +55,13 @@ export function Component({}: {}) {
           displayName,
           description,
         })
-        .catch(e => console.error(e)) // an error here is not critical
+        .catch(e =>
+          // an error here is not critical
+          store.log.error(
+            'Failed to update scene profile during creation',
+            e.toString(),
+          ),
+        )
       // follow the scene
       await store.api.app.bsky.graph.follow
         .create(
@@ -70,7 +76,13 @@ export function Component({}: {}) {
             createdAt: new Date().toISOString(),
           },
         )
-        .catch(e => console.error(e)) // an error here is not critical
+        .catch(e =>
+          // an error here is not critical
+          store.log.error(
+            'Failed to follow scene after creation',
+            e.toString(),
+          ),
+        )
       Toast.show('Scene created')
       store.shell.closeModal()
       store.nav.navigate(`/profile/${fullHandle}`)
@@ -82,7 +94,7 @@ export function Component({}: {}) {
       } else if (e instanceof AppBskyActorCreateScene.HandleNotAvailableError) {
         setError(`The handle "${handle}" is not available.`)
       } else {
-        console.error(e)
+        store.log.error('Failed to create scene', e.toString())
         setError(
           'Failed to create the scene. Check your internet connection and try again.',
         )
diff --git a/src/view/com/modals/InviteToScene.tsx b/src/view/com/modals/InviteToScene.tsx
index a73440179..6fe17b4dc 100644
--- a/src/view/com/modals/InviteToScene.tsx
+++ b/src/view/com/modals/InviteToScene.tsx
@@ -84,9 +84,9 @@ export const Component = observer(function Component({
       )
       setCreatedInvites({[follow.did]: assertionUri, ...createdInvites})
       Toast.show('Invite sent')
-    } catch (e) {
+    } catch (e: any) {
       setError('There was an issue with the invite. Please try again.')
-      console.error(e)
+      store.log.error('Failed to invite user to scene', e.toString())
     }
   }
   const onPressUndo = async (subjectDid: string, assertionUri: string) => {
@@ -98,9 +98,9 @@ export const Component = observer(function Component({
         rkey: urip.rkey,
       })
       setCreatedInvites(_omit(createdInvites, [subjectDid]))
-    } catch (e) {
+    } catch (e: any) {
       setError('There was an issue with the invite. Please try again.')
-      console.error(e)
+      store.log.error('Failed to delete a scene invite', e.toString())
     }
   }
 
@@ -117,9 +117,9 @@ export const Component = observer(function Component({
         ...deletedPendingInvites,
       })
       Toast.show('Invite removed')
-    } catch (e) {
+    } catch (e: any) {
       setError('There was an issue with the invite. Please try again.')
-      console.error(e)
+      store.log.error('Failed to delete an invite', e.toString())
     }
   }
 
diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx
index 91a01db4d..6406a598b 100644
--- a/src/view/com/notifications/Feed.tsx
+++ b/src/view/com/notifications/Feed.tsx
@@ -36,10 +36,24 @@ export const Feed = observer(function Feed({
     return <FeedItem item={item} />
   }
   const onRefresh = () => {
-    view.refresh().catch(err => console.error('Failed to refresh', err))
+    view
+      .refresh()
+      .catch(err =>
+        view.rootStore.log.error(
+          'Failed to refresh notifications feed',
+          err.toString(),
+        ),
+      )
   }
   const onEndReached = () => {
-    view.loadMore().catch(err => console.error('Failed to load more', err))
+    view
+      .loadMore()
+      .catch(err =>
+        view.rootStore.log.error(
+          'Failed to load more notifications',
+          err.toString(),
+        ),
+      )
   }
   let data
   if (view.hasLoaded) {
diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx
index 0efdfe2e4..4ca20aedd 100644
--- a/src/view/com/post-thread/PostRepostedBy.tsx
+++ b/src/view/com/post-thread/PostRepostedBy.tsx
@@ -22,15 +22,15 @@ export const PostRepostedBy = observer(function PostRepostedBy({
 
   useEffect(() => {
     if (view?.params.uri === uri) {
-      console.log('Reposted by doing nothing')
       return // no change needed? or trigger refresh?
     }
-    console.log('Fetching Reposted by', uri)
     const newView = new RepostedByViewModel(store, {uri})
     setView(newView)
     newView
       .setup()
-      .catch(err => console.error('Failed to fetch reposted by', err))
+      .catch(err =>
+        store.log.error('Failed to fetch reposted by', err.toString()),
+      )
   }, [uri, view?.params.uri, store])
 
   const onRefresh = () => {
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index 8c22cc8b7..187fe6c11 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -18,7 +18,14 @@ export const PostThread = observer(function PostThread({
   const ref = useRef<FlatList>(null)
   const posts = view.thread ? Array.from(flattenThread(view.thread)) : []
   const onRefresh = () => {
-    view?.refresh().catch(err => console.error('Failed to refresh', err))
+    view
+      ?.refresh()
+      .catch(err =>
+        view.rootStore.log.error(
+          'Failed to refresh posts thread',
+          err.toString(),
+        ),
+      )
   }
   const onLayout = () => {
     const index = posts.findIndex(post => post._isHighlightedPost)
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index ae2bd6681..456a6f465 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -72,12 +72,12 @@ export const PostThreadItem = observer(function PostThreadItem({
   const onPressToggleRepost = () => {
     item
       .toggleRepost()
-      .catch(e => console.error('Failed to toggle repost', record, e))
+      .catch(e => store.log.error('Failed to toggle repost', e.toString()))
   }
   const onPressToggleUpvote = () => {
     item
       .toggleUpvote()
-      .catch(e => console.error('Failed to toggle upvote', record, e))
+      .catch(e => store.log.error('Failed to toggle upvote', e.toString()))
   }
   const onCopyPostText = () => {
     Clipboard.setString(record.text)
@@ -90,7 +90,7 @@ export const PostThreadItem = observer(function PostThreadItem({
         Toast.show('Post deleted')
       },
       e => {
-        console.error(e)
+        store.log.error('Failed to delete post', e.toString())
         Toast.show('Failed to delete post, please try again')
       },
     )
diff --git a/src/view/com/post-thread/PostVotedBy.tsx b/src/view/com/post-thread/PostVotedBy.tsx
index 96a335919..21559e432 100644
--- a/src/view/com/post-thread/PostVotedBy.tsx
+++ b/src/view/com/post-thread/PostVotedBy.tsx
@@ -24,13 +24,13 @@ export const PostVotedBy = observer(function PostVotedBy({
 
   useEffect(() => {
     if (view?.params.uri === uri) {
-      console.log('Voted by doing nothing')
       return // no change needed? or trigger refresh?
     }
-    console.log('Fetching voted by', uri)
     const newView = new VotesViewModel(store, {uri, direction})
     setView(newView)
-    newView.setup().catch(err => console.error('Failed to fetch voted by', err))
+    newView
+      .setup()
+      .catch(err => store.log.error('Failed to fetch voted by', err.toString()))
   }, [uri, view?.params.uri, store])
 
   const onRefresh = () => {
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index e82498a7d..d55027a94 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -47,7 +47,9 @@ export const Post = observer(function Post({
     }
     const newView = new PostThreadViewModel(store, {uri, depth: 0})
     setView(newView)
-    newView.setup().catch(err => console.error('Failed to fetch post', err))
+    newView
+      .setup()
+      .catch(err => store.log.error('Failed to fetch post', err.toString()))
   }, [initView, uri, view?.params.uri, store])
 
   // deleted
@@ -110,12 +112,12 @@ export const Post = observer(function Post({
   const onPressToggleRepost = () => {
     item
       .toggleRepost()
-      .catch(e => console.error('Failed to toggle repost', record, e))
+      .catch(e => store.log.error('Failed to toggle repost', e.toString()))
   }
   const onPressToggleUpvote = () => {
     item
       .toggleUpvote()
-      .catch(e => console.error('Failed to toggle upvote', record, e))
+      .catch(e => store.log.error('Failed to toggle upvote', e.toString()))
   }
   const onCopyPostText = () => {
     Clipboard.setString(record.text)
@@ -128,7 +130,7 @@ export const Post = observer(function Post({
         Toast.show('Post deleted')
       },
       e => {
-        console.error(e)
+        store.log.error('Failed to delete post', e.toString())
         Toast.show('Failed to delete post, please try again')
       },
     )
diff --git a/src/view/com/post/PostText.tsx b/src/view/com/post/PostText.tsx
index 436768292..4e8761eb5 100644
--- a/src/view/com/post/PostText.tsx
+++ b/src/view/com/post/PostText.tsx
@@ -23,7 +23,9 @@ export const PostText = observer(function PostText({
     }
     const newModel = new PostModel(store, uri)
     setModel(newModel)
-    newModel.setup().catch(err => console.error('Failed to fetch post', err))
+    newModel
+      .setup()
+      .catch(err => store.log.error('Failed to fetch post', err.toString()))
   }, [uri, model?.uri, store])
 
   // loading
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index 02141acef..61ecf0a8f 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -53,10 +53,21 @@ export const Feed = observer(function Feed({
     }
   }
   const onRefresh = () => {
-    feed.refresh().catch(err => console.error('Failed to refresh', err))
+    feed
+      .refresh()
+      .catch(err =>
+        feed.rootStore.log.error(
+          'Failed to refresh posts feed',
+          err.toString(),
+        ),
+      )
   }
   const onEndReached = () => {
-    feed.loadMore().catch(err => console.error('Failed to load more', err))
+    feed
+      .loadMore()
+      .catch(err =>
+        feed.rootStore.log.error('Failed to load more posts', err.toString()),
+      )
   }
   let data
   if (feed.hasLoaded) {
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index 150143a26..dcc4e28d7 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -69,12 +69,12 @@ export const FeedItem = observer(function ({
   const onPressToggleRepost = () => {
     item
       .toggleRepost()
-      .catch(e => console.error('Failed to toggle repost', record, e))
+      .catch(e => store.log.error('Failed to toggle repost', e.toString()))
   }
   const onPressToggleUpvote = () => {
     item
       .toggleUpvote()
-      .catch(e => console.error('Failed to toggle upvote', record, e))
+      .catch(e => store.log.error('Failed to toggle upvote', e.toString()))
   }
   const onCopyPostText = () => {
     Clipboard.setString(record.text)
@@ -87,7 +87,7 @@ export const FeedItem = observer(function ({
         Toast.show('Post deleted')
       },
       e => {
-        console.error(e)
+        store.log.error('Failed to delete post', e.toString())
         Toast.show('Failed to delete post, please try again')
       },
     )
diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx
index 175a582ce..409df05cb 100644
--- a/src/view/com/profile/ProfileFollowers.tsx
+++ b/src/view/com/profile/ProfileFollowers.tsx
@@ -23,15 +23,15 @@ export const ProfileFollowers = observer(function ProfileFollowers({
 
   useEffect(() => {
     if (view?.params.user === name) {
-      console.log('User followers doing nothing')
       return // no change needed? or trigger refresh?
     }
-    console.log('Fetching user followers', name)
     const newView = new UserFollowersViewModel(store, {user: name})
     setView(newView)
     newView
       .setup()
-      .catch(err => console.error('Failed to fetch user followers', err))
+      .catch(err =>
+        store.log.error('Failed to fetch user followers', err.toString()),
+      )
   }, [name, view?.params.user, store])
 
   const onRefresh = () => {
diff --git a/src/view/com/profile/ProfileFollows.tsx b/src/view/com/profile/ProfileFollows.tsx
index 2d40af243..f63cc0107 100644
--- a/src/view/com/profile/ProfileFollows.tsx
+++ b/src/view/com/profile/ProfileFollows.tsx
@@ -23,15 +23,15 @@ export const ProfileFollows = observer(function ProfileFollows({
 
   useEffect(() => {
     if (view?.params.user === name) {
-      console.log('User follows doing nothing')
       return // no change needed? or trigger refresh?
     }
-    console.log('Fetching user follows', name)
     const newView = new UserFollowsViewModel(store, {user: name})
     setView(newView)
     newView
       .setup()
-      .catch(err => console.error('Failed to fetch user follows', err))
+      .catch(err =>
+        store.log.error('Failed to fetch user follows', err.toString()),
+      )
   }, [name, view?.params.user, store])
 
   const onRefresh = () => {
diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx
index 5f0fb6fe2..5a87401b4 100644
--- a/src/view/com/profile/ProfileHeader.tsx
+++ b/src/view/com/profile/ProfileHeader.tsx
@@ -52,7 +52,7 @@ export const ProfileHeader = observer(function ProfileHeader({
           }`,
         )
       },
-      err => console.error('Failed to toggle follow', err),
+      err => store.log.error('Failed to toggle follow', err.toString()),
     )
   }
   const onPressEditProfile = () => {
@@ -94,7 +94,7 @@ export const ProfileHeader = observer(function ProfileHeader({
       await view.muteAccount()
       Toast.show('Account muted')
     } catch (e: any) {
-      console.error(e)
+      store.log.error('Failed to mute account', e.toString())
       Toast.show(`There was an issue! ${e.toString()}`)
     }
   }
@@ -103,7 +103,7 @@ export const ProfileHeader = observer(function ProfileHeader({
       await view.unmuteAccount()
       Toast.show('Account unmuted')
     } catch (e: any) {
-      console.error(e)
+      store.log.error('Failed to unmute account', e.toString())
       Toast.show(`There was an issue! ${e.toString()}`)
     }
   }
diff --git a/src/view/com/profile/ProfileMembers.tsx b/src/view/com/profile/ProfileMembers.tsx
index 0e34865b9..bcba2a4da 100644
--- a/src/view/com/profile/ProfileMembers.tsx
+++ b/src/view/com/profile/ProfileMembers.tsx
@@ -16,13 +16,13 @@ export const ProfileMembers = observer(function ProfileMembers({
 
   useEffect(() => {
     if (view?.params.actor === name) {
-      console.log('Members doing nothing')
       return // no change needed? or trigger refresh?
     }
-    console.log('Fetching members', name)
     const newView = new MembersViewModel(store, {actor: name})
     setView(newView)
-    newView.setup().catch(err => console.error('Failed to fetch members', err))
+    newView
+      .setup()
+      .catch(err => store.log.error('Failed to fetch members', err.toString()))
   }, [name, view?.params.actor, store])
 
   const onRefresh = () => {
diff --git a/src/view/com/util/ViewHeader.tsx b/src/view/com/util/ViewHeader.tsx
index a714c2db4..2e584b764 100644
--- a/src/view/com/util/ViewHeader.tsx
+++ b/src/view/com/util/ViewHeader.tsx
@@ -45,8 +45,7 @@ export const ViewHeader = observer(function ViewHeader({
   }
   const onPressReconnect = () => {
     store.session.connect().catch(e => {
-      // log for debugging but ignore otherwise
-      console.log(e)
+      store.log.warn('Failed to reconnect to server', e)
     })
   }
   if (typeof canGoBack === 'undefined') {
diff --git a/src/view/index.ts b/src/view/index.ts
index b38c0aa50..5602784f3 100644
--- a/src/view/index.ts
+++ b/src/view/index.ts
@@ -4,6 +4,7 @@ import {faAddressCard} from '@fortawesome/free-regular-svg-icons/faAddressCard'
 import {faAngleDown} from '@fortawesome/free-solid-svg-icons/faAngleDown'
 import {faAngleLeft} from '@fortawesome/free-solid-svg-icons/faAngleLeft'
 import {faAngleRight} from '@fortawesome/free-solid-svg-icons/faAngleRight'
+import {faAngleUp} from '@fortawesome/free-solid-svg-icons/faAngleUp'
 import {faArrowLeft} from '@fortawesome/free-solid-svg-icons/faArrowLeft'
 import {faArrowRight} from '@fortawesome/free-solid-svg-icons/faArrowRight'
 import {faArrowUp} from '@fortawesome/free-solid-svg-icons/faArrowUp'
@@ -38,6 +39,7 @@ import {faHeart as fasHeart} from '@fortawesome/free-solid-svg-icons/faHeart'
 import {faHouse} from '@fortawesome/free-solid-svg-icons/faHouse'
 import {faImage as farImage} from '@fortawesome/free-regular-svg-icons/faImage'
 import {faImage} from '@fortawesome/free-solid-svg-icons/faImage'
+import {faInfo} from '@fortawesome/free-solid-svg-icons/faInfo'
 import {faLink} from '@fortawesome/free-solid-svg-icons/faLink'
 import {faLock} from '@fortawesome/free-solid-svg-icons/faLock'
 import {faMagnifyingGlass} from '@fortawesome/free-solid-svg-icons/faMagnifyingGlass'
@@ -71,6 +73,7 @@ export function setup() {
     faAngleDown,
     faAngleLeft,
     faAngleRight,
+    faAngleUp,
     faArrowLeft,
     faArrowRight,
     faArrowUp,
@@ -105,6 +108,7 @@ export function setup() {
     faHouse,
     faImage,
     farImage,
+    faInfo,
     faLink,
     faLock,
     faMagnifyingGlass,
diff --git a/src/view/routes.ts b/src/view/routes.ts
index 3717e0f05..0a2883e69 100644
--- a/src/view/routes.ts
+++ b/src/view/routes.ts
@@ -16,6 +16,7 @@ import {ProfileFollows} from './screens/ProfileFollows'
 import {ProfileMembers} from './screens/ProfileMembers'
 import {Settings} from './screens/Settings'
 import {Debug} from './screens/Debug'
+import {Log} from './screens/Log'
 
 export type ScreenParams = {
   navIdx: [number, number]
@@ -72,7 +73,8 @@ export const routes: Route[] = [
     'retweet',
     r('/profile/(?<name>[^/]+)/post/(?<rkey>[^/]+)/reposted-by'),
   ],
-  [Debug, 'Debug', 'house', r('/debug')],
+  [Debug, 'Debug', 'house', r('/sys/debug')],
+  [Log, 'Log', 'house', r('/sys/log')],
 ]
 
 export function match(url: string): MatchResult {
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 118ba9ed8..dbf665837 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -35,9 +35,9 @@ export const Home = observer(function Home({
     if (store.me.mainFeed.isLoading) {
       return
     }
-    console.log('Polling home feed')
+    store.log.debug('Polling home feed')
     store.me.mainFeed.checkForLatest().catch(e => {
-      console.error('Failed to poll feed', e)
+      store.log.error('Failed to poll feed', e.toString())
     })
   }
 
@@ -49,12 +49,12 @@ export const Home = observer(function Home({
     }
 
     if (hasSetup) {
-      console.log('Updating home feed')
+      store.log.debug('Updating home feed')
       store.me.mainFeed.update()
       doPoll()
     } else {
       store.nav.setTitle(navIdx, 'Home')
-      console.log('Fetching home feed')
+      store.log.debug('Fetching home feed')
       store.me.mainFeed.setup().then(() => {
         if (aborted) return
         setHasSetup(true)
diff --git a/src/view/screens/Log.tsx b/src/view/screens/Log.tsx
new file mode 100644
index 000000000..56337435f
--- /dev/null
+++ b/src/view/screens/Log.tsx
@@ -0,0 +1,100 @@
+import React, {useEffect} from 'react'
+import {ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native'
+import {observer} from 'mobx-react-lite'
+import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
+import {useStores} from '../../state'
+import {ScreenParams} from '../routes'
+import {s} from '../lib/styles'
+import {ViewHeader} from '../com/util/ViewHeader'
+import {Text} from '../com/util/text/Text'
+import {usePalette} from '../lib/hooks/usePalette'
+import {ago} from '../../lib/strings'
+
+export const Log = observer(function Log({navIdx, visible}: ScreenParams) {
+  const pal = usePalette('default')
+  const store = useStores()
+  const [expanded, setExpanded] = React.useState<string[]>([])
+
+  useEffect(() => {
+    if (!visible) {
+      return
+    }
+    store.shell.setMinimalShellMode(false)
+    store.nav.setTitle(navIdx, 'Log')
+  }, [visible, store])
+
+  const toggler = (id: string) => () => {
+    if (expanded.includes(id)) {
+      setExpanded(expanded.filter(v => v !== id))
+    } else {
+      setExpanded([...expanded, id])
+    }
+  }
+
+  return (
+    <View style={[s.flex1]}>
+      <ViewHeader title="Log" />
+      <ScrollView style={s.flex1}>
+        {store.log.entries
+          .slice(0)
+          .reverse()
+          .map(entry => {
+            return (
+              <View key={`entry-${entry.id}`}>
+                <TouchableOpacity
+                  style={[styles.entry, pal.border, pal.view]}
+                  onPress={toggler(entry.id)}>
+                  {entry.type === 'debug' ? (
+                    <FontAwesomeIcon icon="info" />
+                  ) : (
+                    <FontAwesomeIcon icon="exclamation" style={s.red3} />
+                  )}
+                  <Text type="body2" style={[styles.summary, pal.text]}>
+                    {entry.summary}
+                  </Text>
+                  {!!entry.details ? (
+                    <FontAwesomeIcon
+                      icon={
+                        expanded.includes(entry.id) ? 'angle-up' : 'angle-down'
+                      }
+                      style={s.mr5}
+                    />
+                  ) : undefined}
+                  <Text type="body2" style={[styles.ts, pal.textLight]}>
+                    {entry.ts ? ago(entry.ts) : ''}
+                  </Text>
+                </TouchableOpacity>
+                {expanded.includes(entry.id) ? (
+                  <View style={[pal.btn, styles.details]}>
+                    <Text type="body1" style={pal.text}>
+                      {entry.details}
+                    </Text>
+                  </View>
+                ) : undefined}
+              </View>
+            )
+          })}
+        <View style={{height: 100}} />
+      </ScrollView>
+    </View>
+  )
+})
+
+const styles = StyleSheet.create({
+  entry: {
+    flexDirection: 'row',
+    borderTopWidth: 1,
+    paddingVertical: 10,
+    paddingHorizontal: 6,
+  },
+  summary: {
+    flex: 1,
+  },
+  ts: {
+    width: 40,
+  },
+  details: {
+    paddingVertical: 10,
+    paddingHorizontal: 6,
+  },
+})
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index 2257dd221..5a4d9c223 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -14,12 +14,12 @@ export const Notifications = ({navIdx, visible}: ScreenParams) => {
     if (!visible) {
       return
     }
-    console.log('Updating notifications feed')
+    store.log.debug('Updating notifications feed')
     store.me.refreshMemberships() // needed for the invite notifications
     store.me.notifications
       .update()
       .catch(e => {
-        console.error('Error while updating notifications feed', e)
+        store.log.error('Error while updating notifications feed', e.toString())
       })
       .then(() => {
         store.me.notifications.updateReadState()
diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx
index 4caf144bf..86fde1374 100644
--- a/src/view/screens/PostThread.tsx
+++ b/src/view/screens/PostThread.tsx
@@ -31,7 +31,6 @@ export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
     setTitle()
     store.shell.setMinimalShellMode(false)
     if (!view.hasLoaded && !view.isLoading) {
-      console.log('Fetching post thread', uri)
       view.setup().then(
         () => {
           if (!aborted) {
@@ -39,14 +38,14 @@ export const PostThread = ({navIdx, visible, params}: ScreenParams) => {
           }
         },
         err => {
-          console.error('Failed to fetch thread', err)
+          store.log.error('Failed to fetch thread', err.toString())
         },
       )
     }
     return () => {
       aborted = true
     }
-  }, [visible, store.nav, name])
+  }, [visible, store.nav, store.log, name])
 
   return (
     <View style={{flex: 1}}>
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 8dd2dbe33..af011f837 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -40,10 +40,8 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
       return
     }
     if (hasSetup) {
-      console.log('Updating profile for', params.name)
       uiState.update()
     } else {
-      console.log('Fetching profile for', params.name)
       store.nav.setTitle(navIdx, params.name)
       uiState.setup().then(() => {
         if (aborted) return
@@ -64,12 +62,19 @@ export const Profile = observer(({navIdx, visible, params}: ScreenParams) => {
   const onRefresh = () => {
     uiState
       .refresh()
-      .catch((err: any) => console.error('Failed to refresh', err))
+      .catch((err: any) =>
+        store.log.error('Failed to refresh user profile', err.toString()),
+      )
   }
   const onEndReached = () => {
     uiState
       .loadMore()
-      .catch((err: any) => console.error('Failed to load more', err))
+      .catch((err: any) =>
+        store.log.error(
+          'Failed to load more entries in user profile',
+          err.toString(),
+        ),
+      )
   }
   const onPressTryAgain = () => {
     uiState.setup()
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index d7565e9c8..39597152d 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -3,7 +3,7 @@ import {StyleSheet, TouchableOpacity, View} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {useStores} from '../../state'
 import {ScreenParams} from '../routes'
-import {s, colors} from '../lib/styles'
+import {s} from '../lib/styles'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {Link} from '../com/util/Link'
 import {Text} from '../com/util/text/Text'
@@ -32,7 +32,7 @@ export const Settings = observer(function Settings({
   return (
     <View style={[s.flex1]}>
       <ViewHeader title="Settings" />
-      <View style={[s.mt10, s.pl10, s.pr10]}>
+      <View style={[s.mt10, s.pl10, s.pr10, s.flex1]}>
         <View style={[s.flexRow]}>
           <Text style={pal.text}>Signed in as</Text>
           <View style={s.flex1} />
@@ -61,9 +61,23 @@ export const Settings = observer(function Settings({
             </View>
           </View>
         </Link>
-        <Link href="/debug" title="Debug tools">
+        <View style={s.flex1} />
+        <Text type="overline1" style={[s.mb5]}>
+          Advanced
+        </Text>
+        <Link
+          style={[pal.view, s.p10, s.mb2]}
+          href="/sys/log"
+          title="System log">
+          <Text style={pal.link}>System log</Text>
+        </Link>
+        <Link
+          style={[pal.view, s.p10, s.mb2]}
+          href="/sys/debug"
+          title="Debug tools">
           <Text style={pal.link}>Debug tools</Text>
         </Link>
+        <View style={{height: 100}} />
       </View>
     </View>
   )