Merge pull request #1813 from bluesky-social/eric/app-903-extract-logger-into-singleton
Add new loggerzio/stable
commit
e49a3d8a56
|
@ -1 +1,3 @@
|
||||||
SENTRY_AUTH_TOKEN=
|
SENTRY_AUTH_TOKEN=
|
||||||
|
EXPO_PUBLIC_LOG_LEVEL=debug
|
||||||
|
EXPO_PUBLIC_LOG_DEBUG=
|
||||||
|
|
22
package.json
22
package.json
|
@ -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__",
|
||||||
|
|
|
@ -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'
|
|
@ -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(
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -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`._
|
|
@ -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())
|
||||||
|
})
|
|
@ -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)
|
||||||
|
})
|
||||||
|
})
|
|
@ -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
|
|
@ -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)
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
export {Native as Sentry} from 'sentry-expo'
|
|
@ -0,0 +1 @@
|
||||||
|
export {Browser as Sentry} from 'sentry-expo'
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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')
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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])
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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])
|
||||||
|
|
||||||
|
|
|
@ -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])
|
||||||
|
|
||||||
|
|
|
@ -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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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])
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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')
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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')
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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])
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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')
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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])
|
||||||
|
|
||||||
|
|
|
@ -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])
|
||||||
|
|
||||||
|
|
|
@ -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})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
|
@ -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])
|
||||||
|
|
|
@ -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])
|
||||||
|
|
||||||
|
|
|
@ -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],
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
},
|
},
|
||||||
|
|
19
yarn.lock
19
yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue