Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client

Logger improvements (#7729)

* Remove enablement

* Refactor context and filtering

* Fix imports, simplify transports config

* Migrate usages of debug context

* Re-org, add colors and grouping to console logging

* Remove temp default context

* Remove manual prefix

* Move colorizing out of console transport body

* Reduce reuse

* Pass through context

* Ensure bitdrift is enabled in dev

* Enable Sentry on web only

* Clean up types

* Docs

* Format

* Update tests

* Clean up tests

* No positional args

* Revert Sentry changes

* Clean up context, use it, pass metadata through to Bitdrift

* Fix up debugging

* Clean up metadata before passing to Bitdrift

* Correct transports

* Reserve context prop on metadata and include in transports

* Update tests

authored by

Eric Bailey and committed by
GitHub
7c36ea11 9e9ffd5c

+567 -498
+1 -1
src/App.native.tsx
··· 1 1 import 'react-native-url-polyfill/auto' 2 - import '#/lib/sentry' // must be near top 2 + import '#/logger/sentry/setup' 3 3 import '#/lib/bitdrift' // must be near top 4 4 import '#/view/icons' 5 5
+1 -1
src/App.web.tsx
··· 1 - import '#/lib/sentry' // must be near top 1 + import '#/logger/sentry/setup' // must be near top 2 2 import '#/view/icons' 3 3 import './style.css' 4 4
+1 -1
src/components/FeedCard.tsx
··· 271 271 } 272 272 Toast.show(_(msg`Feeds updated!`)) 273 273 } catch (err: any) { 274 - logger.error(err, {context: `FeedCard: failed to update feeds`, pin}) 274 + logger.error(err, {message: `FeedCard: failed to update feeds`, pin}) 275 275 Toast.show(_(msg`Failed to update feeds`), 'xmark') 276 276 } 277 277 },
+1 -1
src/components/dialogs/PostInteractionSettingsDialog.tsx
··· 209 209 props.control.close() 210 210 } catch (e: any) { 211 211 logger.error(`Failed to save post interaction settings`, { 212 - context: 'PostInteractionSettingsDialogControlledInner', 212 + source: 'PostInteractionSettingsDialogControlledInner', 213 213 safeMessage: e.message, 214 214 }) 215 215 Toast.show(
+7 -14
src/lib/hooks/useNotificationHandler.ts
··· 6 6 import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' 7 7 import {NavigationProp} from '#/lib/routes/types' 8 8 import {logEvent} from '#/lib/statsig/statsig' 9 - import {logger} from '#/logger' 9 + import {Logger} from '#/logger' 10 10 import {isAndroid} from '#/platform/detection' 11 11 import {useCurrentConvoId} from '#/state/messages/current-convo-id' 12 12 import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed' ··· 49 49 // These need to stay outside the hook to persist between account switches 50 50 let storedPayload: NotificationPayload | undefined 51 51 let prevDate = 0 52 + 53 + const logger = Logger.create(Logger.Context.Notifications) 52 54 53 55 export function useNotificationsHandler() { 54 56 const queryClient = useQueryClient() ··· 186 188 return DEFAULT_HANDLER_OPTIONS 187 189 } 188 190 189 - logger.debug( 190 - 'Notifications: received', 191 - {e}, 192 - logger.DebugContext.notifications, 193 - ) 191 + logger.debug('Notifications: received', {e}) 194 192 195 193 const payload = e.request.trigger.payload as NotificationPayload 196 194 if ( ··· 217 215 } 218 216 prevDate = e.notification.date 219 217 220 - logger.debug( 221 - 'Notifications: response received', 222 - { 223 - actionIdentifier: e.actionIdentifier, 224 - }, 225 - logger.DebugContext.notifications, 226 - ) 218 + logger.debug('Notifications: response received', { 219 + actionIdentifier: e.actionIdentifier, 220 + }) 227 221 228 222 if ( 229 223 e.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER && ··· 235 229 logger.debug( 236 230 'User pressed a notification, opening notifications tab', 237 231 {}, 238 - logger.DebugContext.notifications, 239 232 ) 240 233 logEvent('notifications:openApp', {}) 241 234 invalidateCachedUnreadPage()
+7 -9
src/lib/notifications/notifications.ts
··· 4 4 import {BskyAgent} from '@atproto/api' 5 5 6 6 import {logEvent} from '#/lib/statsig/statsig' 7 - import {logger} from '#/logger' 7 + import {Logger} from '#/logger' 8 8 import {devicePlatform, isAndroid, isNative} from '#/platform/detection' 9 9 import {SessionAccount, useAgent, useSession} from '#/state/session' 10 10 import BackgroundNotificationHandler from '../../../modules/expo-background-notification-handler' ··· 13 13 serviceUrl?.includes('staging') 14 14 ? 'did:web:api.staging.bsky.dev' 15 15 : 'did:web:api.bsky.app' 16 + 17 + const logger = Logger.create(Logger.Context.Notifications) 16 18 17 19 async function registerPushToken( 18 20 agent: BskyAgent, ··· 26 28 token: token.data, 27 29 appId: 'xyz.blueskyweb.app', 28 30 }) 29 - logger.debug( 30 - 'Notifications: Sent push token (init)', 31 - { 32 - tokenType: token.type, 33 - token: token.data, 34 - }, 35 - logger.DebugContext.notifications, 36 - ) 31 + logger.debug('Notifications: Sent push token (init)', { 32 + tokenType: token.type, 33 + token: token.data, 34 + }) 37 35 } catch (error) { 38 36 logger.error('Notifications: Failed to set push token', {message: error}) 39 37 }
src/lib/sentry.ts src/logger/sentry/setup/index.ts
+24 -78
src/logger/README.md
··· 1 1 # Logger 2 2 3 - Simple logger for Bluesky. Supports log levels, debug contexts, and separate 4 - transports for production, dev, and test mode. 3 + Simple logger for Bluesky. 5 4 6 5 ## At a Glance 7 6 8 7 ```typescript 9 - import { logger } from '#/logger' 8 + import { logger, Logger } from '#/logger' 10 9 11 - logger.debug(message[, metadata, debugContext]) 12 - logger.info(message[, metadata]) 13 - logger.log(message[, metadata]) 14 - logger.warn(message[, metadata]) 15 - logger.error(error[, metadata]) 16 - ``` 10 + // or, optionally create new instance with custom context 11 + // const logger = Logger.create(Logger.Context.Notifications) 17 12 18 - #### Modes 13 + // for dev-only logs 14 + logger.debug(message, {}) 19 15 20 - The "modes" referred to here are inferred from `process.env.NODE_ENV`, 21 - which matches how React Native sets the `__DEV__` global. 16 + // for production breadcrumbs 17 + logger.info(message, {}) 22 18 23 - #### Log Levels 19 + // seldom used, prefer `info` 20 + logger.log(message, {}) 24 21 25 - Log levels are used to filter which logs are either printed to the console 26 - and/or sent to Sentry and other reporting services. To configure, set the 27 - `EXPO_PUBLIC_LOG_LEVEL` environment variable in `.env` to one of `debug`, 28 - `info`, `log`, `warn`, or `error`. 22 + // for non-error issues to look into, seldom used, prefer `error` 23 + logger.warn(message, {}) 29 24 30 - This variable should be `info` in production, and `debug` in dev. If it gets too 31 - noisy in dev, simply set it to a higher level, such as `warn`. 25 + // for known errors without an exception, use a string 26 + logger.error(`known error`, {}) 32 27 33 - ## Usage 34 - 35 - ```typescript 36 - import { logger } from '#/logger'; 37 - ``` 38 - 39 - ### `logger.error` 40 - 41 - The `error` level is for... well, errors. These are sent to Sentry in production mode. 42 - 43 - `error`, along with all log levels, supports an additional parameter, `metadata: Record<string, unknown>`. Use this to provide values to the [Sentry 44 - breadcrumb](https://docs.sentry.io/platforms/react-native/enriching-events/breadcrumbs/#manual-breadcrumbs). 45 - 46 - ```typescript 28 + // for unknown exceptions 47 29 try { 48 - // some async code 49 30 } catch (e) { 50 - logger.error(e, { ...metadata }); 31 + logger.error(e, {message: `explain error`}]) 51 32 } 52 33 ``` 53 34 54 - ### `logger.warn` 55 - 56 - Warnings will be sent to Sentry as a separate Issue with level `warning`, as 57 - well as as breadcrumbs, with a severity level of `warning` 58 - 59 - ### `logger.log` 35 + #### Log Levels 60 36 61 - Logs with level `log` will be sent to Sentry as a separate Issue with level `log`, as 62 - well as as breadcrumbs, with a severity level of `default`. 63 - 64 - ### `logger.info` 65 - 66 - The `info` level should be used for information that would be helpful in a 67 - tracing context, like Sentry. In production mode, `info` logs are sent 68 - to Sentry as breadcrumbs, which decorate log levels above `info` such as `log`, 69 - `warn`, and `error`. 37 + Log level defaults to `info`. You can set this via the `EXPO_PUBLIC_LOG_LEVEL` 38 + env var in `.env.local`. 70 39 71 - ### `logger.debug` 40 + #### Filtering debugs by context 72 41 73 - Debug level is really only intended for local development. Use this instead of 74 - `console.log`. 75 - 76 - ```typescript 77 - logger.debug(message, { ...metadata }); 78 - ``` 79 - 80 - Inspired by [debug](https://www.npmjs.com/package/debug), when writing debug 81 - logs, you can optionally pass a _context_, which can be then filtered when in 82 - debug mode. 83 - 84 - This value should be related to the feature, component, or screen 85 - the code is running within, and **it should be defined in `#/logger/debugContext`**. 86 - This way we know if a relevant context already exists, and we can trace all 87 - active contexts in use in our app. This const enum is conveniently available on 88 - the `logger` at `logger.DebugContext`. 89 - 90 - For example, a debug log like this: 91 - 92 - ```typescript 93 - logger.debug(message, {}, logger.DebugContext.composer); 94 - ``` 95 - 96 - Would be logged to the console in dev mode if `EXPO_PUBLIC_LOG_LEVEL=debug`, _or_ if you 97 - pass a separate environment variable `LOG_DEBUG=composer`. This variable supports 98 - multiple contexts using commas like `LOG_DEBUG=composer,profile`, and _automatically 99 - sets the log level to `debug`, regardless of `EXPO_PUBLIC_LOG_LEVEL`._ 42 + Debug logs are dev-only, and not enabled by default. Once enabled, they can get 43 + noisy. So you can filter them by setting the `EXPO_PUBLIC_LOG_DEBUG` env var 44 + e.g. `EXPO_PUBLIC_LOG_DEBUG=notifications`. These values can be comma-separated 45 + and include wildcards.
+5 -2
src/logger/__tests__/logDump.test.ts
··· 1 1 import {expect, test} from '@jest/globals' 2 2 3 - import {ConsoleTransportEntry, LogLevel} from '#/logger' 4 - import {add, getEntries} from '#/logger/logDump' 3 + import {add, ConsoleTransportEntry, getEntries} from '#/logger/logDump' 4 + import {LogContext, LogLevel} from '#/logger/types' 5 5 6 6 test('works', () => { 7 7 const items: ConsoleTransportEntry[] = [ 8 8 { 9 9 id: '1', 10 10 level: LogLevel.Debug, 11 + context: LogContext.Default, 11 12 message: 'hello', 12 13 metadata: {}, 13 14 timestamp: Date.now(), ··· 15 16 { 16 17 id: '2', 17 18 level: LogLevel.Debug, 19 + context: LogContext.Default, 18 20 message: 'hello', 19 21 metadata: {}, 20 22 timestamp: Date.now(), ··· 22 24 { 23 25 id: '3', 24 26 level: LogLevel.Debug, 27 + context: LogContext.Default, 25 28 message: 'hello', 26 29 metadata: {}, 27 30 timestamp: Date.now(),
+116 -78
src/logger/__tests__/logger.test.ts
··· 2 2 import * as Sentry from '@sentry/react-native' 3 3 import {nanoid} from 'nanoid/non-secure' 4 4 5 - import {Logger, LogLevel, sentryTransport} from '#/logger' 6 - 7 - jest.mock('#/env', () => ({ 8 - /* 9 - * Forces debug mode for tests using the default logger. Most tests create 10 - * their own logger instance. 11 - */ 12 - LOG_LEVEL: 'debug', 13 - LOG_DEBUG: '', 14 - })) 5 + import {Logger} from '#/logger' 6 + import {sentryTransport} from '#/logger/transports/sentry' 7 + import {LogLevel} from '#/logger/types' 15 8 16 9 jest.mock('@sentry/react-native', () => ({ 17 10 addBreadcrumb: jest.fn(), ··· 26 19 describe('general functionality', () => { 27 20 test('default params', () => { 28 21 const logger = new Logger() 29 - expect(logger.enabled).toBeFalsy() 30 - expect(logger.level).toEqual(LogLevel.Debug) // mocked above 31 - }) 32 - 33 - test('can override default params', () => { 34 - const logger = new Logger({ 35 - enabled: true, 36 - level: LogLevel.Info, 37 - }) 38 - expect(logger.enabled).toBeTruthy() 39 22 expect(logger.level).toEqual(LogLevel.Info) 40 23 }) 41 24 42 - test('disabled logger does not report', () => { 25 + test('can override default params', () => { 43 26 const logger = new Logger({ 44 - enabled: false, 45 27 level: LogLevel.Debug, 46 28 }) 47 - 48 - const mockTransport = jest.fn() 49 - 50 - logger.addTransport(mockTransport) 51 - logger.debug('message') 52 - 53 - expect(mockTransport).not.toHaveBeenCalled() 29 + expect(logger.level).toEqual(LogLevel.Debug) 54 30 }) 55 31 56 - test('disablement', () => { 32 + test('contextFilter overrides level', () => { 57 33 const logger = new Logger({ 58 - enabled: true, 59 - level: LogLevel.Debug, 34 + level: LogLevel.Info, 35 + contextFilter: 'test', 60 36 }) 61 - 62 - logger.disable() 63 - 64 - const mockTransport = jest.fn() 65 - 66 - logger.addTransport(mockTransport) 67 - logger.debug('message') 68 - 69 - expect(mockTransport).not.toHaveBeenCalled() 70 - }) 71 - 72 - test('passing debug contexts automatically enables debug mode', () => { 73 - const logger = new Logger({debug: 'specific'}) 74 37 expect(logger.level).toEqual(LogLevel.Debug) 75 38 }) 76 39 77 40 test('supports extra metadata', () => { 78 41 const timestamp = Date.now() 79 - const logger = new Logger({enabled: true}) 42 + const logger = new Logger({}) 80 43 81 44 const mockTransport = jest.fn() 82 45 ··· 87 50 88 51 expect(mockTransport).toHaveBeenCalledWith( 89 52 LogLevel.Warn, 53 + undefined, 90 54 'message', 91 55 extra, 92 56 timestamp, ··· 95 59 96 60 test('supports nullish/falsy metadata', () => { 97 61 const timestamp = Date.now() 98 - const logger = new Logger({enabled: true}) 62 + const logger = new Logger({}) 99 63 100 64 const mockTransport = jest.fn() 101 65 ··· 105 69 logger.warn('a', null) 106 70 expect(mockTransport).toHaveBeenCalledWith( 107 71 LogLevel.Warn, 72 + undefined, 108 73 'a', 109 74 {}, 110 75 timestamp, ··· 114 79 logger.warn('b', false) 115 80 expect(mockTransport).toHaveBeenCalledWith( 116 81 LogLevel.Warn, 82 + undefined, 117 83 'b', 118 84 {}, 119 85 timestamp, ··· 123 89 logger.warn('c', 0) 124 90 expect(mockTransport).toHaveBeenCalledWith( 125 91 LogLevel.Warn, 92 + undefined, 126 93 'c', 127 94 {}, 128 95 timestamp, ··· 130 97 131 98 remove() 132 99 133 - logger.addTransport((level, message, metadata) => { 100 + logger.addTransport((level, context, message, metadata) => { 134 101 expect(typeof metadata).toEqual('object') 135 102 }) 136 103 ··· 143 110 const timestamp = Date.now() 144 111 const sentryTimestamp = timestamp / 1000 145 112 146 - sentryTransport(LogLevel.Debug, message, {}, timestamp) 113 + sentryTransport( 114 + LogLevel.Debug, 115 + Logger.Context.Default, 116 + message, 117 + {}, 118 + timestamp, 119 + ) 147 120 expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({ 121 + category: Logger.Context.Default, 148 122 message, 149 - data: {}, 123 + data: {context: 'logger'}, 150 124 type: 'default', 151 125 level: LogLevel.Debug, 152 126 timestamp: sentryTimestamp, ··· 154 128 155 129 sentryTransport( 156 130 LogLevel.Info, 131 + Logger.Context.Default, 157 132 message, 158 133 {type: 'info', prop: true}, 159 134 timestamp, 160 135 ) 161 136 expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({ 137 + category: Logger.Context.Default, 162 138 message, 163 - data: {prop: true}, 139 + data: {prop: true, context: 'logger'}, 164 140 type: 'info', 165 141 level: LogLevel.Info, 166 142 timestamp: sentryTimestamp, 167 143 }) 168 144 169 - sentryTransport(LogLevel.Log, message, {}, timestamp) 145 + sentryTransport( 146 + LogLevel.Log, 147 + Logger.Context.Default, 148 + message, 149 + {}, 150 + timestamp, 151 + ) 170 152 expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({ 153 + category: Logger.Context.Default, 171 154 message, 172 - data: {}, 155 + data: {context: 'logger'}, 173 156 type: 'default', 174 157 level: 'debug', // Sentry bug, log becomes debug 175 158 timestamp: sentryTimestamp, ··· 177 160 jest.runAllTimers() 178 161 expect(Sentry.captureMessage).toHaveBeenCalledWith(message, { 179 162 level: 'log', 180 - tags: undefined, 181 - extra: {}, 163 + tags: {category: 'logger'}, 164 + extra: {context: 'logger'}, 182 165 }) 183 166 184 - sentryTransport(LogLevel.Warn, message, {}, timestamp) 167 + sentryTransport( 168 + LogLevel.Warn, 169 + Logger.Context.Default, 170 + message, 171 + {}, 172 + timestamp, 173 + ) 185 174 expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({ 175 + category: Logger.Context.Default, 186 176 message, 187 - data: {}, 177 + data: {context: 'logger'}, 188 178 type: 'default', 189 179 level: 'warning', 190 180 timestamp: sentryTimestamp, ··· 192 182 jest.runAllTimers() 193 183 expect(Sentry.captureMessage).toHaveBeenCalledWith(message, { 194 184 level: 'warning', 195 - tags: undefined, 196 - extra: {}, 185 + tags: {category: 'logger'}, 186 + extra: {context: 'logger'}, 197 187 }) 198 188 199 189 const e = new Error('error') ··· 203 193 204 194 sentryTransport( 205 195 LogLevel.Error, 196 + Logger.Context.Default, 206 197 e, 207 198 { 208 199 tags, ··· 212 203 ) 213 204 214 205 expect(Sentry.captureException).toHaveBeenCalledWith(e, { 215 - tags, 206 + tags: { 207 + ...tags, 208 + category: 'logger', 209 + }, 216 210 extra: { 217 211 prop: true, 212 + context: 'logger', 218 213 }, 219 214 }) 220 215 }) ··· 226 221 227 222 sentryTransport( 228 223 LogLevel.Debug, 224 + undefined, 229 225 message, 230 226 {error: new Error('foo')}, 231 227 timestamp, ··· 241 237 242 238 test('add/remove transport', () => { 243 239 const timestamp = Date.now() 244 - const logger = new Logger({enabled: true}) 240 + const logger = new Logger({}) 245 241 const mockTransport = jest.fn() 246 242 247 243 const remove = logger.addTransport(mockTransport) ··· 256 252 expect(mockTransport).toHaveBeenNthCalledWith( 257 253 1, 258 254 LogLevel.Warn, 255 + undefined, 259 256 'warn', 260 257 {}, 261 258 timestamp, ··· 263 260 }) 264 261 }) 265 262 266 - describe('debug contexts', () => { 267 - const mockTransport = jest.fn() 263 + describe('create', () => { 264 + test('create', () => { 265 + const mockTransport = jest.fn() 266 + const timestamp = Date.now() 267 + const message = nanoid() 268 + const logger = Logger.create(Logger.Context.Default) 268 269 270 + logger.addTransport(mockTransport) 271 + logger.info(message, {}) 272 + 273 + expect(mockTransport).toHaveBeenCalledWith( 274 + LogLevel.Info, 275 + Logger.Context.Default, 276 + message, 277 + {}, 278 + timestamp, 279 + ) 280 + }) 281 + }) 282 + 283 + describe('debug contexts', () => { 269 284 test('specific', () => { 285 + const mockTransport = jest.fn() 270 286 const timestamp = Date.now() 271 287 const message = nanoid() 272 288 const logger = new Logger({ 273 - enabled: true, 274 - debug: 'specific', 289 + // @ts-ignore 290 + context: 'specific', 291 + level: LogLevel.Debug, 275 292 }) 276 293 277 294 logger.addTransport(mockTransport) 278 - logger.debug(message, {}, 'specific') 295 + logger.debug(message, {}) 279 296 280 297 expect(mockTransport).toHaveBeenCalledWith( 281 298 LogLevel.Debug, 299 + 'specific', 282 300 message, 283 301 {}, 284 302 timestamp, ··· 286 304 }) 287 305 288 306 test('namespaced', () => { 307 + const mockTransport = jest.fn() 289 308 const timestamp = Date.now() 290 309 const message = nanoid() 291 310 const logger = new Logger({ 292 - enabled: true, 293 - debug: 'namespace*', 311 + // @ts-ignore 312 + context: 'namespace:foo', 313 + contextFilter: 'namespace:*', 314 + level: LogLevel.Debug, 294 315 }) 295 316 296 317 logger.addTransport(mockTransport) 297 - logger.debug(message, {}, 'namespace') 318 + logger.debug(message, {}) 298 319 299 320 expect(mockTransport).toHaveBeenCalledWith( 300 321 LogLevel.Debug, 322 + 'namespace:foo', 301 323 message, 302 324 {}, 303 325 timestamp, ··· 305 327 }) 306 328 307 329 test('ignores inactive', () => { 330 + const mockTransport = jest.fn() 308 331 const timestamp = Date.now() 309 332 const message = nanoid() 310 333 const logger = new Logger({ 311 - enabled: true, 312 - debug: 'namespace:foo:*', 334 + // @ts-ignore 335 + context: 'namespace:bar:baz', 336 + contextFilter: 'namespace:foo:*', 313 337 }) 314 338 315 339 logger.addTransport(mockTransport) 316 - logger.debug(message, {}, 'namespace:bar:baz') 340 + logger.debug(message, {}) 317 341 318 342 expect(mockTransport).not.toHaveBeenCalledWith( 319 343 LogLevel.Debug, 344 + 'namespace:bar:baz', 320 345 message, 321 346 {}, 322 347 timestamp, ··· 328 353 test('debug', () => { 329 354 const timestamp = Date.now() 330 355 const logger = new Logger({ 331 - enabled: true, 332 356 level: LogLevel.Debug, 333 357 }) 334 358 const message = nanoid() ··· 339 363 logger.debug(message) 340 364 expect(mockTransport).toHaveBeenCalledWith( 341 365 LogLevel.Debug, 366 + undefined, 342 367 message, 343 368 {}, 344 369 timestamp, ··· 347 372 logger.info(message) 348 373 expect(mockTransport).toHaveBeenCalledWith( 349 374 LogLevel.Info, 375 + undefined, 350 376 message, 351 377 {}, 352 378 timestamp, ··· 355 381 logger.warn(message) 356 382 expect(mockTransport).toHaveBeenCalledWith( 357 383 LogLevel.Warn, 384 + undefined, 358 385 message, 359 386 {}, 360 387 timestamp, ··· 362 389 363 390 const e = new Error(message) 364 391 logger.error(e) 365 - expect(mockTransport).toHaveBeenCalledWith(LogLevel.Error, e, {}, timestamp) 392 + expect(mockTransport).toHaveBeenCalledWith( 393 + LogLevel.Error, 394 + undefined, 395 + e, 396 + {}, 397 + timestamp, 398 + ) 366 399 }) 367 400 368 401 test('info', () => { 369 402 const timestamp = Date.now() 370 403 const logger = new Logger({ 371 - enabled: true, 372 404 level: LogLevel.Info, 373 405 }) 374 406 const message = nanoid() ··· 382 414 logger.info(message) 383 415 expect(mockTransport).toHaveBeenCalledWith( 384 416 LogLevel.Info, 417 + undefined, 385 418 message, 386 419 {}, 387 420 timestamp, ··· 391 424 test('warn', () => { 392 425 const timestamp = Date.now() 393 426 const logger = new Logger({ 394 - enabled: true, 395 427 level: LogLevel.Warn, 396 428 }) 397 429 const message = nanoid() ··· 408 440 logger.warn(message) 409 441 expect(mockTransport).toHaveBeenCalledWith( 410 442 LogLevel.Warn, 443 + undefined, 411 444 message, 412 445 {}, 413 446 timestamp, ··· 417 450 test('error', () => { 418 451 const timestamp = Date.now() 419 452 const logger = new Logger({ 420 - enabled: true, 421 453 level: LogLevel.Error, 422 454 }) 423 455 const message = nanoid() ··· 436 468 437 469 const e = new Error('original message') 438 470 logger.error(e) 439 - expect(mockTransport).toHaveBeenCalledWith(LogLevel.Error, e, {}, timestamp) 471 + expect(mockTransport).toHaveBeenCalledWith( 472 + LogLevel.Error, 473 + undefined, 474 + e, 475 + {}, 476 + timestamp, 477 + ) 440 478 }) 441 479 })
-22
src/logger/bitdriftTransport.ts
··· 1 - import { 2 - debug as bdDebug, 3 - error as bdError, 4 - info as bdInfo, 5 - warn as bdWarn, 6 - } from '../lib/bitdrift' 7 - import {LogLevel, Transport} from './types' 8 - 9 - export function createBitdriftTransport(): Transport { 10 - const logFunctions = { 11 - [LogLevel.Debug]: bdDebug, 12 - [LogLevel.Info]: bdInfo, 13 - [LogLevel.Log]: bdInfo, 14 - [LogLevel.Warn]: bdWarn, 15 - [LogLevel.Error]: bdError, 16 - } as const 17 - 18 - return (level, message) => { 19 - const log = logFunctions[level] 20 - log('' + message) 21 - } 22 - }
-13
src/logger/debugContext.ts
··· 1 - /** 2 - * *Do not import this directly.* Instead, use the shortcut reference `logger.DebugContext`. 3 - * 4 - * Add debug contexts here. Although convention typically calls for enums ito 5 - * be capitalized, for parity with the `LOG_DEBUG` env var, please use all 6 - * lowercase. 7 - */ 8 - export const DebugContext = { 9 - // e.g. composer: 'composer' 10 - session: 'session', 11 - notifications: 'notifications', 12 - convo: 'convo', 13 - } as const
+75 -213
src/logger/index.ts
··· 1 - import format from 'date-fns/format' 2 1 import {nanoid} from 'nanoid/non-secure' 3 2 4 - import {isNetworkError} from '#/lib/strings/errors' 5 - import {DebugContext} from '#/logger/debugContext' 6 3 import {add} from '#/logger/logDump' 7 - import {Sentry} from '#/logger/sentry' 8 - import * as env from '#/env' 9 - import {createBitdriftTransport} from './bitdriftTransport' 10 - import {Metadata} from './types' 11 - import {ConsoleTransportEntry, LogLevel, Transport} from './types' 12 - 13 - export {LogLevel} 14 - export type {ConsoleTransportEntry, Transport} 15 - 16 - const enabledLogLevels: { 17 - [key in LogLevel]: LogLevel[] 18 - } = { 19 - [LogLevel.Debug]: [ 20 - LogLevel.Debug, 21 - LogLevel.Info, 22 - LogLevel.Log, 23 - LogLevel.Warn, 24 - LogLevel.Error, 25 - ], 26 - [LogLevel.Info]: [LogLevel.Info, LogLevel.Log, LogLevel.Warn, LogLevel.Error], 27 - [LogLevel.Log]: [LogLevel.Log, LogLevel.Warn, LogLevel.Error], 28 - [LogLevel.Warn]: [LogLevel.Warn, LogLevel.Error], 29 - [LogLevel.Error]: [LogLevel.Error], 30 - } 31 - 32 - export function prepareMetadata(metadata: Metadata): Metadata { 33 - return Object.keys(metadata).reduce((acc, key) => { 34 - let value = metadata[key] 35 - if (value instanceof Error) { 36 - value = value.toString() 37 - } 38 - return {...acc, [key]: value} 39 - }, {}) 40 - } 41 - 42 - /** 43 - * Used in dev mode to nicely log to the console 44 - */ 45 - export const consoleTransport: Transport = ( 46 - level, 47 - message, 48 - metadata, 49 - timestamp, 50 - ) => { 51 - const extra = Object.keys(metadata).length 52 - ? ' ' + JSON.stringify(prepareMetadata(metadata), null, ' ') 53 - : '' 54 - const log = { 55 - [LogLevel.Debug]: console.debug, 56 - [LogLevel.Info]: console.info, 57 - [LogLevel.Log]: console.log, 58 - [LogLevel.Warn]: console.warn, 59 - [LogLevel.Error]: console.error, 60 - }[level] 61 - 62 - if (message instanceof Error) { 63 - console.info( 64 - `${format(timestamp, 'HH:mm:ss')} ${message.toString()}${extra}`, 65 - ) 66 - log(message) 67 - } else { 68 - log(`${format(timestamp, 'HH:mm:ss')} ${message.toString()}${extra}`) 69 - } 70 - } 71 - 72 - export const sentryTransport: Transport = ( 73 - level, 74 - message, 75 - {type, tags, ...metadata}, 76 - timestamp, 77 - ) => { 78 - const meta = prepareMetadata(metadata) 79 - 80 - /** 81 - * If a string, report a breadcrumb 82 - */ 83 - if (typeof message === 'string') { 84 - const severity = ( 85 - { 86 - [LogLevel.Debug]: 'debug', 87 - [LogLevel.Info]: 'info', 88 - [LogLevel.Log]: 'log', // Sentry value here is undefined 89 - [LogLevel.Warn]: 'warning', 90 - [LogLevel.Error]: 'error', 91 - } as const 92 - )[level] 93 - 94 - Sentry.addBreadcrumb({ 95 - message, 96 - data: meta, 97 - type: type || 'default', 98 - level: severity, 99 - timestamp: timestamp / 1000, // Sentry expects seconds 100 - }) 4 + import {bitdriftTransport} from '#/logger/transports/bitdrift' 5 + import {consoleTransport} from '#/logger/transports/console' 6 + import {sentryTransport} from '#/logger/transports/sentry' 7 + import {LogContext, LogLevel, Metadata, Transport} from '#/logger/types' 8 + import {enabledLogLevels} from '#/logger/util' 101 9 102 - // We don't want to send any network errors to sentry 103 - if (isNetworkError(message)) { 104 - return 10 + const TRANSPORTS: Transport[] = (function configureTransports() { 11 + switch (process.env.NODE_ENV) { 12 + case 'production': { 13 + return [sentryTransport, bitdriftTransport].filter(Boolean) as Transport[] 105 14 } 106 - 107 - /** 108 - * Send all higher levels with `captureMessage`, with appropriate severity 109 - * level 110 - */ 111 - if (level === 'error' || level === 'warn' || level === 'log') { 112 - const messageLevel = ({ 113 - [LogLevel.Log]: 'log', 114 - [LogLevel.Warn]: 'warning', 115 - [LogLevel.Error]: 'error', 116 - }[level] || 'log') as Sentry.Breadcrumb['level'] 117 - // Defer non-critical messages so they're sent in a batch 118 - queueMessageForSentry(message, { 119 - level: messageLevel, 120 - tags, 121 - extra: meta, 122 - }) 15 + case 'test': { 16 + return [] 123 17 } 124 - } else { 125 - /** 126 - * It's otherwise an Error and should be reported with captureException 127 - */ 128 - Sentry.captureException(message, { 129 - tags, 130 - extra: meta, 131 - }) 132 - } 133 - } 134 - 135 - const queuedMessages: [string, Parameters<typeof Sentry.captureMessage>[1]][] = 136 - [] 137 - let sentrySendTimeout: ReturnType<typeof setTimeout> | null = null 138 - function queueMessageForSentry( 139 - message: string, 140 - captureContext: Parameters<typeof Sentry.captureMessage>[1], 141 - ) { 142 - queuedMessages.push([message, captureContext]) 143 - if (!sentrySendTimeout) { 144 - // Throttle sending messages with a leading delay 145 - // so that we can get Sentry out of the critical path. 146 - sentrySendTimeout = setTimeout(() => { 147 - sentrySendTimeout = null 148 - sendQueuedMessages() 149 - }, 7000) 150 - } 151 - } 152 - function sendQueuedMessages() { 153 - while (queuedMessages.length > 0) { 154 - const record = queuedMessages.shift() 155 - if (record) { 156 - Sentry.captureMessage(record[0], record[1]) 18 + default: { 19 + return [consoleTransport] 157 20 } 158 21 } 159 - } 22 + })() 160 23 161 - /** 162 - * Main class. Defaults are provided in the constructor so that subclasses are 163 - * technically possible, if we need to go that route in the future. 164 - */ 165 24 export class Logger { 166 - LogLevel = LogLevel 167 - DebugContext = DebugContext 25 + static Level = LogLevel 26 + static Context = LogContext 168 27 169 - enabled: boolean 170 28 level: LogLevel 171 - transports: Transport[] = [] 29 + context: LogContext | undefined = undefined 30 + contextFilter: string = '' 172 31 173 32 protected debugContextRegexes: RegExp[] = [] 33 + protected transports: Transport[] = [] 34 + 35 + static create(context?: LogContext) { 36 + const logger = new Logger({ 37 + level: process.env.EXPO_PUBLIC_LOG_LEVEL as LogLevel, 38 + context, 39 + contextFilter: process.env.EXPO_PUBLIC_LOG_DEBUG || '', 40 + }) 41 + for (const transport of TRANSPORTS) { 42 + logger.addTransport(transport) 43 + } 44 + return logger 45 + } 174 46 175 47 constructor({ 176 - enabled = process.env.NODE_ENV !== 'test', 177 - level = env.LOG_LEVEL as LogLevel, 178 - debug = env.LOG_DEBUG || '', 48 + level, 49 + context, 50 + contextFilter, 179 51 }: { 180 - enabled?: boolean 181 52 level?: LogLevel 182 - debug?: string 53 + context?: LogContext 54 + contextFilter?: string 183 55 } = {}) { 184 - this.enabled = enabled !== false 185 - this.level = debug ? LogLevel.Debug : level ?? LogLevel.Info // default to info 186 - this.debugContextRegexes = (debug || '').split(',').map(context => { 187 - return new RegExp(context.replace(/[^\w:*]/, '').replace(/\*/g, '.*')) 188 - }) 56 + this.context = context 57 + this.level = level || LogLevel.Info 58 + this.contextFilter = contextFilter || '' 59 + if (this.contextFilter) { 60 + this.level = LogLevel.Debug 61 + } 62 + this.debugContextRegexes = (this.contextFilter || '') 63 + .split(',') 64 + .map(filter => { 65 + return new RegExp(filter.replace(/[^\w:*-]/, '').replace(/\*/g, '.*')) 66 + }) 189 67 } 190 68 191 - debug(message: string, metadata: Metadata = {}, context?: string) { 192 - if (context && !this.debugContextRegexes.find(reg => reg.test(context))) 193 - return 194 - this.transport(LogLevel.Debug, message, metadata) 69 + debug(message: string, metadata: Metadata = {}) { 70 + this.transport({level: LogLevel.Debug, message, metadata}) 195 71 } 196 72 197 73 info(message: string, metadata: Metadata = {}) { 198 - this.transport(LogLevel.Info, message, metadata) 74 + this.transport({level: LogLevel.Info, message, metadata}) 199 75 } 200 76 201 77 log(message: string, metadata: Metadata = {}) { 202 - this.transport(LogLevel.Log, message, metadata) 78 + this.transport({level: LogLevel.Log, message, metadata}) 203 79 } 204 80 205 81 warn(message: string, metadata: Metadata = {}) { 206 - this.transport(LogLevel.Warn, message, metadata) 82 + this.transport({level: LogLevel.Warn, message, metadata}) 207 83 } 208 84 209 85 error(error: Error | string, metadata: Metadata = {}) { 210 - this.transport(LogLevel.Error, error, metadata) 86 + this.transport({level: LogLevel.Error, message: error, metadata}) 211 87 } 212 88 213 89 addTransport(transport: Transport) { ··· 217 93 } 218 94 } 219 95 220 - disable() { 221 - this.enabled = false 222 - } 223 - 224 - enable() { 225 - this.enabled = true 226 - } 227 - 228 - protected transport( 229 - level: LogLevel, 230 - message: string | Error, 231 - metadata: Metadata = {}, 232 - ) { 233 - if (!this.enabled) return 96 + protected transport({ 97 + level, 98 + message, 99 + metadata = {}, 100 + }: { 101 + level: LogLevel 102 + message: string | Error 103 + metadata: Metadata 104 + }) { 105 + if ( 106 + level === LogLevel.Debug && 107 + !!this.contextFilter && 108 + !!this.context && 109 + !this.debugContextRegexes.find(reg => reg.test(this.context!)) 110 + ) 111 + return 234 112 235 113 const timestamp = Date.now() 236 114 const meta = metadata || {} ··· 240 118 id: nanoid(), 241 119 timestamp, 242 120 level, 121 + context: this.context, 243 122 message, 244 123 metadata: meta, 245 124 }) ··· 247 126 if (!enabledLogLevels[this.level].includes(level)) return 248 127 249 128 for (const transport of this.transports) { 250 - transport(level, message, meta, timestamp) 129 + transport(level, this.context, message, meta, timestamp) 251 130 } 252 131 } 253 132 } 254 133 255 134 /** 256 - * Logger instance. See `@/logger/README` for docs. 135 + * Default logger instance. See `@/logger/README` for docs. 257 136 * 258 137 * Basic usage: 259 138 * 260 - * `logger.debug(message[, metadata, debugContext])` 139 + * `logger.debug(message[, metadata])` 261 140 * `logger.info(message[, metadata])` 141 + * `logger.log(message[, metadata])` 262 142 * `logger.warn(message[, metadata])` 263 143 * `logger.error(error[, metadata])` 264 - * `logger.disable()` 265 - * `logger.enable()` 266 144 */ 267 - export const logger = new Logger() 268 - 269 - if (process.env.NODE_ENV !== 'test') { 270 - logger.addTransport(createBitdriftTransport()) 271 - } 272 - 273 - if (process.env.NODE_ENV !== 'test') { 274 - if (__DEV__) { 275 - logger.addTransport(consoleTransport) 276 - /* 277 - * Comment this out to enable Sentry transport in dev 278 - */ 279 - // logger.addTransport(sentryTransport) 280 - } else { 281 - logger.addTransport(sentryTransport) 282 - } 283 - } 145 + export const logger = Logger.create(Logger.Context.Default)
+10 -1
src/logger/logDump.ts
··· 1 - import type {ConsoleTransportEntry} from '#/logger' 1 + import type {LogContext, LogLevel, Metadata} from '#/logger/types' 2 + 3 + export type ConsoleTransportEntry = { 4 + id: string 5 + timestamp: number 6 + level: LogLevel 7 + context: LogContext | undefined 8 + message: string | Error 9 + metadata: Metadata 10 + } 2 11 3 12 let entries: ConsoleTransportEntry[] = [] 4 13
src/logger/sentry/index.ts src/logger/sentry/lib/index.ts
src/logger/sentry/index.web.ts src/logger/sentry/lib/index.web.ts
+30
src/logger/transports/bitdrift.ts
··· 1 + import { 2 + debug as bdDebug, 3 + error as bdError, 4 + info as bdInfo, 5 + warn as bdWarn, 6 + } from '#/lib/bitdrift' 7 + import {LogLevel, Transport} from '#/logger/types' 8 + import {prepareMetadata} from '#/logger/util' 9 + 10 + const logFunctions = { 11 + [LogLevel.Debug]: bdDebug, 12 + [LogLevel.Info]: bdInfo, 13 + [LogLevel.Log]: bdInfo, 14 + [LogLevel.Warn]: bdWarn, 15 + [LogLevel.Error]: bdError, 16 + } as const 17 + 18 + export const bitdriftTransport: Transport = ( 19 + level, 20 + context, 21 + message, 22 + metadata, 23 + ) => { 24 + const log = logFunctions[level] 25 + log(message.toString(), { 26 + // match Sentry payload 27 + context, 28 + ...prepareMetadata(metadata), 29 + }) 30 + }
+90
src/logger/transports/console.ts
··· 1 + import format from 'date-fns/format' 2 + 3 + import {LogLevel, Transport} from '#/logger/types' 4 + import {prepareMetadata} from '#/logger/util' 5 + import {isWeb} from '#/platform/detection' 6 + 7 + /** 8 + * Used in dev mode to nicely log to the console 9 + */ 10 + export const consoleTransport: Transport = ( 11 + level, 12 + context, 13 + message, 14 + metadata, 15 + timestamp, 16 + ) => { 17 + const hasMetadata = Object.keys(metadata).length 18 + const colorize = withColor( 19 + { 20 + [LogLevel.Debug]: colors.magenta, 21 + [LogLevel.Info]: colors.blue, 22 + [LogLevel.Log]: colors.green, 23 + [LogLevel.Warn]: colors.yellow, 24 + [LogLevel.Error]: colors.red, 25 + }[level], 26 + ) 27 + 28 + let msg = `${colorize(format(timestamp, 'HH:mm:ss'))}` 29 + if (context) { 30 + msg += ` ${colorize(`(${context})`)}` 31 + } 32 + if (message) { 33 + msg += ` ${message.toString()}` 34 + } 35 + 36 + if (isWeb) { 37 + if (hasMetadata) { 38 + console.groupCollapsed(msg) 39 + console.log(metadata) 40 + console.groupEnd() 41 + } else { 42 + console.log(msg) 43 + } 44 + if (message instanceof Error) { 45 + // for stacktrace 46 + console.error(message) 47 + } 48 + } else { 49 + if (hasMetadata) { 50 + msg += ` ${JSON.stringify(prepareMetadata(metadata), null, 2)}` 51 + } 52 + console.log(msg) 53 + if (message instanceof Error) { 54 + // for stacktrace 55 + console.error(message) 56 + } 57 + } 58 + } 59 + 60 + /** 61 + * Color handling copied from Kleur 62 + * 63 + * @see https://github.com/lukeed/kleur/blob/fa3454483899ddab550d08c18c028e6db1aab0e5/colors.mjs#L13 64 + */ 65 + const colors: { 66 + [key: string]: [number, number] 67 + } = { 68 + default: [0, 0], 69 + blue: [36, 39], 70 + green: [32, 39], 71 + magenta: [35, 39], 72 + red: [31, 39], 73 + yellow: [33, 39], 74 + } 75 + 76 + function withColor([x, y]: [number, number]) { 77 + const rgx = new RegExp(`\\x1b\\[${y}m`, 'g') 78 + const open = `\x1b[${x}m`, 79 + close = `\x1b[${y}m` 80 + 81 + return function (txt: string) { 82 + if (txt == null) return txt 83 + 84 + return ( 85 + open + 86 + (~('' + txt).indexOf(close) ? txt.replace(rgx, close + open) : txt) + 87 + close 88 + ) 89 + } 90 + }
+102
src/logger/transports/sentry.ts
··· 1 + import {isNetworkError} from '#/lib/strings/errors' 2 + import {Sentry} from '#/logger/sentry/lib' 3 + import {LogLevel, Transport} from '#/logger/types' 4 + import {prepareMetadata} from '#/logger/util' 5 + 6 + export const sentryTransport: Transport = ( 7 + level, 8 + context, 9 + message, 10 + {type, tags, ...metadata}, 11 + timestamp, 12 + ) => { 13 + const meta = { 14 + // match Bitdrift payload 15 + context, 16 + ...prepareMetadata(metadata), 17 + } 18 + let _tags = tags || {} 19 + _tags = { 20 + // use `category` to match breadcrumbs 21 + category: context, 22 + ...tags, 23 + } 24 + 25 + /** 26 + * If a string, report a breadcrumb 27 + */ 28 + if (typeof message === 'string') { 29 + const severity = ( 30 + { 31 + [LogLevel.Debug]: 'debug', 32 + [LogLevel.Info]: 'info', 33 + [LogLevel.Log]: 'log', // Sentry value here is undefined 34 + [LogLevel.Warn]: 'warning', 35 + [LogLevel.Error]: 'error', 36 + } as const 37 + )[level] 38 + 39 + Sentry.addBreadcrumb({ 40 + category: context, 41 + message, 42 + data: meta, 43 + type: type || 'default', 44 + level: severity, 45 + timestamp: timestamp / 1000, // Sentry expects seconds 46 + }) 47 + 48 + // We don't want to send any network errors to sentry 49 + if (isNetworkError(message)) { 50 + return 51 + } 52 + 53 + /** 54 + * Send all higher levels with `captureMessage`, with appropriate severity 55 + * level 56 + */ 57 + if (level === 'error' || level === 'warn' || level === 'log') { 58 + // Defer non-critical messages so they're sent in a batch 59 + queueMessageForSentry(message, { 60 + level: severity, 61 + tags: _tags, 62 + extra: meta, 63 + }) 64 + } 65 + } else { 66 + /** 67 + * It's otherwise an Error and should be reported with captureException 68 + */ 69 + Sentry.captureException(message, { 70 + tags: _tags, 71 + extra: meta, 72 + }) 73 + } 74 + } 75 + 76 + const queuedMessages: [string, Parameters<typeof Sentry.captureMessage>[1]][] = 77 + [] 78 + let sentrySendTimeout: ReturnType<typeof setTimeout> | null = null 79 + 80 + function queueMessageForSentry( 81 + message: string, 82 + captureContext: Parameters<typeof Sentry.captureMessage>[1], 83 + ) { 84 + queuedMessages.push([message, captureContext]) 85 + if (!sentrySendTimeout) { 86 + // Throttle sending messages with a leading delay 87 + // so that we can get Sentry out of the critical path. 88 + sentrySendTimeout = setTimeout(() => { 89 + sentrySendTimeout = null 90 + sendQueuedMessages() 91 + }, 7000) 92 + } 93 + } 94 + 95 + function sendQueuedMessages() { 96 + while (queuedMessages.length > 0) { 97 + const record = queuedMessages.shift() 98 + if (record) { 99 + Sentry.captureMessage(record[0], record[1]) 100 + } 101 + } 102 + }
+31 -18
src/logger/types.ts
··· 1 - import type {Sentry} from '#/logger/sentry' 1 + /** 2 + * DO NOT IMPORT THIS DIRECTLY 3 + * 4 + * Logger contexts, defined here and used via `Logger.Context.*` static prop. 5 + */ 6 + export enum LogContext { 7 + Default = 'logger', 8 + Session = 'session', 9 + Notifications = 'notifications', 10 + ConversationAgent = 'conversation-agent', 11 + DMsAgent = 'dms-agent', 12 + } 2 13 3 14 export enum LogLevel { 4 15 Debug = 'debug', ··· 10 21 11 22 export type Transport = ( 12 23 level: LogLevel, 24 + context: LogContext | undefined, 13 25 message: string | Error, 14 26 metadata: Metadata, 15 27 timestamp: number, ··· 20 32 * `captureException` parameter, `CaptureContext`. 21 33 */ 22 34 export type Metadata = { 35 + /** 36 + * Reserved for appending `LogContext` to logging payloads 37 + */ 38 + context?: undefined 39 + 23 40 /** 24 41 * Applied as Sentry breadcrumb types. Defaults to `default`. 25 42 * ··· 43 60 * @see https://github.com/getsentry/sentry-javascript/blob/903addf9a1a1534a6cb2ba3143654b918a86f6dd/packages/types/src/misc.ts#L65 44 61 */ 45 62 tags?: { 46 - [key: string]: 47 - | number 48 - | string 49 - | boolean 50 - | bigint 51 - | symbol 52 - | null 53 - | undefined 63 + [key: string]: number | string | boolean | null | undefined 54 64 } 55 65 56 66 /** 57 67 * Any additional data, passed through to Sentry as `extra` param on 58 68 * exceptions, or the `data` param on breadcrumbs. 59 69 */ 60 - [key: string]: unknown 61 - } & Parameters<typeof Sentry.captureException>[1] 62 - 63 - export type ConsoleTransportEntry = { 64 - id: string 65 - timestamp: number 66 - level: LogLevel 67 - message: string | Error 68 - metadata: Metadata 70 + [key: string]: Serializable | Error | unknown 69 71 } 72 + 73 + export type Serializable = 74 + | string 75 + | number 76 + | boolean 77 + | null 78 + | undefined 79 + | Serializable[] 80 + | { 81 + [key: string]: Serializable 82 + }
+29
src/logger/util.ts
··· 1 + import {LogLevel, Metadata, Serializable} from '#/logger/types' 2 + 3 + export const enabledLogLevels: { 4 + [key in LogLevel]: LogLevel[] 5 + } = { 6 + [LogLevel.Debug]: [ 7 + LogLevel.Debug, 8 + LogLevel.Info, 9 + LogLevel.Log, 10 + LogLevel.Warn, 11 + LogLevel.Error, 12 + ], 13 + [LogLevel.Info]: [LogLevel.Info, LogLevel.Log, LogLevel.Warn, LogLevel.Error], 14 + [LogLevel.Log]: [LogLevel.Log, LogLevel.Warn, LogLevel.Error], 15 + [LogLevel.Warn]: [LogLevel.Warn, LogLevel.Error], 16 + [LogLevel.Error]: [LogLevel.Error], 17 + } 18 + 19 + export function prepareMetadata( 20 + metadata: Metadata, 21 + ): Record<string, Serializable> { 22 + return Object.keys(metadata).reduce((acc, key) => { 23 + let value = metadata[key] 24 + if (value instanceof Error) { 25 + value = value.toString() 26 + } 27 + return {...acc, [key]: value} 28 + }, {}) 29 + }
+1 -1
src/screens/Deactivated.tsx
··· 96 96 } 97 97 98 98 logger.error(e, { 99 - context: 'Failed to activate account', 99 + message: 'Failed to activate account', 100 100 }) 101 101 } finally { 102 102 setPending(false)
+1 -1
src/screens/ModerationInteractionSettings/index.tsx
··· 102 102 Toast.show(_(msg`Settings saved`)) 103 103 } catch (e: any) { 104 104 logger.error(`Failed to save post interaction settings`, { 105 - context: 'ModerationInteractionSettingsScreen', 105 + source: 'ModerationInteractionSettingsScreen', 106 106 safeMessage: e.message, 107 107 }) 108 108 setError(_(msg`Failed to save settings. Please try again.`))
+1 -1
src/screens/Settings/components/DeactivateAccountDialog.tsx
··· 61 61 } 62 62 63 63 logger.error(e, { 64 - context: 'Failed to deactivate account', 64 + message: 'Failed to deactivate account', 65 65 }) 66 66 } finally { 67 67 setPending(false)
+18 -23
src/state/messages/convo/agent.ts
··· 10 10 import {nanoid} from 'nanoid/non-secure' 11 11 12 12 import {networkRetry} from '#/lib/async/retry' 13 - import {logger} from '#/logger' 13 + import {Logger} from '#/logger' 14 14 import {isNative} from '#/platform/detection' 15 15 import { 16 16 ACTIVE_POLL_INTERVAL, ··· 33 33 import {MessagesEventBus} from '#/state/messages/events/agent' 34 34 import {MessagesEventBusError} from '#/state/messages/events/types' 35 35 import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const' 36 + 37 + const logger = Logger.create(Logger.Context.ConversationAgent) 36 38 37 39 export function isConvoItemMessage( 38 40 item: ConvoItem, ··· 125 127 126 128 getSnapshot(): ConvoState { 127 129 if (!this.snapshot) this.snapshot = this.generateSnapshot() 128 - // logger.debug('Convo: snapshotted', {}, logger.DebugContext.convo) 130 + // logger.debug('Convo: snapshotted', {}) 129 131 return this.snapshot 130 132 } 131 133 ··· 375 377 break 376 378 } 377 379 378 - logger.debug( 379 - `Convo: dispatch '${action.event}'`, 380 - { 381 - id: this.id, 382 - prev: prevStatus, 383 - next: this.status, 384 - }, 385 - logger.DebugContext.convo, 386 - ) 380 + logger.debug(`Convo: dispatch '${action.event}'`, { 381 + id: this.id, 382 + prev: prevStatus, 383 + next: this.status, 384 + }) 387 385 388 386 this.updateLastActiveTimestamp() 389 387 this.commit() ··· 471 469 this.dispatch({event: ConvoDispatchEvent.Ready}) 472 470 } 473 471 } catch (e: any) { 474 - logger.error(e, {context: 'Convo: setup failed'}) 472 + logger.error(e, {message: 'Convo: setup failed'}) 475 473 476 474 this.dispatch({ 477 475 event: ConvoDispatchEvent.Error, ··· 576 574 this.sender = sender || this.sender 577 575 this.recipients = recipients || this.recipients 578 576 } catch (e: any) { 579 - logger.error(e, {context: `Convo: failed to refresh convo`}) 577 + logger.error(e, {message: `Convo: failed to refresh convo`}) 580 578 } 581 579 } 582 580 ··· 586 584 } 587 585 | undefined 588 586 async fetchMessageHistory() { 589 - logger.debug('Convo: fetch message history', {}, logger.DebugContext.convo) 587 + logger.debug('Convo: fetch message history', {}) 590 588 591 589 /* 592 590 * If oldestRev is null, we've fetched all history. ··· 773 771 // Ignore empty messages for now since they have no other purpose atm 774 772 if (!message.text.trim() && !message.embed) return 775 773 776 - logger.debug('Convo: send message', {}, logger.DebugContext.convo) 774 + logger.debug('Convo: send message', {}) 777 775 778 776 const tempId = nanoid() 779 777 ··· 793 791 logger.debug( 794 792 `Convo: processing messages (${this.pendingMessages.size} remaining)`, 795 793 {}, 796 - logger.DebugContext.convo, 797 794 ) 798 795 799 796 const pendingMessage = Array.from(this.pendingMessages.values()).shift() ··· 837 834 // continue queue processing 838 835 await this.processPendingMessages() 839 836 } catch (e: any) { 840 - logger.error(e, {context: `Convo: failed to send message`}) 837 + logger.error(e, {message: `Convo: failed to send message`}) 841 838 this.handleSendMessageFailure(e) 842 839 this.isProcessingPendingMessages = false 843 840 } ··· 883 880 } else { 884 881 this.pendingMessageFailure = 'unrecoverable' 885 882 logger.error(e, { 886 - context: `Convo handleSendMessageFailure received unknown error`, 883 + message: `Convo handleSendMessageFailure received unknown error`, 887 884 }) 888 885 } 889 886 ··· 902 899 logger.debug( 903 900 `Convo: batch retrying ${this.pendingMessages.size} pending messages`, 904 901 {}, 905 - logger.DebugContext.convo, 906 902 ) 907 903 908 904 try { ··· 937 933 logger.debug( 938 934 `Convo: sent ${this.pendingMessages.size} pending messages`, 939 935 {}, 940 - logger.DebugContext.convo, 941 936 ) 942 937 } catch (e: any) { 943 - logger.error(e, {context: `Convo: failed to batch retry messages`}) 938 + logger.error(e, {message: `Convo: failed to batch retry messages`}) 944 939 this.handleSendMessageFailure(e) 945 940 } 946 941 } 947 942 948 943 async deleteMessage(messageId: string) { 949 - logger.debug('Convo: delete message', {}, logger.DebugContext.convo) 944 + logger.debug('Convo: delete message', {}) 950 945 951 946 this.deletedMessages.add(messageId) 952 947 this.commit() ··· 962 957 ) 963 958 }) 964 959 } catch (e: any) { 965 - logger.error(e, {context: `Convo: failed to delete message`}) 960 + logger.error(e, {message: `Convo: failed to delete message`}) 966 961 this.deletedMessages.delete(messageId) 967 962 this.commit() 968 963 throw e
+14 -18
src/state/messages/events/agent.ts
··· 3 3 import {nanoid} from 'nanoid/non-secure' 4 4 5 5 import {networkRetry} from '#/lib/async/retry' 6 - import {logger} from '#/logger' 6 + import {Logger} from '#/logger' 7 7 import { 8 8 BACKGROUND_POLL_INTERVAL, 9 9 DEFAULT_POLL_INTERVAL, ··· 19 19 import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const' 20 20 21 21 const LOGGER_CONTEXT = 'MessagesEventBus' 22 + const logger = Logger.create(Logger.Context.DMsAgent) 22 23 23 24 export class MessagesEventBus { 24 25 private id: string ··· 90 91 } 91 92 92 93 background() { 93 - logger.debug(`${LOGGER_CONTEXT}: background`, {}, logger.DebugContext.convo) 94 + logger.debug(`${LOGGER_CONTEXT}: background`, {}) 94 95 this.dispatch({event: MessagesEventBusDispatchEvent.Background}) 95 96 } 96 97 97 98 suspend() { 98 - logger.debug(`${LOGGER_CONTEXT}: suspend`, {}, logger.DebugContext.convo) 99 + logger.debug(`${LOGGER_CONTEXT}: suspend`, {}) 99 100 this.dispatch({event: MessagesEventBusDispatchEvent.Suspend}) 100 101 } 101 102 102 103 resume() { 103 - logger.debug(`${LOGGER_CONTEXT}: resume`, {}, logger.DebugContext.convo) 104 + logger.debug(`${LOGGER_CONTEXT}: resume`, {}) 104 105 this.dispatch({event: MessagesEventBusDispatchEvent.Resume}) 105 106 } 106 107 ··· 222 223 break 223 224 } 224 225 225 - logger.debug( 226 - `${LOGGER_CONTEXT}: dispatch '${action.event}'`, 227 - { 228 - id: this.id, 229 - prev: prevStatus, 230 - next: this.status, 231 - }, 232 - logger.DebugContext.convo, 233 - ) 226 + logger.debug(`${LOGGER_CONTEXT}: dispatch '${action.event}'`, { 227 + id: this.id, 228 + prev: prevStatus, 229 + next: this.status, 230 + }) 234 231 } 235 232 236 233 private async init() { 237 - logger.debug(`${LOGGER_CONTEXT}: init`, {}, logger.DebugContext.convo) 234 + logger.debug(`${LOGGER_CONTEXT}: init`, {}) 238 235 239 236 try { 240 237 const response = await networkRetry(2, () => { ··· 258 255 this.dispatch({event: MessagesEventBusDispatchEvent.Ready}) 259 256 } catch (e: any) { 260 257 logger.error(e, { 261 - context: `${LOGGER_CONTEXT}: init failed`, 258 + message: `${LOGGER_CONTEXT}: init failed`, 262 259 }) 263 260 264 261 this.dispatch({ ··· 327 324 // this.requestedPollIntervals.values(), 328 325 // ), 329 326 // }, 330 - // logger.DebugContext.convo, 331 327 // ) 332 328 333 329 try { ··· 372 368 this.emitter.emit('event', {type: 'logs', logs: batch}) 373 369 } catch (e: any) { 374 370 logger.error(e, { 375 - context: `${LOGGER_CONTEXT}: process latest events`, 371 + message: `${LOGGER_CONTEXT}: process latest events`, 376 372 }) 377 373 } 378 374 } 379 375 } catch (e: any) { 380 - logger.error(e, {context: `${LOGGER_CONTEXT}: poll events failed`}) 376 + logger.error(e, {message: `${LOGGER_CONTEXT}: poll events failed`}) 381 377 382 378 this.dispatch({ 383 379 event: MessagesEventBusDispatchEvent.Error,
+2 -2
src/state/session/agent.ts
··· 166 166 }) 167 167 } catch (e: any) { 168 168 logger.error(e, { 169 - context: `session: createAgentAndCreateAccount failed to save personal details and feeds`, 169 + message: `session: createAgentAndCreateAccount failed to save personal details and feeds`, 170 170 }) 171 171 } 172 172 } else { ··· 177 177 // snooze first prompt after signup, defer to next prompt 178 178 snoozeEmailConfirmationPrompt() 179 179 } catch (e: any) { 180 - logger.error(e, {context: `session: failed snoozeEmailConfirmationPrompt`}) 180 + logger.error(e, {message: `session: failed snoozeEmailConfirmationPrompt`}) 181 181 } 182 182 183 183 return agent.prepare(gates, moderation, onSessionChange)