From fd93bf2146b9c379945d3e315cb53cdb44887c8e Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Fri, 3 Nov 2023 16:34:14 -0500 Subject: [PATCH 1/5] Add new logger --- .env.example | 2 + package.json | 22 +- src/env.ts | 9 + src/logger/README.md | 99 +++++++ src/logger/__tests__/logger.test.ts | 424 ++++++++++++++++++++++++++++ src/logger/debugContext.ts | 10 + src/logger/index.ts | 290 +++++++++++++++++++ src/logger/logDump.ts | 12 + src/logger/sentry/index.ts | 1 + src/logger/sentry/index.web.ts | 1 + src/state/models/root-store.ts | 4 +- src/view/screens/Log.tsx | 13 +- yarn.lock | 19 ++ 13 files changed, 888 insertions(+), 18 deletions(-) create mode 100644 src/env.ts create mode 100644 src/logger/README.md create mode 100644 src/logger/__tests__/logger.test.ts create mode 100644 src/logger/debugContext.ts create mode 100644 src/logger/index.ts create mode 100644 src/logger/logDump.ts create mode 100644 src/logger/sentry/index.ts create mode 100644 src/logger/sentry/index.web.ts diff --git a/.env.example b/.env.example index b4213aea24..d4db46ab97 100644 --- a/.env.example +++ b/.env.example @@ -1 +1,3 @@ SENTRY_AUTH_TOKEN= +EXPO_PUBLIC_LOG_LEVEL=debug +EXPO_PUBLIC_LOG_DEBUG= diff --git a/package.json b/package.json index 3e280eb0d9..efb0942df4 100644 --- a/package.json +++ b/package.json @@ -13,21 +13,21 @@ "start": "expo start --dev-client", "start:prod": "expo start --dev-client --no-dev --minify", "clean-cache": "rm -rf node_modules/.cache/babel-loader/*", - "test": "jest --forceExit --testTimeout=20000 --bail", - "test-watch": "jest --watchAll", - "test-ci": "jest --ci --forceExit --reporters=default --reporters=jest-junit", - "test-coverage": "jest --coverage", + "test": "NODE_ENV=test jest --forceExit --testTimeout=20000 --bail", + "test-watch": "NODE_ENV=test jest --watchAll", + "test-ci": "NODE_ENV=test jest --ci --forceExit --reporters=default --reporters=jest-junit", + "test-coverage": "NODE_ENV=test jest --coverage", "lint": "eslint ./src --ext .js,.jsx,.ts,.tsx", "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:metro": "RN_SRC_EXT=e2e.ts,e2e.tsx expo run:ios", "e2e:build": "detox build -c ios.sim.debug", "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all", - "perf:test": "maestro test", - "perf:test:run": "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:results": "flashlight report .perf/results.json", - "perf:measure": "flashlight measure", + "perf:test": "NODE_ENV=test maestro test", + "perf:test:run": "NODE_ENV=test maestro test __e2e__/maestro/scroll.yaml", + "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": "NODE_ENV=test flashlight report .perf/results.json", + "perf:measure": "NODE_ENV=test flashlight measure", "build:apk": "eas build -p android --profile dev-android-apk" }, "dependencies": { @@ -80,6 +80,7 @@ "babel-plugin-transform-remove-console": "^6.9.4", "base64-js": "^1.5.1", "bcp-47-match": "^2.0.3", + "date-fns": "^2.30.0", "email-validator": "^2.0.4", "emoji-mart": "^5.5.2", "eventemitter3": "^5.0.1", @@ -118,6 +119,7 @@ "mobx": "^6.6.1", "mobx-react-lite": "^3.4.0", "mobx-utils": "^6.0.6", + "nanoid": "^5.0.2", "normalize-url": "^8.0.0", "patch-package": "^6.5.1", "postinstall-postinstall": "^2.1.0", @@ -240,7 +242,7 @@ "\\.[jt]sx?$": "babel-jest" }, "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": [ "__tests__/.*/__mocks__", diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 0000000000..7b255e7ea6 --- /dev/null +++ b/src/env.ts @@ -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' diff --git a/src/logger/README.md b/src/logger/README.md new file mode 100644 index 0000000000..1dfd5da23d --- /dev/null +++ b/src/logger/README.md @@ -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`. 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`._ diff --git a/src/logger/__tests__/logger.test.ts b/src/logger/__tests__/logger.test.ts new file mode 100644 index 0000000000..46a5be6107 --- /dev/null +++ b/src/logger/__tests__/logger.test.ts @@ -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) + }) +}) diff --git a/src/logger/debugContext.ts b/src/logger/debugContext.ts new file mode 100644 index 0000000000..658f4b18bb --- /dev/null +++ b/src/logger/debugContext.ts @@ -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 diff --git a/src/logger/index.ts b/src/logger/index.ts new file mode 100644 index 0000000000..28b720910e --- /dev/null +++ b/src/logger/index.ts @@ -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[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) +} diff --git a/src/logger/logDump.ts b/src/logger/logDump.ts new file mode 100644 index 0000000000..777ff77727 --- /dev/null +++ b/src/logger/logDump.ts @@ -0,0 +1,12 @@ +import {ConsoleTransportEntry} from '#/logger' + +let entries: ConsoleTransportEntry[] = [] + +export function add(entry: ConsoleTransportEntry) { + entries.unshift(entry) + entries = entries.slice(0, 50) +} + +export function getEntries() { + return entries +} diff --git a/src/logger/sentry/index.ts b/src/logger/sentry/index.ts new file mode 100644 index 0000000000..a2ed8452d0 --- /dev/null +++ b/src/logger/sentry/index.ts @@ -0,0 +1 @@ +export {Native as Sentry} from 'sentry-expo' diff --git a/src/logger/sentry/index.web.ts b/src/logger/sentry/index.web.ts new file mode 100644 index 0000000000..072b997f44 --- /dev/null +++ b/src/logger/sentry/index.web.ts @@ -0,0 +1 @@ +export {Browser as Sentry} from 'sentry-expo' diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts index 363a81c0f7..c9b460ba49 100644 --- a/src/state/models/root-store.ts +++ b/src/state/models/root-store.ts @@ -8,7 +8,6 @@ import {createContext, useContext} from 'react' import {DeviceEventEmitter, EmitterSubscription} from 'react-native' import {z} from 'zod' import {isObj, hasProp} from 'lib/type-guards' -import {LogModel} from './log' import {SessionModel} from './session' import {ShellUiModel} from './ui/shell' import {HandleResolutionsCache} from './cache/handle-resolutions' @@ -23,6 +22,7 @@ import {ImageSizesCache} from './cache/image-sizes' import {MutedThreads} from './muted-threads' import {Reminders} from './ui/reminders' import {reset as resetNavigation} from '../../Navigation' +import {logger} from '#/logger' // TEMPORARY (APP-700) // remove after backend testing finishes @@ -41,7 +41,7 @@ export type AppInfo = z.infer export class RootStoreModel { agent: BskyAgent appInfo?: AppInfo - log = new LogModel() + log = logger session = new SessionModel(this) shell = new ShellUiModel(this) preferences = new PreferencesModel(this) diff --git a/src/view/screens/Log.tsx b/src/view/screens/Log.tsx index 4a747e5bf7..9389bd5ae3 100644 --- a/src/view/screens/Log.tsx +++ b/src/view/screens/Log.tsx @@ -10,6 +10,7 @@ import {s} from 'lib/styles' import {ViewHeader} from '../com/util/ViewHeader' import {Text} from '../com/util/text/Text' import {usePalette} from 'lib/hooks/usePalette' +import {getEntries} from '#/logger/logDump' import {ago} from 'lib/strings/time' export const LogScreen = observer(function Log({}: NativeStackScreenProps< @@ -38,7 +39,7 @@ export const LogScreen = observer(function Log({}: NativeStackScreenProps< - {store.log.entries + {getEntries() .slice(0) .reverse() .map(entry => { @@ -49,15 +50,15 @@ export const LogScreen = observer(function Log({}: NativeStackScreenProps< onPress={toggler(entry.id)} accessibilityLabel="View debug entry" accessibilityHint="Opens additional details for a debug entry"> - {entry.type === 'debug' ? ( + {entry.level === 'debug' ? ( ) : ( )} - {entry.summary} + {String(entry.message)} - {entry.details ? ( + {entry.metadata && Object.keys(entry.metadata).length ? ( ) : undefined} - {entry.ts ? ago(entry.ts) : ''} + {ago(entry.timestamp)} {expanded.includes(entry.id) ? ( - {entry.details} + {JSON.stringify(entry.metadata, null, 2)} diff --git a/yarn.lock b/yarn.lock index 593c068b74..cd4f71a7a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1517,6 +1517,13 @@ dependencies: 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": version "7.22.5" 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-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: version "1.11.9" 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" 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: version "1.0.2" resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806" From 5fd41ad5a2734e1721a82134efe5487e818b790d Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Sat, 4 Nov 2023 12:28:30 -0500 Subject: [PATCH 2/5] Comment out sentry in prod for now --- src/logger/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logger/index.ts b/src/logger/index.ts index 28b720910e..3de2b90461 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -286,5 +286,5 @@ if (env.IS_DEV && !env.IS_TEST) { */ // logger.addTransport(sentryTransport); } else if (env.IS_PROD) { - logger.addTransport(sentryTransport) + // logger.addTransport(sentryTransport) } From df0dcf32f99631c52b039c7f1934481924d37465 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Sat, 4 Nov 2023 12:42:27 -0500 Subject: [PATCH 3/5] Fix immediate TS errors --- src/lib/api/index.ts | 7 +++---- src/lib/hooks/useOTAUpdate.ts | 12 +++++------ src/lib/notifications/notifications.ts | 19 +++++++++--------- src/state/models/content/feed-source.ts | 10 +++++----- src/state/models/content/list.ts | 8 +++++--- src/state/models/feeds/notifications.ts | 2 +- src/state/models/feeds/post.ts | 15 +++++++------- src/state/models/invited-users.ts | 7 +++---- src/state/models/me.ts | 22 ++++++++++++++------- src/state/models/media/image.ts | 2 +- src/view/com/lists/ListItems.tsx | 4 ++-- src/view/com/lists/ListsList.tsx | 4 ++-- src/view/com/notifications/Feed.tsx | 8 ++++++-- src/view/com/post-thread/PostThread.tsx | 2 +- src/view/com/post-thread/PostThreadItem.tsx | 8 ++++---- src/view/com/post/Post.tsx | 8 ++++---- src/view/com/posts/Feed.tsx | 4 ++-- src/view/com/posts/FeedItem.tsx | 8 ++++---- src/view/screens/PostThread.tsx | 2 +- 19 files changed, 81 insertions(+), 71 deletions(-) diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index f930bd7b16..f75ebbd96c 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -178,10 +178,9 @@ export async function post(store: RootStoreModel, opts: PostOpts) { ) { encoding = 'image/jpeg' } else { - store.log.warn( - 'Unexpected image format for thumbnail, skipping', - opts.extLink.localThumb.path, - ) + store.log.warn('Unexpected image format for thumbnail, skipping', { + thumbnail: opts.extLink.localThumb.path, + }) } if (encoding) { const thumbUploadRes = await uploadBlob( diff --git a/src/lib/hooks/useOTAUpdate.ts b/src/lib/hooks/useOTAUpdate.ts index 5155a808fa..d7855b2d41 100644 --- a/src/lib/hooks/useOTAUpdate.ts +++ b/src/lib/hooks/useOTAUpdate.ts @@ -34,18 +34,18 @@ export function useOTAUpdate() { // show a popup modal showUpdatePopup() } catch (e) { - console.error('useOTAUpdate: Error while checking for update', e) - store.log.error('useOTAUpdate: Error while checking for update', e) + store.log.error('useOTAUpdate: Error while checking for update', { + error: e, + }) } }, [showUpdatePopup, store.log]) const updateEventListener = useCallback( (event: Updates.UpdateEvent) => { store.log.debug('useOTAUpdate: Listening for update...') if (event.type === Updates.UpdateEventType.ERROR) { - store.log.error( - 'useOTAUpdate: Error while listening for update', - event.message, - ) + store.log.error('useOTAUpdate: Error while listening for update', { + message: event.message, + }) } else if (event.type === Updates.UpdateEventType.NO_UPDATE_AVAILABLE) { // Handle no update available // do nothing diff --git a/src/lib/notifications/notifications.ts b/src/lib/notifications/notifications.ts index dfc9a42b13..01b0ba935b 100644 --- a/src/lib/notifications/notifications.ts +++ b/src/lib/notifications/notifications.ts @@ -30,18 +30,18 @@ export function init(store: RootStoreModel) { appId: 'xyz.blueskyweb.app', }) store.log.debug('Notifications: Sent push token (init)', { - type: token.type, + tokenType: token.type, token: token.data, }) } 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 // 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}) => { - store.log.debug('Notifications: Push token changed', {t, type}) + store.log.debug('Notifications: Push token changed', {t, tokenType: type}) if (t) { try { await store.agent.api.app.bsky.notification.registerPush({ @@ -51,11 +51,11 @@ export function init(store: RootStoreModel) { appId: 'xyz.blueskyweb.app', }) store.log.debug('Notifications: Sent push token (event)', { - type, + tokenType: type, token: t, }) } 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 Notifications.addNotificationReceivedListener(event => { - store.log.debug('Notifications: received', event) + store.log.debug('Notifications: received', {event}) if (event.request.trigger.type === 'push') { // refresh notifications in the background store.me.notifications.syncQueue() @@ -84,10 +84,9 @@ export function init(store: RootStoreModel) { // handle notifications that are tapped on const sub = Notifications.addNotificationResponseReceivedListener( response => { - store.log.debug( - 'Notifications: response received', - response.actionIdentifier, - ) + store.log.debug('Notifications: response received', { + actionIdentifier: response.actionIdentifier, + }) if ( response.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER ) { diff --git a/src/state/models/content/feed-source.ts b/src/state/models/content/feed-source.ts index 8dac9b56f0..d1b8fc9dcf 100644 --- a/src/state/models/content/feed-source.ts +++ b/src/state/models/content/feed-source.ts @@ -134,7 +134,7 @@ export class FeedSourceModel { try { await this.rootStore.preferences.addSavedFeed(this.uri) } catch (error) { - this.rootStore.log.error('Failed to save feed', error) + this.rootStore.log.error('Failed to save feed', {error}) } finally { track('CustomFeed:Save') } @@ -147,7 +147,7 @@ export class FeedSourceModel { try { await this.rootStore.preferences.removeSavedFeed(this.uri) } catch (error) { - this.rootStore.log.error('Failed to unsave feed', error) + this.rootStore.log.error('Failed to unsave feed', {error}) } finally { track('CustomFeed:Unsave') } @@ -157,7 +157,7 @@ export class FeedSourceModel { try { await this.rootStore.preferences.addPinnedFeed(this.uri) } catch (error) { - this.rootStore.log.error('Failed to pin feed', error) + this.rootStore.log.error('Failed to pin feed', {error}) } finally { track('CustomFeed:Pin', { name: this.displayName, @@ -194,7 +194,7 @@ export class FeedSourceModel { } catch (e: any) { this.likeUri = undefined 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 { track('CustomFeed:Like') } @@ -215,7 +215,7 @@ export class FeedSourceModel { } catch (e: any) { this.likeUri = uri 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 { track('CustomFeed:Unlike') } diff --git a/src/state/models/content/list.ts b/src/state/models/content/list.ts index 8fb9f4b5ef..985d8d82d8 100644 --- a/src/state/models/content/list.ts +++ b/src/state/models/content/list.ts @@ -339,7 +339,7 @@ export class ListModel { try { await this.rootStore.preferences.addPinnedFeed(this.uri) } catch (error) { - this.rootStore.log.error('Failed to pin feed', error) + this.rootStore.log.error('Failed to pin feed', {error}) } finally { track('CustomFeed:Pin', { name: this.data?.name || '', @@ -455,10 +455,12 @@ export class ListModel { this.error = cleanError(err) this.loadMoreError = cleanError(loadMoreErr) 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) { - this.rootStore.log.error('Failed to fetch user items', loadMoreErr) + this.rootStore.log.error('Failed to fetch user items', { + error: loadMoreErr, + }) } } diff --git a/src/state/models/feeds/notifications.ts b/src/state/models/feeds/notifications.ts index 34f5d4add0..3f18c18d7d 100644 --- a/src/state/models/feeds/notifications.ts +++ b/src/state/models/feeds/notifications.ts @@ -220,7 +220,7 @@ export class NotificationsFeedItemModel { } this.rootStore.log.warn( 'app.bsky.notifications.list served an unsupported record type', - v, + {record: v}, ) } diff --git a/src/state/models/feeds/post.ts b/src/state/models/feeds/post.ts index d46cced756..3def5dce3e 100644 --- a/src/state/models/feeds/post.ts +++ b/src/state/models/feeds/post.ts @@ -42,10 +42,9 @@ export class PostsFeedItemModel { } else { this.postRecord = undefined this.richText = undefined - rootStore.log.warn( - 'Received an invalid app.bsky.feed.post record', - valid.error, - ) + rootStore.log.warn('Received an invalid app.bsky.feed.post record', { + error: valid.error, + }) } } else { this.postRecord = undefined @@ -133,7 +132,7 @@ export class PostsFeedItemModel { track('Post:Like') } } 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') } } 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') } } 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) this.rootStore.emitPostDeleted(this.post.uri) } catch (error) { - this.rootStore.log.error('Failed to delete post', error) + this.rootStore.log.error('Failed to delete post', {error}) } finally { track('Post:Delete') } diff --git a/src/state/models/invited-users.ts b/src/state/models/invited-users.ts index cd36670622..995c4bfb5d 100644 --- a/src/state/models/invited-users.ts +++ b/src/state/models/invited-users.ts @@ -63,10 +63,9 @@ export class InvitedUsers { }) this.rootStore.me.follows.hydrateMany(this.profiles) } catch (e) { - this.rootStore.log.error( - 'Failed to fetch profiles for invited users', - e, - ) + this.rootStore.log.error('Failed to fetch profiles for invited users', { + error: e, + }) } } } diff --git a/src/state/models/me.ts b/src/state/models/me.ts index e7baf5bf28..14b2ef8432 100644 --- a/src/state/models/me.ts +++ b/src/state/models/me.ts @@ -110,13 +110,17 @@ export class MeModel { await this.fetchProfile() this.mainFeed.clear() /* 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 => { - 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 => { - this.rootStore.log.error('Failed to setup notifications model', e) + this.rootStore.log.error('Failed to setup notifications model', { + error: e, + }) }) this.myFeeds.clear() /* dont await */ this.myFeeds.saved.refresh() @@ -184,7 +188,9 @@ export class MeModel { }) }) } 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) } @@ -199,7 +205,9 @@ export class MeModel { this.appPasswords = res.data.passwords }) } 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 } 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) }) } catch (e) { - this.rootStore.log.error('Failed to delete app password', e) + this.rootStore.log.error('Failed to delete app password', {error: e}) } } } diff --git a/src/state/models/media/image.ts b/src/state/models/media/image.ts index c26f9b87c6..4ca0b47c6c 100644 --- a/src/state/models/media/image.ts +++ b/src/state/models/media/image.ts @@ -188,7 +188,7 @@ export class ImageModel implements Omit { this.cropped = cropped }) } catch (err) { - this.rootStore.log.error('Failed to crop photo', err) + this.rootStore.log.error('Failed to crop photo', {error: err}) } } diff --git a/src/view/com/lists/ListItems.tsx b/src/view/com/lists/ListItems.tsx index 855c07d14e..76cd5e7c3b 100644 --- a/src/view/com/lists/ListItems.tsx +++ b/src/view/com/lists/ListItems.tsx @@ -94,7 +94,7 @@ export const ListItems = observer(function ListItemsImpl({ try { await list.refresh() } catch (err) { - list.rootStore.log.error('Failed to refresh lists', err) + list.rootStore.log.error('Failed to refresh lists', {error: err}) } setIsRefreshing(false) }, [list, track, setIsRefreshing]) @@ -104,7 +104,7 @@ export const ListItems = observer(function ListItemsImpl({ try { await list.loadMore() } 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]) diff --git a/src/view/com/lists/ListsList.tsx b/src/view/com/lists/ListsList.tsx index efc874ef34..c0acaa96fd 100644 --- a/src/view/com/lists/ListsList.tsx +++ b/src/view/com/lists/ListsList.tsx @@ -78,7 +78,7 @@ export const ListsList = observer(function ListsListImpl({ try { await listsList.refresh() } catch (err) { - listsList.rootStore.log.error('Failed to refresh lists', err) + listsList.rootStore.log.error('Failed to refresh lists', {error: err}) } setIsRefreshing(false) }, [listsList, track, setIsRefreshing]) @@ -88,7 +88,7 @@ export const ListsList = observer(function ListsListImpl({ try { await listsList.loadMore() } 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]) diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx index 4ca22282da..ef16f598c7 100644 --- a/src/view/com/notifications/Feed.tsx +++ b/src/view/com/notifications/Feed.tsx @@ -61,7 +61,9 @@ export const Feed = observer(function Feed({ setIsPTRing(true) await view.refresh() } catch (err) { - view.rootStore.log.error('Failed to refresh notifications feed', err) + view.rootStore.log.error('Failed to refresh notifications feed', { + error: err, + }) } finally { setIsPTRing(false) } @@ -71,7 +73,9 @@ export const Feed = observer(function Feed({ try { await view.loadMore() } catch (err) { - view.rootStore.log.error('Failed to load more notifications', err) + view.rootStore.log.error('Failed to load more notifications', { + error: err, + }) } }, [view]) diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index 378ef50283..b0728a8a6e 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -119,7 +119,7 @@ export const PostThread = observer(function PostThread({ try { view?.refresh() } 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) }, [view, setIsRefreshing]) diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 8976a7e2cb..4302691655 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -111,13 +111,13 @@ export const PostThreadItem = observer(function PostThreadItem({ const onPressToggleRepost = React.useCallback(() => { return item .toggleRepost() - .catch(e => store.log.error('Failed to toggle repost', e)) + .catch(e => store.log.error('Failed to toggle repost', {error: e})) }, [item, store]) const onPressToggleLike = React.useCallback(() => { return item .toggleLike() - .catch(e => store.log.error('Failed to toggle like', e)) + .catch(e => store.log.error('Failed to toggle like', {error: e})) }, [item, store]) const onCopyPostText = React.useCallback(() => { @@ -138,7 +138,7 @@ export const PostThreadItem = observer(function PostThreadItem({ Toast.show('You will now receive notifications for this thread') } } catch (e) { - store.log.error('Failed to toggle thread mute', e) + store.log.error('Failed to toggle thread mute', {error: e}) } }, [item, store]) @@ -149,7 +149,7 @@ export const PostThreadItem = observer(function PostThreadItem({ Toast.show('Post deleted') }, 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') }, ) diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index e3c948e5d0..8f862f3216 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -142,13 +142,13 @@ const PostLoaded = observer(function PostLoadedImpl({ const onPressToggleRepost = React.useCallback(() => { return item .toggleRepost() - .catch(e => store.log.error('Failed to toggle repost', e)) + .catch(e => store.log.error('Failed to toggle repost', {error: e})) }, [item, store]) const onPressToggleLike = React.useCallback(() => { return item .toggleLike() - .catch(e => store.log.error('Failed to toggle like', e)) + .catch(e => store.log.error('Failed to toggle like', {error: e})) }, [item, store]) const onCopyPostText = React.useCallback(() => { @@ -169,7 +169,7 @@ const PostLoaded = observer(function PostLoadedImpl({ Toast.show('You will now receive notifications for this thread') } } catch (e) { - store.log.error('Failed to toggle thread mute', e) + store.log.error('Failed to toggle thread mute', {error: e}) } }, [item, store]) @@ -180,7 +180,7 @@ const PostLoaded = observer(function PostLoadedImpl({ Toast.show('Post deleted') }, 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') }, ) diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx index 0578036d9b..7d54fd842e 100644 --- a/src/view/com/posts/Feed.tsx +++ b/src/view/com/posts/Feed.tsx @@ -92,7 +92,7 @@ export const Feed = observer(function Feed({ try { await feed.refresh() } 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) }, [feed, track, setIsRefreshing]) @@ -104,7 +104,7 @@ export const Feed = observer(function Feed({ try { await feed.loadMore() } 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]) diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 441621638f..4d49eba6c9 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -94,14 +94,14 @@ export const FeedItem = observer(function FeedItemImpl({ track('FeedItem:PostRepost') return item .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]) const onPressToggleLike = React.useCallback(() => { track('FeedItem:PostLike') return item .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]) const onCopyPostText = React.useCallback(() => { @@ -123,7 +123,7 @@ export const FeedItem = observer(function FeedItemImpl({ Toast.show('You will now receive notifications for this thread') } } catch (e) { - store.log.error('Failed to toggle thread mute', e) + store.log.error('Failed to toggle thread mute', {error: e}) } }, [track, item, store]) @@ -135,7 +135,7 @@ export const FeedItem = observer(function FeedItemImpl({ Toast.show('Post deleted') }, 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') }, ) diff --git a/src/view/screens/PostThread.tsx b/src/view/screens/PostThread.tsx index d4447f1392..5f15adcc5e 100644 --- a/src/view/screens/PostThread.tsx +++ b/src/view/screens/PostThread.tsx @@ -38,7 +38,7 @@ export const PostThreadScreen = withAuthRequired( InteractionManager.runAfterInteractions(() => { if (!view.hasLoaded && !view.isLoading) { view.setup().catch(err => { - store.log.error('Failed to fetch thread', err) + store.log.error('Failed to fetch thread', {error: err}) }) } }) From 7e29ebbadbf6246a664a2bf45b3798b3ecd87955 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Sat, 4 Nov 2023 12:58:50 -0500 Subject: [PATCH 4/5] Fix other error logs while I'm at it --- src/lib/hooks/useFollowProfile.ts | 4 ++-- src/state/index.ts | 2 +- src/state/models/content/post-thread.ts | 2 +- src/state/models/content/profile.ts | 2 +- src/state/models/discovery/feeds.ts | 2 +- src/state/models/discovery/suggested-actors.ts | 2 +- src/state/models/feeds/notifications.ts | 17 ++++++++++------- src/state/models/feeds/post.ts | 2 +- src/state/models/feeds/posts.ts | 9 ++++----- src/state/models/lists/actor-feeds.ts | 2 +- src/state/models/lists/blocked-accounts.ts | 2 +- src/state/models/lists/likes.ts | 2 +- src/state/models/lists/lists-list.ts | 6 ++++-- src/state/models/lists/muted-accounts.ts | 2 +- src/state/models/lists/reposted-by.ts | 2 +- src/state/models/lists/user-followers.ts | 2 +- src/state/models/root-store.ts | 4 ++-- src/state/models/ui/create-account.ts | 4 ++-- src/state/models/ui/profile.ts | 12 +++++++++--- src/state/models/ui/saved-feeds.ts | 2 +- src/view/com/auth/login/Login.tsx | 8 ++++---- src/view/com/composer/photos/OpenCameraBtn.tsx | 2 +- src/view/com/composer/useExternalLinkFetch.ts | 8 +++++--- src/view/com/feeds/FeedSourceCard.tsx | 4 ++-- src/view/com/modals/AddAppPasswords.tsx | 2 +- src/view/com/modals/ChangeHandle.tsx | 6 +++--- .../com/modals/ContentFilteringSettings.tsx | 4 ++-- src/view/com/modals/UserAddRemoveLists.tsx | 4 ++-- src/view/com/post-thread/PostLikedBy.tsx | 8 ++++++-- src/view/com/post-thread/PostRepostedBy.tsx | 4 ++-- src/view/com/posts/FeedErrorMessage.tsx | 2 +- src/view/com/profile/ProfileFollowers.tsx | 14 ++++++++------ src/view/com/profile/ProfileHeader.tsx | 10 +++++----- src/view/screens/ModerationBlockedAccounts.tsx | 2 +- src/view/screens/ModerationMutedAccounts.tsx | 2 +- src/view/screens/Profile.tsx | 12 ++++++------ src/view/screens/ProfileFeed.tsx | 6 +++--- src/view/screens/ProfileList.tsx | 2 +- src/view/screens/SavedFeeds.tsx | 6 +++--- src/view/screens/Settings.tsx | 2 +- 40 files changed, 104 insertions(+), 86 deletions(-) diff --git a/src/lib/hooks/useFollowProfile.ts b/src/lib/hooks/useFollowProfile.ts index 6220daba86..ca3f7ab8e4 100644 --- a/src/lib/hooks/useFollowProfile.ts +++ b/src/lib/hooks/useFollowProfile.ts @@ -22,7 +22,7 @@ export function useFollowProfile(profile: AppBskyActorDefs.ProfileViewBasic) { following: false, } } catch (e: any) { - store.log.error('Failed to delete follow', e) + store.log.error('Failed to delete follow', {error: e}) throw e } } else if (state === FollowState.NotFollowing) { @@ -40,7 +40,7 @@ export function useFollowProfile(profile: AppBskyActorDefs.ProfileViewBasic) { following: true, } } catch (e: any) { - store.log.error('Failed to create follow', e) + store.log.error('Failed to create follow', {error: e}) throw e } } diff --git a/src/state/index.ts b/src/state/index.ts index 42687a229a..2c81c0ddfc 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -25,7 +25,7 @@ export async function setupState(serviceUri = DEFAULT_SERVICE) { rootStore.log.debug('Initial hydrate', {hasSession: !!data.session}) rootStore.hydrate(data) } 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() diff --git a/src/state/models/content/post-thread.ts b/src/state/models/content/post-thread.ts index a862c27d32..cf6377da7b 100644 --- a/src/state/models/content/post-thread.ts +++ b/src/state/models/content/post-thread.ts @@ -163,7 +163,7 @@ export class PostThreadModel { this.hasLoaded = true this.error = cleanError(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 } diff --git a/src/state/models/content/profile.ts b/src/state/models/content/profile.ts index 5333e71166..0050970e63 100644 --- a/src/state/models/content/profile.ts +++ b/src/state/models/content/profile.ts @@ -235,7 +235,7 @@ export class ProfileModel { this.hasLoaded = true this.error = cleanError(err) if (err) { - this.rootStore.log.error('Failed to fetch profile', err) + this.rootStore.log.error('Failed to fetch profile', {error: err}) } } diff --git a/src/state/models/discovery/feeds.ts b/src/state/models/discovery/feeds.ts index 1a00f802cc..3902f3ac17 100644 --- a/src/state/models/discovery/feeds.ts +++ b/src/state/models/discovery/feeds.ts @@ -120,7 +120,7 @@ export class FeedsDiscoveryModel { this.hasLoaded = true this.error = cleanError(err) if (err) { - this.rootStore.log.error('Failed to fetch popular feeds', err) + this.rootStore.log.error('Failed to fetch popular feeds', {error: err}) } } diff --git a/src/state/models/discovery/suggested-actors.ts b/src/state/models/discovery/suggested-actors.ts index d270267eee..8776fcd854 100644 --- a/src/state/models/discovery/suggested-actors.ts +++ b/src/state/models/discovery/suggested-actors.ts @@ -144,7 +144,7 @@ export class SuggestedActorsModel { this.hasLoaded = true this.error = cleanError(err) if (err) { - this.rootStore.log.error('Failed to fetch suggested actors', err) + this.rootStore.log.error('Failed to fetch suggested actors', {error: err}) } } } diff --git a/src/state/models/feeds/notifications.ts b/src/state/models/feeds/notifications.ts index 3f18c18d7d..a834b543a0 100644 --- a/src/state/models/feeds/notifications.ts +++ b/src/state/models/feeds/notifications.ts @@ -401,7 +401,9 @@ export class NotificationsFeedModel { this._setQueued(this._filterNotifications(queueModels)) this._countUnread() } catch (e) { - this.rootStore.log.error('NotificationsModel:syncQueue failed', {e}) + this.rootStore.log.error('NotificationsModel:syncQueue failed', { + error: e, + }) } finally { this.lock.release() } @@ -481,7 +483,9 @@ export class NotificationsFeedModel { this.lastSync ? this.lastSync.toISOString() : undefined, ) } 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.loadMoreError = cleanError(loadMoreError) if (error) { - this.rootStore.log.error('Failed to fetch notifications', error) + this.rootStore.log.error('Failed to fetch notifications', {error}) } if (loadMoreError) { - this.rootStore.log.error( - 'Failed to load more notifications', - loadMoreError, - ) + this.rootStore.log.error('Failed to load more notifications', { + error: loadMoreError, + }) } } diff --git a/src/state/models/feeds/post.ts b/src/state/models/feeds/post.ts index 3def5dce3e..be34171046 100644 --- a/src/state/models/feeds/post.ts +++ b/src/state/models/feeds/post.ts @@ -51,7 +51,7 @@ export class PostsFeedItemModel { this.richText = undefined rootStore.log.warn( '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 diff --git a/src/state/models/feeds/posts.ts b/src/state/models/feeds/posts.ts index 3c580aca9f..5c10ae4c78 100644 --- a/src/state/models/feeds/posts.ts +++ b/src/state/models/feeds/posts.ts @@ -324,13 +324,12 @@ export class PostsFeedModel { this.knownError = detectKnownError(this.feedType, error) this.loadMoreError = cleanError(loadMoreError) if (error) { - this.rootStore.log.error('Posts feed request failed', error) + this.rootStore.log.error('Posts feed request failed', {error}) } if (loadMoreError) { - this.rootStore.log.error( - 'Posts feed load-more request failed', - loadMoreError, - ) + this.rootStore.log.error('Posts feed load-more request failed', { + error: loadMoreError, + }) } } diff --git a/src/state/models/lists/actor-feeds.ts b/src/state/models/lists/actor-feeds.ts index d2bd7680b2..65da765f13 100644 --- a/src/state/models/lists/actor-feeds.ts +++ b/src/state/models/lists/actor-feeds.ts @@ -98,7 +98,7 @@ export class ActorFeedsModel { this.hasLoaded = true this.error = cleanError(err) if (err) { - this.rootStore.log.error('Failed to fetch user followers', err) + this.rootStore.log.error('Failed to fetch user followers', {error: err}) } } diff --git a/src/state/models/lists/blocked-accounts.ts b/src/state/models/lists/blocked-accounts.ts index 20eef8affa..b4495b5435 100644 --- a/src/state/models/lists/blocked-accounts.ts +++ b/src/state/models/lists/blocked-accounts.ts @@ -86,7 +86,7 @@ export class BlockedAccountsModel { this.hasLoaded = true this.error = cleanError(err) if (err) { - this.rootStore.log.error('Failed to fetch user followers', err) + this.rootStore.log.error('Failed to fetch user followers', {error: err}) } } diff --git a/src/state/models/lists/likes.ts b/src/state/models/lists/likes.ts index dd3cf18a33..61e480e19d 100644 --- a/src/state/models/lists/likes.ts +++ b/src/state/models/lists/likes.ts @@ -97,7 +97,7 @@ export class LikesModel { this.hasLoaded = true this.error = cleanError(err) if (err) { - this.rootStore.log.error('Failed to fetch likes', err) + this.rootStore.log.error('Failed to fetch likes', {error: err}) } } diff --git a/src/state/models/lists/lists-list.ts b/src/state/models/lists/lists-list.ts index 42638757a0..7415d06d7c 100644 --- a/src/state/models/lists/lists-list.ts +++ b/src/state/models/lists/lists-list.ts @@ -204,10 +204,12 @@ export class ListsListModel { this.error = cleanError(err) this.loadMoreError = cleanError(loadMoreErr) 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) { - this.rootStore.log.error('Failed to fetch user lists', loadMoreErr) + this.rootStore.log.error('Failed to fetch user lists', { + error: loadMoreErr, + }) } } diff --git a/src/state/models/lists/muted-accounts.ts b/src/state/models/lists/muted-accounts.ts index 9c3e1157b6..bc9e53e5cb 100644 --- a/src/state/models/lists/muted-accounts.ts +++ b/src/state/models/lists/muted-accounts.ts @@ -86,7 +86,7 @@ export class MutedAccountsModel { this.hasLoaded = true this.error = cleanError(err) if (err) { - this.rootStore.log.error('Failed to fetch user followers', err) + this.rootStore.log.error('Failed to fetch user followers', {error: err}) } } diff --git a/src/state/models/lists/reposted-by.ts b/src/state/models/lists/reposted-by.ts index 5d4fc107d7..fe639fd0e8 100644 --- a/src/state/models/lists/reposted-by.ts +++ b/src/state/models/lists/reposted-by.ts @@ -100,7 +100,7 @@ export class RepostedByModel { this.hasLoaded = true this.error = cleanError(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}) } } diff --git a/src/state/models/lists/user-followers.ts b/src/state/models/lists/user-followers.ts index 1f817c33c0..d76ecce1a1 100644 --- a/src/state/models/lists/user-followers.ts +++ b/src/state/models/lists/user-followers.ts @@ -99,7 +99,7 @@ export class UserFollowersModel { this.hasLoaded = true this.error = cleanError(err) if (err) { - this.rootStore.log.error('Failed to fetch user followers', err) + this.rootStore.log.error('Failed to fetch user followers', {error: err}) } } diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts index c9b460ba49..621c87c117 100644 --- a/src/state/models/root-store.ts +++ b/src/state/models/root-store.ts @@ -130,7 +130,7 @@ export class RootStoreModel { }) this.updateSessionState() } 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.preferences.sync() } catch (e: any) { - this.log.error('Failed to fetch latest state', e) + this.log.error('Failed to fetch latest state', {error: e}) } } diff --git a/src/state/models/ui/create-account.ts b/src/state/models/ui/create-account.ts index 9f11a9b31c..3bd39ba767 100644 --- a/src/state/models/ui/create-account.ts +++ b/src/state/models/ui/create-account.ts @@ -78,7 +78,7 @@ export class CreateAccountModel { } catch (err: any) { this.rootStore.log.warn( `Failed to fetch service description for ${this.serviceUrl}`, - err, + {error: err}, ) this.setError( 'Unable to contact your service. Please check your Internet connection.', @@ -127,7 +127,7 @@ export class CreateAccountModel { errMsg = '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.setError(cleanError(errMsg)) throw e diff --git a/src/state/models/ui/profile.ts b/src/state/models/ui/profile.ts index 8525426bf2..47a99a8fc5 100644 --- a/src/state/models/ui/profile.ts +++ b/src/state/models/ui/profile.ts @@ -223,10 +223,14 @@ export class ProfileUiModel { await Promise.all([ this.profile .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 .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(() => { this.isAuthenticatedUser = @@ -237,7 +241,9 @@ export class ProfileUiModel { this.lists.source = this.profile.did this.lists .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() { diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts index 667bc03a35..72055abeb4 100644 --- a/src/state/models/ui/saved-feeds.ts +++ b/src/state/models/ui/saved-feeds.ts @@ -126,7 +126,7 @@ export class SavedFeedsModel { this.hasLoaded = true this.error = cleanError(err) if (err) { - this.rootStore.log.error('Failed to fetch user feeds', err) + this.rootStore.log.error('Failed to fetch user feeds', {err}) } } diff --git a/src/view/com/auth/login/Login.tsx b/src/view/com/auth/login/Login.tsx index 8ec4cc8123..348580faee 100644 --- a/src/view/com/auth/login/Login.tsx +++ b/src/view/com/auth/login/Login.tsx @@ -83,7 +83,7 @@ export const Login = ({onPressBack}: {onPressBack: () => void}) => { } store.log.warn( `Failed to fetch service description for ${serviceUrl}`, - err, + {error: err}, ) setError( 'Unable to contact your service. Please check your Internet connection.', @@ -349,7 +349,7 @@ const LoginForm = ({ }) } catch (e: any) { const errMsg = e.toString() - store.log.warn('Failed to login', e) + store.log.warn('Failed to login', {error: e}) setIsProcessing(false) if (errMsg.includes('Authentication Required')) { setError('Invalid username or password') @@ -578,7 +578,7 @@ const ForgotPasswordForm = ({ onEmailSent() } catch (e: any) { 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) if (isNetworkError(e)) { setError( @@ -734,7 +734,7 @@ const SetNewPasswordForm = ({ onPasswordSet() } catch (e: any) { 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) if (isNetworkError(e)) { setError( diff --git a/src/view/com/composer/photos/OpenCameraBtn.tsx b/src/view/com/composer/photos/OpenCameraBtn.tsx index a6917b36d9..bd4d4b65ad 100644 --- a/src/view/com/composer/photos/OpenCameraBtn.tsx +++ b/src/view/com/composer/photos/OpenCameraBtn.tsx @@ -39,7 +39,7 @@ export function OpenCameraBtn({gallery}: Props) { gallery.add(img) } catch (err: any) { // ignore - store.log.warn('Error using camera', err) + store.log.warn('Error using camera', {error: err}) } }, [gallery, track, store, requestCameraAccessIfNeeded]) diff --git a/src/view/com/composer/useExternalLinkFetch.ts b/src/view/com/composer/useExternalLinkFetch.ts index 6592ed5729..aa5b4b4314 100644 --- a/src/view/com/composer/useExternalLinkFetch.ts +++ b/src/view/com/composer/useExternalLinkFetch.ts @@ -46,7 +46,9 @@ export function useExternalLinkFetch({ setExtLink(undefined) }, 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) }, ) @@ -64,7 +66,7 @@ export function useExternalLinkFetch({ }) }, err => { - store.log.error('Failed to fetch feed for embedding', {err}) + store.log.error('Failed to fetch feed for embedding', {error: err}) setExtLink(undefined) }, ) @@ -82,7 +84,7 @@ export function useExternalLinkFetch({ }) }, err => { - store.log.error('Failed to fetch list for embedding', {err}) + store.log.error('Failed to fetch list for embedding', {error: err}) setExtLink(undefined) }, ) diff --git a/src/view/com/feeds/FeedSourceCard.tsx b/src/view/com/feeds/FeedSourceCard.tsx index 6b5a572b40..060a4b6388 100644 --- a/src/view/com/feeds/FeedSourceCard.tsx +++ b/src/view/com/feeds/FeedSourceCard.tsx @@ -45,7 +45,7 @@ export const FeedSourceCard = observer(function FeedSourceCardImpl({ Toast.show('Removed from my feeds') } catch (e) { 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') } catch (e) { 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]) diff --git a/src/view/com/modals/AddAppPasswords.tsx b/src/view/com/modals/AddAppPasswords.tsx index 2a8672131f..71199e34bc 100644 --- a/src/view/com/modals/AddAppPasswords.tsx +++ b/src/view/com/modals/AddAppPasswords.tsx @@ -95,7 +95,7 @@ export function Component({}: {}) { } } catch (e) { 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}) } } diff --git a/src/view/com/modals/ChangeHandle.tsx b/src/view/com/modals/ChangeHandle.tsx index a1226680e2..2fb1a503ac 100644 --- a/src/view/com/modals/ChangeHandle.tsx +++ b/src/view/com/modals/ChangeHandle.tsx @@ -69,7 +69,7 @@ export function Component({onChanged}: {onChanged: () => void}) { `Failed to fetch service description for ${String( store.agent.service, )}`, - err, + {error: err}, ) setError( 'Unable to contact your service. Please check your Internet connection.', @@ -113,7 +113,7 @@ export function Component({onChanged}: {onChanged: () => void}) { onChanged() } catch (err: any) { setError(cleanError(err)) - store.log.error('Failed to update handle', {handle, err}) + store.log.error('Failed to update handle', {handle, error: err}) } finally { setProcessing(false) } @@ -343,7 +343,7 @@ function CustomHandleForm({ } } catch (err: any) { setError(cleanError(err)) - store.log.error('Failed to verify domain', {handle, err}) + store.log.error('Failed to verify domain', {handle, error: err}) } finally { setIsVerifying(false) } diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx index aa0674d7a4..b78846bdcb 100644 --- a/src/view/com/modals/ContentFilteringSettings.tsx +++ b/src/view/com/modals/ContentFilteringSettings.tsx @@ -103,7 +103,7 @@ const AdultContentEnabledPref = observer( Toast.show( '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( '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], diff --git a/src/view/com/modals/UserAddRemoveLists.tsx b/src/view/com/modals/UserAddRemoveLists.tsx index ff048ca298..32da6403f8 100644 --- a/src/view/com/modals/UserAddRemoveLists.tsx +++ b/src/view/com/modals/UserAddRemoveLists.tsx @@ -62,7 +62,7 @@ export const Component = observer(function UserAddRemoveListsImpl({ setMembershipsLoaded(true) }, err => { - store.log.error('Failed to fetch memberships', {err}) + store.log.error('Failed to fetch memberships', {error: err}) }, ) }, [memberships, listsList, store, setSelected, setMembershipsLoaded]) @@ -76,7 +76,7 @@ export const Component = observer(function UserAddRemoveListsImpl({ try { changes = await memberships.updateTo(selected) } catch (err) { - store.log.error('Failed to update memberships', {err}) + store.log.error('Failed to update memberships', {error: err}) return } Toast.show('Lists updated') diff --git a/src/view/com/post-thread/PostLikedBy.tsx b/src/view/com/post-thread/PostLikedBy.tsx index 574fe1e8e9..d16e8d3063 100644 --- a/src/view/com/post-thread/PostLikedBy.tsx +++ b/src/view/com/post-thread/PostLikedBy.tsx @@ -18,7 +18,9 @@ export const PostLikedBy = observer(function PostLikedByImpl({ const view = React.useMemo(() => new LikesModel(store, {uri}), [store, uri]) 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]) const onRefresh = () => { @@ -27,7 +29,9 @@ export const PostLikedBy = observer(function PostLikedByImpl({ const onEndReached = () => { view .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) { diff --git a/src/view/com/post-thread/PostRepostedBy.tsx b/src/view/com/post-thread/PostRepostedBy.tsx index e4b592779e..0e681777ed 100644 --- a/src/view/com/post-thread/PostRepostedBy.tsx +++ b/src/view/com/post-thread/PostRepostedBy.tsx @@ -23,7 +23,7 @@ export const PostRepostedBy = observer(function PostRepostedByImpl({ useEffect(() => { view .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]) const onRefresh = () => { @@ -33,7 +33,7 @@ export const PostRepostedBy = observer(function PostRepostedByImpl({ view .loadMore() .catch(err => - view?.rootStore.log.error('Failed to load more reposts', err), + view?.rootStore.log.error('Failed to load more reposts', {error: err}), ) } diff --git a/src/view/com/posts/FeedErrorMessage.tsx b/src/view/com/posts/FeedErrorMessage.tsx index 51c735e316..52fa9246da 100644 --- a/src/view/com/posts/FeedErrorMessage.tsx +++ b/src/view/com/posts/FeedErrorMessage.tsx @@ -73,7 +73,7 @@ function FeedgenErrorMessage({ Toast.show( '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() { diff --git a/src/view/com/profile/ProfileFollowers.tsx b/src/view/com/profile/ProfileFollowers.tsx index beb9609b62..7e41b13145 100644 --- a/src/view/com/profile/ProfileFollowers.tsx +++ b/src/view/com/profile/ProfileFollowers.tsx @@ -26,18 +26,20 @@ export const ProfileFollowers = observer(function ProfileFollowers({ useEffect(() => { view .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]) const onRefresh = () => { view.refresh() } const onEndReached = () => { - view - .loadMore() - .catch(err => - view?.rootStore.log.error('Failed to load more followers', err), - ) + view.loadMore().catch(err => + view?.rootStore.log.error('Failed to load more followers', { + error: err, + }), + ) } if (!view.hasLoaded) { diff --git a/src/view/com/profile/ProfileHeader.tsx b/src/view/com/profile/ProfileHeader.tsx index 082fbc0bcd..d4fd88ca89 100644 --- a/src/view/com/profile/ProfileHeader.tsx +++ b/src/view/com/profile/ProfileHeader.tsx @@ -150,7 +150,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ : '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]) @@ -193,7 +193,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ await view.muteAccount() Toast.show('Account muted') } 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()}`) } }, [track, view, store]) @@ -204,7 +204,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ await view.unmuteAccount() Toast.show('Account unmuted') } 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()}`) } }, [track, view, store]) @@ -222,7 +222,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ onRefreshAll() Toast.show('Account blocked') } 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()}`) } }, @@ -242,7 +242,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoadedImpl({ onRefreshAll() Toast.show('Account unblocked') } 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()}`) } }, diff --git a/src/view/screens/ModerationBlockedAccounts.tsx b/src/view/screens/ModerationBlockedAccounts.tsx index 7bbb6beeea..a32c5e36e1 100644 --- a/src/view/screens/ModerationBlockedAccounts.tsx +++ b/src/view/screens/ModerationBlockedAccounts.tsx @@ -52,7 +52,7 @@ export const ModerationBlockedAccounts = withAuthRequired( blockedAccounts .loadMore() .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]) diff --git a/src/view/screens/ModerationMutedAccounts.tsx b/src/view/screens/ModerationMutedAccounts.tsx index 31c46e640a..61911717a1 100644 --- a/src/view/screens/ModerationMutedAccounts.tsx +++ b/src/view/screens/ModerationMutedAccounts.tsx @@ -49,7 +49,7 @@ export const ModerationMutedAccounts = withAuthRequired( mutedAccounts .loadMore() .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]) diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 6c5a84e83a..d353c411fa 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -108,15 +108,15 @@ export const ProfileScreen = withAuthRequired( uiState .refresh() .catch((err: any) => - store.log.error('Failed to refresh user profile', err), + store.log.error('Failed to refresh user profile', {error: err}), ) }, [uiState, store]) const onEndReached = React.useCallback(() => { - uiState - .loadMore() - .catch((err: any) => - store.log.error('Failed to load more entries in user profile', err), - ) + uiState.loadMore().catch((err: any) => + store.log.error('Failed to load more entries in user profile', { + error: err, + }), + ) }, [uiState, store]) const onPressTryAgain = React.useCallback(() => { uiState.setup() diff --git a/src/view/screens/ProfileFeed.tsx b/src/view/screens/ProfileFeed.tsx index 70e52bf7aa..a76f2eb602 100644 --- a/src/view/screens/ProfileFeed.tsx +++ b/src/view/screens/ProfileFeed.tsx @@ -165,7 +165,7 @@ export const ProfileFeedScreenInner = observer( Toast.show( '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]) @@ -181,7 +181,7 @@ export const ProfileFeedScreenInner = observer( Toast.show( '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]) @@ -190,7 +190,7 @@ export const ProfileFeedScreenInner = observer( if (feedInfo) { feedInfo.togglePin().catch(e => { 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]) diff --git a/src/view/screens/ProfileList.tsx b/src/view/screens/ProfileList.tsx index 4fc4b6c7f5..1aa2a21b8c 100644 --- a/src/view/screens/ProfileList.tsx +++ b/src/view/screens/ProfileList.tsx @@ -272,7 +272,7 @@ const Header = observer(function HeaderImpl({ Haptics.default() list.togglePin().catch(e => { 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]) diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx index 0f62782880..900bb06aa4 100644 --- a/src/view/screens/SavedFeeds.tsx +++ b/src/view/screens/SavedFeeds.tsx @@ -166,14 +166,14 @@ const ListItem = observer(function ListItemImpl({ Haptics.default() item.togglePin().catch(e => { 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]) const onPressUp = useCallback( () => savedFeeds.movePinnedFeed(item, 'up').catch(e => { 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], ) @@ -181,7 +181,7 @@ const ListItem = observer(function ListItemImpl({ () => savedFeeds.movePinnedFeed(item, 'down').catch(e => { 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], ) diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx index 2112ec7d12..c2c6d1efa9 100644 --- a/src/view/screens/Settings.tsx +++ b/src/view/screens/Settings.tsx @@ -112,7 +112,7 @@ export const SettingsScreen = withAuthRequired( err => { store.log.error( 'Failed to reload from server after handle update', - {err}, + {error: err}, ) setIsSwitching(false) }, From 0c76866757367953cc6e7cc8c9ad9fcfdb00a862 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Sat, 4 Nov 2023 13:09:12 -0500 Subject: [PATCH 5/5] Add logDump tests, ensure Log screen is reversed --- src/logger/__tests__/logDump.test.ts | 36 ++++++++++++++++++++++++++++ src/logger/logDump.ts | 2 +- src/view/screens/Log.tsx | 1 - 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 src/logger/__tests__/logDump.test.ts diff --git a/src/logger/__tests__/logDump.test.ts b/src/logger/__tests__/logDump.test.ts new file mode 100644 index 0000000000..135998223d --- /dev/null +++ b/src/logger/__tests__/logDump.test.ts @@ -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()) +}) diff --git a/src/logger/logDump.ts b/src/logger/logDump.ts index 777ff77727..ec64bf4bd2 100644 --- a/src/logger/logDump.ts +++ b/src/logger/logDump.ts @@ -1,4 +1,4 @@ -import {ConsoleTransportEntry} from '#/logger' +import type {ConsoleTransportEntry} from '#/logger' let entries: ConsoleTransportEntry[] = [] diff --git a/src/view/screens/Log.tsx b/src/view/screens/Log.tsx index 9389bd5ae3..6ae61888d2 100644 --- a/src/view/screens/Log.tsx +++ b/src/view/screens/Log.tsx @@ -41,7 +41,6 @@ export const LogScreen = observer(function Log({}: NativeStackScreenProps< {getEntries() .slice(0) - .reverse() .map(entry => { return (