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
This commit is contained in:
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import 'react-native-url-polyfill/auto'
|
import 'react-native-url-polyfill/auto'
|
||||||
import '#/lib/sentry' // must be near top
|
import '#/logger/sentry/setup'
|
||||||
import '#/lib/bitdrift' // must be near top
|
import '#/lib/bitdrift' // must be near top
|
||||||
import '#/view/icons'
|
import '#/view/icons'
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import '#/lib/sentry' // must be near top
|
import '#/logger/sentry/setup' // must be near top
|
||||||
import '#/view/icons'
|
import '#/view/icons'
|
||||||
import './style.css'
|
import './style.css'
|
||||||
|
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ function SaveButtonInner({
|
|||||||
}
|
}
|
||||||
Toast.show(_(msg`Feeds updated!`))
|
Toast.show(_(msg`Feeds updated!`))
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.error(err, {context: `FeedCard: failed to update feeds`, pin})
|
logger.error(err, {message: `FeedCard: failed to update feeds`, pin})
|
||||||
Toast.show(_(msg`Failed to update feeds`), 'xmark')
|
Toast.show(_(msg`Failed to update feeds`), 'xmark')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export function PostInteractionSettingsDialogControlledInner(
|
|||||||
props.control.close()
|
props.control.close()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(`Failed to save post interaction settings`, {
|
logger.error(`Failed to save post interaction settings`, {
|
||||||
context: 'PostInteractionSettingsDialogControlledInner',
|
source: 'PostInteractionSettingsDialogControlledInner',
|
||||||
safeMessage: e.message,
|
safeMessage: e.message,
|
||||||
})
|
})
|
||||||
Toast.show(
|
Toast.show(
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {useQueryClient} from '@tanstack/react-query'
|
|||||||
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
|
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
|
||||||
import {NavigationProp} from '#/lib/routes/types'
|
import {NavigationProp} from '#/lib/routes/types'
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
import {logger} from '#/logger'
|
import {Logger} from '#/logger'
|
||||||
import {isAndroid} from '#/platform/detection'
|
import {isAndroid} from '#/platform/detection'
|
||||||
import {useCurrentConvoId} from '#/state/messages/current-convo-id'
|
import {useCurrentConvoId} from '#/state/messages/current-convo-id'
|
||||||
import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
|
import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
|
||||||
@@ -50,6 +50,8 @@ const DEFAULT_HANDLER_OPTIONS = {
|
|||||||
let storedPayload: NotificationPayload | undefined
|
let storedPayload: NotificationPayload | undefined
|
||||||
let prevDate = 0
|
let prevDate = 0
|
||||||
|
|
||||||
|
const logger = Logger.create(Logger.Context.Notifications)
|
||||||
|
|
||||||
export function useNotificationsHandler() {
|
export function useNotificationsHandler() {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const {currentAccount, accounts} = useSession()
|
const {currentAccount, accounts} = useSession()
|
||||||
@@ -186,11 +188,7 @@ export function useNotificationsHandler() {
|
|||||||
return DEFAULT_HANDLER_OPTIONS
|
return DEFAULT_HANDLER_OPTIONS
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(
|
logger.debug('Notifications: received', {e})
|
||||||
'Notifications: received',
|
|
||||||
{e},
|
|
||||||
logger.DebugContext.notifications,
|
|
||||||
)
|
|
||||||
|
|
||||||
const payload = e.request.trigger.payload as NotificationPayload
|
const payload = e.request.trigger.payload as NotificationPayload
|
||||||
if (
|
if (
|
||||||
@@ -217,13 +215,9 @@ export function useNotificationsHandler() {
|
|||||||
}
|
}
|
||||||
prevDate = e.notification.date
|
prevDate = e.notification.date
|
||||||
|
|
||||||
logger.debug(
|
logger.debug('Notifications: response received', {
|
||||||
'Notifications: response received',
|
|
||||||
{
|
|
||||||
actionIdentifier: e.actionIdentifier,
|
actionIdentifier: e.actionIdentifier,
|
||||||
},
|
})
|
||||||
logger.DebugContext.notifications,
|
|
||||||
)
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
e.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER &&
|
e.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER &&
|
||||||
@@ -235,7 +229,6 @@ export function useNotificationsHandler() {
|
|||||||
logger.debug(
|
logger.debug(
|
||||||
'User pressed a notification, opening notifications tab',
|
'User pressed a notification, opening notifications tab',
|
||||||
{},
|
{},
|
||||||
logger.DebugContext.notifications,
|
|
||||||
)
|
)
|
||||||
logEvent('notifications:openApp', {})
|
logEvent('notifications:openApp', {})
|
||||||
invalidateCachedUnreadPage()
|
invalidateCachedUnreadPage()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {getBadgeCountAsync, setBadgeCountAsync} from 'expo-notifications'
|
|||||||
import {BskyAgent} from '@atproto/api'
|
import {BskyAgent} from '@atproto/api'
|
||||||
|
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
import {logger} from '#/logger'
|
import {Logger} from '#/logger'
|
||||||
import {devicePlatform, isAndroid, isNative} from '#/platform/detection'
|
import {devicePlatform, isAndroid, isNative} from '#/platform/detection'
|
||||||
import {SessionAccount, useAgent, useSession} from '#/state/session'
|
import {SessionAccount, useAgent, useSession} from '#/state/session'
|
||||||
import BackgroundNotificationHandler from '../../../modules/expo-background-notification-handler'
|
import BackgroundNotificationHandler from '../../../modules/expo-background-notification-handler'
|
||||||
@@ -14,6 +14,8 @@ const SERVICE_DID = (serviceUrl?: string) =>
|
|||||||
? 'did:web:api.staging.bsky.dev'
|
? 'did:web:api.staging.bsky.dev'
|
||||||
: 'did:web:api.bsky.app'
|
: 'did:web:api.bsky.app'
|
||||||
|
|
||||||
|
const logger = Logger.create(Logger.Context.Notifications)
|
||||||
|
|
||||||
async function registerPushToken(
|
async function registerPushToken(
|
||||||
agent: BskyAgent,
|
agent: BskyAgent,
|
||||||
account: SessionAccount,
|
account: SessionAccount,
|
||||||
@@ -26,14 +28,10 @@ async function registerPushToken(
|
|||||||
token: token.data,
|
token: token.data,
|
||||||
appId: 'xyz.blueskyweb.app',
|
appId: 'xyz.blueskyweb.app',
|
||||||
})
|
})
|
||||||
logger.debug(
|
logger.debug('Notifications: Sent push token (init)', {
|
||||||
'Notifications: Sent push token (init)',
|
|
||||||
{
|
|
||||||
tokenType: token.type,
|
tokenType: token.type,
|
||||||
token: token.data,
|
token: token.data,
|
||||||
},
|
})
|
||||||
logger.DebugContext.notifications,
|
|
||||||
)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Notifications: Failed to set push token', {message: error})
|
logger.error('Notifications: Failed to set push token', {message: error})
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-78
@@ -1,99 +1,45 @@
|
|||||||
# Logger
|
# Logger
|
||||||
|
|
||||||
Simple logger for Bluesky. Supports log levels, debug contexts, and separate
|
Simple logger for Bluesky.
|
||||||
transports for production, dev, and test mode.
|
|
||||||
|
|
||||||
## At a Glance
|
## At a Glance
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { logger } from '#/logger'
|
import { logger, Logger } from '#/logger'
|
||||||
|
|
||||||
logger.debug(message[, metadata, debugContext])
|
// or, optionally create new instance with custom context
|
||||||
logger.info(message[, metadata])
|
// const logger = Logger.create(Logger.Context.Notifications)
|
||||||
logger.log(message[, metadata])
|
|
||||||
logger.warn(message[, metadata])
|
|
||||||
logger.error(error[, metadata])
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Modes
|
// for dev-only logs
|
||||||
|
logger.debug(message, {})
|
||||||
|
|
||||||
The "modes" referred to here are inferred from `process.env.NODE_ENV`,
|
// for production breadcrumbs
|
||||||
which matches how React Native sets the `__DEV__` global.
|
logger.info(message, {})
|
||||||
|
|
||||||
#### Log Levels
|
// seldom used, prefer `info`
|
||||||
|
logger.log(message, {})
|
||||||
|
|
||||||
Log levels are used to filter which logs are either printed to the console
|
// for non-error issues to look into, seldom used, prefer `error`
|
||||||
and/or sent to Sentry and other reporting services. To configure, set the
|
logger.warn(message, {})
|
||||||
`EXPO_PUBLIC_LOG_LEVEL` environment variable in `.env` to one of `debug`,
|
|
||||||
`info`, `log`, `warn`, or `error`.
|
|
||||||
|
|
||||||
This variable should be `info` in production, and `debug` in dev. If it gets too
|
// for known errors without an exception, use a string
|
||||||
noisy in dev, simply set it to a higher level, such as `warn`.
|
logger.error(`known error`, {})
|
||||||
|
|
||||||
## Usage
|
// for unknown exceptions
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { logger } from '#/logger';
|
|
||||||
```
|
|
||||||
|
|
||||||
### `logger.error`
|
|
||||||
|
|
||||||
The `error` level is for... well, errors. These are sent to Sentry in production mode.
|
|
||||||
|
|
||||||
`error`, along with all log levels, supports an additional parameter, `metadata: Record<string, unknown>`. Use this to provide values to the [Sentry
|
|
||||||
breadcrumb](https://docs.sentry.io/platforms/react-native/enriching-events/breadcrumbs/#manual-breadcrumbs).
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
try {
|
try {
|
||||||
// some async code
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(e, { ...metadata });
|
logger.error(e, {message: `explain error`}])
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### `logger.warn`
|
#### Log Levels
|
||||||
|
|
||||||
Warnings will be sent to Sentry as a separate Issue with level `warning`, as
|
Log level defaults to `info`. You can set this via the `EXPO_PUBLIC_LOG_LEVEL`
|
||||||
well as as breadcrumbs, with a severity level of `warning`
|
env var in `.env.local`.
|
||||||
|
|
||||||
### `logger.log`
|
#### Filtering debugs by context
|
||||||
|
|
||||||
Logs with level `log` will be sent to Sentry as a separate Issue with level `log`, as
|
Debug logs are dev-only, and not enabled by default. Once enabled, they can get
|
||||||
well as as breadcrumbs, with a severity level of `default`.
|
noisy. So you can filter them by setting the `EXPO_PUBLIC_LOG_DEBUG` env var
|
||||||
|
e.g. `EXPO_PUBLIC_LOG_DEBUG=notifications`. These values can be comma-separated
|
||||||
### `logger.info`
|
and include wildcards.
|
||||||
|
|
||||||
The `info` level should be used for information that would be helpful in a
|
|
||||||
tracing context, like Sentry. In production mode, `info` logs are sent
|
|
||||||
to Sentry as breadcrumbs, which decorate log levels above `info` such as `log`,
|
|
||||||
`warn`, and `error`.
|
|
||||||
|
|
||||||
### `logger.debug`
|
|
||||||
|
|
||||||
Debug level is really only intended for local development. Use this instead of
|
|
||||||
`console.log`.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
logger.debug(message, { ...metadata });
|
|
||||||
```
|
|
||||||
|
|
||||||
Inspired by [debug](https://www.npmjs.com/package/debug), when writing debug
|
|
||||||
logs, you can optionally pass a _context_, which can be then filtered when in
|
|
||||||
debug mode.
|
|
||||||
|
|
||||||
This value should be related to the feature, component, or screen
|
|
||||||
the code is running within, and **it should be defined in `#/logger/debugContext`**.
|
|
||||||
This way we know if a relevant context already exists, and we can trace all
|
|
||||||
active contexts in use in our app. This const enum is conveniently available on
|
|
||||||
the `logger` at `logger.DebugContext`.
|
|
||||||
|
|
||||||
For example, a debug log like this:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
logger.debug(message, {}, logger.DebugContext.composer);
|
|
||||||
```
|
|
||||||
|
|
||||||
Would be logged to the console in dev mode if `EXPO_PUBLIC_LOG_LEVEL=debug`, _or_ if you
|
|
||||||
pass a separate environment variable `LOG_DEBUG=composer`. This variable supports
|
|
||||||
multiple contexts using commas like `LOG_DEBUG=composer,profile`, and _automatically
|
|
||||||
sets the log level to `debug`, regardless of `EXPO_PUBLIC_LOG_LEVEL`._
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import {expect, test} from '@jest/globals'
|
import {expect, test} from '@jest/globals'
|
||||||
|
|
||||||
import {ConsoleTransportEntry, LogLevel} from '#/logger'
|
import {add, ConsoleTransportEntry, getEntries} from '#/logger/logDump'
|
||||||
import {add, getEntries} from '#/logger/logDump'
|
import {LogContext, LogLevel} from '#/logger/types'
|
||||||
|
|
||||||
test('works', () => {
|
test('works', () => {
|
||||||
const items: ConsoleTransportEntry[] = [
|
const items: ConsoleTransportEntry[] = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
level: LogLevel.Debug,
|
level: LogLevel.Debug,
|
||||||
|
context: LogContext.Default,
|
||||||
message: 'hello',
|
message: 'hello',
|
||||||
metadata: {},
|
metadata: {},
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
@@ -15,6 +16,7 @@ test('works', () => {
|
|||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
level: LogLevel.Debug,
|
level: LogLevel.Debug,
|
||||||
|
context: LogContext.Default,
|
||||||
message: 'hello',
|
message: 'hello',
|
||||||
metadata: {},
|
metadata: {},
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
@@ -22,6 +24,7 @@ test('works', () => {
|
|||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
level: LogLevel.Debug,
|
level: LogLevel.Debug,
|
||||||
|
context: LogContext.Default,
|
||||||
message: 'hello',
|
message: 'hello',
|
||||||
metadata: {},
|
metadata: {},
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
|
|||||||
@@ -2,16 +2,9 @@ import {beforeAll, describe, expect, jest, test} from '@jest/globals'
|
|||||||
import * as Sentry from '@sentry/react-native'
|
import * as Sentry from '@sentry/react-native'
|
||||||
import {nanoid} from 'nanoid/non-secure'
|
import {nanoid} from 'nanoid/non-secure'
|
||||||
|
|
||||||
import {Logger, LogLevel, sentryTransport} from '#/logger'
|
import {Logger} from '#/logger'
|
||||||
|
import {sentryTransport} from '#/logger/transports/sentry'
|
||||||
jest.mock('#/env', () => ({
|
import {LogLevel} from '#/logger/types'
|
||||||
/*
|
|
||||||
* Forces debug mode for tests using the default logger. Most tests create
|
|
||||||
* their own logger instance.
|
|
||||||
*/
|
|
||||||
LOG_LEVEL: 'debug',
|
|
||||||
LOG_DEBUG: '',
|
|
||||||
}))
|
|
||||||
|
|
||||||
jest.mock('@sentry/react-native', () => ({
|
jest.mock('@sentry/react-native', () => ({
|
||||||
addBreadcrumb: jest.fn(),
|
addBreadcrumb: jest.fn(),
|
||||||
@@ -26,57 +19,27 @@ beforeAll(() => {
|
|||||||
describe('general functionality', () => {
|
describe('general functionality', () => {
|
||||||
test('default params', () => {
|
test('default params', () => {
|
||||||
const logger = new Logger()
|
const logger = new Logger()
|
||||||
expect(logger.enabled).toBeFalsy()
|
expect(logger.level).toEqual(LogLevel.Info)
|
||||||
expect(logger.level).toEqual(LogLevel.Debug) // mocked above
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test('can override default params', () => {
|
test('can override default params', () => {
|
||||||
const logger = new Logger({
|
const logger = new Logger({
|
||||||
enabled: true,
|
level: LogLevel.Debug,
|
||||||
|
})
|
||||||
|
expect(logger.level).toEqual(LogLevel.Debug)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('contextFilter overrides level', () => {
|
||||||
|
const logger = new Logger({
|
||||||
level: LogLevel.Info,
|
level: LogLevel.Info,
|
||||||
|
contextFilter: 'test',
|
||||||
})
|
})
|
||||||
expect(logger.enabled).toBeTruthy()
|
|
||||||
expect(logger.level).toEqual(LogLevel.Info)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('disabled logger does not report', () => {
|
|
||||||
const logger = new Logger({
|
|
||||||
enabled: false,
|
|
||||||
level: LogLevel.Debug,
|
|
||||||
})
|
|
||||||
|
|
||||||
const mockTransport = jest.fn()
|
|
||||||
|
|
||||||
logger.addTransport(mockTransport)
|
|
||||||
logger.debug('message')
|
|
||||||
|
|
||||||
expect(mockTransport).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('disablement', () => {
|
|
||||||
const logger = new Logger({
|
|
||||||
enabled: true,
|
|
||||||
level: LogLevel.Debug,
|
|
||||||
})
|
|
||||||
|
|
||||||
logger.disable()
|
|
||||||
|
|
||||||
const mockTransport = jest.fn()
|
|
||||||
|
|
||||||
logger.addTransport(mockTransport)
|
|
||||||
logger.debug('message')
|
|
||||||
|
|
||||||
expect(mockTransport).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('passing debug contexts automatically enables debug mode', () => {
|
|
||||||
const logger = new Logger({debug: 'specific'})
|
|
||||||
expect(logger.level).toEqual(LogLevel.Debug)
|
expect(logger.level).toEqual(LogLevel.Debug)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('supports extra metadata', () => {
|
test('supports extra metadata', () => {
|
||||||
const timestamp = Date.now()
|
const timestamp = Date.now()
|
||||||
const logger = new Logger({enabled: true})
|
const logger = new Logger({})
|
||||||
|
|
||||||
const mockTransport = jest.fn()
|
const mockTransport = jest.fn()
|
||||||
|
|
||||||
@@ -87,6 +50,7 @@ describe('general functionality', () => {
|
|||||||
|
|
||||||
expect(mockTransport).toHaveBeenCalledWith(
|
expect(mockTransport).toHaveBeenCalledWith(
|
||||||
LogLevel.Warn,
|
LogLevel.Warn,
|
||||||
|
undefined,
|
||||||
'message',
|
'message',
|
||||||
extra,
|
extra,
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -95,7 +59,7 @@ describe('general functionality', () => {
|
|||||||
|
|
||||||
test('supports nullish/falsy metadata', () => {
|
test('supports nullish/falsy metadata', () => {
|
||||||
const timestamp = Date.now()
|
const timestamp = Date.now()
|
||||||
const logger = new Logger({enabled: true})
|
const logger = new Logger({})
|
||||||
|
|
||||||
const mockTransport = jest.fn()
|
const mockTransport = jest.fn()
|
||||||
|
|
||||||
@@ -105,6 +69,7 @@ describe('general functionality', () => {
|
|||||||
logger.warn('a', null)
|
logger.warn('a', null)
|
||||||
expect(mockTransport).toHaveBeenCalledWith(
|
expect(mockTransport).toHaveBeenCalledWith(
|
||||||
LogLevel.Warn,
|
LogLevel.Warn,
|
||||||
|
undefined,
|
||||||
'a',
|
'a',
|
||||||
{},
|
{},
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -114,6 +79,7 @@ describe('general functionality', () => {
|
|||||||
logger.warn('b', false)
|
logger.warn('b', false)
|
||||||
expect(mockTransport).toHaveBeenCalledWith(
|
expect(mockTransport).toHaveBeenCalledWith(
|
||||||
LogLevel.Warn,
|
LogLevel.Warn,
|
||||||
|
undefined,
|
||||||
'b',
|
'b',
|
||||||
{},
|
{},
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -123,6 +89,7 @@ describe('general functionality', () => {
|
|||||||
logger.warn('c', 0)
|
logger.warn('c', 0)
|
||||||
expect(mockTransport).toHaveBeenCalledWith(
|
expect(mockTransport).toHaveBeenCalledWith(
|
||||||
LogLevel.Warn,
|
LogLevel.Warn,
|
||||||
|
undefined,
|
||||||
'c',
|
'c',
|
||||||
{},
|
{},
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -130,7 +97,7 @@ describe('general functionality', () => {
|
|||||||
|
|
||||||
remove()
|
remove()
|
||||||
|
|
||||||
logger.addTransport((level, message, metadata) => {
|
logger.addTransport((level, context, message, metadata) => {
|
||||||
expect(typeof metadata).toEqual('object')
|
expect(typeof metadata).toEqual('object')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -143,10 +110,17 @@ describe('general functionality', () => {
|
|||||||
const timestamp = Date.now()
|
const timestamp = Date.now()
|
||||||
const sentryTimestamp = timestamp / 1000
|
const sentryTimestamp = timestamp / 1000
|
||||||
|
|
||||||
sentryTransport(LogLevel.Debug, message, {}, timestamp)
|
sentryTransport(
|
||||||
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
LogLevel.Debug,
|
||||||
|
Logger.Context.Default,
|
||||||
message,
|
message,
|
||||||
data: {},
|
{},
|
||||||
|
timestamp,
|
||||||
|
)
|
||||||
|
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
||||||
|
category: Logger.Context.Default,
|
||||||
|
message,
|
||||||
|
data: {context: 'logger'},
|
||||||
type: 'default',
|
type: 'default',
|
||||||
level: LogLevel.Debug,
|
level: LogLevel.Debug,
|
||||||
timestamp: sentryTimestamp,
|
timestamp: sentryTimestamp,
|
||||||
@@ -154,22 +128,31 @@ describe('general functionality', () => {
|
|||||||
|
|
||||||
sentryTransport(
|
sentryTransport(
|
||||||
LogLevel.Info,
|
LogLevel.Info,
|
||||||
|
Logger.Context.Default,
|
||||||
message,
|
message,
|
||||||
{type: 'info', prop: true},
|
{type: 'info', prop: true},
|
||||||
timestamp,
|
timestamp,
|
||||||
)
|
)
|
||||||
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
||||||
|
category: Logger.Context.Default,
|
||||||
message,
|
message,
|
||||||
data: {prop: true},
|
data: {prop: true, context: 'logger'},
|
||||||
type: 'info',
|
type: 'info',
|
||||||
level: LogLevel.Info,
|
level: LogLevel.Info,
|
||||||
timestamp: sentryTimestamp,
|
timestamp: sentryTimestamp,
|
||||||
})
|
})
|
||||||
|
|
||||||
sentryTransport(LogLevel.Log, message, {}, timestamp)
|
sentryTransport(
|
||||||
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
LogLevel.Log,
|
||||||
|
Logger.Context.Default,
|
||||||
message,
|
message,
|
||||||
data: {},
|
{},
|
||||||
|
timestamp,
|
||||||
|
)
|
||||||
|
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
||||||
|
category: Logger.Context.Default,
|
||||||
|
message,
|
||||||
|
data: {context: 'logger'},
|
||||||
type: 'default',
|
type: 'default',
|
||||||
level: 'debug', // Sentry bug, log becomes debug
|
level: 'debug', // Sentry bug, log becomes debug
|
||||||
timestamp: sentryTimestamp,
|
timestamp: sentryTimestamp,
|
||||||
@@ -177,14 +160,21 @@ describe('general functionality', () => {
|
|||||||
jest.runAllTimers()
|
jest.runAllTimers()
|
||||||
expect(Sentry.captureMessage).toHaveBeenCalledWith(message, {
|
expect(Sentry.captureMessage).toHaveBeenCalledWith(message, {
|
||||||
level: 'log',
|
level: 'log',
|
||||||
tags: undefined,
|
tags: {category: 'logger'},
|
||||||
extra: {},
|
extra: {context: 'logger'},
|
||||||
})
|
})
|
||||||
|
|
||||||
sentryTransport(LogLevel.Warn, message, {}, timestamp)
|
sentryTransport(
|
||||||
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
LogLevel.Warn,
|
||||||
|
Logger.Context.Default,
|
||||||
message,
|
message,
|
||||||
data: {},
|
{},
|
||||||
|
timestamp,
|
||||||
|
)
|
||||||
|
expect(Sentry.addBreadcrumb).toHaveBeenCalledWith({
|
||||||
|
category: Logger.Context.Default,
|
||||||
|
message,
|
||||||
|
data: {context: 'logger'},
|
||||||
type: 'default',
|
type: 'default',
|
||||||
level: 'warning',
|
level: 'warning',
|
||||||
timestamp: sentryTimestamp,
|
timestamp: sentryTimestamp,
|
||||||
@@ -192,8 +182,8 @@ describe('general functionality', () => {
|
|||||||
jest.runAllTimers()
|
jest.runAllTimers()
|
||||||
expect(Sentry.captureMessage).toHaveBeenCalledWith(message, {
|
expect(Sentry.captureMessage).toHaveBeenCalledWith(message, {
|
||||||
level: 'warning',
|
level: 'warning',
|
||||||
tags: undefined,
|
tags: {category: 'logger'},
|
||||||
extra: {},
|
extra: {context: 'logger'},
|
||||||
})
|
})
|
||||||
|
|
||||||
const e = new Error('error')
|
const e = new Error('error')
|
||||||
@@ -203,6 +193,7 @@ describe('general functionality', () => {
|
|||||||
|
|
||||||
sentryTransport(
|
sentryTransport(
|
||||||
LogLevel.Error,
|
LogLevel.Error,
|
||||||
|
Logger.Context.Default,
|
||||||
e,
|
e,
|
||||||
{
|
{
|
||||||
tags,
|
tags,
|
||||||
@@ -212,9 +203,13 @@ describe('general functionality', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(Sentry.captureException).toHaveBeenCalledWith(e, {
|
expect(Sentry.captureException).toHaveBeenCalledWith(e, {
|
||||||
tags,
|
tags: {
|
||||||
|
...tags,
|
||||||
|
category: 'logger',
|
||||||
|
},
|
||||||
extra: {
|
extra: {
|
||||||
prop: true,
|
prop: true,
|
||||||
|
context: 'logger',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -226,6 +221,7 @@ describe('general functionality', () => {
|
|||||||
|
|
||||||
sentryTransport(
|
sentryTransport(
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
|
undefined,
|
||||||
message,
|
message,
|
||||||
{error: new Error('foo')},
|
{error: new Error('foo')},
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -241,7 +237,7 @@ describe('general functionality', () => {
|
|||||||
|
|
||||||
test('add/remove transport', () => {
|
test('add/remove transport', () => {
|
||||||
const timestamp = Date.now()
|
const timestamp = Date.now()
|
||||||
const logger = new Logger({enabled: true})
|
const logger = new Logger({})
|
||||||
const mockTransport = jest.fn()
|
const mockTransport = jest.fn()
|
||||||
|
|
||||||
const remove = logger.addTransport(mockTransport)
|
const remove = logger.addTransport(mockTransport)
|
||||||
@@ -256,6 +252,7 @@ describe('general functionality', () => {
|
|||||||
expect(mockTransport).toHaveBeenNthCalledWith(
|
expect(mockTransport).toHaveBeenNthCalledWith(
|
||||||
1,
|
1,
|
||||||
LogLevel.Warn,
|
LogLevel.Warn,
|
||||||
|
undefined,
|
||||||
'warn',
|
'warn',
|
||||||
{},
|
{},
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -263,22 +260,43 @@ describe('general functionality', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('debug contexts', () => {
|
describe('create', () => {
|
||||||
|
test('create', () => {
|
||||||
const mockTransport = jest.fn()
|
const mockTransport = jest.fn()
|
||||||
|
const timestamp = Date.now()
|
||||||
|
const message = nanoid()
|
||||||
|
const logger = Logger.create(Logger.Context.Default)
|
||||||
|
|
||||||
|
logger.addTransport(mockTransport)
|
||||||
|
logger.info(message, {})
|
||||||
|
|
||||||
|
expect(mockTransport).toHaveBeenCalledWith(
|
||||||
|
LogLevel.Info,
|
||||||
|
Logger.Context.Default,
|
||||||
|
message,
|
||||||
|
{},
|
||||||
|
timestamp,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('debug contexts', () => {
|
||||||
test('specific', () => {
|
test('specific', () => {
|
||||||
|
const mockTransport = jest.fn()
|
||||||
const timestamp = Date.now()
|
const timestamp = Date.now()
|
||||||
const message = nanoid()
|
const message = nanoid()
|
||||||
const logger = new Logger({
|
const logger = new Logger({
|
||||||
enabled: true,
|
// @ts-ignore
|
||||||
debug: 'specific',
|
context: 'specific',
|
||||||
|
level: LogLevel.Debug,
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.addTransport(mockTransport)
|
logger.addTransport(mockTransport)
|
||||||
logger.debug(message, {}, 'specific')
|
logger.debug(message, {})
|
||||||
|
|
||||||
expect(mockTransport).toHaveBeenCalledWith(
|
expect(mockTransport).toHaveBeenCalledWith(
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
|
'specific',
|
||||||
message,
|
message,
|
||||||
{},
|
{},
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -286,18 +304,22 @@ describe('debug contexts', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('namespaced', () => {
|
test('namespaced', () => {
|
||||||
|
const mockTransport = jest.fn()
|
||||||
const timestamp = Date.now()
|
const timestamp = Date.now()
|
||||||
const message = nanoid()
|
const message = nanoid()
|
||||||
const logger = new Logger({
|
const logger = new Logger({
|
||||||
enabled: true,
|
// @ts-ignore
|
||||||
debug: 'namespace*',
|
context: 'namespace:foo',
|
||||||
|
contextFilter: 'namespace:*',
|
||||||
|
level: LogLevel.Debug,
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.addTransport(mockTransport)
|
logger.addTransport(mockTransport)
|
||||||
logger.debug(message, {}, 'namespace')
|
logger.debug(message, {})
|
||||||
|
|
||||||
expect(mockTransport).toHaveBeenCalledWith(
|
expect(mockTransport).toHaveBeenCalledWith(
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
|
'namespace:foo',
|
||||||
message,
|
message,
|
||||||
{},
|
{},
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -305,18 +327,21 @@ describe('debug contexts', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('ignores inactive', () => {
|
test('ignores inactive', () => {
|
||||||
|
const mockTransport = jest.fn()
|
||||||
const timestamp = Date.now()
|
const timestamp = Date.now()
|
||||||
const message = nanoid()
|
const message = nanoid()
|
||||||
const logger = new Logger({
|
const logger = new Logger({
|
||||||
enabled: true,
|
// @ts-ignore
|
||||||
debug: 'namespace:foo:*',
|
context: 'namespace:bar:baz',
|
||||||
|
contextFilter: 'namespace:foo:*',
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.addTransport(mockTransport)
|
logger.addTransport(mockTransport)
|
||||||
logger.debug(message, {}, 'namespace:bar:baz')
|
logger.debug(message, {})
|
||||||
|
|
||||||
expect(mockTransport).not.toHaveBeenCalledWith(
|
expect(mockTransport).not.toHaveBeenCalledWith(
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
|
'namespace:bar:baz',
|
||||||
message,
|
message,
|
||||||
{},
|
{},
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -328,7 +353,6 @@ describe('supports levels', () => {
|
|||||||
test('debug', () => {
|
test('debug', () => {
|
||||||
const timestamp = Date.now()
|
const timestamp = Date.now()
|
||||||
const logger = new Logger({
|
const logger = new Logger({
|
||||||
enabled: true,
|
|
||||||
level: LogLevel.Debug,
|
level: LogLevel.Debug,
|
||||||
})
|
})
|
||||||
const message = nanoid()
|
const message = nanoid()
|
||||||
@@ -339,6 +363,7 @@ describe('supports levels', () => {
|
|||||||
logger.debug(message)
|
logger.debug(message)
|
||||||
expect(mockTransport).toHaveBeenCalledWith(
|
expect(mockTransport).toHaveBeenCalledWith(
|
||||||
LogLevel.Debug,
|
LogLevel.Debug,
|
||||||
|
undefined,
|
||||||
message,
|
message,
|
||||||
{},
|
{},
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -347,6 +372,7 @@ describe('supports levels', () => {
|
|||||||
logger.info(message)
|
logger.info(message)
|
||||||
expect(mockTransport).toHaveBeenCalledWith(
|
expect(mockTransport).toHaveBeenCalledWith(
|
||||||
LogLevel.Info,
|
LogLevel.Info,
|
||||||
|
undefined,
|
||||||
message,
|
message,
|
||||||
{},
|
{},
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -355,6 +381,7 @@ describe('supports levels', () => {
|
|||||||
logger.warn(message)
|
logger.warn(message)
|
||||||
expect(mockTransport).toHaveBeenCalledWith(
|
expect(mockTransport).toHaveBeenCalledWith(
|
||||||
LogLevel.Warn,
|
LogLevel.Warn,
|
||||||
|
undefined,
|
||||||
message,
|
message,
|
||||||
{},
|
{},
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -362,13 +389,18 @@ describe('supports levels', () => {
|
|||||||
|
|
||||||
const e = new Error(message)
|
const e = new Error(message)
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
expect(mockTransport).toHaveBeenCalledWith(LogLevel.Error, e, {}, timestamp)
|
expect(mockTransport).toHaveBeenCalledWith(
|
||||||
|
LogLevel.Error,
|
||||||
|
undefined,
|
||||||
|
e,
|
||||||
|
{},
|
||||||
|
timestamp,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('info', () => {
|
test('info', () => {
|
||||||
const timestamp = Date.now()
|
const timestamp = Date.now()
|
||||||
const logger = new Logger({
|
const logger = new Logger({
|
||||||
enabled: true,
|
|
||||||
level: LogLevel.Info,
|
level: LogLevel.Info,
|
||||||
})
|
})
|
||||||
const message = nanoid()
|
const message = nanoid()
|
||||||
@@ -382,6 +414,7 @@ describe('supports levels', () => {
|
|||||||
logger.info(message)
|
logger.info(message)
|
||||||
expect(mockTransport).toHaveBeenCalledWith(
|
expect(mockTransport).toHaveBeenCalledWith(
|
||||||
LogLevel.Info,
|
LogLevel.Info,
|
||||||
|
undefined,
|
||||||
message,
|
message,
|
||||||
{},
|
{},
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -391,7 +424,6 @@ describe('supports levels', () => {
|
|||||||
test('warn', () => {
|
test('warn', () => {
|
||||||
const timestamp = Date.now()
|
const timestamp = Date.now()
|
||||||
const logger = new Logger({
|
const logger = new Logger({
|
||||||
enabled: true,
|
|
||||||
level: LogLevel.Warn,
|
level: LogLevel.Warn,
|
||||||
})
|
})
|
||||||
const message = nanoid()
|
const message = nanoid()
|
||||||
@@ -408,6 +440,7 @@ describe('supports levels', () => {
|
|||||||
logger.warn(message)
|
logger.warn(message)
|
||||||
expect(mockTransport).toHaveBeenCalledWith(
|
expect(mockTransport).toHaveBeenCalledWith(
|
||||||
LogLevel.Warn,
|
LogLevel.Warn,
|
||||||
|
undefined,
|
||||||
message,
|
message,
|
||||||
{},
|
{},
|
||||||
timestamp,
|
timestamp,
|
||||||
@@ -417,7 +450,6 @@ describe('supports levels', () => {
|
|||||||
test('error', () => {
|
test('error', () => {
|
||||||
const timestamp = Date.now()
|
const timestamp = Date.now()
|
||||||
const logger = new Logger({
|
const logger = new Logger({
|
||||||
enabled: true,
|
|
||||||
level: LogLevel.Error,
|
level: LogLevel.Error,
|
||||||
})
|
})
|
||||||
const message = nanoid()
|
const message = nanoid()
|
||||||
@@ -436,6 +468,12 @@ describe('supports levels', () => {
|
|||||||
|
|
||||||
const e = new Error('original message')
|
const e = new Error('original message')
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
expect(mockTransport).toHaveBeenCalledWith(LogLevel.Error, e, {}, timestamp)
|
expect(mockTransport).toHaveBeenCalledWith(
|
||||||
|
LogLevel.Error,
|
||||||
|
undefined,
|
||||||
|
e,
|
||||||
|
{},
|
||||||
|
timestamp,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
import {
|
|
||||||
debug as bdDebug,
|
|
||||||
error as bdError,
|
|
||||||
info as bdInfo,
|
|
||||||
warn as bdWarn,
|
|
||||||
} from '../lib/bitdrift'
|
|
||||||
import {LogLevel, Transport} from './types'
|
|
||||||
|
|
||||||
export function createBitdriftTransport(): Transport {
|
|
||||||
const logFunctions = {
|
|
||||||
[LogLevel.Debug]: bdDebug,
|
|
||||||
[LogLevel.Info]: bdInfo,
|
|
||||||
[LogLevel.Log]: bdInfo,
|
|
||||||
[LogLevel.Warn]: bdWarn,
|
|
||||||
[LogLevel.Error]: bdError,
|
|
||||||
} as const
|
|
||||||
|
|
||||||
return (level, message) => {
|
|
||||||
const log = logFunctions[level]
|
|
||||||
log('' + message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
/**
|
|
||||||
* *Do not import this directly.* Instead, use the shortcut reference `logger.DebugContext`.
|
|
||||||
*
|
|
||||||
* Add debug contexts here. Although convention typically calls for enums ito
|
|
||||||
* be capitalized, for parity with the `LOG_DEBUG` env var, please use all
|
|
||||||
* lowercase.
|
|
||||||
*/
|
|
||||||
export const DebugContext = {
|
|
||||||
// e.g. composer: 'composer'
|
|
||||||
session: 'session',
|
|
||||||
notifications: 'notifications',
|
|
||||||
convo: 'convo',
|
|
||||||
} as const
|
|
||||||
+74
-212
@@ -1,213 +1,89 @@
|
|||||||
import format from 'date-fns/format'
|
|
||||||
import {nanoid} from 'nanoid/non-secure'
|
import {nanoid} from 'nanoid/non-secure'
|
||||||
|
|
||||||
import {isNetworkError} from '#/lib/strings/errors'
|
|
||||||
import {DebugContext} from '#/logger/debugContext'
|
|
||||||
import {add} from '#/logger/logDump'
|
import {add} from '#/logger/logDump'
|
||||||
import {Sentry} from '#/logger/sentry'
|
import {bitdriftTransport} from '#/logger/transports/bitdrift'
|
||||||
import * as env from '#/env'
|
import {consoleTransport} from '#/logger/transports/console'
|
||||||
import {createBitdriftTransport} from './bitdriftTransport'
|
import {sentryTransport} from '#/logger/transports/sentry'
|
||||||
import {Metadata} from './types'
|
import {LogContext, LogLevel, Metadata, Transport} from '#/logger/types'
|
||||||
import {ConsoleTransportEntry, LogLevel, Transport} from './types'
|
import {enabledLogLevels} from '#/logger/util'
|
||||||
|
|
||||||
export {LogLevel}
|
const TRANSPORTS: Transport[] = (function configureTransports() {
|
||||||
export type {ConsoleTransportEntry, Transport}
|
switch (process.env.NODE_ENV) {
|
||||||
|
case 'production': {
|
||||||
const enabledLogLevels: {
|
return [sentryTransport, bitdriftTransport].filter(Boolean) as Transport[]
|
||||||
[key in LogLevel]: LogLevel[]
|
|
||||||
} = {
|
|
||||||
[LogLevel.Debug]: [
|
|
||||||
LogLevel.Debug,
|
|
||||||
LogLevel.Info,
|
|
||||||
LogLevel.Log,
|
|
||||||
LogLevel.Warn,
|
|
||||||
LogLevel.Error,
|
|
||||||
],
|
|
||||||
[LogLevel.Info]: [LogLevel.Info, LogLevel.Log, LogLevel.Warn, LogLevel.Error],
|
|
||||||
[LogLevel.Log]: [LogLevel.Log, LogLevel.Warn, LogLevel.Error],
|
|
||||||
[LogLevel.Warn]: [LogLevel.Warn, LogLevel.Error],
|
|
||||||
[LogLevel.Error]: [LogLevel.Error],
|
|
||||||
}
|
|
||||||
|
|
||||||
export function prepareMetadata(metadata: Metadata): Metadata {
|
|
||||||
return Object.keys(metadata).reduce((acc, key) => {
|
|
||||||
let value = metadata[key]
|
|
||||||
if (value instanceof Error) {
|
|
||||||
value = value.toString()
|
|
||||||
}
|
}
|
||||||
return {...acc, [key]: value}
|
case 'test': {
|
||||||
}, {})
|
return []
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used in dev mode to nicely log to the console
|
|
||||||
*/
|
|
||||||
export const consoleTransport: Transport = (
|
|
||||||
level,
|
|
||||||
message,
|
|
||||||
metadata,
|
|
||||||
timestamp,
|
|
||||||
) => {
|
|
||||||
const extra = Object.keys(metadata).length
|
|
||||||
? ' ' + JSON.stringify(prepareMetadata(metadata), null, ' ')
|
|
||||||
: ''
|
|
||||||
const log = {
|
|
||||||
[LogLevel.Debug]: console.debug,
|
|
||||||
[LogLevel.Info]: console.info,
|
|
||||||
[LogLevel.Log]: console.log,
|
|
||||||
[LogLevel.Warn]: console.warn,
|
|
||||||
[LogLevel.Error]: console.error,
|
|
||||||
}[level]
|
|
||||||
|
|
||||||
if (message instanceof Error) {
|
|
||||||
console.info(
|
|
||||||
`${format(timestamp, 'HH:mm:ss')} ${message.toString()}${extra}`,
|
|
||||||
)
|
|
||||||
log(message)
|
|
||||||
} else {
|
|
||||||
log(`${format(timestamp, 'HH:mm:ss')} ${message.toString()}${extra}`)
|
|
||||||
}
|
}
|
||||||
}
|
default: {
|
||||||
|
return [consoleTransport]
|
||||||
export const sentryTransport: Transport = (
|
|
||||||
level,
|
|
||||||
message,
|
|
||||||
{type, tags, ...metadata},
|
|
||||||
timestamp,
|
|
||||||
) => {
|
|
||||||
const meta = prepareMetadata(metadata)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If a string, report a breadcrumb
|
|
||||||
*/
|
|
||||||
if (typeof message === 'string') {
|
|
||||||
const severity = (
|
|
||||||
{
|
|
||||||
[LogLevel.Debug]: 'debug',
|
|
||||||
[LogLevel.Info]: 'info',
|
|
||||||
[LogLevel.Log]: 'log', // Sentry value here is undefined
|
|
||||||
[LogLevel.Warn]: 'warning',
|
|
||||||
[LogLevel.Error]: 'error',
|
|
||||||
} as const
|
|
||||||
)[level]
|
|
||||||
|
|
||||||
Sentry.addBreadcrumb({
|
|
||||||
message,
|
|
||||||
data: meta,
|
|
||||||
type: type || 'default',
|
|
||||||
level: severity,
|
|
||||||
timestamp: timestamp / 1000, // Sentry expects seconds
|
|
||||||
})
|
|
||||||
|
|
||||||
// We don't want to send any network errors to sentry
|
|
||||||
if (isNetworkError(message)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send all higher levels with `captureMessage`, with appropriate severity
|
|
||||||
* level
|
|
||||||
*/
|
|
||||||
if (level === 'error' || level === 'warn' || level === 'log') {
|
|
||||||
const messageLevel = ({
|
|
||||||
[LogLevel.Log]: 'log',
|
|
||||||
[LogLevel.Warn]: 'warning',
|
|
||||||
[LogLevel.Error]: 'error',
|
|
||||||
}[level] || 'log') as Sentry.Breadcrumb['level']
|
|
||||||
// Defer non-critical messages so they're sent in a batch
|
|
||||||
queueMessageForSentry(message, {
|
|
||||||
level: messageLevel,
|
|
||||||
tags,
|
|
||||||
extra: meta,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
/**
|
|
||||||
* It's otherwise an Error and should be reported with captureException
|
|
||||||
*/
|
|
||||||
Sentry.captureException(message, {
|
|
||||||
tags,
|
|
||||||
extra: meta,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const queuedMessages: [string, Parameters<typeof Sentry.captureMessage>[1]][] =
|
|
||||||
[]
|
|
||||||
let sentrySendTimeout: ReturnType<typeof setTimeout> | null = null
|
|
||||||
function queueMessageForSentry(
|
|
||||||
message: string,
|
|
||||||
captureContext: Parameters<typeof Sentry.captureMessage>[1],
|
|
||||||
) {
|
|
||||||
queuedMessages.push([message, captureContext])
|
|
||||||
if (!sentrySendTimeout) {
|
|
||||||
// Throttle sending messages with a leading delay
|
|
||||||
// so that we can get Sentry out of the critical path.
|
|
||||||
sentrySendTimeout = setTimeout(() => {
|
|
||||||
sentrySendTimeout = null
|
|
||||||
sendQueuedMessages()
|
|
||||||
}, 7000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function sendQueuedMessages() {
|
|
||||||
while (queuedMessages.length > 0) {
|
|
||||||
const record = queuedMessages.shift()
|
|
||||||
if (record) {
|
|
||||||
Sentry.captureMessage(record[0], record[1])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})()
|
||||||
|
|
||||||
/**
|
|
||||||
* Main class. Defaults are provided in the constructor so that subclasses are
|
|
||||||
* technically possible, if we need to go that route in the future.
|
|
||||||
*/
|
|
||||||
export class Logger {
|
export class Logger {
|
||||||
LogLevel = LogLevel
|
static Level = LogLevel
|
||||||
DebugContext = DebugContext
|
static Context = LogContext
|
||||||
|
|
||||||
enabled: boolean
|
|
||||||
level: LogLevel
|
level: LogLevel
|
||||||
transports: Transport[] = []
|
context: LogContext | undefined = undefined
|
||||||
|
contextFilter: string = ''
|
||||||
|
|
||||||
protected debugContextRegexes: RegExp[] = []
|
protected debugContextRegexes: RegExp[] = []
|
||||||
|
protected transports: Transport[] = []
|
||||||
|
|
||||||
|
static create(context?: LogContext) {
|
||||||
|
const logger = new Logger({
|
||||||
|
level: process.env.EXPO_PUBLIC_LOG_LEVEL as LogLevel,
|
||||||
|
context,
|
||||||
|
contextFilter: process.env.EXPO_PUBLIC_LOG_DEBUG || '',
|
||||||
|
})
|
||||||
|
for (const transport of TRANSPORTS) {
|
||||||
|
logger.addTransport(transport)
|
||||||
|
}
|
||||||
|
return logger
|
||||||
|
}
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
enabled = process.env.NODE_ENV !== 'test',
|
level,
|
||||||
level = env.LOG_LEVEL as LogLevel,
|
context,
|
||||||
debug = env.LOG_DEBUG || '',
|
contextFilter,
|
||||||
}: {
|
}: {
|
||||||
enabled?: boolean
|
|
||||||
level?: LogLevel
|
level?: LogLevel
|
||||||
debug?: string
|
context?: LogContext
|
||||||
|
contextFilter?: string
|
||||||
} = {}) {
|
} = {}) {
|
||||||
this.enabled = enabled !== false
|
this.context = context
|
||||||
this.level = debug ? LogLevel.Debug : level ?? LogLevel.Info // default to info
|
this.level = level || LogLevel.Info
|
||||||
this.debugContextRegexes = (debug || '').split(',').map(context => {
|
this.contextFilter = contextFilter || ''
|
||||||
return new RegExp(context.replace(/[^\w:*]/, '').replace(/\*/g, '.*'))
|
if (this.contextFilter) {
|
||||||
|
this.level = LogLevel.Debug
|
||||||
|
}
|
||||||
|
this.debugContextRegexes = (this.contextFilter || '')
|
||||||
|
.split(',')
|
||||||
|
.map(filter => {
|
||||||
|
return new RegExp(filter.replace(/[^\w:*-]/, '').replace(/\*/g, '.*'))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
debug(message: string, metadata: Metadata = {}, context?: string) {
|
debug(message: string, metadata: Metadata = {}) {
|
||||||
if (context && !this.debugContextRegexes.find(reg => reg.test(context)))
|
this.transport({level: LogLevel.Debug, message, metadata})
|
||||||
return
|
|
||||||
this.transport(LogLevel.Debug, message, metadata)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
info(message: string, metadata: Metadata = {}) {
|
info(message: string, metadata: Metadata = {}) {
|
||||||
this.transport(LogLevel.Info, message, metadata)
|
this.transport({level: LogLevel.Info, message, metadata})
|
||||||
}
|
}
|
||||||
|
|
||||||
log(message: string, metadata: Metadata = {}) {
|
log(message: string, metadata: Metadata = {}) {
|
||||||
this.transport(LogLevel.Log, message, metadata)
|
this.transport({level: LogLevel.Log, message, metadata})
|
||||||
}
|
}
|
||||||
|
|
||||||
warn(message: string, metadata: Metadata = {}) {
|
warn(message: string, metadata: Metadata = {}) {
|
||||||
this.transport(LogLevel.Warn, message, metadata)
|
this.transport({level: LogLevel.Warn, message, metadata})
|
||||||
}
|
}
|
||||||
|
|
||||||
error(error: Error | string, metadata: Metadata = {}) {
|
error(error: Error | string, metadata: Metadata = {}) {
|
||||||
this.transport(LogLevel.Error, error, metadata)
|
this.transport({level: LogLevel.Error, message: error, metadata})
|
||||||
}
|
}
|
||||||
|
|
||||||
addTransport(transport: Transport) {
|
addTransport(transport: Transport) {
|
||||||
@@ -217,20 +93,22 @@ export class Logger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
disable() {
|
protected transport({
|
||||||
this.enabled = false
|
level,
|
||||||
}
|
message,
|
||||||
|
metadata = {},
|
||||||
enable() {
|
}: {
|
||||||
this.enabled = true
|
level: LogLevel
|
||||||
}
|
message: string | Error
|
||||||
|
metadata: Metadata
|
||||||
protected transport(
|
}) {
|
||||||
level: LogLevel,
|
if (
|
||||||
message: string | Error,
|
level === LogLevel.Debug &&
|
||||||
metadata: Metadata = {},
|
!!this.contextFilter &&
|
||||||
) {
|
!!this.context &&
|
||||||
if (!this.enabled) return
|
!this.debugContextRegexes.find(reg => reg.test(this.context!))
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
const timestamp = Date.now()
|
const timestamp = Date.now()
|
||||||
const meta = metadata || {}
|
const meta = metadata || {}
|
||||||
@@ -240,6 +118,7 @@ export class Logger {
|
|||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
timestamp,
|
timestamp,
|
||||||
level,
|
level,
|
||||||
|
context: this.context,
|
||||||
message,
|
message,
|
||||||
metadata: meta,
|
metadata: meta,
|
||||||
})
|
})
|
||||||
@@ -247,37 +126,20 @@ export class Logger {
|
|||||||
if (!enabledLogLevels[this.level].includes(level)) return
|
if (!enabledLogLevels[this.level].includes(level)) return
|
||||||
|
|
||||||
for (const transport of this.transports) {
|
for (const transport of this.transports) {
|
||||||
transport(level, message, meta, timestamp)
|
transport(level, this.context, message, meta, timestamp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger instance. See `@/logger/README` for docs.
|
* Default logger instance. See `@/logger/README` for docs.
|
||||||
*
|
*
|
||||||
* Basic usage:
|
* Basic usage:
|
||||||
*
|
*
|
||||||
* `logger.debug(message[, metadata, debugContext])`
|
* `logger.debug(message[, metadata])`
|
||||||
* `logger.info(message[, metadata])`
|
* `logger.info(message[, metadata])`
|
||||||
|
* `logger.log(message[, metadata])`
|
||||||
* `logger.warn(message[, metadata])`
|
* `logger.warn(message[, metadata])`
|
||||||
* `logger.error(error[, metadata])`
|
* `logger.error(error[, metadata])`
|
||||||
* `logger.disable()`
|
|
||||||
* `logger.enable()`
|
|
||||||
*/
|
*/
|
||||||
export const logger = new Logger()
|
export const logger = Logger.create(Logger.Context.Default)
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
|
||||||
logger.addTransport(createBitdriftTransport())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
|
||||||
if (__DEV__) {
|
|
||||||
logger.addTransport(consoleTransport)
|
|
||||||
/*
|
|
||||||
* Comment this out to enable Sentry transport in dev
|
|
||||||
*/
|
|
||||||
// logger.addTransport(sentryTransport)
|
|
||||||
} else {
|
|
||||||
logger.addTransport(sentryTransport)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+10
-1
@@ -1,4 +1,13 @@
|
|||||||
import type {ConsoleTransportEntry} from '#/logger'
|
import type {LogContext, LogLevel, Metadata} from '#/logger/types'
|
||||||
|
|
||||||
|
export type ConsoleTransportEntry = {
|
||||||
|
id: string
|
||||||
|
timestamp: number
|
||||||
|
level: LogLevel
|
||||||
|
context: LogContext | undefined
|
||||||
|
message: string | Error
|
||||||
|
metadata: Metadata
|
||||||
|
}
|
||||||
|
|
||||||
let entries: ConsoleTransportEntry[] = []
|
let entries: ConsoleTransportEntry[] = []
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import {
|
||||||
|
debug as bdDebug,
|
||||||
|
error as bdError,
|
||||||
|
info as bdInfo,
|
||||||
|
warn as bdWarn,
|
||||||
|
} from '#/lib/bitdrift'
|
||||||
|
import {LogLevel, Transport} from '#/logger/types'
|
||||||
|
import {prepareMetadata} from '#/logger/util'
|
||||||
|
|
||||||
|
const logFunctions = {
|
||||||
|
[LogLevel.Debug]: bdDebug,
|
||||||
|
[LogLevel.Info]: bdInfo,
|
||||||
|
[LogLevel.Log]: bdInfo,
|
||||||
|
[LogLevel.Warn]: bdWarn,
|
||||||
|
[LogLevel.Error]: bdError,
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export const bitdriftTransport: Transport = (
|
||||||
|
level,
|
||||||
|
context,
|
||||||
|
message,
|
||||||
|
metadata,
|
||||||
|
) => {
|
||||||
|
const log = logFunctions[level]
|
||||||
|
log(message.toString(), {
|
||||||
|
// match Sentry payload
|
||||||
|
context,
|
||||||
|
...prepareMetadata(metadata),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import format from 'date-fns/format'
|
||||||
|
|
||||||
|
import {LogLevel, Transport} from '#/logger/types'
|
||||||
|
import {prepareMetadata} from '#/logger/util'
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used in dev mode to nicely log to the console
|
||||||
|
*/
|
||||||
|
export const consoleTransport: Transport = (
|
||||||
|
level,
|
||||||
|
context,
|
||||||
|
message,
|
||||||
|
metadata,
|
||||||
|
timestamp,
|
||||||
|
) => {
|
||||||
|
const hasMetadata = Object.keys(metadata).length
|
||||||
|
const colorize = withColor(
|
||||||
|
{
|
||||||
|
[LogLevel.Debug]: colors.magenta,
|
||||||
|
[LogLevel.Info]: colors.blue,
|
||||||
|
[LogLevel.Log]: colors.green,
|
||||||
|
[LogLevel.Warn]: colors.yellow,
|
||||||
|
[LogLevel.Error]: colors.red,
|
||||||
|
}[level],
|
||||||
|
)
|
||||||
|
|
||||||
|
let msg = `${colorize(format(timestamp, 'HH:mm:ss'))}`
|
||||||
|
if (context) {
|
||||||
|
msg += ` ${colorize(`(${context})`)}`
|
||||||
|
}
|
||||||
|
if (message) {
|
||||||
|
msg += ` ${message.toString()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWeb) {
|
||||||
|
if (hasMetadata) {
|
||||||
|
console.groupCollapsed(msg)
|
||||||
|
console.log(metadata)
|
||||||
|
console.groupEnd()
|
||||||
|
} else {
|
||||||
|
console.log(msg)
|
||||||
|
}
|
||||||
|
if (message instanceof Error) {
|
||||||
|
// for stacktrace
|
||||||
|
console.error(message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (hasMetadata) {
|
||||||
|
msg += ` ${JSON.stringify(prepareMetadata(metadata), null, 2)}`
|
||||||
|
}
|
||||||
|
console.log(msg)
|
||||||
|
if (message instanceof Error) {
|
||||||
|
// for stacktrace
|
||||||
|
console.error(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Color handling copied from Kleur
|
||||||
|
*
|
||||||
|
* @see https://github.com/lukeed/kleur/blob/fa3454483899ddab550d08c18c028e6db1aab0e5/colors.mjs#L13
|
||||||
|
*/
|
||||||
|
const colors: {
|
||||||
|
[key: string]: [number, number]
|
||||||
|
} = {
|
||||||
|
default: [0, 0],
|
||||||
|
blue: [36, 39],
|
||||||
|
green: [32, 39],
|
||||||
|
magenta: [35, 39],
|
||||||
|
red: [31, 39],
|
||||||
|
yellow: [33, 39],
|
||||||
|
}
|
||||||
|
|
||||||
|
function withColor([x, y]: [number, number]) {
|
||||||
|
const rgx = new RegExp(`\\x1b\\[${y}m`, 'g')
|
||||||
|
const open = `\x1b[${x}m`,
|
||||||
|
close = `\x1b[${y}m`
|
||||||
|
|
||||||
|
return function (txt: string) {
|
||||||
|
if (txt == null) return txt
|
||||||
|
|
||||||
|
return (
|
||||||
|
open +
|
||||||
|
(~('' + txt).indexOf(close) ? txt.replace(rgx, close + open) : txt) +
|
||||||
|
close
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import {isNetworkError} from '#/lib/strings/errors'
|
||||||
|
import {Sentry} from '#/logger/sentry/lib'
|
||||||
|
import {LogLevel, Transport} from '#/logger/types'
|
||||||
|
import {prepareMetadata} from '#/logger/util'
|
||||||
|
|
||||||
|
export const sentryTransport: Transport = (
|
||||||
|
level,
|
||||||
|
context,
|
||||||
|
message,
|
||||||
|
{type, tags, ...metadata},
|
||||||
|
timestamp,
|
||||||
|
) => {
|
||||||
|
const meta = {
|
||||||
|
// match Bitdrift payload
|
||||||
|
context,
|
||||||
|
...prepareMetadata(metadata),
|
||||||
|
}
|
||||||
|
let _tags = tags || {}
|
||||||
|
_tags = {
|
||||||
|
// use `category` to match breadcrumbs
|
||||||
|
category: context,
|
||||||
|
...tags,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If a string, report a breadcrumb
|
||||||
|
*/
|
||||||
|
if (typeof message === 'string') {
|
||||||
|
const severity = (
|
||||||
|
{
|
||||||
|
[LogLevel.Debug]: 'debug',
|
||||||
|
[LogLevel.Info]: 'info',
|
||||||
|
[LogLevel.Log]: 'log', // Sentry value here is undefined
|
||||||
|
[LogLevel.Warn]: 'warning',
|
||||||
|
[LogLevel.Error]: 'error',
|
||||||
|
} as const
|
||||||
|
)[level]
|
||||||
|
|
||||||
|
Sentry.addBreadcrumb({
|
||||||
|
category: context,
|
||||||
|
message,
|
||||||
|
data: meta,
|
||||||
|
type: type || 'default',
|
||||||
|
level: severity,
|
||||||
|
timestamp: timestamp / 1000, // Sentry expects seconds
|
||||||
|
})
|
||||||
|
|
||||||
|
// We don't want to send any network errors to sentry
|
||||||
|
if (isNetworkError(message)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send all higher levels with `captureMessage`, with appropriate severity
|
||||||
|
* level
|
||||||
|
*/
|
||||||
|
if (level === 'error' || level === 'warn' || level === 'log') {
|
||||||
|
// Defer non-critical messages so they're sent in a batch
|
||||||
|
queueMessageForSentry(message, {
|
||||||
|
level: severity,
|
||||||
|
tags: _tags,
|
||||||
|
extra: meta,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
/**
|
||||||
|
* It's otherwise an Error and should be reported with captureException
|
||||||
|
*/
|
||||||
|
Sentry.captureException(message, {
|
||||||
|
tags: _tags,
|
||||||
|
extra: meta,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const queuedMessages: [string, Parameters<typeof Sentry.captureMessage>[1]][] =
|
||||||
|
[]
|
||||||
|
let sentrySendTimeout: ReturnType<typeof setTimeout> | null = null
|
||||||
|
|
||||||
|
function queueMessageForSentry(
|
||||||
|
message: string,
|
||||||
|
captureContext: Parameters<typeof Sentry.captureMessage>[1],
|
||||||
|
) {
|
||||||
|
queuedMessages.push([message, captureContext])
|
||||||
|
if (!sentrySendTimeout) {
|
||||||
|
// Throttle sending messages with a leading delay
|
||||||
|
// so that we can get Sentry out of the critical path.
|
||||||
|
sentrySendTimeout = setTimeout(() => {
|
||||||
|
sentrySendTimeout = null
|
||||||
|
sendQueuedMessages()
|
||||||
|
}, 7000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sendQueuedMessages() {
|
||||||
|
while (queuedMessages.length > 0) {
|
||||||
|
const record = queuedMessages.shift()
|
||||||
|
if (record) {
|
||||||
|
Sentry.captureMessage(record[0], record[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+31
-18
@@ -1,4 +1,15 @@
|
|||||||
import type {Sentry} from '#/logger/sentry'
|
/**
|
||||||
|
* DO NOT IMPORT THIS DIRECTLY
|
||||||
|
*
|
||||||
|
* Logger contexts, defined here and used via `Logger.Context.*` static prop.
|
||||||
|
*/
|
||||||
|
export enum LogContext {
|
||||||
|
Default = 'logger',
|
||||||
|
Session = 'session',
|
||||||
|
Notifications = 'notifications',
|
||||||
|
ConversationAgent = 'conversation-agent',
|
||||||
|
DMsAgent = 'dms-agent',
|
||||||
|
}
|
||||||
|
|
||||||
export enum LogLevel {
|
export enum LogLevel {
|
||||||
Debug = 'debug',
|
Debug = 'debug',
|
||||||
@@ -10,6 +21,7 @@ export enum LogLevel {
|
|||||||
|
|
||||||
export type Transport = (
|
export type Transport = (
|
||||||
level: LogLevel,
|
level: LogLevel,
|
||||||
|
context: LogContext | undefined,
|
||||||
message: string | Error,
|
message: string | Error,
|
||||||
metadata: Metadata,
|
metadata: Metadata,
|
||||||
timestamp: number,
|
timestamp: number,
|
||||||
@@ -20,6 +32,11 @@ export type Transport = (
|
|||||||
* `captureException` parameter, `CaptureContext`.
|
* `captureException` parameter, `CaptureContext`.
|
||||||
*/
|
*/
|
||||||
export type Metadata = {
|
export type Metadata = {
|
||||||
|
/**
|
||||||
|
* Reserved for appending `LogContext` to logging payloads
|
||||||
|
*/
|
||||||
|
context?: undefined
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applied as Sentry breadcrumb types. Defaults to `default`.
|
* Applied as Sentry breadcrumb types. Defaults to `default`.
|
||||||
*
|
*
|
||||||
@@ -43,27 +60,23 @@ export type Metadata = {
|
|||||||
* @see https://github.com/getsentry/sentry-javascript/blob/903addf9a1a1534a6cb2ba3143654b918a86f6dd/packages/types/src/misc.ts#L65
|
* @see https://github.com/getsentry/sentry-javascript/blob/903addf9a1a1534a6cb2ba3143654b918a86f6dd/packages/types/src/misc.ts#L65
|
||||||
*/
|
*/
|
||||||
tags?: {
|
tags?: {
|
||||||
[key: string]:
|
[key: string]: number | string | boolean | null | undefined
|
||||||
| number
|
|
||||||
| string
|
|
||||||
| boolean
|
|
||||||
| bigint
|
|
||||||
| symbol
|
|
||||||
| null
|
|
||||||
| undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Any additional data, passed through to Sentry as `extra` param on
|
* Any additional data, passed through to Sentry as `extra` param on
|
||||||
* exceptions, or the `data` param on breadcrumbs.
|
* exceptions, or the `data` param on breadcrumbs.
|
||||||
*/
|
*/
|
||||||
[key: string]: unknown
|
[key: string]: Serializable | Error | unknown
|
||||||
} & Parameters<typeof Sentry.captureException>[1]
|
|
||||||
|
|
||||||
export type ConsoleTransportEntry = {
|
|
||||||
id: string
|
|
||||||
timestamp: number
|
|
||||||
level: LogLevel
|
|
||||||
message: string | Error
|
|
||||||
metadata: Metadata
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Serializable =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| undefined
|
||||||
|
| Serializable[]
|
||||||
|
| {
|
||||||
|
[key: string]: Serializable
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import {LogLevel, Metadata, Serializable} from '#/logger/types'
|
||||||
|
|
||||||
|
export const enabledLogLevels: {
|
||||||
|
[key in LogLevel]: LogLevel[]
|
||||||
|
} = {
|
||||||
|
[LogLevel.Debug]: [
|
||||||
|
LogLevel.Debug,
|
||||||
|
LogLevel.Info,
|
||||||
|
LogLevel.Log,
|
||||||
|
LogLevel.Warn,
|
||||||
|
LogLevel.Error,
|
||||||
|
],
|
||||||
|
[LogLevel.Info]: [LogLevel.Info, LogLevel.Log, LogLevel.Warn, LogLevel.Error],
|
||||||
|
[LogLevel.Log]: [LogLevel.Log, LogLevel.Warn, LogLevel.Error],
|
||||||
|
[LogLevel.Warn]: [LogLevel.Warn, LogLevel.Error],
|
||||||
|
[LogLevel.Error]: [LogLevel.Error],
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prepareMetadata(
|
||||||
|
metadata: Metadata,
|
||||||
|
): Record<string, Serializable> {
|
||||||
|
return Object.keys(metadata).reduce((acc, key) => {
|
||||||
|
let value = metadata[key]
|
||||||
|
if (value instanceof Error) {
|
||||||
|
value = value.toString()
|
||||||
|
}
|
||||||
|
return {...acc, [key]: value}
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
@@ -96,7 +96,7 @@ export function Deactivated() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.error(e, {
|
logger.error(e, {
|
||||||
context: 'Failed to activate account',
|
message: 'Failed to activate account',
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setPending(false)
|
setPending(false)
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) {
|
|||||||
Toast.show(_(msg`Settings saved`))
|
Toast.show(_(msg`Settings saved`))
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(`Failed to save post interaction settings`, {
|
logger.error(`Failed to save post interaction settings`, {
|
||||||
context: 'ModerationInteractionSettingsScreen',
|
source: 'ModerationInteractionSettingsScreen',
|
||||||
safeMessage: e.message,
|
safeMessage: e.message,
|
||||||
})
|
})
|
||||||
setError(_(msg`Failed to save settings. Please try again.`))
|
setError(_(msg`Failed to save settings. Please try again.`))
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ function DeactivateAccountDialogInner({
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.error(e, {
|
logger.error(e, {
|
||||||
context: 'Failed to deactivate account',
|
message: 'Failed to deactivate account',
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
setPending(false)
|
setPending(false)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import EventEmitter from 'eventemitter3'
|
|||||||
import {nanoid} from 'nanoid/non-secure'
|
import {nanoid} from 'nanoid/non-secure'
|
||||||
|
|
||||||
import {networkRetry} from '#/lib/async/retry'
|
import {networkRetry} from '#/lib/async/retry'
|
||||||
import {logger} from '#/logger'
|
import {Logger} from '#/logger'
|
||||||
import {isNative} from '#/platform/detection'
|
import {isNative} from '#/platform/detection'
|
||||||
import {
|
import {
|
||||||
ACTIVE_POLL_INTERVAL,
|
ACTIVE_POLL_INTERVAL,
|
||||||
@@ -34,6 +34,8 @@ import {MessagesEventBus} from '#/state/messages/events/agent'
|
|||||||
import {MessagesEventBusError} from '#/state/messages/events/types'
|
import {MessagesEventBusError} from '#/state/messages/events/types'
|
||||||
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
|
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
|
||||||
|
|
||||||
|
const logger = Logger.create(Logger.Context.ConversationAgent)
|
||||||
|
|
||||||
export function isConvoItemMessage(
|
export function isConvoItemMessage(
|
||||||
item: ConvoItem,
|
item: ConvoItem,
|
||||||
): item is ConvoItem & {type: 'message'} {
|
): item is ConvoItem & {type: 'message'} {
|
||||||
@@ -125,7 +127,7 @@ export class Convo {
|
|||||||
|
|
||||||
getSnapshot(): ConvoState {
|
getSnapshot(): ConvoState {
|
||||||
if (!this.snapshot) this.snapshot = this.generateSnapshot()
|
if (!this.snapshot) this.snapshot = this.generateSnapshot()
|
||||||
// logger.debug('Convo: snapshotted', {}, logger.DebugContext.convo)
|
// logger.debug('Convo: snapshotted', {})
|
||||||
return this.snapshot
|
return this.snapshot
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -375,15 +377,11 @@ export class Convo {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(`Convo: dispatch '${action.event}'`, {
|
||||||
`Convo: dispatch '${action.event}'`,
|
|
||||||
{
|
|
||||||
id: this.id,
|
id: this.id,
|
||||||
prev: prevStatus,
|
prev: prevStatus,
|
||||||
next: this.status,
|
next: this.status,
|
||||||
},
|
})
|
||||||
logger.DebugContext.convo,
|
|
||||||
)
|
|
||||||
|
|
||||||
this.updateLastActiveTimestamp()
|
this.updateLastActiveTimestamp()
|
||||||
this.commit()
|
this.commit()
|
||||||
@@ -471,7 +469,7 @@ export class Convo {
|
|||||||
this.dispatch({event: ConvoDispatchEvent.Ready})
|
this.dispatch({event: ConvoDispatchEvent.Ready})
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(e, {context: 'Convo: setup failed'})
|
logger.error(e, {message: 'Convo: setup failed'})
|
||||||
|
|
||||||
this.dispatch({
|
this.dispatch({
|
||||||
event: ConvoDispatchEvent.Error,
|
event: ConvoDispatchEvent.Error,
|
||||||
@@ -576,7 +574,7 @@ export class Convo {
|
|||||||
this.sender = sender || this.sender
|
this.sender = sender || this.sender
|
||||||
this.recipients = recipients || this.recipients
|
this.recipients = recipients || this.recipients
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(e, {context: `Convo: failed to refresh convo`})
|
logger.error(e, {message: `Convo: failed to refresh convo`})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,7 +584,7 @@ export class Convo {
|
|||||||
}
|
}
|
||||||
| undefined
|
| undefined
|
||||||
async fetchMessageHistory() {
|
async fetchMessageHistory() {
|
||||||
logger.debug('Convo: fetch message history', {}, logger.DebugContext.convo)
|
logger.debug('Convo: fetch message history', {})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If oldestRev is null, we've fetched all history.
|
* If oldestRev is null, we've fetched all history.
|
||||||
@@ -773,7 +771,7 @@ export class Convo {
|
|||||||
// Ignore empty messages for now since they have no other purpose atm
|
// Ignore empty messages for now since they have no other purpose atm
|
||||||
if (!message.text.trim() && !message.embed) return
|
if (!message.text.trim() && !message.embed) return
|
||||||
|
|
||||||
logger.debug('Convo: send message', {}, logger.DebugContext.convo)
|
logger.debug('Convo: send message', {})
|
||||||
|
|
||||||
const tempId = nanoid()
|
const tempId = nanoid()
|
||||||
|
|
||||||
@@ -793,7 +791,6 @@ export class Convo {
|
|||||||
logger.debug(
|
logger.debug(
|
||||||
`Convo: processing messages (${this.pendingMessages.size} remaining)`,
|
`Convo: processing messages (${this.pendingMessages.size} remaining)`,
|
||||||
{},
|
{},
|
||||||
logger.DebugContext.convo,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const pendingMessage = Array.from(this.pendingMessages.values()).shift()
|
const pendingMessage = Array.from(this.pendingMessages.values()).shift()
|
||||||
@@ -837,7 +834,7 @@ export class Convo {
|
|||||||
// continue queue processing
|
// continue queue processing
|
||||||
await this.processPendingMessages()
|
await this.processPendingMessages()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(e, {context: `Convo: failed to send message`})
|
logger.error(e, {message: `Convo: failed to send message`})
|
||||||
this.handleSendMessageFailure(e)
|
this.handleSendMessageFailure(e)
|
||||||
this.isProcessingPendingMessages = false
|
this.isProcessingPendingMessages = false
|
||||||
}
|
}
|
||||||
@@ -883,7 +880,7 @@ export class Convo {
|
|||||||
} else {
|
} else {
|
||||||
this.pendingMessageFailure = 'unrecoverable'
|
this.pendingMessageFailure = 'unrecoverable'
|
||||||
logger.error(e, {
|
logger.error(e, {
|
||||||
context: `Convo handleSendMessageFailure received unknown error`,
|
message: `Convo handleSendMessageFailure received unknown error`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -902,7 +899,6 @@ export class Convo {
|
|||||||
logger.debug(
|
logger.debug(
|
||||||
`Convo: batch retrying ${this.pendingMessages.size} pending messages`,
|
`Convo: batch retrying ${this.pendingMessages.size} pending messages`,
|
||||||
{},
|
{},
|
||||||
logger.DebugContext.convo,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -937,16 +933,15 @@ export class Convo {
|
|||||||
logger.debug(
|
logger.debug(
|
||||||
`Convo: sent ${this.pendingMessages.size} pending messages`,
|
`Convo: sent ${this.pendingMessages.size} pending messages`,
|
||||||
{},
|
{},
|
||||||
logger.DebugContext.convo,
|
|
||||||
)
|
)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(e, {context: `Convo: failed to batch retry messages`})
|
logger.error(e, {message: `Convo: failed to batch retry messages`})
|
||||||
this.handleSendMessageFailure(e)
|
this.handleSendMessageFailure(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteMessage(messageId: string) {
|
async deleteMessage(messageId: string) {
|
||||||
logger.debug('Convo: delete message', {}, logger.DebugContext.convo)
|
logger.debug('Convo: delete message', {})
|
||||||
|
|
||||||
this.deletedMessages.add(messageId)
|
this.deletedMessages.add(messageId)
|
||||||
this.commit()
|
this.commit()
|
||||||
@@ -962,7 +957,7 @@ export class Convo {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(e, {context: `Convo: failed to delete message`})
|
logger.error(e, {message: `Convo: failed to delete message`})
|
||||||
this.deletedMessages.delete(messageId)
|
this.deletedMessages.delete(messageId)
|
||||||
this.commit()
|
this.commit()
|
||||||
throw e
|
throw e
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import EventEmitter from 'eventemitter3'
|
|||||||
import {nanoid} from 'nanoid/non-secure'
|
import {nanoid} from 'nanoid/non-secure'
|
||||||
|
|
||||||
import {networkRetry} from '#/lib/async/retry'
|
import {networkRetry} from '#/lib/async/retry'
|
||||||
import {logger} from '#/logger'
|
import {Logger} from '#/logger'
|
||||||
import {
|
import {
|
||||||
BACKGROUND_POLL_INTERVAL,
|
BACKGROUND_POLL_INTERVAL,
|
||||||
DEFAULT_POLL_INTERVAL,
|
DEFAULT_POLL_INTERVAL,
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
|
import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
|
||||||
|
|
||||||
const LOGGER_CONTEXT = 'MessagesEventBus'
|
const LOGGER_CONTEXT = 'MessagesEventBus'
|
||||||
|
const logger = Logger.create(Logger.Context.DMsAgent)
|
||||||
|
|
||||||
export class MessagesEventBus {
|
export class MessagesEventBus {
|
||||||
private id: string
|
private id: string
|
||||||
@@ -90,17 +91,17 @@ export class MessagesEventBus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
background() {
|
background() {
|
||||||
logger.debug(`${LOGGER_CONTEXT}: background`, {}, logger.DebugContext.convo)
|
logger.debug(`${LOGGER_CONTEXT}: background`, {})
|
||||||
this.dispatch({event: MessagesEventBusDispatchEvent.Background})
|
this.dispatch({event: MessagesEventBusDispatchEvent.Background})
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend() {
|
suspend() {
|
||||||
logger.debug(`${LOGGER_CONTEXT}: suspend`, {}, logger.DebugContext.convo)
|
logger.debug(`${LOGGER_CONTEXT}: suspend`, {})
|
||||||
this.dispatch({event: MessagesEventBusDispatchEvent.Suspend})
|
this.dispatch({event: MessagesEventBusDispatchEvent.Suspend})
|
||||||
}
|
}
|
||||||
|
|
||||||
resume() {
|
resume() {
|
||||||
logger.debug(`${LOGGER_CONTEXT}: resume`, {}, logger.DebugContext.convo)
|
logger.debug(`${LOGGER_CONTEXT}: resume`, {})
|
||||||
this.dispatch({event: MessagesEventBusDispatchEvent.Resume})
|
this.dispatch({event: MessagesEventBusDispatchEvent.Resume})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,19 +223,15 @@ export class MessagesEventBus {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(`${LOGGER_CONTEXT}: dispatch '${action.event}'`, {
|
||||||
`${LOGGER_CONTEXT}: dispatch '${action.event}'`,
|
|
||||||
{
|
|
||||||
id: this.id,
|
id: this.id,
|
||||||
prev: prevStatus,
|
prev: prevStatus,
|
||||||
next: this.status,
|
next: this.status,
|
||||||
},
|
})
|
||||||
logger.DebugContext.convo,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async init() {
|
private async init() {
|
||||||
logger.debug(`${LOGGER_CONTEXT}: init`, {}, logger.DebugContext.convo)
|
logger.debug(`${LOGGER_CONTEXT}: init`, {})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await networkRetry(2, () => {
|
const response = await networkRetry(2, () => {
|
||||||
@@ -258,7 +255,7 @@ export class MessagesEventBus {
|
|||||||
this.dispatch({event: MessagesEventBusDispatchEvent.Ready})
|
this.dispatch({event: MessagesEventBusDispatchEvent.Ready})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(e, {
|
logger.error(e, {
|
||||||
context: `${LOGGER_CONTEXT}: init failed`,
|
message: `${LOGGER_CONTEXT}: init failed`,
|
||||||
})
|
})
|
||||||
|
|
||||||
this.dispatch({
|
this.dispatch({
|
||||||
@@ -327,7 +324,6 @@ export class MessagesEventBus {
|
|||||||
// this.requestedPollIntervals.values(),
|
// this.requestedPollIntervals.values(),
|
||||||
// ),
|
// ),
|
||||||
// },
|
// },
|
||||||
// logger.DebugContext.convo,
|
|
||||||
// )
|
// )
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -372,12 +368,12 @@ export class MessagesEventBus {
|
|||||||
this.emitter.emit('event', {type: 'logs', logs: batch})
|
this.emitter.emit('event', {type: 'logs', logs: batch})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(e, {
|
logger.error(e, {
|
||||||
context: `${LOGGER_CONTEXT}: process latest events`,
|
message: `${LOGGER_CONTEXT}: process latest events`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(e, {context: `${LOGGER_CONTEXT}: poll events failed`})
|
logger.error(e, {message: `${LOGGER_CONTEXT}: poll events failed`})
|
||||||
|
|
||||||
this.dispatch({
|
this.dispatch({
|
||||||
event: MessagesEventBusDispatchEvent.Error,
|
event: MessagesEventBusDispatchEvent.Error,
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export async function createAgentAndCreateAccount(
|
|||||||
})
|
})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(e, {
|
logger.error(e, {
|
||||||
context: `session: createAgentAndCreateAccount failed to save personal details and feeds`,
|
message: `session: createAgentAndCreateAccount failed to save personal details and feeds`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -177,7 +177,7 @@ export async function createAgentAndCreateAccount(
|
|||||||
// snooze first prompt after signup, defer to next prompt
|
// snooze first prompt after signup, defer to next prompt
|
||||||
snoozeEmailConfirmationPrompt()
|
snoozeEmailConfirmationPrompt()
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.error(e, {context: `session: failed snoozeEmailConfirmationPrompt`})
|
logger.error(e, {message: `session: failed snoozeEmailConfirmationPrompt`})
|
||||||
}
|
}
|
||||||
|
|
||||||
return agent.prepare(gates, moderation, onSessionChange)
|
return agent.prepare(gates, moderation, onSessionChange)
|
||||||
|
|||||||
Reference in New Issue
Block a user