Bluesky app fork with some witchin' additions 💫
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #1813 from bluesky-social/eric/app-903-extract-logger-into-singleton

Add new logger

authored by

Eric Bailey and committed by
GitHub
e49a3d8a 46c2564e

+1109 -176
+2
.env.example
··· 1 1 SENTRY_AUTH_TOKEN= 2 + EXPO_PUBLIC_LOG_LEVEL=debug 3 + EXPO_PUBLIC_LOG_DEBUG=
+12 -10
package.json
··· 13 13 "start": "expo start --dev-client", 14 14 "start:prod": "expo start --dev-client --no-dev --minify", 15 15 "clean-cache": "rm -rf node_modules/.cache/babel-loader/*", 16 - "test": "jest --forceExit --testTimeout=20000 --bail", 17 - "test-watch": "jest --watchAll", 18 - "test-ci": "jest --ci --forceExit --reporters=default --reporters=jest-junit", 19 - "test-coverage": "jest --coverage", 16 + "test": "NODE_ENV=test jest --forceExit --testTimeout=20000 --bail", 17 + "test-watch": "NODE_ENV=test jest --watchAll", 18 + "test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit", 19 + "test-coverage": "NODE_ENV=test jest --coverage", 20 20 "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx", 21 21 "typecheck": "tsc --project ./tsconfig.check.json", 22 22 "e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node __e2e__/mock-server.ts", 23 23 "e2e:metro": "RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", 24 24 "e2e:build": "detox build -c ios.sim.debug", 25 25 "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all", 26 - "perf:test": "maestro test", 27 - "perf:test:run": "maestro test __e2e__/maestro/scroll.yaml", 28 - "perf:test:measure": "flashlight test --bundleId xyz.blueskyweb.app --testCommand 'yarn perf:test' --duration 150000 --resultsFilePath .perf/results.json", 29 - "perf:test:results": "flashlight report .perf/results.json", 30 - "perf:measure": "flashlight measure", 26 + "perf:test": "NODE_ENV=test maestro test", 27 + "perf:test:run": "NODE_ENV=test maestro test __e2e__/maestro/scroll.yaml", 28 + "perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand 'yarn perf:test' --duration 150000 --resultsFilePath .perf/results.json", 29 + "perf:test:results": "NODE_ENV=test flashlight report .perf/results.json", 30 + "perf:measure": "NODE_ENV=test flashlight measure", 31 31 "build:apk": "eas build -p android --profile dev-android-apk" 32 32 }, 33 33 "dependencies": { ··· 80 80 "babel-plugin-transform-remove-console": "^6.9.4", 81 81 "base64-js": "^1.5.1", 82 82 "bcp-47-match": "^2.0.3", 83 + "date-fns": "^2.30.0", 83 84 "email-validator": "^2.0.4", 84 85 "emoji-mart": "^5.5.2", 85 86 "eventemitter3": "^5.0.1", ··· 118 119 "mobx": "^6.6.1", 119 120 "mobx-react-lite": "^3.4.0", 120 121 "mobx-utils": "^6.0.6", 122 + "nanoid": "^5.0.2", 121 123 "normalize-url": "^8.0.0", 122 124 "patch-package": "^6.5.1", 123 125 "postinstall-postinstall": "^2.1.0", ··· 240 242 "\\.[jt]sx?$": "babel-jest" 241 243 }, 242 244 "transformIgnorePatterns": [ 243 - "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg|@sentry/.*|sentry-expo|bcp-47-match)" 245 + "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|nanoid|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg|@sentry/.*|sentry-expo|bcp-47-match)" 244 246 ], 245 247 "modulePathIgnorePatterns": [ 246 248 "__tests__/.*/__mocks__",
+9
src/env.ts
··· 1 + export const IS_TEST = process.env.NODE_ENV === 'test' 2 + export const IS_DEV = __DEV__ 3 + export const IS_PROD = !IS_DEV 4 + export const LOG_DEBUG = process.env.EXPO_PUBLIC_LOG_DEBUG || '' 5 + export const LOG_LEVEL = (process.env.EXPO_PUBLIC_LOG_LEVEL || 'info') as 6 + | 'debug' 7 + | 'info' 8 + | 'warn' 9 + | 'error'
+3 -4
src/lib/api/index.ts
··· 178 178 ) { 179 179 encoding = 'image/jpeg' 180 180 } else { 181 - store.log.warn( 182 - 'Unexpected image format for thumbnail, skipping', 183 - opts.extLink.localThumb.path, 184 - ) 181 + store.log.warn('Unexpected image format for thumbnail, skipping', { 182 + thumbnail: opts.extLink.localThumb.path, 183 + }) 185 184 } 186 185 if (encoding) { 187 186 const thumbUploadRes = await uploadBlob(
+2 -2
src/lib/hooks/useFollowProfile.ts
··· 22 22 following: false, 23 23 } 24 24 } catch (e: any) { 25 - store.log.error('Failed to delete follow', e) 25 + store.log.error('Failed to delete follow', {error: e}) 26 26 throw e 27 27 } 28 28 } else if (state === FollowState.NotFollowing) { ··· 40 40 following: true, 41 41 } 42 42 } catch (e: any) { 43 - store.log.error('Failed to create follow', e) 43 + store.log.error('Failed to create follow', {error: e}) 44 44 throw e 45 45 } 46 46 }
+6 -6
src/lib/hooks/useOTAUpdate.ts
··· 34 34 // show a popup modal 35 35 showUpdatePopup() 36 36 } catch (e) { 37 - console.error('useOTAUpdate: Error while checking for update', e) 38 - store.log.error('useOTAUpdate: Error while checking for update', e) 37 + store.log.error('useOTAUpdate: Error while checking for update', { 38 + error: e, 39 + }) 39 40 } 40 41 }, [showUpdatePopup, store.log]) 41 42 const updateEventListener = useCallback( 42 43 (event: Updates.UpdateEvent) => { 43 44 store.log.debug('useOTAUpdate: Listening for update...') 44 45 if (event.type === Updates.UpdateEventType.ERROR) { 45 - store.log.error( 46 - 'useOTAUpdate: Error while listening for update', 47 - event.message, 48 - ) 46 + store.log.error('useOTAUpdate: Error while listening for update', { 47 + message: event.message, 48 + }) 49 49 } else if (event.type === Updates.UpdateEventType.NO_UPDATE_AVAILABLE) { 50 50 // Handle no update available 51 51 // do nothing
+9 -10
src/lib/notifications/notifications.ts
··· 30 30 appId: 'xyz.blueskyweb.app', 31 31 }) 32 32 store.log.debug('Notifications: Sent push token (init)', { 33 - type: token.type, 33 + tokenType: token.type, 34 34 token: token.data, 35 35 }) 36 36 } catch (error) { 37 - store.log.error('Notifications: Failed to set push token', error) 37 + store.log.error('Notifications: Failed to set push token', {error}) 38 38 } 39 39 } 40 40 41 41 // listens for new changes to the push token 42 42 // In rare situations, a push token may be changed by the push notification service while the app is running. When a token is rolled, the old one becomes invalid and sending notifications to it will fail. A push token listener will let you handle this situation gracefully by registering the new token with your backend right away. 43 43 Notifications.addPushTokenListener(async ({data: t, type}) => { 44 - store.log.debug('Notifications: Push token changed', {t, type}) 44 + store.log.debug('Notifications: Push token changed', {t, tokenType: type}) 45 45 if (t) { 46 46 try { 47 47 await store.agent.api.app.bsky.notification.registerPush({ ··· 51 51 appId: 'xyz.blueskyweb.app', 52 52 }) 53 53 store.log.debug('Notifications: Sent push token (event)', { 54 - type, 54 + tokenType: type, 55 55 token: t, 56 56 }) 57 57 } catch (error) { 58 - store.log.error('Notifications: Failed to set push token', error) 58 + store.log.error('Notifications: Failed to set push token', {error}) 59 59 } 60 60 } 61 61 }) ··· 63 63 64 64 // handle notifications that are received, both in the foreground or background 65 65 Notifications.addNotificationReceivedListener(event => { 66 - store.log.debug('Notifications: received', event) 66 + store.log.debug('Notifications: received', {event}) 67 67 if (event.request.trigger.type === 'push') { 68 68 // refresh notifications in the background 69 69 store.me.notifications.syncQueue() ··· 84 84 // handle notifications that are tapped on 85 85 const sub = Notifications.addNotificationResponseReceivedListener( 86 86 response => { 87 - store.log.debug( 88 - 'Notifications: response received', 89 - response.actionIdentifier, 90 - ) 87 + store.log.debug('Notifications: response received', { 88 + actionIdentifier: response.actionIdentifier, 89 + }) 91 90 if ( 92 91 response.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER 93 92 ) {
+99
src/logger/README.md
··· 1 + # Logger 2 + 3 + Simple logger for Bluesky. Supports log levels, debug contexts, and separate 4 + transports for production, dev, and test mode. 5 + 6 + ## At a Glance 7 + 8 + ```typescript 9 + import { logger } from '#/logger' 10 + 11 + logger.debug(message[, metadata, debugContext]) 12 + logger.info(message[, metadata]) 13 + logger.log(message[, metadata]) 14 + logger.warn(message[, metadata]) 15 + logger.error(error[, metadata]) 16 + ``` 17 + 18 + #### Modes 19 + 20 + The "modes" referred to here are inferred from the values exported from `#/env`. 21 + Basically, the booleans `IS_DEV`, `IS_TEST`, and `IS_PROD`. 22 + 23 + #### Log Levels 24 + 25 + Log levels are used to filter which logs are either printed to the console 26 + and/or sent to Sentry and other reporting services. To configure, set the 27 + `EXPO_PUBLIC_LOG_LEVEL` environment variable in `.env` to one of `debug`, 28 + `info`, `log`, `warn`, or `error`. 29 + 30 + This variable should be `info` in production, and `debug` in dev. If it gets too 31 + noisy in dev, simply set it to a higher level, such as `warn`. 32 + 33 + ## Usage 34 + 35 + ```typescript 36 + import { logger } from '#/logger'; 37 + ``` 38 + 39 + ### `logger.error` 40 + 41 + The `error` level is for... well, errors. These are sent to Sentry in production mode. 42 + 43 + `error`, along with all log levels, supports an additional parameter, `metadata: Record<string, unknown>`. Use this to provide values to the [Sentry 44 + breadcrumb](https://docs.sentry.io/platforms/react-native/enriching-events/breadcrumbs/#manual-breadcrumbs). 45 + 46 + ```typescript 47 + try { 48 + // some async code 49 + } catch (e) { 50 + logger.error(e, { ...metadata }); 51 + } 52 + ``` 53 + 54 + ### `logger.warn` 55 + 56 + Warnings will be sent to Sentry as a separate Issue with level `warning`, as 57 + well as as breadcrumbs, with a severity level of `warning` 58 + 59 + ### `logger.log` 60 + 61 + Logs with level `log` will be sent to Sentry as a separate Issue with level `log`, as 62 + well as as breadcrumbs, with a severity level of `default`. 63 + 64 + ### `logger.info` 65 + 66 + The `info` level should be used for information that would be helpful in a 67 + tracing context, like Sentry. In production mode, `info` logs are sent 68 + to Sentry as breadcrumbs, which decorate log levels above `info` such as `log`, 69 + `warn`, and `error`. 70 + 71 + ### `logger.debug` 72 + 73 + Debug level is really only intended for local development. Use this instead of 74 + `console.log`. 75 + 76 + ```typescript 77 + logger.debug(message, { ...metadata }); 78 + ``` 79 + 80 + Inspired by [debug](https://www.npmjs.com/package/debug), when writing debug 81 + logs, you can optionally pass a _context_, which can be then filtered when in 82 + debug mode. 83 + 84 + This value should be related to the feature, component, or screen 85 + the code is running within, and **it should be defined in `#/logger/debugContext`**. 86 + This way we know if a relevant context already exists, and we can trace all 87 + active contexts in use in our app. This const enum is conveniently available on 88 + the `logger` at `logger.DebugContext`. 89 + 90 + For example, a debug log like this: 91 + 92 + ```typescript 93 + logger.debug(message, {}, logger.DebugContext.composer); 94 + ``` 95 + 96 + Would be logged to the console in dev mode if `EXPO_PUBLIC_LOG_LEVEL=debug`, _or_ if you 97 + pass a separate environment variable `LOG_DEBUG=composer`. This variable supports 98 + multiple contexts using commas like `LOG_DEBUG=composer,profile`, and _automatically 99 + sets the log level to `debug`, regardless of `EXPO_PUBLIC_LOG_LEVEL`._
+36
src/logger/__tests__/logDump.test.ts
··· 1 + import {expect, test} from '@jest/globals' 2 + 3 + import {ConsoleTransportEntry, LogLevel} from '#/logger' 4 + import {add, getEntries} from '#/logger/logDump' 5 + 6 + test('works', () => { 7 + const items: ConsoleTransportEntry[] = [ 8 + { 9 + id: '1', 10 + level: LogLevel.Debug, 11 + message: 'hello', 12 + metadata: {}, 13 + timestamp: Date.now(), 14 + }, 15 + { 16 + id: '2', 17 + level: LogLevel.Debug, 18 + message: 'hello', 19 + metadata: {}, 20 + timestamp: Date.now(), 21 + }, 22 + { 23 + id: '3', 24 + level: LogLevel.Debug, 25 + message: 'hello', 26 + metadata: {}, 27 + timestamp: Date.now(), 28 + }, 29 + ] 30 + 31 + for (const item of items) { 32 + add(item) 33 + } 34 + 35 + expect(getEntries()).toEqual(items.reverse()) 36 + })
+424
src/logger/__tests__/logger.test.ts
··· 1 + import {nanoid} from 'nanoid/non-secure' 2 + import {jest, describe, expect, test, beforeAll} from '@jest/globals' 3 + import {Native as Sentry} from 'sentry-expo' 4 + 5 + import {Logger, LogLevel, sentryTransport} from '#/logger' 6 + 7 + jest.mock('#/env', () => ({ 8 + IS_TEST: true, 9 + IS_DEV: false, 10 + IS_PROD: false, 11 + /* 12 + * Forces debug mode for tests using the default logger. Most tests create 13 + * their own logger instance. 14 + */ 15 + LOG_LEVEL: 'debug', 16 + LOG_DEBUG: '', 17 + })) 18 + 19 + jest.mock('sentry-expo', () => ({ 20 + Native: { 21 + addBreadcrumb: jest.fn(), 22 + captureException: jest.fn(), 23 + captureMessage: jest.fn(), 24 + }, 25 + })) 26 + 27 + beforeAll(() => { 28 + jest.useFakeTimers() 29 + }) 30 + 31 + describe('general functionality', () => { 32 + test('default params', () => { 33 + const logger = new Logger() 34 + expect(logger.enabled).toBeFalsy() 35 + expect(logger.level).toEqual(LogLevel.Debug) // mocked above 36 + }) 37 + 38 + test('can override default params', () => { 39 + const logger = new Logger({ 40 + enabled: true, 41 + level: LogLevel.Info, 42 + }) 43 + expect(logger.enabled).toBeTruthy() 44 + expect(logger.level).toEqual(LogLevel.Info) 45 + }) 46 + 47 + test('disabled logger does not report', () => { 48 + const logger = new Logger({ 49 + enabled: false, 50 + level: LogLevel.Debug, 51 + }) 52 + 53 + const mockTransport = jest.fn() 54 + 55 + logger.addTransport(mockTransport) 56 + logger.debug('message') 57 + 58 + expect(mockTransport).not.toHaveBeenCalled() 59 + }) 60 + 61 + test('disablement', () => { 62 + const logger = new Logger({ 63 + enabled: true, 64 + level: LogLevel.Debug, 65 + }) 66 + 67 + logger.disable() 68 + 69 + const mockTransport = jest.fn() 70 + 71 + logger.addTransport(mockTransport) 72 + logger.debug('message') 73 + 74 + expect(mockTransport).not.toHaveBeenCalled() 75 + }) 76 + 77 + test('passing debug contexts automatically enables debug mode', () => { 78 + const logger = new Logger({debug: 'specific'}) 79 + expect(logger.level).toEqual(LogLevel.Debug) 80 + }) 81 + 82 + test('supports extra metadata', () => { 83 + const timestamp = Date.now() 84 + const logger = new Logger({enabled: true}) 85 + 86 + const mockTransport = jest.fn() 87 + 88 + logger.addTransport(mockTransport) 89 + 90 + const extra = {foo: true} 91 + logger.warn('message', extra) 92 + 93 + expect(mockTransport).toHaveBeenCalledWith( 94 + LogLevel.Warn, 95 + 'message', 96 + extra, 97 + timestamp, 98 + ) 99 + }) 100 + 101 + test('supports nullish/falsy metadata', () => { 102 + const timestamp = Date.now() 103 + const logger = new Logger({enabled: true}) 104 + 105 + const mockTransport = jest.fn() 106 + 107 + const remove = logger.addTransport(mockTransport) 108 + 109 + // @ts-expect-error testing the JS case 110 + logger.warn('a', null) 111 + expect(mockTransport).toHaveBeenCalledWith( 112 + LogLevel.Warn, 113 + 'a', 114 + {}, 115 + timestamp, 116 + ) 117 + 118 + // @ts-expect-error testing the JS case 119 + logger.warn('b', false) 120 + expect(mockTransport).toHaveBeenCalledWith( 121 + LogLevel.Warn, 122 + 'b', 123 + {}, 124 + timestamp, 125 + ) 126 + 127 + // @ts-expect-error testing the JS case 128 + logger.warn('c', 0) 129 + expect(mockTransport).toHaveBeenCalledWith( 130 + LogLevel.Warn, 131 + 'c', 132 + {}, 133 + timestamp, 134 + ) 135 + 136 + remove() 137 + 138 + logger.addTransport((level, message, metadata) => { 139 + expect(typeof metadata).toEqual('object') 140 + }) 141 + 142 + // @ts-expect-error testing the JS case 143 + logger.warn('message', null) 144 + }) 145 + 146 + test('sentryTransport', () => { 147 + const message = 'message' 148 + const timestamp = Date.now() 149 + const sentryTimestamp = timestamp / 1000 150 + 151 + sentryTransport(LogLevel.Debug, message, {}, timestamp) 152 + expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({ 153 + message, 154 + data: {}, 155 + type: 'default', 156 + level: LogLevel.Debug, 157 + timestamp: sentryTimestamp, 158 + }) 159 + 160 + sentryTransport( 161 + LogLevel.Info, 162 + message, 163 + {type: 'info', prop: true}, 164 + timestamp, 165 + ) 166 + expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({ 167 + message, 168 + data: {prop: true}, 169 + type: 'info', 170 + level: LogLevel.Info, 171 + timestamp: sentryTimestamp, 172 + }) 173 + 174 + sentryTransport(LogLevel.Log, message, {}, timestamp) 175 + expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({ 176 + message, 177 + data: {}, 178 + type: 'default', 179 + level: 'debug', // Sentry bug, log becomes debug 180 + timestamp: sentryTimestamp, 181 + }) 182 + expect(Sentry.captureMessage).toHaveBeenCalledWith(message, { 183 + level: 'log', 184 + tags: undefined, 185 + extra: {}, 186 + }) 187 + 188 + sentryTransport(LogLevel.Warn, message, {}, timestamp) 189 + expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({ 190 + message, 191 + data: {}, 192 + type: 'default', 193 + level: 'warning', 194 + timestamp: sentryTimestamp, 195 + }) 196 + expect(Sentry.captureMessage).toHaveBeenCalledWith(message, { 197 + level: 'warning', 198 + tags: undefined, 199 + extra: {}, 200 + }) 201 + 202 + const e = new Error('error') 203 + const tags = { 204 + prop: 'prop', 205 + } 206 + 207 + sentryTransport( 208 + LogLevel.Error, 209 + e, 210 + { 211 + tags, 212 + prop: true, 213 + }, 214 + timestamp, 215 + ) 216 + 217 + expect(Sentry.captureException).toHaveBeenCalledWith(e, { 218 + tags, 219 + extra: { 220 + prop: true, 221 + }, 222 + }) 223 + }) 224 + 225 + test('add/remove transport', () => { 226 + const timestamp = Date.now() 227 + const logger = new Logger({enabled: true}) 228 + const mockTransport = jest.fn() 229 + 230 + const remove = logger.addTransport(mockTransport) 231 + 232 + logger.warn('warn') 233 + 234 + remove() 235 + 236 + logger.warn('warn') 237 + 238 + // only called once bc it was removed 239 + expect(mockTransport).toHaveBeenNthCalledWith( 240 + 1, 241 + LogLevel.Warn, 242 + 'warn', 243 + {}, 244 + timestamp, 245 + ) 246 + }) 247 + }) 248 + 249 + describe('debug contexts', () => { 250 + const mockTransport = jest.fn() 251 + 252 + test('specific', () => { 253 + const timestamp = Date.now() 254 + const message = nanoid() 255 + const logger = new Logger({ 256 + enabled: true, 257 + debug: 'specific', 258 + }) 259 + 260 + logger.addTransport(mockTransport) 261 + logger.debug(message, {}, 'specific') 262 + 263 + expect(mockTransport).toHaveBeenCalledWith( 264 + LogLevel.Debug, 265 + message, 266 + {}, 267 + timestamp, 268 + ) 269 + }) 270 + 271 + test('namespaced', () => { 272 + const timestamp = Date.now() 273 + const message = nanoid() 274 + const logger = new Logger({ 275 + enabled: true, 276 + debug: 'namespace*', 277 + }) 278 + 279 + logger.addTransport(mockTransport) 280 + logger.debug(message, {}, 'namespace') 281 + 282 + expect(mockTransport).toHaveBeenCalledWith( 283 + LogLevel.Debug, 284 + message, 285 + {}, 286 + timestamp, 287 + ) 288 + }) 289 + 290 + test('ignores inactive', () => { 291 + const timestamp = Date.now() 292 + const message = nanoid() 293 + const logger = new Logger({ 294 + enabled: true, 295 + debug: 'namespace:foo:*', 296 + }) 297 + 298 + logger.addTransport(mockTransport) 299 + logger.debug(message, {}, 'namespace:bar:baz') 300 + 301 + expect(mockTransport).not.toHaveBeenCalledWith( 302 + LogLevel.Debug, 303 + message, 304 + {}, 305 + timestamp, 306 + ) 307 + }) 308 + }) 309 + 310 + describe('supports levels', () => { 311 + test('debug', () => { 312 + const timestamp = Date.now() 313 + const logger = new Logger({ 314 + enabled: true, 315 + level: LogLevel.Debug, 316 + }) 317 + const message = nanoid() 318 + const mockTransport = jest.fn() 319 + 320 + logger.addTransport(mockTransport) 321 + 322 + logger.debug(message) 323 + expect(mockTransport).toHaveBeenCalledWith( 324 + LogLevel.Debug, 325 + message, 326 + {}, 327 + timestamp, 328 + ) 329 + 330 + logger.info(message) 331 + expect(mockTransport).toHaveBeenCalledWith( 332 + LogLevel.Info, 333 + message, 334 + {}, 335 + timestamp, 336 + ) 337 + 338 + logger.warn(message) 339 + expect(mockTransport).toHaveBeenCalledWith( 340 + LogLevel.Warn, 341 + message, 342 + {}, 343 + timestamp, 344 + ) 345 + 346 + const e = new Error(message) 347 + logger.error(e) 348 + expect(mockTransport).toHaveBeenCalledWith(LogLevel.Error, e, {}, timestamp) 349 + }) 350 + 351 + test('info', () => { 352 + const timestamp = Date.now() 353 + const logger = new Logger({ 354 + enabled: true, 355 + level: LogLevel.Info, 356 + }) 357 + const message = nanoid() 358 + const mockTransport = jest.fn() 359 + 360 + logger.addTransport(mockTransport) 361 + 362 + logger.debug(message) 363 + expect(mockTransport).not.toHaveBeenCalled() 364 + 365 + logger.info(message) 366 + expect(mockTransport).toHaveBeenCalledWith( 367 + LogLevel.Info, 368 + message, 369 + {}, 370 + timestamp, 371 + ) 372 + }) 373 + 374 + test('warn', () => { 375 + const timestamp = Date.now() 376 + const logger = new Logger({ 377 + enabled: true, 378 + level: LogLevel.Warn, 379 + }) 380 + const message = nanoid() 381 + const mockTransport = jest.fn() 382 + 383 + logger.addTransport(mockTransport) 384 + 385 + logger.debug(message) 386 + expect(mockTransport).not.toHaveBeenCalled() 387 + 388 + logger.info(message) 389 + expect(mockTransport).not.toHaveBeenCalled() 390 + 391 + logger.warn(message) 392 + expect(mockTransport).toHaveBeenCalledWith( 393 + LogLevel.Warn, 394 + message, 395 + {}, 396 + timestamp, 397 + ) 398 + }) 399 + 400 + test('error', () => { 401 + const timestamp = Date.now() 402 + const logger = new Logger({ 403 + enabled: true, 404 + level: LogLevel.Error, 405 + }) 406 + const message = nanoid() 407 + const mockTransport = jest.fn() 408 + 409 + logger.addTransport(mockTransport) 410 + 411 + logger.debug(message) 412 + expect(mockTransport).not.toHaveBeenCalled() 413 + 414 + logger.info(message) 415 + expect(mockTransport).not.toHaveBeenCalled() 416 + 417 + logger.warn(message) 418 + expect(mockTransport).not.toHaveBeenCalled() 419 + 420 + const e = new Error('original message') 421 + logger.error(e) 422 + expect(mockTransport).toHaveBeenCalledWith(LogLevel.Error, e, {}, timestamp) 423 + }) 424 + })
+10
src/logger/debugContext.ts
··· 1 + /** 2 + * *Do not import this directly.* Instead, use the shortcut reference `logger.DebugContext`. 3 + * 4 + * Add debug contexts here. Although convention typically calls for enums ito 5 + * be capitalized, for parity with the `LOG_DEBUG` env var, please use all 6 + * lowercase. 7 + */ 8 + export const DebugContext = { 9 + // e.g. composer: 'composer' 10 + } as const
+290
src/logger/index.ts
··· 1 + import format from 'date-fns/format' 2 + import {nanoid} from 'nanoid/non-secure' 3 + 4 + import {Sentry} from '#/logger/sentry' 5 + import * as env from '#/env' 6 + import {DebugContext} from '#/logger/debugContext' 7 + import {add} from '#/logger/logDump' 8 + 9 + export enum LogLevel { 10 + Debug = 'debug', 11 + Info = 'info', 12 + Log = 'log', 13 + Warn = 'warn', 14 + Error = 'error', 15 + } 16 + 17 + type Transport = ( 18 + level: LogLevel, 19 + message: string | Error, 20 + metadata: Metadata, 21 + timestamp: number, 22 + ) => void 23 + 24 + /** 25 + * A union of some of Sentry's breadcrumb properties as well as Sentry's 26 + * `captureException` parameter, `CaptureContext`. 27 + */ 28 + type Metadata = { 29 + /** 30 + * Applied as Sentry breadcrumb types. Defaults to `default`. 31 + * 32 + * @see https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types 33 + */ 34 + type?: 35 + | 'default' 36 + | 'debug' 37 + | 'error' 38 + | 'navigation' 39 + | 'http' 40 + | 'info' 41 + | 'query' 42 + | 'transaction' 43 + | 'ui' 44 + | 'user' 45 + 46 + /** 47 + * Passed through to `Sentry.captureException` 48 + * 49 + * @see https://github.com/getsentry/sentry-javascript/blob/903addf9a1a1534a6cb2ba3143654b918a86f6dd/packages/types/src/misc.ts#L65 50 + */ 51 + tags?: { 52 + [key: string]: 53 + | number 54 + | string 55 + | boolean 56 + | bigint 57 + | symbol 58 + | null 59 + | undefined 60 + } 61 + 62 + /** 63 + * Any additional data, passed through to Sentry as `extra` param on 64 + * exceptions, or the `data` param on breadcrumbs. 65 + */ 66 + [key: string]: unknown 67 + } & Parameters<typeof Sentry.captureException>[1] 68 + 69 + export type ConsoleTransportEntry = { 70 + id: string 71 + timestamp: number 72 + level: LogLevel 73 + message: string | Error 74 + metadata: Metadata 75 + } 76 + 77 + const enabledLogLevels: { 78 + [key in LogLevel]: LogLevel[] 79 + } = { 80 + [LogLevel.Debug]: [ 81 + LogLevel.Debug, 82 + LogLevel.Info, 83 + LogLevel.Log, 84 + LogLevel.Warn, 85 + LogLevel.Error, 86 + ], 87 + [LogLevel.Info]: [LogLevel.Info, LogLevel.Log, LogLevel.Warn, LogLevel.Error], 88 + [LogLevel.Log]: [LogLevel.Log, LogLevel.Warn, LogLevel.Error], 89 + [LogLevel.Warn]: [LogLevel.Warn, LogLevel.Error], 90 + [LogLevel.Error]: [LogLevel.Error], 91 + } 92 + 93 + /** 94 + * Used in dev mode to nicely log to the console 95 + */ 96 + export const consoleTransport: Transport = ( 97 + level, 98 + message, 99 + metadata, 100 + timestamp, 101 + ) => { 102 + const extra = Object.keys(metadata).length 103 + ? ' ' + JSON.stringify(metadata, null, ' ') 104 + : '' 105 + const log = { 106 + [LogLevel.Debug]: console.debug, 107 + [LogLevel.Info]: console.info, 108 + [LogLevel.Log]: console.log, 109 + [LogLevel.Warn]: console.warn, 110 + [LogLevel.Error]: console.error, 111 + }[level] 112 + 113 + log(`${format(timestamp, 'HH:mm:ss')} ${message.toString()}${extra}`) 114 + } 115 + 116 + export const sentryTransport: Transport = ( 117 + level, 118 + message, 119 + {type, tags, ...metadata}, 120 + timestamp, 121 + ) => { 122 + /** 123 + * If a string, report a breadcrumb 124 + */ 125 + if (typeof message === 'string') { 126 + const severity = ( 127 + { 128 + [LogLevel.Debug]: 'debug', 129 + [LogLevel.Info]: 'info', 130 + [LogLevel.Log]: 'log', // Sentry value here is undefined 131 + [LogLevel.Warn]: 'warning', 132 + [LogLevel.Error]: 'error', 133 + } as const 134 + )[level] 135 + 136 + Sentry.addBreadcrumb({ 137 + message, 138 + data: metadata, 139 + type: type || 'default', 140 + level: severity, 141 + timestamp: timestamp / 1000, // Sentry expects seconds 142 + }) 143 + 144 + /** 145 + * Send all higher levels with `captureMessage`, with appropriate severity 146 + * level 147 + */ 148 + if (level === 'error' || level === 'warn' || level === 'log') { 149 + const messageLevel = ({ 150 + [LogLevel.Log]: 'log', 151 + [LogLevel.Warn]: 'warning', 152 + [LogLevel.Error]: 'error', 153 + }[level] || 'log') as Sentry.Breadcrumb['level'] 154 + 155 + Sentry.captureMessage(message, { 156 + level: messageLevel, 157 + tags, 158 + extra: metadata, 159 + }) 160 + } 161 + } else { 162 + /** 163 + * It's otherwise an Error and should be reported with captureException 164 + */ 165 + Sentry.captureException(message, { 166 + tags, 167 + extra: metadata, 168 + }) 169 + } 170 + } 171 + 172 + /** 173 + * Main class. Defaults are provided in the constructor so that subclasses are 174 + * technically possible, if we need to go that route in the future. 175 + */ 176 + export class Logger { 177 + LogLevel = LogLevel 178 + DebugContext = DebugContext 179 + 180 + enabled: boolean 181 + level: LogLevel 182 + transports: Transport[] = [] 183 + 184 + protected debugContextRegexes: RegExp[] = [] 185 + 186 + constructor({ 187 + enabled = !env.IS_TEST, 188 + level = env.LOG_LEVEL as LogLevel, 189 + debug = env.LOG_DEBUG || '', 190 + }: { 191 + enabled?: boolean 192 + level?: LogLevel 193 + debug?: string 194 + } = {}) { 195 + this.enabled = enabled !== false 196 + this.level = debug ? LogLevel.Debug : level ?? LogLevel.Info // default to info 197 + this.debugContextRegexes = (debug || '').split(',').map(context => { 198 + return new RegExp(context.replace(/[^\w:*]/, '').replace(/\*/g, '.*')) 199 + }) 200 + } 201 + 202 + debug(message: string, metadata: Metadata = {}, context?: string) { 203 + if (context && !this.debugContextRegexes.find(reg => reg.test(context))) 204 + return 205 + this.transport(LogLevel.Debug, message, metadata) 206 + } 207 + 208 + info(message: string, metadata: Metadata = {}) { 209 + this.transport(LogLevel.Info, message, metadata) 210 + } 211 + 212 + log(message: string, metadata: Metadata = {}) { 213 + this.transport(LogLevel.Log, message, metadata) 214 + } 215 + 216 + warn(message: string, metadata: Metadata = {}) { 217 + this.transport(LogLevel.Warn, message, metadata) 218 + } 219 + 220 + error(error: Error | string, metadata: Metadata = {}) { 221 + this.transport(LogLevel.Error, error, metadata) 222 + } 223 + 224 + addTransport(transport: Transport) { 225 + this.transports.push(transport) 226 + return () => { 227 + this.transports.splice(this.transports.indexOf(transport), 1) 228 + } 229 + } 230 + 231 + disable() { 232 + this.enabled = false 233 + } 234 + 235 + enable() { 236 + this.enabled = true 237 + } 238 + 239 + protected transport( 240 + level: LogLevel, 241 + message: string | Error, 242 + metadata: Metadata = {}, 243 + ) { 244 + if (!this.enabled) return 245 + if (!enabledLogLevels[this.level].includes(level)) return 246 + 247 + const timestamp = Date.now() 248 + const meta = metadata || {} 249 + 250 + for (const transport of this.transports) { 251 + transport(level, message, meta, timestamp) 252 + } 253 + 254 + add({ 255 + id: nanoid(), 256 + timestamp, 257 + level, 258 + message, 259 + metadata: meta, 260 + }) 261 + } 262 + } 263 + 264 + /** 265 + * Logger instance. See `@/logger/README` for docs. 266 + * 267 + * Basic usage: 268 + * 269 + * `logger.debug(message[, metadata, debugContext])` 270 + * `logger.info(message[, metadata])` 271 + * `logger.warn(message[, metadata])` 272 + * `logger.error(error[, metadata])` 273 + * `logger.disable()` 274 + * `logger.enable()` 275 + */ 276 + export const logger = new Logger() 277 + 278 + /** 279 + * Report to console in dev, Sentry in prod, nothing in test. 280 + */ 281 + if (env.IS_DEV && !env.IS_TEST) { 282 + logger.addTransport(consoleTransport) 283 + 284 + /** 285 + * Uncomment this to test Sentry in dev 286 + */ 287 + // logger.addTransport(sentryTransport); 288 + } else if (env.IS_PROD) { 289 + // logger.addTransport(sentryTransport) 290 + }
+12
src/logger/logDump.ts
··· 1 + import type {ConsoleTransportEntry} from '#/logger' 2 + 3 + let entries: ConsoleTransportEntry[] = [] 4 + 5 + export function add(entry: ConsoleTransportEntry) { 6 + entries.unshift(entry) 7 + entries = entries.slice(0, 50) 8 + } 9 + 10 + export function getEntries() { 11 + return entries 12 + }
+1
src/logger/sentry/index.ts
··· 1 + export {Native as Sentry} from 'sentry-expo'
+1
src/logger/sentry/index.web.ts
··· 1 + export {Browser as Sentry} from 'sentry-expo'
+1 -1
src/state/index.ts
··· 25 25 rootStore.log.debug('Initial hydrate', {hasSession: !!data.session}) 26 26 rootStore.hydrate(data) 27 27 } catch (e: any) { 28 - rootStore.log.error('Failed to load state from storage', e) 28 + rootStore.log.error('Failed to load state from storage', {error: e}) 29 29 } 30 30 rootStore.attemptSessionResumption() 31 31
+5 -5
src/state/models/content/feed-source.ts
··· 134 134 try { 135 135 await this.rootStore.preferences.addSavedFeed(this.uri) 136 136 } catch (error) { 137 - this.rootStore.log.error('Failed to save feed', error) 137 + this.rootStore.log.error('Failed to save feed', {error}) 138 138 } finally { 139 139 track('CustomFeed:Save') 140 140 } ··· 147 147 try { 148 148 await this.rootStore.preferences.removeSavedFeed(this.uri) 149 149 } catch (error) { 150 - this.rootStore.log.error('Failed to unsave feed', error) 150 + this.rootStore.log.error('Failed to unsave feed', {error}) 151 151 } finally { 152 152 track('CustomFeed:Unsave') 153 153 } ··· 157 157 try { 158 158 await this.rootStore.preferences.addPinnedFeed(this.uri) 159 159 } catch (error) { 160 - this.rootStore.log.error('Failed to pin feed', error) 160 + this.rootStore.log.error('Failed to pin feed', {error}) 161 161 } finally { 162 162 track('CustomFeed:Pin', { 163 163 name: this.displayName, ··· 194 194 } catch (e: any) { 195 195 this.likeUri = undefined 196 196 this.likeCount = (this.likeCount || 1) - 1 197 - this.rootStore.log.error('Failed to like feed', e) 197 + this.rootStore.log.error('Failed to like feed', {error: e}) 198 198 } finally { 199 199 track('CustomFeed:Like') 200 200 } ··· 215 215 } catch (e: any) { 216 216 this.likeUri = uri 217 217 this.likeCount = (this.likeCount || 0) + 1 218 - this.rootStore.log.error('Failed to unlike feed', e) 218 + this.rootStore.log.error('Failed to unlike feed', {error: e}) 219 219 } finally { 220 220 track('CustomFeed:Unlike') 221 221 }
+5 -3
src/state/models/content/list.ts
··· 339 339 try { 340 340 await this.rootStore.preferences.addPinnedFeed(this.uri) 341 341 } catch (error) { 342 - this.rootStore.log.error('Failed to pin feed', error) 342 + this.rootStore.log.error('Failed to pin feed', {error}) 343 343 } finally { 344 344 track('CustomFeed:Pin', { 345 345 name: this.data?.name || '', ··· 455 455 this.error = cleanError(err) 456 456 this.loadMoreError = cleanError(loadMoreErr) 457 457 if (err) { 458 - this.rootStore.log.error('Failed to fetch user items', err) 458 + this.rootStore.log.error('Failed to fetch user items', {error: err}) 459 459 } 460 460 if (loadMoreErr) { 461 - this.rootStore.log.error('Failed to fetch user items', loadMoreErr) 461 + this.rootStore.log.error('Failed to fetch user items', { 462 + error: loadMoreErr, 463 + }) 462 464 } 463 465 } 464 466
+1 -1
src/state/models/content/post-thread.ts
··· 163 163 this.hasLoaded = true 164 164 this.error = cleanError(err) 165 165 if (err) { 166 - this.rootStore.log.error('Failed to fetch post thread', err) 166 + this.rootStore.log.error('Failed to fetch post thread', {error: err}) 167 167 } 168 168 this.notFound = err instanceof GetPostThread.NotFoundError 169 169 }
+1 -1
src/state/models/content/profile.ts
··· 235 235 this.hasLoaded = true 236 236 this.error = cleanError(err) 237 237 if (err) { 238 - this.rootStore.log.error('Failed to fetch profile', err) 238 + this.rootStore.log.error('Failed to fetch profile', {error: err}) 239 239 } 240 240 } 241 241
+1 -1
src/state/models/discovery/feeds.ts
··· 120 120 this.hasLoaded = true 121 121 this.error = cleanError(err) 122 122 if (err) { 123 - this.rootStore.log.error('Failed to fetch popular feeds', err) 123 + this.rootStore.log.error('Failed to fetch popular feeds', {error: err}) 124 124 } 125 125 } 126 126
+1 -1
src/state/models/discovery/suggested-actors.ts
··· 144 144 this.hasLoaded = true 145 145 this.error = cleanError(err) 146 146 if (err) { 147 - this.rootStore.log.error('Failed to fetch suggested actors', err) 147 + this.rootStore.log.error('Failed to fetch suggested actors', {error: err}) 148 148 } 149 149 } 150 150 }
+11 -8
src/state/models/feeds/notifications.ts
··· 220 220 } 221 221 this.rootStore.log.warn( 222 222 'app.bsky.notifications.list served an unsupported record type', 223 - v, 223 + {record: v}, 224 224 ) 225 225 } 226 226 ··· 401 401 this._setQueued(this._filterNotifications(queueModels)) 402 402 this._countUnread() 403 403 } catch (e) { 404 - this.rootStore.log.error('NotificationsModel:syncQueue failed', {e}) 404 + this.rootStore.log.error('NotificationsModel:syncQueue failed', { 405 + error: e, 406 + }) 405 407 } finally { 406 408 this.lock.release() 407 409 } ··· 481 483 this.lastSync ? this.lastSync.toISOString() : undefined, 482 484 ) 483 485 } catch (e: any) { 484 - this.rootStore.log.warn('Failed to update notifications read state', e) 486 + this.rootStore.log.warn('Failed to update notifications read state', { 487 + error: e, 488 + }) 485 489 } 486 490 } 487 491 ··· 501 505 this.error = cleanError(error) 502 506 this.loadMoreError = cleanError(loadMoreError) 503 507 if (error) { 504 - this.rootStore.log.error('Failed to fetch notifications', error) 508 + this.rootStore.log.error('Failed to fetch notifications', {error}) 505 509 } 506 510 if (loadMoreError) { 507 - this.rootStore.log.error( 508 - 'Failed to load more notifications', 509 - loadMoreError, 510 - ) 511 + this.rootStore.log.error('Failed to load more notifications', { 512 + error: loadMoreError, 513 + }) 511 514 } 512 515 } 513 516
+8 -9
src/state/models/feeds/post.ts
··· 42 42 } else { 43 43 this.postRecord = undefined 44 44 this.richText = undefined 45 - rootStore.log.warn( 46 - 'Received an invalid app.bsky.feed.post record', 47 - valid.error, 48 - ) 45 + rootStore.log.warn('Received an invalid app.bsky.feed.post record', { 46 + error: valid.error, 47 + }) 49 48 } 50 49 } else { 51 50 this.postRecord = undefined 52 51 this.richText = undefined 53 52 rootStore.log.warn( 54 53 'app.bsky.feed.getTimeline or app.bsky.feed.getAuthorFeed served an unexpected record type', 55 - this.post.record, 54 + {record: this.post.record}, 56 55 ) 57 56 } 58 57 this.reply = v.reply ··· 133 132 track('Post:Like') 134 133 } 135 134 } catch (error) { 136 - this.rootStore.log.error('Failed to toggle like', error) 135 + this.rootStore.log.error('Failed to toggle like', {error}) 137 136 } 138 137 } 139 138 ··· 168 167 track('Post:Repost') 169 168 } 170 169 } catch (error) { 171 - this.rootStore.log.error('Failed to toggle repost', error) 170 + this.rootStore.log.error('Failed to toggle repost', {error}) 172 171 } 173 172 } 174 173 ··· 182 181 track('Post:ThreadMute') 183 182 } 184 183 } catch (error) { 185 - this.rootStore.log.error('Failed to toggle thread mute', error) 184 + this.rootStore.log.error('Failed to toggle thread mute', {error}) 186 185 } 187 186 } 188 187 ··· 191 190 await this.rootStore.agent.deletePost(this.post.uri) 192 191 this.rootStore.emitPostDeleted(this.post.uri) 193 192 } catch (error) { 194 - this.rootStore.log.error('Failed to delete post', error) 193 + this.rootStore.log.error('Failed to delete post', {error}) 195 194 } finally { 196 195 track('Post:Delete') 197 196 }
+4 -5
src/state/models/feeds/posts.ts
··· 324 324 this.knownError = detectKnownError(this.feedType, error) 325 325 this.loadMoreError = cleanError(loadMoreError) 326 326 if (error) { 327 - this.rootStore.log.error('Posts feed request failed', error) 327 + this.rootStore.log.error('Posts feed request failed', {error}) 328 328 } 329 329 if (loadMoreError) { 330 - this.rootStore.log.error( 331 - 'Posts feed load-more request failed', 332 - loadMoreError, 333 - ) 330 + this.rootStore.log.error('Posts feed load-more request failed', { 331 + error: loadMoreError, 332 + }) 334 333 } 335 334 } 336 335
+3 -4
src/state/models/invited-users.ts
··· 63 63 }) 64 64 this.rootStore.me.follows.hydrateMany(this.profiles) 65 65 } catch (e) { 66 - this.rootStore.log.error( 67 - 'Failed to fetch profiles for invited users', 68 - e, 69 - ) 66 + this.rootStore.log.error('Failed to fetch profiles for invited users', { 67 + error: e, 68 + }) 70 69 } 71 70 } 72 71 }
+1 -1
src/state/models/lists/actor-feeds.ts
··· 98 98 this.hasLoaded = true 99 99 this.error = cleanError(err) 100 100 if (err) { 101 - this.rootStore.log.error('Failed to fetch user followers', err) 101 + this.rootStore.log.error('Failed to fetch user followers', {error: err}) 102 102 } 103 103 } 104 104
+1 -1
src/state/models/lists/blocked-accounts.ts
··· 86 86 this.hasLoaded = true 87 87 this.error = cleanError(err) 88 88 if (err) { 89 - this.rootStore.log.error('Failed to fetch user followers', err) 89 + this.rootStore.log.error('Failed to fetch user followers', {error: err}) 90 90 } 91 91 } 92 92
+1 -1
src/state/models/lists/likes.ts
··· 97 97 this.hasLoaded = true 98 98 this.error = cleanError(err) 99 99 if (err) { 100 - this.rootStore.log.error('Failed to fetch likes', err) 100 + this.rootStore.log.error('Failed to fetch likes', {error: err}) 101 101 } 102 102 } 103 103
+4 -2
src/state/models/lists/lists-list.ts
··· 204 204 this.error = cleanError(err) 205 205 this.loadMoreError = cleanError(loadMoreErr) 206 206 if (err) { 207 - this.rootStore.log.error('Failed to fetch user lists', err) 207 + this.rootStore.log.error('Failed to fetch user lists', {error: err}) 208 208 } 209 209 if (loadMoreErr) { 210 - this.rootStore.log.error('Failed to fetch user lists', loadMoreErr) 210 + this.rootStore.log.error('Failed to fetch user lists', { 211 + error: loadMoreErr, 212 + }) 211 213 } 212 214 } 213 215
+1 -1
src/state/models/lists/muted-accounts.ts
··· 86 86 this.hasLoaded = true 87 87 this.error = cleanError(err) 88 88 if (err) { 89 - this.rootStore.log.error('Failed to fetch user followers', err) 89 + this.rootStore.log.error('Failed to fetch user followers', {error: err}) 90 90 } 91 91 } 92 92
+1 -1
src/state/models/lists/reposted-by.ts
··· 100 100 this.hasLoaded = true 101 101 this.error = cleanError(err) 102 102 if (err) { 103 - this.rootStore.log.error('Failed to fetch reposted by view', err) 103 + this.rootStore.log.error('Failed to fetch reposted by view', {error: err}) 104 104 } 105 105 } 106 106
+1 -1
src/state/models/lists/user-followers.ts
··· 99 99 this.hasLoaded = true 100 100 this.error = cleanError(err) 101 101 if (err) { 102 - this.rootStore.log.error('Failed to fetch user followers', err) 102 + this.rootStore.log.error('Failed to fetch user followers', {error: err}) 103 103 } 104 104 } 105 105
+15 -7
src/state/models/me.ts
··· 110 110 await this.fetchProfile() 111 111 this.mainFeed.clear() 112 112 /* dont await */ this.mainFeed.setup().catch(e => { 113 - this.rootStore.log.error('Failed to setup main feed model', e) 113 + this.rootStore.log.error('Failed to setup main feed model', {error: e}) 114 114 }) 115 115 /* dont await */ this.notifications.setup().catch(e => { 116 - this.rootStore.log.error('Failed to setup notifications model', e) 116 + this.rootStore.log.error('Failed to setup notifications model', { 117 + error: e, 118 + }) 117 119 }) 118 120 /* dont await */ this.notifications.setup().catch(e => { 119 - this.rootStore.log.error('Failed to setup notifications model', e) 121 + this.rootStore.log.error('Failed to setup notifications model', { 122 + error: e, 123 + }) 120 124 }) 121 125 this.myFeeds.clear() 122 126 /* dont await */ this.myFeeds.saved.refresh() ··· 184 188 }) 185 189 }) 186 190 } catch (e) { 187 - this.rootStore.log.error('Failed to fetch user invite codes', e) 191 + this.rootStore.log.error('Failed to fetch user invite codes', { 192 + error: e, 193 + }) 188 194 } 189 195 await this.rootStore.invitedUsers.fetch(this.invites) 190 196 } ··· 199 205 this.appPasswords = res.data.passwords 200 206 }) 201 207 } catch (e) { 202 - this.rootStore.log.error('Failed to fetch user app passwords', e) 208 + this.rootStore.log.error('Failed to fetch user app passwords', { 209 + error: e, 210 + }) 203 211 } 204 212 } 205 213 } ··· 220 228 }) 221 229 return res.data 222 230 } catch (e) { 223 - this.rootStore.log.error('Failed to create app password', e) 231 + this.rootStore.log.error('Failed to create app password', {error: e}) 224 232 } 225 233 } 226 234 } ··· 235 243 this.appPasswords = this.appPasswords.filter(p => p.name !== name) 236 244 }) 237 245 } catch (e) { 238 - this.rootStore.log.error('Failed to delete app password', e) 246 + this.rootStore.log.error('Failed to delete app password', {error: e}) 239 247 } 240 248 } 241 249 }
+1 -1
src/state/models/media/image.ts
··· 188 188 this.cropped = cropped 189 189 }) 190 190 } catch (err) { 191 - this.rootStore.log.error('Failed to crop photo', err) 191 + this.rootStore.log.error('Failed to crop photo', {error: err}) 192 192 } 193 193 } 194 194
+4 -4
src/state/models/root-store.ts
··· 8 8 import {DeviceEventEmitter, EmitterSubscription} from 'react-native' 9 9 import {z} from 'zod' 10 10 import {isObj, hasProp} from 'lib/type-guards' 11 - import {LogModel} from './log' 12 11 import {SessionModel} from './session' 13 12 import {ShellUiModel} from './ui/shell' 14 13 import {HandleResolutionsCache} from './cache/handle-resolutions' ··· 23 22 import {MutedThreads} from './muted-threads' 24 23 import {Reminders} from './ui/reminders' 25 24 import {reset as resetNavigation} from '../../Navigation' 25 + import {logger} from '#/logger' 26 26 27 27 // TEMPORARY (APP-700) 28 28 // remove after backend testing finishes ··· 41 41 export class RootStoreModel { 42 42 agent: BskyAgent 43 43 appInfo?: AppInfo 44 - log = new LogModel() 44 + log = logger 45 45 session = new SessionModel(this) 46 46 shell = new ShellUiModel(this) 47 47 preferences = new PreferencesModel(this) ··· 130 130 }) 131 131 this.updateSessionState() 132 132 } catch (e: any) { 133 - this.log.warn('Failed to initialize session', e) 133 + this.log.warn('Failed to initialize session', {error: e}) 134 134 } 135 135 } 136 136 ··· 184 184 await this.me.updateIfNeeded() 185 185 await this.preferences.sync() 186 186 } catch (e: any) { 187 - this.log.error('Failed to fetch latest state', e) 187 + this.log.error('Failed to fetch latest state', {error: e}) 188 188 } 189 189 } 190 190
+2 -2
src/state/models/ui/create-account.ts
··· 78 78 } catch (err: any) { 79 79 this.rootStore.log.warn( 80 80 `Failed to fetch service description for ${this.serviceUrl}`, 81 - err, 81 + {error: err}, 82 82 ) 83 83 this.setError( 84 84 'Unable to contact your service. Please check your Internet connection.', ··· 127 127 errMsg = 128 128 'Invite code not accepted. Check that you input it correctly and try again.' 129 129 } 130 - this.rootStore.log.error('Failed to create account', e) 130 + this.rootStore.log.error('Failed to create account', {error: e}) 131 131 this.setIsProcessing(false) 132 132 this.setError(cleanError(errMsg)) 133 133 throw e
+9 -3
src/state/models/ui/profile.ts
··· 223 223 await Promise.all([ 224 224 this.profile 225 225 .setup() 226 - .catch(err => this.rootStore.log.error('Failed to fetch profile', err)), 226 + .catch(err => 227 + this.rootStore.log.error('Failed to fetch profile', {error: err}), 228 + ), 227 229 this.feed 228 230 .setup() 229 - .catch(err => this.rootStore.log.error('Failed to fetch feed', err)), 231 + .catch(err => 232 + this.rootStore.log.error('Failed to fetch feed', {error: err}), 233 + ), 230 234 ]) 231 235 runInAction(() => { 232 236 this.isAuthenticatedUser = ··· 237 241 this.lists.source = this.profile.did 238 242 this.lists 239 243 .loadMore() 240 - .catch(err => this.rootStore.log.error('Failed to fetch lists', err)) 244 + .catch(err => 245 + this.rootStore.log.error('Failed to fetch lists', {error: err}), 246 + ) 241 247 } 242 248 243 249 async refresh() {
+1 -1
src/state/models/ui/saved-feeds.ts
··· 126 126 this.hasLoaded = true 127 127 this.error = cleanError(err) 128 128 if (err) { 129 - this.rootStore.log.error('Failed to fetch user feeds', err) 129 + this.rootStore.log.error('Failed to fetch user feeds', {err}) 130 130 } 131 131 } 132 132
+4 -4
src/view/com/auth/login/Login.tsx
··· 83 83 } 84 84 store.log.warn( 85 85 `Failed to fetch service description for ${serviceUrl}`, 86 - err, 86 + {error: err}, 87 87 ) 88 88 setError( 89 89 'Unable to contact your service. Please check your Internet connection.', ··· 349 349 }) 350 350 } catch (e: any) { 351 351 const errMsg = e.toString() 352 - store.log.warn('Failed to login', e) 352 + store.log.warn('Failed to login', {error: e}) 353 353 setIsProcessing(false) 354 354 if (errMsg.includes('Authentication Required')) { 355 355 setError('Invalid username or password') ··· 578 578 onEmailSent() 579 579 } catch (e: any) { 580 580 const errMsg = e.toString() 581 - store.log.warn('Failed to request password reset', e) 581 + store.log.warn('Failed to request password reset', {error: e}) 582 582 setIsProcessing(false) 583 583 if (isNetworkError(e)) { 584 584 setError( ··· 734 734 onPasswordSet() 735 735 } catch (e: any) { 736 736 const errMsg = e.toString() 737 - store.log.warn('Failed to set new password', e) 737 + store.log.warn('Failed to set new password', {error: e}) 738 738 setIsProcessing(false) 739 739 if (isNetworkError(e)) { 740 740 setError(
+1 -1
src/view/com/composer/photos/OpenCameraBtn.tsx
··· 39 39 gallery.add(img) 40 40 } catch (err: any) { 41 41 // ignore 42 - store.log.warn('Error using camera', err) 42 + store.log.warn('Error using camera', {error: err}) 43 43 } 44 44 }, [gallery, track, store, requestCameraAccessIfNeeded]) 45 45
+5 -3
src/view/com/composer/useExternalLinkFetch.ts
··· 46 46 setExtLink(undefined) 47 47 }, 48 48 err => { 49 - store.log.error('Failed to fetch post for quote embedding', {err}) 49 + store.log.error('Failed to fetch post for quote embedding', { 50 + error: err, 51 + }) 50 52 setExtLink(undefined) 51 53 }, 52 54 ) ··· 64 66 }) 65 67 }, 66 68 err => { 67 - store.log.error('Failed to fetch feed for embedding', {err}) 69 + store.log.error('Failed to fetch feed for embedding', {error: err}) 68 70 setExtLink(undefined) 69 71 }, 70 72 ) ··· 82 84 }) 83 85 }, 84 86 err => { 85 - store.log.error('Failed to fetch list for embedding', {err}) 87 + store.log.error('Failed to fetch list for embedding', {error: err}) 86 88 setExtLink(undefined) 87 89 }, 88 90 )
+2 -2
src/view/com/feeds/FeedSourceCard.tsx
··· 45 45 Toast.show('Removed from my feeds') 46 46 } catch (e) { 47 47 Toast.show('There was an issue contacting your server') 48 - store.log.error('Failed to unsave feed', {e}) 48 + store.log.error('Failed to unsave feed', {error: e}) 49 49 } 50 50 }, 51 51 }) ··· 55 55 Toast.show('Added to my feeds') 56 56 } catch (e) { 57 57 Toast.show('There was an issue contacting your server') 58 - store.log.error('Failed to save feed', {e}) 58 + store.log.error('Failed to save feed', {error: e}) 59 59 } 60 60 } 61 61 }, [store, item])
+2 -2
src/view/com/lists/ListItems.tsx
··· 94 94 try { 95 95 await list.refresh() 96 96 } catch (err) { 97 - list.rootStore.log.error('Failed to refresh lists', err) 97 + list.rootStore.log.error('Failed to refresh lists', {error: err}) 98 98 } 99 99 setIsRefreshing(false) 100 100 }, [list, track, setIsRefreshing]) ··· 104 104 try { 105 105 await list.loadMore() 106 106 } catch (err) { 107 - list.rootStore.log.error('Failed to load more lists', err) 107 + list.rootStore.log.error('Failed to load more lists', {error: err}) 108 108 } 109 109 }, [list, track]) 110 110
+2 -2
src/view/com/lists/ListsList.tsx
··· 78 78 try { 79 79 await listsList.refresh() 80 80 } catch (err) { 81 - listsList.rootStore.log.error('Failed to refresh lists', err) 81 + listsList.rootStore.log.error('Failed to refresh lists', {error: err}) 82 82 } 83 83 setIsRefreshing(false) 84 84 }, [listsList, track, setIsRefreshing]) ··· 88 88 try { 89 89 await listsList.loadMore() 90 90 } catch (err) { 91 - listsList.rootStore.log.error('Failed to load more lists', err) 91 + listsList.rootStore.log.error('Failed to load more lists', {error: err}) 92 92 } 93 93 }, [listsList, track]) 94 94
+1 -1
src/view/com/modals/AddAppPasswords.tsx
··· 95 95 } 96 96 } catch (e) { 97 97 Toast.show('Failed to create app password.') 98 - store.log.error('Failed to create app password', {e}) 98 + store.log.error('Failed to create app password', {error: e}) 99 99 } 100 100 } 101 101
+3 -3
src/view/com/modals/ChangeHandle.tsx
··· 69 69 `Failed to fetch service description for ${String( 70 70 store.agent.service, 71 71 )}`, 72 - err, 72 + {error: err}, 73 73 ) 74 74 setError( 75 75 'Unable to contact your service. Please check your Internet connection.', ··· 113 113 onChanged() 114 114 } catch (err: any) { 115 115 setError(cleanError(err)) 116 - store.log.error('Failed to update handle', {handle, err}) 116 + store.log.error('Failed to update handle', {handle, error: err}) 117 117 } finally { 118 118 setProcessing(false) 119 119 } ··· 343 343 } 344 344 } catch (err: any) { 345 345 setError(cleanError(err)) 346 - store.log.error('Failed to verify domain', {handle, err}) 346 + store.log.error('Failed to verify domain', {handle, error: err}) 347 347 } finally { 348 348 setIsVerifying(false) 349 349 }
+2 -2
src/view/com/modals/ContentFilteringSettings.tsx
··· 103 103 Toast.show( 104 104 'There was an issue syncing your preferences with the server', 105 105 ) 106 - store.log.error('Failed to update preferences with server', {e}) 106 + store.log.error('Failed to update preferences with server', {error: e}) 107 107 } 108 108 } 109 109 ··· 168 168 Toast.show( 169 169 'There was an issue syncing your preferences with the server', 170 170 ) 171 - store.log.error('Failed to update preferences with server', {e}) 171 + store.log.error('Failed to update preferences with server', {error: e}) 172 172 } 173 173 }, 174 174 [store, group],
+2 -2
src/view/com/modals/UserAddRemoveLists.tsx
··· 62 62 setMembershipsLoaded(true) 63 63 }, 64 64 err => { 65 - store.log.error('Failed to fetch memberships', {err}) 65 + store.log.error('Failed to fetch memberships', {error: err}) 66 66 }, 67 67 ) 68 68 }, [memberships, listsList, store, setSelected, setMembershipsLoaded]) ··· 76 76 try { 77 77 changes = await memberships.updateTo(selected) 78 78 } catch (err) { 79 - store.log.error('Failed to update memberships', {err}) 79 + store.log.error('Failed to update memberships', {error: err}) 80 80 return 81 81 } 82 82 Toast.show('Lists updated')
+6 -2
src/view/com/notifications/Feed.tsx
··· 61 61 setIsPTRing(true) 62 62 await view.refresh() 63 63 } catch (err) { 64 - view.rootStore.log.error('Failed to refresh notifications feed', err) 64 + view.rootStore.log.error('Failed to refresh notifications feed', { 65 + error: err, 66 + }) 65 67 } finally { 66 68 setIsPTRing(false) 67 69 } ··· 71 73 try { 72 74 await view.loadMore() 73 75 } catch (err) { 74 - view.rootStore.log.error('Failed to load more notifications', err) 76 + view.rootStore.log.error('Failed to load more notifications', { 77 + error: err, 78 + }) 75 79 } 76 80 }, [view]) 77 81
+6 -2
src/view/com/post-thread/PostLikedBy.tsx
··· 18 18 const view = React.useMemo(() => new LikesModel(store, {uri}), [store, uri]) 19 19 20 20 useEffect(() => { 21 - view.loadMore().catch(err => store.log.error('Failed to fetch likes', err)) 21 + view 22 + .loadMore() 23 + .catch(err => store.log.error('Failed to fetch likes', {error: err})) 22 24 }, [view, store.log]) 23 25 24 26 const onRefresh = () => { ··· 27 29 const onEndReached = () => { 28 30 view 29 31 .loadMore() 30 - .catch(err => view?.rootStore.log.error('Failed to load more likes', err)) 32 + .catch(err => 33 + view?.rootStore.log.error('Failed to load more likes', {error: err}), 34 + ) 31 35 } 32 36 33 37 if (!view.hasLoaded) {
+2 -2
src/view/com/post-thread/PostRepostedBy.tsx
··· 23 23 useEffect(() => { 24 24 view 25 25 .loadMore() 26 - .catch(err => store.log.error('Failed to fetch reposts', err)) 26 + .catch(err => store.log.error('Failed to fetch reposts', {error: err})) 27 27 }, [view, store.log]) 28 28 29 29 const onRefresh = () => { ··· 33 33 view 34 34 .loadMore() 35 35 .catch(err => 36 - view?.rootStore.log.error('Failed to load more reposts', err), 36 + view?.rootStore.log.error('Failed to load more reposts', {error: err}), 37 37 ) 38 38 } 39 39
+1 -1
src/view/com/post-thread/PostThread.tsx
··· 119 119 try { 120 120 view?.refresh() 121 121 } catch (err) { 122 - view.rootStore.log.error('Failed to refresh posts thread', err) 122 + view.rootStore.log.error('Failed to refresh posts thread', {error: err}) 123 123 } 124 124 setIsRefreshing(false) 125 125 }, [view, setIsRefreshing])
+4 -4
src/view/com/post-thread/PostThreadItem.tsx
··· 111 111 const onPressToggleRepost = React.useCallback(() => { 112 112 return item 113 113 .toggleRepost() 114 - .catch(e => store.log.error('Failed to toggle repost', e)) 114 + .catch(e => store.log.error('Failed to toggle repost', {error: e})) 115 115 }, [item, store]) 116 116 117 117 const onPressToggleLike = React.useCallback(() => { 118 118 return item 119 119 .toggleLike() 120 - .catch(e => store.log.error('Failed to toggle like', e)) 120 + .catch(e => store.log.error('Failed to toggle like', {error: e})) 121 121 }, [item, store]) 122 122 123 123 const onCopyPostText = React.useCallback(() => { ··· 138 138 Toast.show('You will now receive notifications for this thread') 139 139 } 140 140 } catch (e) { 141 - store.log.error('Failed to toggle thread mute', e) 141 + store.log.error('Failed to toggle thread mute', {error: e}) 142 142 } 143 143 }, [item, store]) 144 144 ··· 149 149 Toast.show('Post deleted') 150 150 }, 151 151 e => { 152 - store.log.error('Failed to delete post', e) 152 + store.log.error('Failed to delete post', {error: e}) 153 153 Toast.show('Failed to delete post, please try again') 154 154 }, 155 155 )
+4 -4
src/view/com/post/Post.tsx
··· 142 142 const onPressToggleRepost = React.useCallback(() => { 143 143 return item 144 144 .toggleRepost() 145 - .catch(e => store.log.error('Failed to toggle repost', e)) 145 + .catch(e => store.log.error('Failed to toggle repost', {error: e})) 146 146 }, [item, store]) 147 147 148 148 const onPressToggleLike = React.useCallback(() => { 149 149 return item 150 150 .toggleLike() 151 - .catch(e => store.log.error('Failed to toggle like', e)) 151 + .catch(e => store.log.error('Failed to toggle like', {error: e})) 152 152 }, [item, store]) 153 153 154 154 const onCopyPostText = React.useCallback(() => { ··· 169 169 Toast.show('You will now receive notifications for this thread') 170 170 } 171 171 } catch (e) { 172 - store.log.error('Failed to toggle thread mute', e) 172 + store.log.error('Failed to toggle thread mute', {error: e}) 173 173 } 174 174 }, [item, store]) 175 175 ··· 180 180 Toast.show('Post deleted') 181 181 }, 182 182 e => { 183 - store.log.error('Failed to delete post', e) 183 + store.log.error('Failed to delete post', {error: e}) 184 184 Toast.show('Failed to delete post, please try again') 185 185 }, 186 186 )
+2 -2
src/view/com/posts/Feed.tsx
··· 92 92 try { 93 93 await feed.refresh() 94 94 } catch (err) { 95 - feed.rootStore.log.error('Failed to refresh posts feed', err) 95 + feed.rootStore.log.error('Failed to refresh posts feed', {error: err}) 96 96 } 97 97 setIsRefreshing(false) 98 98 }, [feed, track, setIsRefreshing]) ··· 104 104 try { 105 105 await feed.loadMore() 106 106 } catch (err) { 107 - feed.rootStore.log.error('Failed to load more posts', err) 107 + feed.rootStore.log.error('Failed to load more posts', {error: err}) 108 108 } 109 109 }, [feed, track]) 110 110
+1 -1
src/view/com/posts/FeedErrorMessage.tsx
··· 73 73 Toast.show( 74 74 'There was an an issue removing this feed. Please check your internet connection and try again.', 75 75 ) 76 - store.log.error('Failed to remove feed', {err}) 76 + store.log.error('Failed to remove feed', {error: err}) 77 77 } 78 78 }, 79 79 onPressCancel() {
+4 -4
src/view/com/posts/FeedItem.tsx
··· 94 94 track('FeedItem:PostRepost') 95 95 return item 96 96 .toggleRepost() 97 - .catch(e => store.log.error('Failed to toggle repost', e)) 97 + .catch(e => store.log.error('Failed to toggle repost', {error: e})) 98 98 }, [track, item, store]) 99 99 100 100 const onPressToggleLike = React.useCallback(() => { 101 101 track('FeedItem:PostLike') 102 102 return item 103 103 .toggleLike() 104 - .catch(e => store.log.error('Failed to toggle like', e)) 104 + .catch(e => store.log.error('Failed to toggle like', {error: e})) 105 105 }, [track, item, store]) 106 106 107 107 const onCopyPostText = React.useCallback(() => { ··· 123 123 Toast.show('You will now receive notifications for this thread') 124 124 } 125 125 } catch (e) { 126 - store.log.error('Failed to toggle thread mute', e) 126 + store.log.error('Failed to toggle thread mute', {error: e}) 127 127 } 128 128 }, [track, item, store]) 129 129 ··· 135 135 Toast.show('Post deleted') 136 136 }, 137 137 e => { 138 - store.log.error('Failed to delete post', e) 138 + store.log.error('Failed to delete post', {error: e}) 139 139 Toast.show('Failed to delete post, please try again') 140 140 }, 141 141 )
+8 -6
src/view/com/profile/ProfileFollowers.tsx
··· 26 26 useEffect(() => { 27 27 view 28 28 .loadMore() 29 - .catch(err => store.log.error('Failed to fetch user followers', err)) 29 + .catch(err => 30 + store.log.error('Failed to fetch user followers', {error: err}), 31 + ) 30 32 }, [view, store.log]) 31 33 32 34 const onRefresh = () => { 33 35 view.refresh() 34 36 } 35 37 const onEndReached = () => { 36 - view 37 - .loadMore() 38 - .catch(err => 39 - view?.rootStore.log.error('Failed to load more followers', err), 40 - ) 38 + view.loadMore().catch(err => 39 + view?.rootStore.log.error('Failed to load more followers', { 40 + error: err, 41 + }), 42 + ) 41 43 } 42 44 43 45 if (!view.hasLoaded) {
+5 -5
src/view/com/profile/ProfileHeader.tsx
··· 150 150 : 'ProfileHeader:UnfollowButtonClicked', 151 151 ) 152 152 }, 153 - err => store.log.error('Failed to toggle follow', err), 153 + err => store.log.error('Failed to toggle follow', {error: err}), 154 154 ) 155 155 }, [track, view, store.log, setShowSuggestedFollows]) 156 156 ··· 193 193 await view.muteAccount() 194 194 Toast.show('Account muted') 195 195 } catch (e: any) { 196 - store.log.error('Failed to mute account', e) 196 + store.log.error('Failed to mute account', {error: e}) 197 197 Toast.show(`There was an issue! ${e.toString()}`) 198 198 } 199 199 }, [track, view, store]) ··· 204 204 await view.unmuteAccount() 205 205 Toast.show('Account unmuted') 206 206 } catch (e: any) { 207 - store.log.error('Failed to unmute account', e) 207 + store.log.error('Failed to unmute account', {error: e}) 208 208 Toast.show(`There was an issue! ${e.toString()}`) 209 209 } 210 210 }, [track, view, store]) ··· 222 222 onRefreshAll() 223 223 Toast.show('Account blocked') 224 224 } catch (e: any) { 225 - store.log.error('Failed to block account', e) 225 + store.log.error('Failed to block account', {error: e}) 226 226 Toast.show(`There was an issue! ${e.toString()}`) 227 227 } 228 228 }, ··· 242 242 onRefreshAll() 243 243 Toast.show('Account unblocked') 244 244 } catch (e: any) { 245 - store.log.error('Failed to unblock account', e) 245 + store.log.error('Failed to unblock account', {error: e}) 246 246 Toast.show(`There was an issue! ${e.toString()}`) 247 247 } 248 248 },
+7 -7
src/view/screens/Log.tsx
··· 10 10 import {ViewHeader} from '../com/util/ViewHeader' 11 11 import {Text} from '../com/util/text/Text' 12 12 import {usePalette} from 'lib/hooks/usePalette' 13 + import {getEntries} from '#/logger/logDump' 13 14 import {ago} from 'lib/strings/time' 14 15 15 16 export const LogScreen = observer(function Log({}: NativeStackScreenProps< ··· 38 39 <View style={[s.flex1]}> 39 40 <ViewHeader title="Log" /> 40 41 <ScrollView style={s.flex1}> 41 - {store.log.entries 42 + {getEntries() 42 43 .slice(0) 43 - .reverse() 44 44 .map(entry => { 45 45 return ( 46 46 <View key={`entry-${entry.id}`}> ··· 49 49 onPress={toggler(entry.id)} 50 50 accessibilityLabel="View debug entry" 51 51 accessibilityHint="Opens additional details for a debug entry"> 52 - {entry.type === 'debug' ? ( 52 + {entry.level === 'debug' ? ( 53 53 <FontAwesomeIcon icon="info" /> 54 54 ) : ( 55 55 <FontAwesomeIcon icon="exclamation" style={s.red3} /> 56 56 )} 57 57 <Text type="sm" style={[styles.summary, pal.text]}> 58 - {entry.summary} 58 + {String(entry.message)} 59 59 </Text> 60 - {entry.details ? ( 60 + {entry.metadata && Object.keys(entry.metadata).length ? ( 61 61 <FontAwesomeIcon 62 62 icon={ 63 63 expanded.includes(entry.id) ? 'angle-up' : 'angle-down' ··· 66 66 /> 67 67 ) : undefined} 68 68 <Text type="sm" style={[styles.ts, pal.textLight]}> 69 - {entry.ts ? ago(entry.ts) : ''} 69 + {ago(entry.timestamp)} 70 70 </Text> 71 71 </TouchableOpacity> 72 72 {expanded.includes(entry.id) ? ( 73 73 <View style={[pal.view, s.pl10, s.pr10, s.pb10]}> 74 74 <View style={[pal.btn, styles.details]}> 75 75 <Text type="mono" style={pal.text}> 76 - {entry.details} 76 + {JSON.stringify(entry.metadata, null, 2)} 77 77 </Text> 78 78 </View> 79 79 </View>
+1 -1
src/view/screens/ModerationBlockedAccounts.tsx
··· 52 52 blockedAccounts 53 53 .loadMore() 54 54 .catch(err => 55 - store.log.error('Failed to load more blocked accounts', err), 55 + store.log.error('Failed to load more blocked accounts', {error: err}), 56 56 ) 57 57 }, [blockedAccounts, store]) 58 58
+1 -1
src/view/screens/ModerationMutedAccounts.tsx
··· 49 49 mutedAccounts 50 50 .loadMore() 51 51 .catch(err => 52 - store.log.error('Failed to load more muted accounts', err), 52 + store.log.error('Failed to load more muted accounts', {error: err}), 53 53 ) 54 54 }, [mutedAccounts, store]) 55 55
+1 -1
src/view/screens/PostThread.tsx
··· 38 38 InteractionManager.runAfterInteractions(() => { 39 39 if (!view.hasLoaded && !view.isLoading) { 40 40 view.setup().catch(err => { 41 - store.log.error('Failed to fetch thread', err) 41 + store.log.error('Failed to fetch thread', {error: err}) 42 42 }) 43 43 } 44 44 })
+6 -6
src/view/screens/Profile.tsx
··· 108 108 uiState 109 109 .refresh() 110 110 .catch((err: any) => 111 - store.log.error('Failed to refresh user profile', err), 111 + store.log.error('Failed to refresh user profile', {error: err}), 112 112 ) 113 113 }, [uiState, store]) 114 114 const onEndReached = React.useCallback(() => { 115 - uiState 116 - .loadMore() 117 - .catch((err: any) => 118 - store.log.error('Failed to load more entries in user profile', err), 119 - ) 115 + uiState.loadMore().catch((err: any) => 116 + store.log.error('Failed to load more entries in user profile', { 117 + error: err, 118 + }), 119 + ) 120 120 }, [uiState, store]) 121 121 const onPressTryAgain = React.useCallback(() => { 122 122 uiState.setup()
+3 -3
src/view/screens/ProfileFeed.tsx
··· 165 165 Toast.show( 166 166 'There was an an issue updating your feeds, please check your internet connection and try again.', 167 167 ) 168 - store.log.error('Failed up update feeds', {err}) 168 + store.log.error('Failed up update feeds', {error: err}) 169 169 } 170 170 }, [store, feedInfo]) 171 171 ··· 181 181 Toast.show( 182 182 'There was an an issue contacting the server, please check your internet connection and try again.', 183 183 ) 184 - store.log.error('Failed up toggle like', {err}) 184 + store.log.error('Failed up toggle like', {error: err}) 185 185 } 186 186 }, [store, feedInfo]) 187 187 ··· 190 190 if (feedInfo) { 191 191 feedInfo.togglePin().catch(e => { 192 192 Toast.show('There was an issue contacting the server') 193 - store.log.error('Failed to toggle pinned feed', {e}) 193 + store.log.error('Failed to toggle pinned feed', {error: e}) 194 194 }) 195 195 } 196 196 }, [store, feedInfo])
+1 -1
src/view/screens/ProfileList.tsx
··· 272 272 Haptics.default() 273 273 list.togglePin().catch(e => { 274 274 Toast.show('There was an issue contacting the server') 275 - store.log.error('Failed to toggle pinned list', {e}) 275 + store.log.error('Failed to toggle pinned list', {error: e}) 276 276 }) 277 277 }, [store, list]) 278 278
+3 -3
src/view/screens/SavedFeeds.tsx
··· 166 166 Haptics.default() 167 167 item.togglePin().catch(e => { 168 168 Toast.show('There was an issue contacting the server') 169 - store.log.error('Failed to toggle pinned feed', {e}) 169 + store.log.error('Failed to toggle pinned feed', {error: e}) 170 170 }) 171 171 }, [item, store]) 172 172 const onPressUp = useCallback( 173 173 () => 174 174 savedFeeds.movePinnedFeed(item, 'up').catch(e => { 175 175 Toast.show('There was an issue contacting the server') 176 - store.log.error('Failed to set pinned feed order', {e}) 176 + store.log.error('Failed to set pinned feed order', {error: e}) 177 177 }), 178 178 [store, savedFeeds, item], 179 179 ) ··· 181 181 () => 182 182 savedFeeds.movePinnedFeed(item, 'down').catch(e => { 183 183 Toast.show('There was an issue contacting the server') 184 - store.log.error('Failed to set pinned feed order', {e}) 184 + store.log.error('Failed to set pinned feed order', {error: e}) 185 185 }), 186 186 [store, savedFeeds, item], 187 187 )
+1 -1
src/view/screens/Settings.tsx
··· 112 112 err => { 113 113 store.log.error( 114 114 'Failed to reload from server after handle update', 115 - {err}, 115 + {error: err}, 116 116 ) 117 117 setIsSwitching(false) 118 118 },
+19
yarn.lock
··· 1517 1517 dependencies: 1518 1518 regenerator-runtime "^0.14.0" 1519 1519 1520 + "@babel/runtime@^7.21.0": 1521 + version "7.23.2" 1522 + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885" 1523 + integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg== 1524 + dependencies: 1525 + regenerator-runtime "^0.14.0" 1526 + 1520 1527 "@babel/template@^7.0.0", "@babel/template@^7.22.5", "@babel/template@^7.3.3": 1521 1528 version "7.22.5" 1522 1529 resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" ··· 8014 8021 whatwg-mimetype "^3.0.0" 8015 8022 whatwg-url "^11.0.0" 8016 8023 8024 + date-fns@^2.30.0: 8025 + version "2.30.0" 8026 + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" 8027 + integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== 8028 + dependencies: 8029 + "@babel/runtime" "^7.21.0" 8030 + 8017 8031 dayjs@^1.8.15: 8018 8032 version "1.11.9" 8019 8033 resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.9.tgz#9ca491933fadd0a60a2c19f6c237c03517d71d1a" ··· 13672 13686 version "3.3.6" 13673 13687 resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" 13674 13688 integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== 13689 + 13690 + nanoid@^5.0.2: 13691 + version "5.0.2" 13692 + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.2.tgz#97588ebc70166d0feaf73ccd2799bb4ceaebf692" 13693 + integrity sha512-2ustYUX1R2rL/Br5B/FMhi8d5/QzvkJ912rBYxskcpu0myTHzSZfTr1LAS2Sm7jxRUObRrSBFoyzwAhL49aVSg== 13675 13694 13676 13695 napi-build-utils@^1.0.1: 13677 13696 version "1.0.2"