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

Add new logger
zio/stable
Eric Bailey 2023-11-04 13:12:46 -05:00 committed by GitHub
commit e49a3d8a56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
70 changed files with 1109 additions and 176 deletions

View File

@ -1 +1,3 @@
SENTRY_AUTH_TOKEN= SENTRY_AUTH_TOKEN=
EXPO_PUBLIC_LOG_LEVEL=debug
EXPO_PUBLIC_LOG_DEBUG=

View File

@ -13,21 +13,21 @@
"start": "expo start --dev-client", "start": "expo start --dev-client",
"start:prod": "expo start --dev-client --no-dev --minify", "start:prod": "expo start --dev-client --no-dev --minify",
"clean-cache": "rm -rf node_modules/.cache/babel-loader/*", "clean-cache": "rm -rf node_modules/.cache/babel-loader/*",
"test": "jest --forceExit --testTimeout=20000 --bail", "test": "NODE_ENV=test jest --forceExit --testTimeout=20000 --bail",
"test-watch": "jest --watchAll", "test-watch": "NODE_ENV=test jest --watchAll",
"test-ci": "jest --ci --forceExit --reporters=default --reporters=jest-junit", "test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit",
"test-coverage": "jest --coverage", "test-coverage": "NODE_ENV=test jest --coverage",
"lint": "eslint ./src --ext .js,.jsx,.ts,.tsx", "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx",
"typecheck": "tsc --project ./tsconfig.check.json", "typecheck": "tsc --project ./tsconfig.check.json",
"e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node __e2e__/mock-server.ts", "e2e:mock-server": "./jest/dev-infra/with-test-redis-and-db.sh ts-node __e2e__/mock-server.ts",
"e2e:metro": "RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", "e2e:metro": "RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios",
"e2e:build": "detox build -c ios.sim.debug", "e2e:build": "detox build -c ios.sim.debug",
"e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all", "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all",
"perf:test": "maestro test", "perf:test": "NODE_ENV=test maestro test",
"perf:test:run": "maestro test __e2e__/maestro/scroll.yaml", "perf:test:run": "NODE_ENV=test maestro test __e2e__/maestro/scroll.yaml",
"perf:test:measure": "flashlight test --bundleId xyz.blueskyweb.app --testCommand 'yarn perf:test' --duration 150000 --resultsFilePath .perf/results.json", "perf:test:measure": "NODE_ENV=test flashlight test --bundleId xyz.blueskyweb.app --testCommand 'yarn perf:test' --duration 150000 --resultsFilePath .perf/results.json",
"perf:test:results": "flashlight report .perf/results.json", "perf:test:results": "NODE_ENV=test flashlight report .perf/results.json",
"perf:measure": "flashlight measure", "perf:measure": "NODE_ENV=test flashlight measure",
"build:apk": "eas build -p android --profile dev-android-apk" "build:apk": "eas build -p android --profile dev-android-apk"
}, },
"dependencies": { "dependencies": {
@ -80,6 +80,7 @@
"babel-plugin-transform-remove-console": "^6.9.4", "babel-plugin-transform-remove-console": "^6.9.4",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"bcp-47-match": "^2.0.3", "bcp-47-match": "^2.0.3",
"date-fns": "^2.30.0",
"email-validator": "^2.0.4", "email-validator": "^2.0.4",
"emoji-mart": "^5.5.2", "emoji-mart": "^5.5.2",
"eventemitter3": "^5.0.1", "eventemitter3": "^5.0.1",
@ -118,6 +119,7 @@
"mobx": "^6.6.1", "mobx": "^6.6.1",
"mobx-react-lite": "^3.4.0", "mobx-react-lite": "^3.4.0",
"mobx-utils": "^6.0.6", "mobx-utils": "^6.0.6",
"nanoid": "^5.0.2",
"normalize-url": "^8.0.0", "normalize-url": "^8.0.0",
"patch-package": "^6.5.1", "patch-package": "^6.5.1",
"postinstall-postinstall": "^2.1.0", "postinstall-postinstall": "^2.1.0",
@ -240,7 +242,7 @@
"\\.[jt]sx?$": "babel-jest" "\\.[jt]sx?$": "babel-jest"
}, },
"transformIgnorePatterns": [ "transformIgnorePatterns": [
"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)" "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)"
], ],
"modulePathIgnorePatterns": [ "modulePathIgnorePatterns": [
"__tests__/.*/__mocks__", "__tests__/.*/__mocks__",

9
src/env.ts 100644
View File

@ -0,0 +1,9 @@
export const IS_TEST = process.env.NODE_ENV === 'test'
export const IS_DEV = __DEV__
export const IS_PROD = !IS_DEV
export const LOG_DEBUG = process.env.EXPO_PUBLIC_LOG_DEBUG || ''
export const LOG_LEVEL = (process.env.EXPO_PUBLIC_LOG_LEVEL || 'info') as
| 'debug'
| 'info'
| 'warn'
| 'error'

View File

@ -178,10 +178,9 @@ export async function post(store: RootStoreModel, opts: PostOpts) {
) { ) {
encoding = 'image/jpeg' encoding = 'image/jpeg'
} else { } else {
store.log.warn( store.log.warn('Unexpected image format for thumbnail, skipping', {
'Unexpected image format for thumbnail, skipping', thumbnail: opts.extLink.localThumb.path,
opts.extLink.localThumb.path, })
)
} }
if (encoding) { if (encoding) {
const thumbUploadRes = await uploadBlob( const thumbUploadRes = await uploadBlob(

View File

@ -22,7 +22,7 @@ export function useFollowProfile(profile: AppBskyActorDefs.ProfileViewBasic) {
following: false, following: false,
} }
} catch (e: any) { } catch (e: any) {
store.log.error('Failed to delete follow', e) store.log.error('Failed to delete follow', {error: e})
throw e throw e
} }
} else if (state === FollowState.NotFollowing) { } else if (state === FollowState.NotFollowing) {
@ -40,7 +40,7 @@ export function useFollowProfile(profile: AppBskyActorDefs.ProfileViewBasic) {
following: true, following: true,
} }
} catch (e: any) { } catch (e: any) {
store.log.error('Failed to create follow', e) store.log.error('Failed to create follow', {error: e})
throw e throw e
} }
} }

View File

@ -34,18 +34,18 @@ export function useOTAUpdate() {
// show a popup modal // show a popup modal
showUpdatePopup() showUpdatePopup()
} catch (e) { } catch (e) {
console.error('useOTAUpdate: Error while checking for update', e) store.log.error('useOTAUpdate: Error while checking for update', {
store.log.error('useOTAUpdate: Error while checking for update', e) error: e,
})
} }
}, [showUpdatePopup, store.log]) }, [showUpdatePopup, store.log])
const updateEventListener = useCallback( const updateEventListener = useCallback(
(event: Updates.UpdateEvent) => { (event: Updates.UpdateEvent) => {
store.log.debug('useOTAUpdate: Listening for update...') store.log.debug('useOTAUpdate: Listening for update...')
if (event.type === Updates.UpdateEventType.ERROR) { if (event.type === Updates.UpdateEventType.ERROR) {
store.log.error( store.log.error('useOTAUpdate: Error while listening for update', {
'useOTAUpdate: Error while listening for update', message: event.message,
event.message, })
)
} else if (event.type === Updates.UpdateEventType.NO_UPDATE_AVAILABLE) { } else if (event.type === Updates.UpdateEventType.NO_UPDATE_AVAILABLE) {
// Handle no update available // Handle no update available
// do nothing // do nothing

View File

@ -30,18 +30,18 @@ export function init(store: RootStoreModel) {
appId: 'xyz.blueskyweb.app', appId: 'xyz.blueskyweb.app',
}) })
store.log.debug('Notifications: Sent push token (init)', { store.log.debug('Notifications: Sent push token (init)', {
type: token.type, tokenType: token.type,
token: token.data, token: token.data,
}) })
} catch (error) { } catch (error) {
store.log.error('Notifications: Failed to set push token', error) store.log.error('Notifications: Failed to set push token', {error})
} }
} }
// listens for new changes to the push token // listens for new changes to the push token
// 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. // 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.
Notifications.addPushTokenListener(async ({data: t, type}) => { Notifications.addPushTokenListener(async ({data: t, type}) => {
store.log.debug('Notifications: Push token changed', {t, type}) store.log.debug('Notifications: Push token changed', {t, tokenType: type})
if (t) { if (t) {
try { try {
await store.agent.api.app.bsky.notification.registerPush({ await store.agent.api.app.bsky.notification.registerPush({
@ -51,11 +51,11 @@ export function init(store: RootStoreModel) {
appId: 'xyz.blueskyweb.app', appId: 'xyz.blueskyweb.app',
}) })
store.log.debug('Notifications: Sent push token (event)', { store.log.debug('Notifications: Sent push token (event)', {
type, tokenType: type,
token: t, token: t,
}) })
} catch (error) { } catch (error) {
store.log.error('Notifications: Failed to set push token', error) store.log.error('Notifications: Failed to set push token', {error})
} }
} }
}) })
@ -63,7 +63,7 @@ export function init(store: RootStoreModel) {
// handle notifications that are received, both in the foreground or background // handle notifications that are received, both in the foreground or background
Notifications.addNotificationReceivedListener(event => { Notifications.addNotificationReceivedListener(event => {
store.log.debug('Notifications: received', event) store.log.debug('Notifications: received', {event})
if (event.request.trigger.type === 'push') { if (event.request.trigger.type === 'push') {
// refresh notifications in the background // refresh notifications in the background
store.me.notifications.syncQueue() store.me.notifications.syncQueue()
@ -84,10 +84,9 @@ export function init(store: RootStoreModel) {
// handle notifications that are tapped on // handle notifications that are tapped on
const sub = Notifications.addNotificationResponseReceivedListener( const sub = Notifications.addNotificationResponseReceivedListener(
response => { response => {
store.log.debug( store.log.debug('Notifications: response received', {
'Notifications: response received', actionIdentifier: response.actionIdentifier,
response.actionIdentifier, })
)
if ( if (
response.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER response.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER
) { ) {

View File

@ -0,0 +1,99 @@
# Logger
Simple logger for Bluesky. Supports log levels, debug contexts, and separate
transports for production, dev, and test mode.
## At a Glance
```typescript
import { logger } from '#/logger'
logger.debug(message[, metadata, debugContext])
logger.info(message[, metadata])
logger.log(message[, metadata])
logger.warn(message[, metadata])
logger.error(error[, metadata])
```
#### Modes
The "modes" referred to here are inferred from the values exported from `#/env`.
Basically, the booleans `IS_DEV`, `IS_TEST`, and `IS_PROD`.
#### Log Levels
Log levels are used to filter which logs are either printed to the console
and/or sent to Sentry and other reporting services. To configure, set the
`EXPO_PUBLIC_LOG_LEVEL` environment variable in `.env` to one of `debug`,
`info`, `log`, `warn`, or `error`.
This variable should be `info` in production, and `debug` in dev. If it gets too
noisy in dev, simply set it to a higher level, such as `warn`.
## Usage
```typescript
import { logger } from '#/logger';
```
### `logger.error`
The `error` level is for... well, errors. These are sent to Sentry in production mode.
`error`, along with all log levels, supports an additional parameter, `metadata: Record<string, unknown>`. Use this to provide values to the [Sentry
breadcrumb](https://docs.sentry.io/platforms/react-native/enriching-events/breadcrumbs/#manual-breadcrumbs).
```typescript
try {
// some async code
} catch (e) {
logger.error(e, { ...metadata });
}
```
### `logger.warn`
Warnings will be sent to Sentry as a separate Issue with level `warning`, as
well as as breadcrumbs, with a severity level of `warning`
### `logger.log`
Logs with level `log` will be sent to Sentry as a separate Issue with level `log`, as
well as as breadcrumbs, with a severity level of `default`.
### `logger.info`
The `info` level should be used for information that would be helpful in a
tracing context, like Sentry. In production mode, `info` logs are sent
to Sentry as breadcrumbs, which decorate log levels above `info` such as `log`,
`warn`, and `error`.
### `logger.debug`
Debug level is really only intended for local development. Use this instead of
`console.log`.
```typescript
logger.debug(message, { ...metadata });
```
Inspired by [debug](https://www.npmjs.com/package/debug), when writing debug
logs, you can optionally pass a _context_, which can be then filtered when in
debug mode.
This value should be related to the feature, component, or screen
the code is running within, and **it should be defined in `#/logger/debugContext`**.
This way we know if a relevant context already exists, and we can trace all
active contexts in use in our app. This const enum is conveniently available on
the `logger` at `logger.DebugContext`.
For example, a debug log like this:
```typescript
logger.debug(message, {}, logger.DebugContext.composer);
```
Would be logged to the console in dev mode if `EXPO_PUBLIC_LOG_LEVEL=debug`, _or_ if you
pass a separate environment variable `LOG_DEBUG=composer`. This variable supports
multiple contexts using commas like `LOG_DEBUG=composer,profile`, and _automatically
sets the log level to `debug`, regardless of `EXPO_PUBLIC_LOG_LEVEL`._

View File

@ -0,0 +1,36 @@
import {expect, test} from '@jest/globals'
import {ConsoleTransportEntry, LogLevel} from '#/logger'
import {add, getEntries} from '#/logger/logDump'
test('works', () => {
const items: ConsoleTransportEntry[] = [
{
id: '1',
level: LogLevel.Debug,
message: 'hello',
metadata: {},
timestamp: Date.now(),
},
{
id: '2',
level: LogLevel.Debug,
message: 'hello',
metadata: {},
timestamp: Date.now(),
},
{
id: '3',
level: LogLevel.Debug,
message: 'hello',
metadata: {},
timestamp: Date.now(),
},
]
for (const item of items) {
add(item)
}
expect(getEntries()).toEqual(items.reverse())
})

View File

@ -0,0 +1,424 @@
import {nanoid} from 'nanoid/non-secure'
import {jest, describe, expect, test, beforeAll} from '@jest/globals'
import {Native as Sentry} from 'sentry-expo'
import {Logger, LogLevel, sentryTransport} from '#/logger'
jest.mock('#/env', () => ({
IS_TEST: true,
IS_DEV: false,
IS_PROD: false,
/*
* Forces debug mode for tests using the default logger. Most tests create
* their own logger instance.
*/
LOG_LEVEL: 'debug',
LOG_DEBUG: '',
}))
jest.mock('sentry-expo', () => ({
Native: {
addBreadcrumb: jest.fn(),
captureException: jest.fn(),
captureMessage: jest.fn(),
},
}))
beforeAll(() => {
jest.useFakeTimers()
})
describe('general functionality', () => {
test('default params', () => {
const logger = new Logger()
expect(logger.enabled).toBeFalsy()
expect(logger.level).toEqual(LogLevel.Debug) // mocked above
})
test('can override default params', () => {
const logger = new Logger({
enabled: true,
level: LogLevel.Info,
})
expect(logger.enabled).toBeTruthy()
expect(logger.level).toEqual(LogLevel.Info)
})
test('disabled logger does not report', () => {
const logger = new Logger({
enabled: false,
level: LogLevel.Debug,
})
const mockTransport = jest.fn()
logger.addTransport(mockTransport)
logger.debug('message')
expect(mockTransport).not.toHaveBeenCalled()
})
test('disablement', () => {
const logger = new Logger({
enabled: true,
level: LogLevel.Debug,
})
logger.disable()
const mockTransport = jest.fn()
logger.addTransport(mockTransport)
logger.debug('message')
expect(mockTransport).not.toHaveBeenCalled()
})
test('passing debug contexts automatically enables debug mode', () => {
const logger = new Logger({debug: 'specific'})
expect(logger.level).toEqual(LogLevel.Debug)
})
test('supports extra metadata', () => {
const timestamp = Date.now()
const logger = new Logger({enabled: true})
const mockTransport = jest.fn()
logger.addTransport(mockTransport)
const extra = {foo: true}
logger.warn('message', extra)
expect(mockTransport).toHaveBeenCalledWith(
LogLevel.Warn,
'message',
extra,
timestamp,
)
})
test('supports nullish/falsy metadata', () => {
const timestamp = Date.now()
const logger = new Logger({enabled: true})
const mockTransport = jest.fn()
const remove = logger.addTransport(mockTransport)
// @ts-expect-error testing the JS case
logger.warn('a', null)
expect(mockTransport).toHaveBeenCalledWith(
LogLevel.Warn,
'a',
{},
timestamp,
)
// @ts-expect-error testing the JS case
logger.warn('b', false)
expect(mockTransport).toHaveBeenCalledWith(
LogLevel.Warn,
'b',
{},
timestamp,
)
// @ts-expect-error testing the JS case
logger.warn('c', 0)
expect(mockTransport).toHaveBeenCalledWith(
LogLevel.Warn,
'c',
{},
timestamp,
)
remove()
logger.addTransport((level, message, metadata) => {
expect(typeof metadata).toEqual('object')
})
// @ts-expect-error testing the JS case
logger.warn('message', null)
})
test('sentryTransport', () => {
const message = 'message'
const timestamp = Date.now()
const sentryTimestamp = timestamp / 1000
sentryTransport(LogLevel.Debug, message, {}, timestamp)
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
message,
data: {},
type: 'default',
level: LogLevel.Debug,
timestamp: sentryTimestamp,
})
sentryTransport(
LogLevel.Info,
message,
{type: 'info', prop: true},
timestamp,
)
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
message,
data: {prop: true},
type: 'info',
level: LogLevel.Info,
timestamp: sentryTimestamp,
})
sentryTransport(LogLevel.Log, message, {}, timestamp)
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
message,
data: {},
type: 'default',
level: 'debug', // Sentry bug, log becomes debug
timestamp: sentryTimestamp,
})
expect(Sentry.captureMessage).toHaveBeenCalledWith(message, {
level: 'log',
tags: undefined,
extra: {},
})
sentryTransport(LogLevel.Warn, message, {}, timestamp)
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
message,
data: {},
type: 'default',
level: 'warning',
timestamp: sentryTimestamp,
})
expect(Sentry.captureMessage).toHaveBeenCalledWith(message, {
level: 'warning',
tags: undefined,
extra: {},
})
const e = new Error('error')
const tags = {
prop: 'prop',
}
sentryTransport(
LogLevel.Error,
e,
{
tags,
prop: true,
},
timestamp,
)
expect(Sentry.captureException).toHaveBeenCalledWith(e, {
tags,
extra: {
prop: true,
},
})
})
test('add/remove transport', () => {
const timestamp = Date.now()
const logger = new Logger({enabled: true})
const mockTransport = jest.fn()
const remove = logger.addTransport(mockTransport)
logger.warn('warn')
remove()
logger.warn('warn')
// only called once bc it was removed
expect(mockTransport).toHaveBeenNthCalledWith(
1,
LogLevel.Warn,
'warn',
{},
timestamp,
)
})
})
describe('debug contexts', () => {
const mockTransport = jest.fn()
test('specific', () => {
const timestamp = Date.now()
const message = nanoid()
const logger = new Logger({
enabled: true,
debug: 'specific',
})
logger.addTransport(mockTransport)
logger.debug(message, {}, 'specific')
expect(mockTransport).toHaveBeenCalledWith(
LogLevel.Debug,
message,
{},
timestamp,
)
})
test('namespaced', () => {
const timestamp = Date.now()
const message = nanoid()
const logger = new Logger({
enabled: true,
debug: 'namespace*',
})
logger.addTransport(mockTransport)
logger.debug(message, {}, 'namespace')
expect(mockTransport).toHaveBeenCalledWith(
LogLevel.Debug,
message,
{},
timestamp,
)
})
test('ignores inactive', () => {
const timestamp = Date.now()
const message = nanoid()
const logger = new Logger({
enabled: true,
debug: 'namespace:foo:*',
})
logger.addTransport(mockTransport)
logger.debug(message, {}, 'namespace:bar:baz')
expect(mockTransport).not.toHaveBeenCalledWith(
LogLevel.Debug,
message,
{},
timestamp,
)
})
})
describe('supports levels', () => {
test('debug', () => {
const timestamp = Date.now()
const logger = new Logger({
enabled: true,
level: LogLevel.Debug,
})
const message = nanoid()
const mockTransport = jest.fn()
logger.addTransport(mockTransport)
logger.debug(message)
expect(mockTransport).toHaveBeenCalledWith(
LogLevel.Debug,
message,
{},
timestamp,
)
logger.info(message)
expect(mockTransport).toHaveBeenCalledWith(
LogLevel.Info,
message,
{},
timestamp,
)
logger.warn(message)
expect(mockTransport).toHaveBeenCalledWith(
LogLevel.Warn,
message,
{},
timestamp,
)
const e = new Error(message)
logger.error(e)
expect(mockTransport).toHaveBeenCalledWith(LogLevel.Error, e, {}, timestamp)
})
test('info', () => {
const timestamp = Date.now()
const logger = new Logger({
enabled: true,
level: LogLevel.Info,
})
const message = nanoid()
const mockTransport = jest.fn()
logger.addTransport(mockTransport)
logger.debug(message)
expect(mockTransport).not.toHaveBeenCalled()
logger.info(message)
expect(mockTransport).toHaveBeenCalledWith(
LogLevel.Info,
message,
{},
timestamp,
)
})
test('warn', () => {
const timestamp = Date.now()
const logger = new Logger({
enabled: true,
level: LogLevel.Warn,
})
const message = nanoid()
const mockTransport = jest.fn()
logger.addTransport(mockTransport)
logger.debug(message)
expect(mockTransport).not.toHaveBeenCalled()
logger.info(message)
expect(mockTransport).not.toHaveBeenCalled()
logger.warn(message)
expect(mockTransport).toHaveBeenCalledWith(
LogLevel.Warn,
message,
{},
timestamp,
)
})
test('error', () => {
const timestamp = Date.now()
const logger = new Logger({
enabled: true,
level: LogLevel.Error,
})
const message = nanoid()
const mockTransport = jest.fn()
logger.addTransport(mockTransport)
logger.debug(message)
expect(mockTransport).not.toHaveBeenCalled()
logger.info(message)
expect(mockTransport).not.toHaveBeenCalled()
logger.warn(message)
expect(mockTransport).not.toHaveBeenCalled()
const e = new Error('original message')
logger.error(e)
expect(mockTransport).toHaveBeenCalledWith(LogLevel.Error, e, {}, timestamp)
})
})

View File

@ -0,0 +1,10 @@
/**
* *Do not import this directly.* Instead, use the shortcut reference `logger.DebugContext`.
*
* Add debug contexts here. Although convention typically calls for enums ito
* be capitalized, for parity with the `LOG_DEBUG` env var, please use all
* lowercase.
*/
export const DebugContext = {
// e.g. composer: 'composer'
} as const

290
src/logger/index.ts 100644
View File

@ -0,0 +1,290 @@
import format from 'date-fns/format'
import {nanoid} from 'nanoid/non-secure'
import {Sentry} from '#/logger/sentry'
import * as env from '#/env'
import {DebugContext} from '#/logger/debugContext'
import {add} from '#/logger/logDump'
export enum LogLevel {
Debug = 'debug',
Info = 'info',
Log = 'log',
Warn = 'warn',
Error = 'error',
}
type Transport = (
level: LogLevel,
message: string | Error,
metadata: Metadata,
timestamp: number,
) => void
/**
* A union of some of Sentry's breadcrumb properties as well as Sentry's
* `captureException` parameter, `CaptureContext`.
*/
type Metadata = {
/**
* Applied as Sentry breadcrumb types. Defaults to `default`.
*
* @see https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/#breadcrumb-types
*/
type?:
| 'default'
| 'debug'
| 'error'
| 'navigation'
| 'http'
| 'info'
| 'query'
| 'transaction'
| 'ui'
| 'user'
/**
* Passed through to `Sentry.captureException`
*
* @see https://github.com/getsentry/sentry-javascript/blob/903addf9a1a1534a6cb2ba3143654b918a86f6dd/packages/types/src/misc.ts#L65
*/
tags?: {
[key: string]:
| number
| string
| boolean
| bigint
| symbol
| null
| undefined
}
/**
* Any additional data, passed through to Sentry as `extra` param on
* exceptions, or the `data` param on breadcrumbs.
*/
[key: string]: unknown
} & Parameters<typeof Sentry.captureException>[1]
export type ConsoleTransportEntry = {
id: string
timestamp: number
level: LogLevel
message: string | Error
metadata: Metadata
}
const enabledLogLevels: {
[key in LogLevel]: LogLevel[]
} = {
[LogLevel.Debug]: [
LogLevel.Debug,
LogLevel.Info,
LogLevel.Log,
LogLevel.Warn,
LogLevel.Error,
],
[LogLevel.Info]: [LogLevel.Info, LogLevel.Log, LogLevel.Warn, LogLevel.Error],
[LogLevel.Log]: [LogLevel.Log, LogLevel.Warn, LogLevel.Error],
[LogLevel.Warn]: [LogLevel.Warn, LogLevel.Error],
[LogLevel.Error]: [LogLevel.Error],
}
/**
* Used in dev mode to nicely log to the console
*/
export const consoleTransport: Transport = (
level,
message,
metadata,
timestamp,
) => {
const extra = Object.keys(metadata).length
? ' ' + JSON.stringify(metadata, null, ' ')
: ''
const log = {
[LogLevel.Debug]: console.debug,
[LogLevel.Info]: console.info,
[LogLevel.Log]: console.log,
[LogLevel.Warn]: console.warn,
[LogLevel.Error]: console.error,
}[level]
log(`${format(timestamp, 'HH:mm:ss')} ${message.toString()}${extra}`)
}
export const sentryTransport: Transport = (
level,
message,
{type, tags, ...metadata},
timestamp,
) => {
/**
* If a string, report a breadcrumb
*/
if (typeof message === 'string') {
const severity = (
{
[LogLevel.Debug]: 'debug',
[LogLevel.Info]: 'info',
[LogLevel.Log]: 'log', // Sentry value here is undefined
[LogLevel.Warn]: 'warning',
[LogLevel.Error]: 'error',
} as const
)[level]
Sentry.addBreadcrumb({
message,
data: metadata,
type: type || 'default',
level: severity,
timestamp: timestamp / 1000, // Sentry expects seconds
})
/**
* Send all higher levels with `captureMessage`, with appropriate severity
* level
*/
if (level === 'error' || level === 'warn' || level === 'log') {
const messageLevel = ({
[LogLevel.Log]: 'log',
[LogLevel.Warn]: 'warning',
[LogLevel.Error]: 'error',
}[level] || 'log') as Sentry.Breadcrumb['level']
Sentry.captureMessage(message, {
level: messageLevel,
tags,
extra: metadata,
})
}
} else {
/**
* It's otherwise an Error and should be reported with captureException
*/
Sentry.captureException(message, {
tags,
extra: metadata,
})
}
}
/**
* Main class. Defaults are provided in the constructor so that subclasses are
* technically possible, if we need to go that route in the future.
*/
export class Logger {
LogLevel = LogLevel
DebugContext = DebugContext
enabled: boolean
level: LogLevel
transports: Transport[] = []
protected debugContextRegexes: RegExp[] = []
constructor({
enabled = !env.IS_TEST,
level = env.LOG_LEVEL as LogLevel,
debug = env.LOG_DEBUG || '',
}: {
enabled?: boolean
level?: LogLevel
debug?: string
} = {}) {
this.enabled = enabled !== false
this.level = debug ? LogLevel.Debug : level ?? LogLevel.Info // default to info
this.debugContextRegexes = (debug || '').split(',').map(context => {
return new RegExp(context.replace(/[^\w:*]/, '').replace(/\*/g, '.*'))
})
}
debug(message: string, metadata: Metadata = {}, context?: string) {
if (context && !this.debugContextRegexes.find(reg => reg.test(context)))
return
this.transport(LogLevel.Debug, message, metadata)
}
info(message: string, metadata: Metadata = {}) {
this.transport(LogLevel.Info, message, metadata)
}
log(message: string, metadata: Metadata = {}) {
this.transport(LogLevel.Log, message, metadata)
}
warn(message: string, metadata: Metadata = {}) {
this.transport(LogLevel.Warn, message, metadata)
}
error(error: Error | string, metadata: Metadata = {}) {
this.transport(LogLevel.Error, error, metadata)
}
addTransport(transport: Transport) {
this.transports.push(transport)
return () => {
this.transports.splice(this.transports.indexOf(transport), 1)
}
}
disable() {
this.enabled = false
}
enable() {
this.enabled = true
}
protected transport(
level: LogLevel,
message: string | Error,
metadata: Metadata = {},
) {
if (!this.enabled) return
if (!enabledLogLevels[this.level].includes(level)) return
const timestamp = Date.now()
const meta = metadata || {}
for (const transport of this.transports) {
transport(level, message, meta, timestamp)
}
add({
id: nanoid(),
timestamp,
level,
message,
metadata: meta,
})
}
}
/**
* Logger instance. See `@/logger/README` for docs.
*
* Basic usage:
*
* `logger.debug(message[, metadata, debugContext])`
* `logger.info(message[, metadata])`
* `logger.warn(message[, metadata])`
* `logger.error(error[, metadata])`
* `logger.disable()`
* `logger.enable()`
*/
export const logger = new Logger()
/**
* Report to console in dev, Sentry in prod, nothing in test.
*/
if (env.IS_DEV && !env.IS_TEST) {
logger.addTransport(consoleTransport)
/**
* Uncomment this to test Sentry in dev
*/
// logger.addTransport(sentryTransport);
} else if (env.IS_PROD) {
// logger.addTransport(sentryTransport)
}

View File

@ -0,0 +1,12 @@
import type {ConsoleTransportEntry} from '#/logger'
let entries: ConsoleTransportEntry[] = []
export function add(entry: ConsoleTransportEntry) {
entries.unshift(entry)
entries = entries.slice(0, 50)
}
export function getEntries() {
return entries
}

View File

@ -0,0 +1 @@
export {Native as Sentry} from 'sentry-expo'

View File

@ -0,0 +1 @@
export {Browser as Sentry} from 'sentry-expo'

View File

@ -25,7 +25,7 @@ export async function setupState(serviceUri = DEFAULT_SERVICE) {
rootStore.log.debug('Initial hydrate', {hasSession: !!data.session}) rootStore.log.debug('Initial hydrate', {hasSession: !!data.session})
rootStore.hydrate(data) rootStore.hydrate(data)
} catch (e: any) { } catch (e: any) {
rootStore.log.error('Failed to load state from storage', e) rootStore.log.error('Failed to load state from storage', {error: e})
} }
rootStore.attemptSessionResumption() rootStore.attemptSessionResumption()

View File

@ -134,7 +134,7 @@ export class FeedSourceModel {
try { try {
await this.rootStore.preferences.addSavedFeed(this.uri) await this.rootStore.preferences.addSavedFeed(this.uri)
} catch (error) { } catch (error) {
this.rootStore.log.error('Failed to save feed', error) this.rootStore.log.error('Failed to save feed', {error})
} finally { } finally {
track('CustomFeed:Save') track('CustomFeed:Save')
} }
@ -147,7 +147,7 @@ export class FeedSourceModel {
try { try {
await this.rootStore.preferences.removeSavedFeed(this.uri) await this.rootStore.preferences.removeSavedFeed(this.uri)
} catch (error) { } catch (error) {
this.rootStore.log.error('Failed to unsave feed', error) this.rootStore.log.error('Failed to unsave feed', {error})
} finally { } finally {
track('CustomFeed:Unsave') track('CustomFeed:Unsave')
} }
@ -157,7 +157,7 @@ export class FeedSourceModel {
try { try {
await this.rootStore.preferences.addPinnedFeed(this.uri) await this.rootStore.preferences.addPinnedFeed(this.uri)
} catch (error) { } catch (error) {
this.rootStore.log.error('Failed to pin feed', error) this.rootStore.log.error('Failed to pin feed', {error})
} finally { } finally {
track('CustomFeed:Pin', { track('CustomFeed:Pin', {
name: this.displayName, name: this.displayName,
@ -194,7 +194,7 @@ export class FeedSourceModel {
} catch (e: any) { } catch (e: any) {
this.likeUri = undefined this.likeUri = undefined
this.likeCount = (this.likeCount || 1) - 1 this.likeCount = (this.likeCount || 1) - 1
this.rootStore.log.error('Failed to like feed', e) this.rootStore.log.error('Failed to like feed', {error: e})
} finally { } finally {
track('CustomFeed:Like') track('CustomFeed:Like')
} }
@ -215,7 +215,7 @@ export class FeedSourceModel {
} catch (e: any) { } catch (e: any) {
this.likeUri = uri this.likeUri = uri
this.likeCount = (this.likeCount || 0) + 1 this.likeCount = (this.likeCount || 0) + 1
this.rootStore.log.error('Failed to unlike feed', e) this.rootStore.log.error('Failed to unlike feed', {error: e})
} finally { } finally {
track('CustomFeed:Unlike') track('CustomFeed:Unlike')
} }

View File

@ -339,7 +339,7 @@ export class ListModel {
try { try {
await this.rootStore.preferences.addPinnedFeed(this.uri) await this.rootStore.preferences.addPinnedFeed(this.uri)
} catch (error) { } catch (error) {
this.rootStore.log.error('Failed to pin feed', error) this.rootStore.log.error('Failed to pin feed', {error})
} finally { } finally {
track('CustomFeed:Pin', { track('CustomFeed:Pin', {
name: this.data?.name || '', name: this.data?.name || '',
@ -455,10 +455,12 @@ export class ListModel {
this.error = cleanError(err) this.error = cleanError(err)
this.loadMoreError = cleanError(loadMoreErr) this.loadMoreError = cleanError(loadMoreErr)
if (err) { if (err) {
this.rootStore.log.error('Failed to fetch user items', err) this.rootStore.log.error('Failed to fetch user items', {error: err})
} }
if (loadMoreErr) { if (loadMoreErr) {
this.rootStore.log.error('Failed to fetch user items', loadMoreErr) this.rootStore.log.error('Failed to fetch user items', {
error: loadMoreErr,
})
} }
} }

View File

@ -163,7 +163,7 @@ export class PostThreadModel {
this.hasLoaded = true this.hasLoaded = true
this.error = cleanError(err) this.error = cleanError(err)
if (err) { if (err) {
this.rootStore.log.error('Failed to fetch post thread', err) this.rootStore.log.error('Failed to fetch post thread', {error: err})
} }
this.notFound = err instanceof GetPostThread.NotFoundError this.notFound = err instanceof GetPostThread.NotFoundError
} }

View File

@ -235,7 +235,7 @@ export class ProfileModel {
this.hasLoaded = true this.hasLoaded = true
this.error = cleanError(err) this.error = cleanError(err)
if (err) { if (err) {
this.rootStore.log.error('Failed to fetch profile', err) this.rootStore.log.error('Failed to fetch profile', {error: err})
} }
} }

View File

@ -120,7 +120,7 @@ export class FeedsDiscoveryModel {
this.hasLoaded = true this.hasLoaded = true
this.error = cleanError(err) this.error = cleanError(err)
if (err) { if (err) {
this.rootStore.log.error('Failed to fetch popular feeds', err) this.rootStore.log.error('Failed to fetch popular feeds', {error: err})
} }
} }

View File

@ -144,7 +144,7 @@ export class SuggestedActorsModel {
this.hasLoaded = true this.hasLoaded = true
this.error = cleanError(err) this.error = cleanError(err)
if (err) { if (err) {
this.rootStore.log.error('Failed to fetch suggested actors', err) this.rootStore.log.error('Failed to fetch suggested actors', {error: err})
} }
} }
} }

View File

@ -220,7 +220,7 @@ export class NotificationsFeedItemModel {
} }
this.rootStore.log.warn( this.rootStore.log.warn(
'app.bsky.notifications.list served an unsupported record type', 'app.bsky.notifications.list served an unsupported record type',
v, {record: v},
) )
} }
@ -401,7 +401,9 @@ export class NotificationsFeedModel {
this._setQueued(this._filterNotifications(queueModels)) this._setQueued(this._filterNotifications(queueModels))
this._countUnread() this._countUnread()
} catch (e) { } catch (e) {
this.rootStore.log.error('NotificationsModel:syncQueue failed', {e}) this.rootStore.log.error('NotificationsModel:syncQueue failed', {
error: e,
})
} finally { } finally {
this.lock.release() this.lock.release()
} }
@ -481,7 +483,9 @@ export class NotificationsFeedModel {
this.lastSync ? this.lastSync.toISOString() : undefined, this.lastSync ? this.lastSync.toISOString() : undefined,
) )
} catch (e: any) { } catch (e: any) {
this.rootStore.log.warn('Failed to update notifications read state', e) this.rootStore.log.warn('Failed to update notifications read state', {
error: e,
})
} }
} }
@ -501,13 +505,12 @@ export class NotificationsFeedModel {
this.error = cleanError(error) this.error = cleanError(error)
this.loadMoreError = cleanError(loadMoreError) this.loadMoreError = cleanError(loadMoreError)
if (error) { if (error) {
this.rootStore.log.error('Failed to fetch notifications', error) this.rootStore.log.error('Failed to fetch notifications', {error})
} }
if (loadMoreError) { if (loadMoreError) {
this.rootStore.log.error( this.rootStore.log.error('Failed to load more notifications', {
'Failed to load more notifications', error: loadMoreError,
loadMoreError, })
)
} }
} }

View File

@ -42,17 +42,16 @@ export class PostsFeedItemModel {
} else { } else {
this.postRecord = undefined this.postRecord = undefined
this.richText = undefined this.richText = undefined
rootStore.log.warn( rootStore.log.warn('Received an invalid app.bsky.feed.post record', {
'Received an invalid app.bsky.feed.post record', error: valid.error,
valid.error, })
)
} }
} else { } else {
this.postRecord = undefined this.postRecord = undefined
this.richText = undefined this.richText = undefined
rootStore.log.warn( rootStore.log.warn(
'app.bsky.feed.getTimeline or app.bsky.feed.getAuthorFeed served an unexpected record type', 'app.bsky.feed.getTimeline or app.bsky.feed.getAuthorFeed served an unexpected record type',
this.post.record, {record: this.post.record},
) )
} }
this.reply = v.reply this.reply = v.reply
@ -133,7 +132,7 @@ export class PostsFeedItemModel {
track('Post:Like') track('Post:Like')
} }
} catch (error) { } catch (error) {
this.rootStore.log.error('Failed to toggle like', error) this.rootStore.log.error('Failed to toggle like', {error})
} }
} }
@ -168,7 +167,7 @@ export class PostsFeedItemModel {
track('Post:Repost') track('Post:Repost')
} }
} catch (error) { } catch (error) {
this.rootStore.log.error('Failed to toggle repost', error) this.rootStore.log.error('Failed to toggle repost', {error})
} }
} }
@ -182,7 +181,7 @@ export class PostsFeedItemModel {
track('Post:ThreadMute') track('Post:ThreadMute')
} }
} catch (error) { } catch (error) {
this.rootStore.log.error('Failed to toggle thread mute', error) this.rootStore.log.error('Failed to toggle thread mute', {error})
} }
} }
@ -191,7 +190,7 @@ export class PostsFeedItemModel {
await this.rootStore.agent.deletePost(this.post.uri) await this.rootStore.agent.deletePost(this.post.uri)
this.rootStore.emitPostDeleted(this.post.uri) this.rootStore.emitPostDeleted(this.post.uri)
} catch (error) { } catch (error) {
this.rootStore.log.error('Failed to delete post', error) this.rootStore.log.error('Failed to delete post', {error})
} finally { } finally {
track('Post:Delete') track('Post:Delete')
} }

View File

@ -324,13 +324,12 @@ export class PostsFeedModel {
this.knownError = detectKnownError(this.feedType, error) this.knownError = detectKnownError(this.feedType, error)
this.loadMoreError = cleanError(loadMoreError) this.loadMoreError = cleanError(loadMoreError)
if (error) { if (error) {
this.rootStore.log.error('Posts feed request failed', error) this.rootStore.log.error('Posts feed request failed', {error})
} }
if (loadMoreError) { if (loadMoreError) {
this.rootStore.log.error( this.rootStore.log.error('Posts feed load-more request failed', {
'Posts feed load-more request failed', error: loadMoreError,
loadMoreError, })
)
} }
} }

View File

@ -63,10 +63,9 @@ export class InvitedUsers {
}) })
this.rootStore.me.follows.hydrateMany(this.profiles) this.rootStore.me.follows.hydrateMany(this.profiles)
} catch (e) { } catch (e) {
this.rootStore.log.error( this.rootStore.log.error('Failed to fetch profiles for invited users', {
'Failed to fetch profiles for invited users', error: e,
e, })
)
} }
} }
} }

View File

@ -98,7 +98,7 @@ export class ActorFeedsModel {
this.hasLoaded = true this.hasLoaded = true
this.error = cleanError(err) this.error = cleanError(err)
if (err) { if (err) {
this.rootStore.log.error('Failed to fetch user followers', err) this.rootStore.log.error('Failed to fetch user followers', {error: err})
} }
} }

View File

@ -86,7 +86,7 @@ export class BlockedAccountsModel {
this.hasLoaded = true this.hasLoaded = true
this.error = cleanError(err) this.error = cleanError(err)
if (err) { if (err) {
this.rootStore.log.error('Failed to fetch user followers', err) this.rootStore.log.error('Failed to fetch user followers', {error: err})
} }
} }

View File

@ -97,7 +97,7 @@ export class LikesModel {
this.hasLoaded = true this.hasLoaded = true
this.error = cleanError(err) this.error = cleanError(err)
if (err) { if (err) {
this.rootStore.log.error('Failed to fetch likes', err) this.rootStore.log.error('Failed to fetch likes', {error: err})
} }
} }

View File

@ -204,10 +204,12 @@ export class ListsListModel {
this.error = cleanError(err) this.error = cleanError(err)
this.loadMoreError = cleanError(loadMoreErr) this.loadMoreError = cleanError(loadMoreErr)
if (err) { if (err) {
this.rootStore.log.error('Failed to fetch user lists', err) this.rootStore.log.error('Failed to fetch user lists', {error: err})
} }
if (loadMoreErr) { if (loadMoreErr) {
this.rootStore.log.error('Failed to fetch user lists', loadMoreErr) this.rootStore.log.error('Failed to fetch user lists', {
error: loadMoreErr,
})
} }
} }

View File

@ -86,7 +86,7 @@ export class MutedAccountsModel {
this.hasLoaded = true this.hasLoaded = true
this.error = cleanError(err) this.error = cleanError(err)
if (err) { if (err) {
this.rootStore.log.error('Failed to fetch user followers', err) this.rootStore.log.error('Failed to fetch user followers', {error: err})
} }
} }

View File

@ -100,7 +100,7 @@ export class RepostedByModel {
this.hasLoaded = true this.hasLoaded = true
this.error = cleanError(err) this.error = cleanError(err)
if (err) { if (err) {
this.rootStore.log.error('Failed to fetch reposted by view', err) this.rootStore.log.error('Failed to fetch reposted by view', {error: err})
} }
} }

View File

@ -99,7 +99,7 @@ export class UserFollowersModel {
this.hasLoaded = true this.hasLoaded = true
this.error = cleanError(err) this.error = cleanError(err)
if (err) { if (err) {
this.rootStore.log.error('Failed to fetch user followers', err) this.rootStore.log.error('Failed to fetch user followers', {error: err})
} }
} }

View File

@ -110,13 +110,17 @@ export class MeModel {
await this.fetchProfile() await this.fetchProfile()
this.mainFeed.clear() this.mainFeed.clear()
/* dont await */ this.mainFeed.setup().catch(e => { /* dont await */ this.mainFeed.setup().catch(e => {
this.rootStore.log.error('Failed to setup main feed model', e) this.rootStore.log.error('Failed to setup main feed model', {error: e})
}) })
/* dont await */ this.notifications.setup().catch(e => { /* dont await */ this.notifications.setup().catch(e => {
this.rootStore.log.error('Failed to setup notifications model', e) this.rootStore.log.error('Failed to setup notifications model', {
error: e,
})
}) })
/* dont await */ this.notifications.setup().catch(e => { /* dont await */ this.notifications.setup().catch(e => {
this.rootStore.log.error('Failed to setup notifications model', e) this.rootStore.log.error('Failed to setup notifications model', {
error: e,
})
}) })
this.myFeeds.clear() this.myFeeds.clear()
/* dont await */ this.myFeeds.saved.refresh() /* dont await */ this.myFeeds.saved.refresh()
@ -184,7 +188,9 @@ export class MeModel {
}) })
}) })
} catch (e) { } catch (e) {
this.rootStore.log.error('Failed to fetch user invite codes', e) this.rootStore.log.error('Failed to fetch user invite codes', {
error: e,
})
} }
await this.rootStore.invitedUsers.fetch(this.invites) await this.rootStore.invitedUsers.fetch(this.invites)
} }
@ -199,7 +205,9 @@ export class MeModel {
this.appPasswords = res.data.passwords this.appPasswords = res.data.passwords
}) })
} catch (e) { } catch (e) {
this.rootStore.log.error('Failed to fetch user app passwords', e) this.rootStore.log.error('Failed to fetch user app passwords', {
error: e,
})
} }
} }
} }
@ -220,7 +228,7 @@ export class MeModel {
}) })
return res.data return res.data
} catch (e) { } catch (e) {
this.rootStore.log.error('Failed to create app password', e) this.rootStore.log.error('Failed to create app password', {error: e})
} }
} }
} }
@ -235,7 +243,7 @@ export class MeModel {
this.appPasswords = this.appPasswords.filter(p => p.name !== name) this.appPasswords = this.appPasswords.filter(p => p.name !== name)
}) })
} catch (e) { } catch (e) {
this.rootStore.log.error('Failed to delete app password', e) this.rootStore.log.error('Failed to delete app password', {error: e})
} }
} }
} }

View File

@ -188,7 +188,7 @@ export class ImageModel implements Omit<RNImage, 'size'> {
this.cropped = cropped this.cropped = cropped
}) })
} catch (err) { } catch (err) {
this.rootStore.log.error('Failed to crop photo', err) this.rootStore.log.error('Failed to crop photo', {error: err})
} }
} }

View File

@ -8,7 +8,6 @@ import {createContext, useContext} from 'react'
import {DeviceEventEmitter, EmitterSubscription} from 'react-native' import {DeviceEventEmitter, EmitterSubscription} from 'react-native'
import {z} from 'zod' import {z} from 'zod'
import {isObj, hasProp} from 'lib/type-guards' import {isObj, hasProp} from 'lib/type-guards'
import {LogModel} from './log'
import {SessionModel} from './session' import {SessionModel} from './session'
import {ShellUiModel} from './ui/shell' import {ShellUiModel} from './ui/shell'
import {HandleResolutionsCache} from './cache/handle-resolutions' import {HandleResolutionsCache} from './cache/handle-resolutions'
@ -23,6 +22,7 @@ import {ImageSizesCache} from './cache/image-sizes'
import {MutedThreads} from './muted-threads' import {MutedThreads} from './muted-threads'
import {Reminders} from './ui/reminders' import {Reminders} from './ui/reminders'
import {reset as resetNavigation} from '../../Navigation' import {reset as resetNavigation} from '../../Navigation'
import {logger} from '#/logger'
// TEMPORARY (APP-700) // TEMPORARY (APP-700)
// remove after backend testing finishes // remove after backend testing finishes
@ -41,7 +41,7 @@ export type AppInfo = z.infer<typeof appInfo>
export class RootStoreModel { export class RootStoreModel {
agent: BskyAgent agent: BskyAgent
appInfo?: AppInfo appInfo?: AppInfo
log = new LogModel() log = logger
session = new SessionModel(this) session = new SessionModel(this)
shell = new ShellUiModel(this) shell = new ShellUiModel(this)
preferences = new PreferencesModel(this) preferences = new PreferencesModel(this)
@ -130,7 +130,7 @@ export class RootStoreModel {
}) })
this.updateSessionState() this.updateSessionState()
} catch (e: any) { } catch (e: any) {
this.log.warn('Failed to initialize session', e) this.log.warn('Failed to initialize session', {error: e})
} }
} }
@ -184,7 +184,7 @@ export class RootStoreModel {
await this.me.updateIfNeeded() await this.me.updateIfNeeded()
await this.preferences.sync() await this.preferences.sync()
} catch (e: any) { } catch (e: any) {
this.log.error('Failed to fetch latest state', e) this.log.error('Failed to fetch latest state', {error: e})
} }
} }

View File

@ -78,7 +78,7 @@ export class CreateAccountModel {
} catch (err: any) { } catch (err: any) {
this.rootStore.log.warn( this.rootStore.log.warn(
`Failed to fetch service description for ${this.serviceUrl}`, `Failed to fetch service description for ${this.serviceUrl}`,
err, {error: err},
) )
this.setError( this.setError(
'Unable to contact your service. Please check your Internet connection.', 'Unable to contact your service. Please check your Internet connection.',
@ -127,7 +127,7 @@ export class CreateAccountModel {
errMsg = errMsg =
'Invite code not accepted. Check that you input it correctly and try again.' 'Invite code not accepted. Check that you input it correctly and try again.'
} }
this.rootStore.log.error('Failed to create account', e) this.rootStore.log.error('Failed to create account', {error: e})
this.setIsProcessing(false) this.setIsProcessing(false)
this.setError(cleanError(errMsg)) this.setError(cleanError(errMsg))
throw e throw e

View File

@ -223,10 +223,14 @@ export class ProfileUiModel {
await Promise.all([ await Promise.all([
this.profile this.profile
.setup() .setup()
.catch(err => this.rootStore.log.error('Failed to fetch profile', err)), .catch(err =>
this.rootStore.log.error('Failed to fetch profile', {error: err}),
),
this.feed this.feed
.setup() .setup()
.catch(err => this.rootStore.log.error('Failed to fetch feed', err)), .catch(err =>
this.rootStore.log.error('Failed to fetch feed', {error: err}),
),
]) ])
runInAction(() => { runInAction(() => {
this.isAuthenticatedUser = this.isAuthenticatedUser =
@ -237,7 +241,9 @@ export class ProfileUiModel {
this.lists.source = this.profile.did this.lists.source = this.profile.did
this.lists this.lists
.loadMore() .loadMore()
.catch(err => this.rootStore.log.error('Failed to fetch lists', err)) .catch(err =>
this.rootStore.log.error('Failed to fetch lists', {error: err}),
)
} }
async refresh() { async refresh() {

View File

@ -126,7 +126,7 @@ export class SavedFeedsModel {
this.hasLoaded = true this.hasLoaded = true
this.error = cleanError(err) this.error = cleanError(err)
if (err) { if (err) {
this.rootStore.log.error('Failed to fetch user feeds', err) this.rootStore.log.error('Failed to fetch user feeds', {err})
} }
} }

View File

@ -83,7 +83,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => {
} }
store.log.warn( store.log.warn(
`Failed to fetch service description for ${serviceUrl}`, `Failed to fetch service description for ${serviceUrl}`,
err, {error: err},
) )
setError( setError(
'Unable to contact your service. Please check your Internet connection.', 'Unable to contact your service. Please check your Internet connection.',
@ -349,7 +349,7 @@ const LoginForm = ({
}) })
} catch (e: any) { } catch (e: any) {
const errMsg = e.toString() const errMsg = e.toString()
store.log.warn('Failed to login', e) store.log.warn('Failed to login', {error: e})
setIsProcessing(false) setIsProcessing(false)
if (errMsg.includes('Authentication Required')) { if (errMsg.includes('Authentication Required')) {
setError('Invalid username or password') setError('Invalid username or password')
@ -578,7 +578,7 @@ const ForgotPasswordForm = ({
onEmailSent() onEmailSent()
} catch (e: any) { } catch (e: any) {
const errMsg = e.toString() const errMsg = e.toString()
store.log.warn('Failed to request password reset', e) store.log.warn('Failed to request password reset', {error: e})
setIsProcessing(false) setIsProcessing(false)
if (isNetworkError(e)) { if (isNetworkError(e)) {
setError( setError(
@ -734,7 +734,7 @@ const SetNewPasswordForm = ({
onPasswordSet() onPasswordSet()
} catch (e: any) { } catch (e: any) {
const errMsg = e.toString() const errMsg = e.toString()
store.log.warn('Failed to set new password', e) store.log.warn('Failed to set new password', {error: e})
setIsProcessing(false) setIsProcessing(false)
if (isNetworkError(e)) { if (isNetworkError(e)) {
setError( setError(

View File

@ -39,7 +39,7 @@ export function OpenCameraBtn({gallery}: Props) {
gallery.add(img) gallery.add(img)
} catch (err: any) { } catch (err: any) {
// ignore // ignore
store.log.warn('Error using camera', err) store.log.warn('Error using camera', {error: err})
} }
}, [gallery, track, store, requestCameraAccessIfNeeded]) }, [gallery, track, store, requestCameraAccessIfNeeded])

View File

@ -46,7 +46,9 @@ export function useExternalLinkFetch({
setExtLink(undefined) setExtLink(undefined)
}, },
err => { err => {
store.log.error('Failed to fetch post for quote embedding', {err}) store.log.error('Failed to fetch post for quote embedding', {
error: err,
})
setExtLink(undefined) setExtLink(undefined)
}, },
) )
@ -64,7 +66,7 @@ export function useExternalLinkFetch({
}) })
}, },
err => { err => {
store.log.error('Failed to fetch feed for embedding', {err}) store.log.error('Failed to fetch feed for embedding', {error: err})
setExtLink(undefined) setExtLink(undefined)
}, },
) )
@ -82,7 +84,7 @@ export function useExternalLinkFetch({
}) })
}, },
err => { err => {
store.log.error('Failed to fetch list for embedding', {err}) store.log.error('Failed to fetch list for embedding', {error: err})
setExtLink(undefined) setExtLink(undefined)
}, },
) )

View File

@ -45,7 +45,7 @@ export const FeedSourceCard = observer(function FeedSourceCardImpl({
Toast.show('Removed from my feeds') Toast.show('Removed from my feeds')
} catch (e) { } catch (e) {
Toast.show('There was an issue contacting your server') Toast.show('There was an issue contacting your server')
store.log.error('Failed to unsave feed', {e}) store.log.error('Failed to unsave feed', {error: e})
} }
}, },
}) })
@ -55,7 +55,7 @@ export const FeedSourceCard = observer(function FeedSourceCardImpl({
Toast.show('Added to my feeds') Toast.show('Added to my feeds')
} catch (e) { } catch (e) {
Toast.show('There was an issue contacting your server') Toast.show('There was an issue contacting your server')
store.log.error('Failed to save feed', {e}) store.log.error('Failed to save feed', {error: e})
} }
} }
}, [store, item]) }, [store, item])

View File

@ -94,7 +94,7 @@ export const ListItems = observer(function ListItemsImpl({
try { try {
await list.refresh() await list.refresh()
} catch (err) { } catch (err) {
list.rootStore.log.error('Failed to refresh lists', err) list.rootStore.log.error('Failed to refresh lists', {error: err})
} }
setIsRefreshing(false) setIsRefreshing(false)
}, [list, track, setIsRefreshing]) }, [list, track, setIsRefreshing])
@ -104,7 +104,7 @@ export const ListItems = observer(function ListItemsImpl({
try { try {
await list.loadMore() await list.loadMore()
} catch (err) { } catch (err) {
list.rootStore.log.error('Failed to load more lists', err) list.rootStore.log.error('Failed to load more lists', {error: err})
} }
}, [list, track]) }, [list, track])

View File

@ -78,7 +78,7 @@ export const ListsList = observer(function ListsListImpl({
try { try {
await listsList.refresh() await listsList.refresh()
} catch (err) { } catch (err) {
listsList.rootStore.log.error('Failed to refresh lists', err) listsList.rootStore.log.error('Failed to refresh lists', {error: err})
} }
setIsRefreshing(false) setIsRefreshing(false)
}, [listsList, track, setIsRefreshing]) }, [listsList, track, setIsRefreshing])
@ -88,7 +88,7 @@ export const ListsList = observer(function ListsListImpl({
try { try {
await listsList.loadMore() await listsList.loadMore()
} catch (err) { } catch (err) {
listsList.rootStore.log.error('Failed to load more lists', err) listsList.rootStore.log.error('Failed to load more lists', {error: err})
} }
}, [listsList, track]) }, [listsList, track])

View File

@ -95,7 +95,7 @@ export function Component({}: {}) {
} }
} catch (e) { } catch (e) {
Toast.show('Failed to create app password.') Toast.show('Failed to create app password.')
store.log.error('Failed to create app password', {e}) store.log.error('Failed to create app password', {error: e})
} }
} }

View File

@ -69,7 +69,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
`Failed to fetch service description for ${String( `Failed to fetch service description for ${String(
store.agent.service, store.agent.service,
)}`, )}`,
err, {error: err},
) )
setError( setError(
'Unable to contact your service. Please check your Internet connection.', 'Unable to contact your service. Please check your Internet connection.',
@ -113,7 +113,7 @@ export function Component({onChanged}: {onChanged: () => void}) {
onChanged() onChanged()
} catch (err: any) { } catch (err: any) {
setError(cleanError(err)) setError(cleanError(err))
store.log.error('Failed to update handle', {handle, err}) store.log.error('Failed to update handle', {handle, error: err})
} finally { } finally {
setProcessing(false) setProcessing(false)
} }
@ -343,7 +343,7 @@ function CustomHandleForm({
} }
} catch (err: any) { } catch (err: any) {
setError(cleanError(err)) setError(cleanError(err))
store.log.error('Failed to verify domain', {handle, err}) store.log.error('Failed to verify domain', {handle, error: err})
} finally { } finally {
setIsVerifying(false) setIsVerifying(false)
} }

View File

@ -103,7 +103,7 @@ const AdultContentEnabledPref = observer(
Toast.show( Toast.show(
'There was an issue syncing your preferences with the server', 'There was an issue syncing your preferences with the server',
) )
store.log.error('Failed to update preferences with server', {e}) store.log.error('Failed to update preferences with server', {error: e})
} }
} }
@ -168,7 +168,7 @@ const ContentLabelPref = observer(function ContentLabelPrefImpl({
Toast.show( Toast.show(
'There was an issue syncing your preferences with the server', 'There was an issue syncing your preferences with the server',
) )
store.log.error('Failed to update preferences with server', {e}) store.log.error('Failed to update preferences with server', {error: e})
} }
}, },
[store, group], [store, group],

View File

@ -62,7 +62,7 @@ export const Component = observer(function UserAddRemoveListsImpl({
setMembershipsLoaded(true) setMembershipsLoaded(true)
}, },
err => { err => {
store.log.error('Failed to fetch memberships', {err}) store.log.error('Failed to fetch memberships', {error: err})
}, },
) )
}, [memberships, listsList, store, setSelected, setMembershipsLoaded]) }, [memberships, listsList, store, setSelected, setMembershipsLoaded])
@ -76,7 +76,7 @@ export const Component = observer(function UserAddRemoveListsImpl({
try { try {
changes = await memberships.updateTo(selected) changes = await memberships.updateTo(selected)
} catch (err) { } catch (err) {
store.log.error('Failed to update memberships', {err}) store.log.error('Failed to update memberships', {error: err})
return return
} }
Toast.show('Lists updated') Toast.show('Lists updated')

View File

@ -61,7 +61,9 @@ export const Feed = observer(function Feed({
setIsPTRing(true) setIsPTRing(true)
await view.refresh() await view.refresh()
} catch (err) { } catch (err) {
view.rootStore.log.error('Failed to refresh notifications feed', err) view.rootStore.log.error('Failed to refresh notifications feed', {
error: err,
})
} finally { } finally {
setIsPTRing(false) setIsPTRing(false)
} }
@ -71,7 +73,9 @@ export const Feed = observer(function Feed({
try { try {
await view.loadMore() await view.loadMore()
} catch (err) { } catch (err) {
view.rootStore.log.error('Failed to load more notifications', err) view.rootStore.log.error('Failed to load more notifications', {
error: err,
})
} }
}, [view]) }, [view])

View File

@ -18,7 +18,9 @@ export const PostLikedBy = observer(function PostLikedByImpl({
const view = React.useMemo(() => new LikesModel(store, {uri}), [store, uri]) const view = React.useMemo(() => new LikesModel(store, {uri}), [store, uri])
useEffect(() => { useEffect(() => {
view.loadMore().catch(err => store.log.error('Failed to fetch likes', err)) view
.loadMore()
.catch(err => store.log.error('Failed to fetch likes', {error: err}))
}, [view, store.log]) }, [view, store.log])
const onRefresh = () => { const onRefresh = () => {
@ -27,7 +29,9 @@ export const PostLikedBy = observer(function PostLikedByImpl({
const onEndReached = () => { const onEndReached = () => {
view view
.loadMore() .loadMore()
.catch(err => view?.rootStore.log.error('Failed to load more likes', err)) .catch(err =>
view?.rootStore.log.error('Failed to load more likes', {error: err}),
)
} }
if (!view.hasLoaded) { if (!view.hasLoaded) {

View File

@ -23,7 +23,7 @@ export const PostRepostedBy = observer(function PostRepostedByImpl({
useEffect(() => { useEffect(() => {
view view
.loadMore() .loadMore()
.catch(err => store.log.error('Failed to fetch reposts', err)) .catch(err => store.log.error('Failed to fetch reposts', {error: err}))
}, [view, store.log]) }, [view, store.log])
const onRefresh = () => { const onRefresh = () => {
@ -33,7 +33,7 @@ export const PostRepostedBy = observer(function PostRepostedByImpl({
view view
.loadMore() .loadMore()
.catch(err => .catch(err =>
view?.rootStore.log.error('Failed to load more reposts', err), view?.rootStore.log.error('Failed to load more reposts', {error: err}),
) )
} }

View File

@ -119,7 +119,7 @@ export const PostThread = observer(function PostThread({
try { try {
view?.refresh() view?.refresh()
} catch (err) { } catch (err) {
view.rootStore.log.error('Failed to refresh posts thread', err) view.rootStore.log.error('Failed to refresh posts thread', {error: err})
} }
setIsRefreshing(false) setIsRefreshing(false)
}, [view, setIsRefreshing]) }, [view, setIsRefreshing])

View File

@ -111,13 +111,13 @@ export const PostThreadItem = observer(function PostThreadItem({
const onPressToggleRepost = React.useCallback(() => { const onPressToggleRepost = React.useCallback(() => {
return item return item
.toggleRepost() .toggleRepost()
.catch(e => store.log.error('Failed to toggle repost', e)) .catch(e => store.log.error('Failed to toggle repost', {error: e}))
}, [item, store]) }, [item, store])
const onPressToggleLike = React.useCallback(() => { const onPressToggleLike = React.useCallback(() => {
return item return item
.toggleLike() .toggleLike()
.catch(e => store.log.error('Failed to toggle like', e)) .catch(e => store.log.error('Failed to toggle like', {error: e}))
}, [item, store]) }, [item, store])
const onCopyPostText = React.useCallback(() => { const onCopyPostText = React.useCallback(() => {
@ -138,7 +138,7 @@ export const PostThreadItem = observer(function PostThreadItem({
Toast.show('You will now receive notifications for this thread') Toast.show('You will now receive notifications for this thread')
} }
} catch (e) { } catch (e) {
store.log.error('Failed to toggle thread mute', e) store.log.error('Failed to toggle thread mute', {error: e})
} }
}, [item, store]) }, [item, store])
@ -149,7 +149,7 @@ export const PostThreadItem = observer(function PostThreadItem({
Toast.show('Post deleted') Toast.show('Post deleted')
}, },
e => { e => {
store.log.error('Failed to delete post', e) store.log.error('Failed to delete post', {error: e})
Toast.show('Failed to delete post, please try again') Toast.show('Failed to delete post, please try again')
}, },
) )

View File

@ -142,13 +142,13 @@ const PostLoaded = observer(function PostLoadedImpl({
const onPressToggleRepost = React.useCallback(() => { const onPressToggleRepost = React.useCallback(() => {
return item return item
.toggleRepost() .toggleRepost()
.catch(e => store.log.error('Failed to toggle repost', e)) .catch(e => store.log.error('Failed to toggle repost', {error: e}))
}, [item, store]) }, [item, store])
const onPressToggleLike = React.useCallback(() => { const onPressToggleLike = React.useCallback(() => {
return item return item
.toggleLike() .toggleLike()
.catch(e => store.log.error('Failed to toggle like', e)) .catch(e => store.log.error('Failed to toggle like', {error: e}))
}, [item, store]) }, [item, store])
const onCopyPostText = React.useCallback(() => { const onCopyPostText = React.useCallback(() => {
@ -169,7 +169,7 @@ const PostLoaded = observer(function PostLoadedImpl({
Toast.show('You will now receive notifications for this thread') Toast.show('You will now receive notifications for this thread')
} }
} catch (e) { } catch (e) {
store.log.error('Failed to toggle thread mute', e) store.log.error('Failed to toggle thread mute', {error: e})
} }
}, [item, store]) }, [item, store])
@ -180,7 +180,7 @@ const PostLoaded = observer(function PostLoadedImpl({
Toast.show('Post deleted') Toast.show('Post deleted')
}, },
e => { e => {
store.log.error('Failed to delete post', e) store.log.error('Failed to delete post', {error: e})
Toast.show('Failed to delete post, please try again') Toast.show('Failed to delete post, please try again')
}, },
) )

View File

@ -92,7 +92,7 @@ export const Feed = observer(function Feed({
try { try {
await feed.refresh() await feed.refresh()
} catch (err) { } catch (err) {
feed.rootStore.log.error('Failed to refresh posts feed', err) feed.rootStore.log.error('Failed to refresh posts feed', {error: err})
} }
setIsRefreshing(false) setIsRefreshing(false)
}, [feed, track, setIsRefreshing]) }, [feed, track, setIsRefreshing])
@ -104,7 +104,7 @@ export const Feed = observer(function Feed({
try { try {
await feed.loadMore() await feed.loadMore()
} catch (err) { } catch (err) {
feed.rootStore.log.error('Failed to load more posts', err) feed.rootStore.log.error('Failed to load more posts', {error: err})
} }
}, [feed, track]) }, [feed, track])

View File

@ -73,7 +73,7 @@ function FeedgenErrorMessage({
Toast.show( Toast.show(
'There was an an issue removing this feed. Please check your internet connection and try again.', 'There was an an issue removing this feed. Please check your internet connection and try again.',
) )
store.log.error('Failed to remove feed', {err}) store.log.error('Failed to remove feed', {error: err})
} }
}, },
onPressCancel() { onPressCancel() {

View File

@ -94,14 +94,14 @@ export const FeedItem = observer(function FeedItemImpl({
track('FeedItem:PostRepost') track('FeedItem:PostRepost')
return item return item
.toggleRepost() .toggleRepost()
.catch(e => store.log.error('Failed to toggle repost', e)) .catch(e => store.log.error('Failed to toggle repost', {error: e}))
}, [track, item, store]) }, [track, item, store])
const onPressToggleLike = React.useCallback(() => { const onPressToggleLike = React.useCallback(() => {
track('FeedItem:PostLike') track('FeedItem:PostLike')
return item return item
.toggleLike() .toggleLike()
.catch(e => store.log.error('Failed to toggle like', e)) .catch(e => store.log.error('Failed to toggle like', {error: e}))
}, [track, item, store]) }, [track, item, store])
const onCopyPostText = React.useCallback(() => { const onCopyPostText = React.useCallback(() => {
@ -123,7 +123,7 @@ export const FeedItem = observer(function FeedItemImpl({
Toast.show('You will now receive notifications for this thread') Toast.show('You will now receive notifications for this thread')
} }
} catch (e) { } catch (e) {
store.log.error('Failed to toggle thread mute', e) store.log.error('Failed to toggle thread mute', {error: e})
} }
}, [track, item, store]) }, [track, item, store])
@ -135,7 +135,7 @@ export const FeedItem = observer(function FeedItemImpl({
Toast.show('Post deleted') Toast.show('Post deleted')
}, },
e => { e => {
store.log.error('Failed to delete post', e) store.log.error('Failed to delete post', {error: e})
Toast.show('Failed to delete post, please try again') Toast.show('Failed to delete post, please try again')
}, },
) )

View File

@ -26,17 +26,19 @@ export const ProfileFollowers = observer(function ProfileFollowers({
useEffect(() => { useEffect(() => {
view view
.loadMore() .loadMore()
.catch(err => store.log.error('Failed to fetch user followers', err)) .catch(err =>
store.log.error('Failed to fetch user followers', {error: err}),
)
}, [view, store.log]) }, [view, store.log])
const onRefresh = () => { const onRefresh = () => {
view.refresh() view.refresh()
} }
const onEndReached = () => { const onEndReached = () => {
view view.loadMore().catch(err =>
.loadMore() view?.rootStore.log.error('Failed to load more followers', {
.catch(err => error: err,
view?.rootStore.log.error('Failed to load more followers', err), }),
) )
} }

View File

@ -150,7 +150,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
: 'ProfileHeader:UnfollowButtonClicked', : 'ProfileHeader:UnfollowButtonClicked',
) )
}, },
err => store.log.error('Failed to toggle follow', err), err => store.log.error('Failed to toggle follow', {error: err}),
) )
}, [track, view, store.log, setShowSuggestedFollows]) }, [track, view, store.log, setShowSuggestedFollows])
@ -193,7 +193,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
await view.muteAccount() await view.muteAccount()
Toast.show('Account muted') Toast.show('Account muted')
} catch (e: any) { } catch (e: any) {
store.log.error('Failed to mute account', e) store.log.error('Failed to mute account', {error: e})
Toast.show(`There was an issue! ${e.toString()}`) Toast.show(`There was an issue! ${e.toString()}`)
} }
}, [track, view, store]) }, [track, view, store])
@ -204,7 +204,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
await view.unmuteAccount() await view.unmuteAccount()
Toast.show('Account unmuted') Toast.show('Account unmuted')
} catch (e: any) { } catch (e: any) {
store.log.error('Failed to unmute account', e) store.log.error('Failed to unmute account', {error: e})
Toast.show(`There was an issue! ${e.toString()}`) Toast.show(`There was an issue! ${e.toString()}`)
} }
}, [track, view, store]) }, [track, view, store])
@ -222,7 +222,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
onRefreshAll() onRefreshAll()
Toast.show('Account blocked') Toast.show('Account blocked')
} catch (e: any) { } catch (e: any) {
store.log.error('Failed to block account', e) store.log.error('Failed to block account', {error: e})
Toast.show(`There was an issue! ${e.toString()}`) Toast.show(`There was an issue! ${e.toString()}`)
} }
}, },
@ -242,7 +242,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({
onRefreshAll() onRefreshAll()
Toast.show('Account unblocked') Toast.show('Account unblocked')
} catch (e: any) { } catch (e: any) {
store.log.error('Failed to unblock account', e) store.log.error('Failed to unblock account', {error: e})
Toast.show(`There was an issue! ${e.toString()}`) Toast.show(`There was an issue! ${e.toString()}`)
} }
}, },

View File

@ -10,6 +10,7 @@ import {s} from 'lib/styles'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {Text} from '../com/util/text/Text' import {Text} from '../com/util/text/Text'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {getEntries} from '#/logger/logDump'
import {ago} from 'lib/strings/time' import {ago} from 'lib/strings/time'
export const LogScreen = observer(function Log({}: NativeStackScreenProps< export const LogScreen = observer(function Log({}: NativeStackScreenProps<
@ -38,9 +39,8 @@ export const LogScreen = observer(function Log({}: NativeStackScreenProps<
<View style={[s.flex1]}> <View style={[s.flex1]}>
<ViewHeader title="Log" /> <ViewHeader title="Log" />
<ScrollView style={s.flex1}> <ScrollView style={s.flex1}>
{store.log.entries {getEntries()
.slice(0) .slice(0)
.reverse()
.map(entry => { .map(entry => {
return ( return (
<View key={`entry-${entry.id}`}> <View key={`entry-${entry.id}`}>
@ -49,15 +49,15 @@ export const LogScreen = observer(function Log({}: NativeStackScreenProps<
onPress={toggler(entry.id)} onPress={toggler(entry.id)}
accessibilityLabel="View debug entry" accessibilityLabel="View debug entry"
accessibilityHint="Opens additional details for a debug entry"> accessibilityHint="Opens additional details for a debug entry">
{entry.type === 'debug' ? ( {entry.level === 'debug' ? (
<FontAwesomeIcon icon="info" /> <FontAwesomeIcon icon="info" />
) : ( ) : (
<FontAwesomeIcon icon="exclamation" style={s.red3} /> <FontAwesomeIcon icon="exclamation" style={s.red3} />
)} )}
<Text type="sm" style={[styles.summary, pal.text]}> <Text type="sm" style={[styles.summary, pal.text]}>
{entry.summary} {String(entry.message)}
</Text> </Text>
{entry.details ? ( {entry.metadata && Object.keys(entry.metadata).length ? (
<FontAwesomeIcon <FontAwesomeIcon
icon={ icon={
expanded.includes(entry.id) ? 'angle-up' : 'angle-down' expanded.includes(entry.id) ? 'angle-up' : 'angle-down'
@ -66,14 +66,14 @@ export const LogScreen = observer(function Log({}: NativeStackScreenProps<
/> />
) : undefined} ) : undefined}
<Text type="sm" style={[styles.ts, pal.textLight]}> <Text type="sm" style={[styles.ts, pal.textLight]}>
{entry.ts ? ago(entry.ts) : ''} {ago(entry.timestamp)}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
{expanded.includes(entry.id) ? ( {expanded.includes(entry.id) ? (
<View style={[pal.view, s.pl10, s.pr10, s.pb10]}> <View style={[pal.view, s.pl10, s.pr10, s.pb10]}>
<View style={[pal.btn, styles.details]}> <View style={[pal.btn, styles.details]}>
<Text type="mono" style={pal.text}> <Text type="mono" style={pal.text}>
{entry.details} {JSON.stringify(entry.metadata, null, 2)}
</Text> </Text>
</View> </View>
</View> </View>

View File

@ -52,7 +52,7 @@ export const ModerationBlockedAccounts = withAuthRequired(
blockedAccounts blockedAccounts
.loadMore() .loadMore()
.catch(err => .catch(err =>
store.log.error('Failed to load more blocked accounts', err), store.log.error('Failed to load more blocked accounts', {error: err}),
) )
}, [blockedAccounts, store]) }, [blockedAccounts, store])

View File

@ -49,7 +49,7 @@ export const ModerationMutedAccounts = withAuthRequired(
mutedAccounts mutedAccounts
.loadMore() .loadMore()
.catch(err => .catch(err =>
store.log.error('Failed to load more muted accounts', err), store.log.error('Failed to load more muted accounts', {error: err}),
) )
}, [mutedAccounts, store]) }, [mutedAccounts, store])

View File

@ -38,7 +38,7 @@ export const PostThreadScreen = withAuthRequired(
InteractionManager.runAfterInteractions(() => { InteractionManager.runAfterInteractions(() => {
if (!view.hasLoaded && !view.isLoading) { if (!view.hasLoaded && !view.isLoading) {
view.setup().catch(err => { view.setup().catch(err => {
store.log.error('Failed to fetch thread', err) store.log.error('Failed to fetch thread', {error: err})
}) })
} }
}) })

View File

@ -108,14 +108,14 @@ export const ProfileScreen = withAuthRequired(
uiState uiState
.refresh() .refresh()
.catch((err: any) => .catch((err: any) =>
store.log.error('Failed to refresh user profile', err), store.log.error('Failed to refresh user profile', {error: err}),
) )
}, [uiState, store]) }, [uiState, store])
const onEndReached = React.useCallback(() => { const onEndReached = React.useCallback(() => {
uiState uiState.loadMore().catch((err: any) =>
.loadMore() store.log.error('Failed to load more entries in user profile', {
.catch((err: any) => error: err,
store.log.error('Failed to load more entries in user profile', err), }),
) )
}, [uiState, store]) }, [uiState, store])
const onPressTryAgain = React.useCallback(() => { const onPressTryAgain = React.useCallback(() => {

View File

@ -165,7 +165,7 @@ export const ProfileFeedScreenInner = observer(
Toast.show( Toast.show(
'There was an an issue updating your feeds, please check your internet connection and try again.', 'There was an an issue updating your feeds, please check your internet connection and try again.',
) )
store.log.error('Failed up update feeds', {err}) store.log.error('Failed up update feeds', {error: err})
} }
}, [store, feedInfo]) }, [store, feedInfo])
@ -181,7 +181,7 @@ export const ProfileFeedScreenInner = observer(
Toast.show( Toast.show(
'There was an an issue contacting the server, please check your internet connection and try again.', 'There was an an issue contacting the server, please check your internet connection and try again.',
) )
store.log.error('Failed up toggle like', {err}) store.log.error('Failed up toggle like', {error: err})
} }
}, [store, feedInfo]) }, [store, feedInfo])
@ -190,7 +190,7 @@ export const ProfileFeedScreenInner = observer(
if (feedInfo) { if (feedInfo) {
feedInfo.togglePin().catch(e => { feedInfo.togglePin().catch(e => {
Toast.show('There was an issue contacting the server') Toast.show('There was an issue contacting the server')
store.log.error('Failed to toggle pinned feed', {e}) store.log.error('Failed to toggle pinned feed', {error: e})
}) })
} }
}, [store, feedInfo]) }, [store, feedInfo])

View File

@ -272,7 +272,7 @@ const Header = observer(function HeaderImpl({
Haptics.default() Haptics.default()
list.togglePin().catch(e => { list.togglePin().catch(e => {
Toast.show('There was an issue contacting the server') Toast.show('There was an issue contacting the server')
store.log.error('Failed to toggle pinned list', {e}) store.log.error('Failed to toggle pinned list', {error: e})
}) })
}, [store, list]) }, [store, list])

View File

@ -166,14 +166,14 @@ const ListItem = observer(function ListItemImpl({
Haptics.default() Haptics.default()
item.togglePin().catch(e => { item.togglePin().catch(e => {
Toast.show('There was an issue contacting the server') Toast.show('There was an issue contacting the server')
store.log.error('Failed to toggle pinned feed', {e}) store.log.error('Failed to toggle pinned feed', {error: e})
}) })
}, [item, store]) }, [item, store])
const onPressUp = useCallback( const onPressUp = useCallback(
() => () =>
savedFeeds.movePinnedFeed(item, 'up').catch(e => { savedFeeds.movePinnedFeed(item, 'up').catch(e => {
Toast.show('There was an issue contacting the server') Toast.show('There was an issue contacting the server')
store.log.error('Failed to set pinned feed order', {e}) store.log.error('Failed to set pinned feed order', {error: e})
}), }),
[store, savedFeeds, item], [store, savedFeeds, item],
) )
@ -181,7 +181,7 @@ const ListItem = observer(function ListItemImpl({
() => () =>
savedFeeds.movePinnedFeed(item, 'down').catch(e => { savedFeeds.movePinnedFeed(item, 'down').catch(e => {
Toast.show('There was an issue contacting the server') Toast.show('There was an issue contacting the server')
store.log.error('Failed to set pinned feed order', {e}) store.log.error('Failed to set pinned feed order', {error: e})
}), }),
[store, savedFeeds, item], [store, savedFeeds, item],
) )

View File

@ -112,7 +112,7 @@ export const SettingsScreen = withAuthRequired(
err => { err => {
store.log.error( store.log.error(
'Failed to reload from server after handle update', 'Failed to reload from server after handle update',
{err}, {error: err},
) )
setIsSwitching(false) setIsSwitching(false)
}, },

View File

@ -1517,6 +1517,13 @@
dependencies: dependencies:
regenerator-runtime "^0.14.0" regenerator-runtime "^0.14.0"
"@babel/runtime@^7.21.0":
version "7.23.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885"
integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==
dependencies:
regenerator-runtime "^0.14.0"
"@babel/template@^7.0.0", "@babel/template@^7.22.5", "@babel/template@^7.3.3": "@babel/template@^7.0.0", "@babel/template@^7.22.5", "@babel/template@^7.3.3":
version "7.22.5" version "7.22.5"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.5.tgz#0c8c4d944509875849bd0344ff0050756eefc6ec"
@ -8014,6 +8021,13 @@ data-urls@^3.0.2:
whatwg-mimetype "^3.0.0" whatwg-mimetype "^3.0.0"
whatwg-url "^11.0.0" whatwg-url "^11.0.0"
date-fns@^2.30.0:
version "2.30.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
dependencies:
"@babel/runtime" "^7.21.0"
dayjs@^1.8.15: dayjs@^1.8.15:
version "1.11.9" version "1.11.9"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.9.tgz#9ca491933fadd0a60a2c19f6c237c03517d71d1a" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.9.tgz#9ca491933fadd0a60a2c19f6c237c03517d71d1a"
@ -13673,6 +13687,11 @@ nanoid@^3.1.23, nanoid@^3.3.1, nanoid@^3.3.6:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
nanoid@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-5.0.2.tgz#97588ebc70166d0feaf73ccd2799bb4ceaebf692"
integrity sha512-2ustYUX1R2rL/Br5B/FMhi8d5/QzvkJ912rBYxskcpu0myTHzSZfTr1LAS2Sm7jxRUObRrSBFoyzwAhL49aVSg==
napi-build-utils@^1.0.1: napi-build-utils@^1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"