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