Re-org, add colors and grouping to console logging
This commit is contained in:
@@ -2,7 +2,8 @@ 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, LogLevel} from '#/logger'
|
||||||
|
import {sentryTransport} from '#/logger/transports/sentry'
|
||||||
|
|
||||||
jest.mock('#/env', () => ({
|
jest.mock('#/env', () => ({
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -1,23 +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
|
|
||||||
|
|
||||||
// TODO
|
|
||||||
return (level, _context, message) => {
|
|
||||||
const log = logFunctions[level]
|
|
||||||
log('' + message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+33
-165
@@ -1,143 +1,25 @@
|
|||||||
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 {LogContext} from '#/logger/logContext'
|
import {LogContext} from '#/logger/logContext'
|
||||||
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 {
|
||||||
import {ConsoleTransportEntry, LogLevel, Transport} from './types'
|
ConsoleTransportEntry,
|
||||||
|
LogLevel,
|
||||||
|
Metadata,
|
||||||
|
Transport,
|
||||||
|
} from '#/logger/types'
|
||||||
|
import {enabledLogLevels} from '#/logger/util'
|
||||||
|
|
||||||
export {LogLevel}
|
export {LogLevel}
|
||||||
export type {ConsoleTransportEntry, Transport}
|
export type {ConsoleTransportEntry, Transport}
|
||||||
|
|
||||||
const enabledLogLevels: {
|
const TRANSPORTS: Transport[] = (function configureTransports() {
|
||||||
[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}
|
|
||||||
}, {})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used in dev mode to nicely log to the console
|
|
||||||
*/
|
|
||||||
export const consoleTransport: Transport = (
|
|
||||||
level,
|
|
||||||
context,
|
|
||||||
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}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const sentryTransport: Transport = (
|
|
||||||
level,
|
|
||||||
context,
|
|
||||||
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 transports: Transport[] = (function configureTransports() {
|
|
||||||
switch (process.env.NODE_ENV) {
|
switch (process.env.NODE_ENV) {
|
||||||
case 'production': {
|
case 'production': {
|
||||||
return [sentryTransport, createBitdriftTransport()]
|
return [sentryTransport, bitdriftTransport]
|
||||||
}
|
}
|
||||||
case 'test': {
|
case 'test': {
|
||||||
return []
|
return []
|
||||||
@@ -148,32 +30,6 @@ const transports: Transport[] = (function configureTransports() {
|
|||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
|
|
||||||
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])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class Logger {
|
export class Logger {
|
||||||
static Level = LogLevel
|
static Level = LogLevel
|
||||||
static Context = LogContext
|
static Context = LogContext
|
||||||
@@ -183,19 +39,24 @@ export class Logger {
|
|||||||
contextFilter: string = ''
|
contextFilter: string = ''
|
||||||
|
|
||||||
protected debugContextRegexes: RegExp[] = []
|
protected debugContextRegexes: RegExp[] = []
|
||||||
|
protected transports: Transport[] = []
|
||||||
|
|
||||||
static create(context: keyof typeof LogContext) {
|
static create(context?: keyof typeof LogContext) {
|
||||||
return new Logger({
|
const logger = new Logger({
|
||||||
level: (env.LOG_LEVEL || LogLevel.Info) as LogLevel,
|
level: (process.env.EXPO_PUBLIC_LOG_LEVEL || LogLevel.Info) as LogLevel,
|
||||||
context,
|
context,
|
||||||
contextFilter: env.LOG_DEBUG || '',
|
contextFilter: process.env.EXPO_PUBLIC_LOG_DEBUG || '',
|
||||||
})
|
})
|
||||||
|
for (const transport of TRANSPORTS) {
|
||||||
|
logger.addTransport(transport)
|
||||||
|
}
|
||||||
|
return logger
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
level = env.LOG_LEVEL as LogLevel,
|
level = process.env.EXPO_PUBLIC_LOG_LEVEL as LogLevel,
|
||||||
context,
|
context,
|
||||||
contextFilter = env.LOG_DEBUG || '',
|
contextFilter = process.env.EXPO_PUBLIC_LOG_DEBUG || '',
|
||||||
}: {
|
}: {
|
||||||
level?: LogLevel
|
level?: LogLevel
|
||||||
context?: keyof typeof LogContext
|
context?: keyof typeof LogContext
|
||||||
@@ -231,6 +92,13 @@ export class Logger {
|
|||||||
this.transport(LogLevel.Error, error, metadata)
|
this.transport(LogLevel.Error, error, metadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addTransport(transport: Transport) {
|
||||||
|
this.transports.push(transport)
|
||||||
|
return () => {
|
||||||
|
this.transports.splice(this.transports.indexOf(transport), 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected transport(
|
protected transport(
|
||||||
level: LogLevel,
|
level: LogLevel,
|
||||||
message: string | Error,
|
message: string | Error,
|
||||||
@@ -258,14 +126,14 @@ export class Logger {
|
|||||||
|
|
||||||
if (!enabledLogLevels[this.level].includes(level)) return
|
if (!enabledLogLevels[this.level].includes(level)) return
|
||||||
|
|
||||||
for (const transport of transports) {
|
for (const transport of this.transports) {
|
||||||
transport(level, this.context, 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:
|
||||||
*
|
*
|
||||||
@@ -276,4 +144,4 @@ export class Logger {
|
|||||||
* `logger.disable()`
|
* `logger.disable()`
|
||||||
* `logger.enable()`
|
* `logger.enable()`
|
||||||
*/
|
*/
|
||||||
export const logger = new Logger()
|
export const logger = Logger.create()
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import {
|
||||||
|
debug as bdDebug,
|
||||||
|
error as bdError,
|
||||||
|
info as bdInfo,
|
||||||
|
warn as bdWarn,
|
||||||
|
} from '#/lib/bitdrift'
|
||||||
|
import {LogLevel, Transport} from '#/logger/types'
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
const log = logFunctions[level]
|
||||||
|
log('' + message)
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
) => {
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import {isNetworkError} from '#/lib/strings/errors'
|
||||||
|
import {Sentry} from '#/logger/sentry'
|
||||||
|
import {LogLevel,Transport} from '#/logger/types'
|
||||||
|
import {prepareMetadata} from '#/logger/util'
|
||||||
|
|
||||||
|
export const sentryTransport: Transport = (
|
||||||
|
level,
|
||||||
|
context,
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import {LogLevel, Metadata} 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): Metadata {
|
||||||
|
return Object.keys(metadata).reduce((acc, key) => {
|
||||||
|
let value = metadata[key]
|
||||||
|
if (value instanceof Error) {
|
||||||
|
value = value.toString()
|
||||||
|
}
|
||||||
|
return {...acc, [key]: value}
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user