diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b4f09d89f381..060fc8a207fd 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -123,6 +123,7 @@ export { captureFeedback } from './feedback'; export type { ReportDialogOptions } from './report-dialog'; export { _INTERNAL_captureLog, _INTERNAL_flushLogsBuffer, _INTERNAL_captureSerializedLog } from './logs/exports'; export { consoleLoggingIntegration } from './logs/console-integration'; +export { consolaLoggingIntegration } from './logs/consola'; export type { FeatureFlag } from './utils/featureFlags'; export { diff --git a/packages/core/src/logs/consola.ts b/packages/core/src/logs/consola.ts new file mode 100644 index 000000000000..a894aa2eaaa7 --- /dev/null +++ b/packages/core/src/logs/consola.ts @@ -0,0 +1,255 @@ +import { getClient } from '../currentScopes'; +import { DEBUG_BUILD } from '../debug-build'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; +import type { LogSeverityLevel } from '../types-hoist/log'; +import { formatConsoleArgs } from '../utils/console'; +import { logger } from '../utils/logger'; +import { _INTERNAL_captureLog } from './exports'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface SentryConsolaReporterOptions { + // empty +} + +/** + * Map consola log types to Sentry log levels + */ +const CONSOLA_TYPE_TO_SENTRY_LEVEL: Record = { + // 0 + silent: 'fatal', + fatal: 'fatal', + error: 'error', + // 1 + warn: 'warn', + // 2 + log: 'info', + // 3 + info: 'info', + success: 'info', + fail: 'info', + ready: 'info', + start: 'info', + box: 'info', + // Verbose + debug: 'debug', + trace: 'trace', + verbose: 'trace', +}; + +/** + * Map consola log levels (numeric) to Sentry levels + */ +function getLogLevelFromNumeric(level: LogLevel): LogSeverityLevel { + if (level === 0) { + return 'error'; + } + if (level === 1) { + return 'warn'; + } + if (level === 2) { + return 'info'; + } + if (level === 3) { + return 'info'; + } + if (level === 4) { + return 'debug'; + } + return 'trace'; +} + +/** + * Sentry reporter for Consola. Requires `_experiments.enableLogs` to be enabled. + * + * @experimental This feature is experimental and may be changed or removed in future versions. + */ +export function createConsolaReporter(options?: SentryConsolaReporterOptions, client = getClient()): ConsolaReporter { + if (!client) { + DEBUG_BUILD && logger.warn('No Sentry client found, Consola reporter disabled'); + return { + log: () => { + // no-op + }, + }; + } + + const { _experiments, normalizeDepth = 3, normalizeMaxBreadth = 1_000 } = client.getOptions(); + + if (!_experiments?.enableLogs) { + DEBUG_BUILD && logger.warn('Consola reporter disabled, _experiments.enableLogs is not enabled'); + return { + log: () => { + // no-op + }, + }; + } + + return { + log: (logObj: LogObject) => { + // Determine Sentry log level + const sentryLevel = CONSOLA_TYPE_TO_SENTRY_LEVEL[logObj.type] ?? getLogLevelFromNumeric(logObj.level); + + // Format the message from consola log object + let message = ''; + const args = [...logObj.args]; + + // Handle message property + if (logObj.message) { + message = String(logObj.message); + } + + // Handle additional property + if (logObj.additional) { + const additionalText = Array.isArray(logObj.additional) + ? logObj.additional.join('\n') + : String(logObj.additional); + if (message) { + message += `\n${additionalText}`; + } else { + message = additionalText; + } + } + + // If no message from properties, format args + if (!message && args.length > 0) { + message = formatConsoleArgs(args, normalizeDepth, normalizeMaxBreadth); + } + + // Build attributes + const attributes: Record = { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.consola.logging', + }; + if (logObj.tag) { + attributes['consola.tag'] = logObj.tag; + } + + _INTERNAL_captureLog({ + level: sentryLevel, + message, + attributes, + }); + }, + }; +} + +/** + * Defines the level of logs as specific numbers or special number types. + * + * @type {0 | 1 | 2 | 3 | 4 | 5 | (number & {})} LogLevel - Represents the log level. + * @default 0 - Represents the default log level. + */ +// eslint-disable-next-line @typescript-eslint/ban-types +type LogLevel = 0 | 1 | 2 | 3 | 4 | 5 | (number & {}); + +/** + * Lists the types of log messages supported by the system. + * + * @type {"silent" | "fatal" | "error" | "warn" | "log" | "info" | "success" | "fail" | "ready" | "start" | "box" | "debug" | "trace" | "verbose"} LogType - Represents the specific type of log message. + */ +type LogType = + // 0 + | 'silent' + | 'fatal' + | 'error' + // 1 + | 'warn' + // 2 + | 'log' + // 3 + | 'info' + | 'success' + | 'fail' + | 'ready' + | 'start' + | 'box' + // Verbose + | 'debug' + | 'trace' + | 'verbose'; + +interface InputLogObject { + /** + * The logging level of the message. See {@link LogLevel}. + * @optional + */ + level?: LogLevel; + + /** + * A string tag to categorise or identify the log message. + * @optional + */ + tag?: string; + + /** + * The type of log message, which affects how it's processed and displayed. See {@link LogType}. + * @optional + */ + type?: LogType; + + /** + * The main log message text. + * @optional + */ + message?: string; + + /** + * Additional text or texts to be logged with the message. + * @optional + */ + additional?: string | string[]; + + /** + * Additional arguments to be logged with the message. + * @optional + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args?: any[]; + + /** + * The date and time when the log message was created. + * @optional + */ + date?: Date; +} + +interface LogObject extends InputLogObject { + /** + * The logging level of the message, overridden if required. See {@link LogLevel}. + */ + level: LogLevel; + + /** + * The type of log message, overridden if required. See {@link LogType}. + */ + type: LogType; + + /** + * A string tag to categorise or identify the log message, overridden if necessary. + */ + tag: string; + + /** + * Additional arguments to be logged with the message, overridden if necessary. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args: any[]; + + /** + * The date and time the log message was created, overridden if necessary. + */ + date: Date; + + /** + * Allows additional custom properties to be set on the log object. + */ + // eslint-disable-next-line @typescript-eslint/member-ordering + [key: string]: unknown; +} + +interface ConsolaReporter { + /** + * Defines how a log message is processed and displayed by this reporter. + * @param logObj The LogObject containing the log information to process. See {@link LogObject}. + */ + log: (logObj: LogObject) => void; +} diff --git a/packages/core/src/logs/console-integration.ts b/packages/core/src/logs/console-integration.ts index 677532c36346..9226fd8101a4 100644 --- a/packages/core/src/logs/console-integration.ts +++ b/packages/core/src/logs/console-integration.ts @@ -5,22 +5,14 @@ import { defineIntegration } from '../integration'; import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; import type { ConsoleLevel } from '../types-hoist/instrument'; import type { IntegrationFn } from '../types-hoist/integration'; -import { isPrimitive } from '../utils/is'; +import { formatConsoleArgs } from '../utils/console'; import { CONSOLE_LEVELS, logger } from '../utils/logger'; -import { normalize } from '../utils/normalize'; -import { GLOBAL_OBJ } from '../utils/worldwide'; import { _INTERNAL_captureLog } from './exports'; interface CaptureConsoleOptions { levels: ConsoleLevel[]; } -type GlobalObjectWithUtil = typeof GLOBAL_OBJ & { - util: { - format: (...args: unknown[]) => string; - }; -}; - const INTEGRATION_NAME = 'ConsoleLogs'; const DEFAULT_ATTRIBUTES = { @@ -88,17 +80,3 @@ const _consoleLoggingIntegration = ((options: Partial = { * ``` */ export const consoleLoggingIntegration = defineIntegration(_consoleLoggingIntegration); - -function formatConsoleArgs(values: unknown[], normalizeDepth: number, normalizeMaxBreadth: number): string { - return 'util' in GLOBAL_OBJ && typeof (GLOBAL_OBJ as GlobalObjectWithUtil).util.format === 'function' - ? (GLOBAL_OBJ as GlobalObjectWithUtil).util.format(...values) - : safeJoinConsoleArgs(values, normalizeDepth, normalizeMaxBreadth); -} - -function safeJoinConsoleArgs(values: unknown[], normalizeDepth: number, normalizeMaxBreadth: number): string { - return values - .map(value => - isPrimitive(value) ? String(value) : JSON.stringify(normalize(value, normalizeDepth, normalizeMaxBreadth)), - ) - .join(' '); -} diff --git a/packages/core/src/utils/console.ts b/packages/core/src/utils/console.ts new file mode 100644 index 000000000000..3df194d74ee4 --- /dev/null +++ b/packages/core/src/utils/console.ts @@ -0,0 +1,22 @@ +import { normalizeAndSafeJoin } from './string'; +import { GLOBAL_OBJ } from './worldwide'; + +type GlobalObjectWithUtil = typeof GLOBAL_OBJ & { + util: { + format: (...args: unknown[]) => string; + }; +}; + +/** + * Format console arguments. + * + * @param values - The values to format. + * @param normalizeDepth - The depth to normalize the values. + * @param normalizeMaxBreadth - The maximum breadth to normalize the values. + * @returns The formatted values. + */ +export function formatConsoleArgs(values: unknown[], normalizeDepth: number, normalizeMaxBreadth: number): string { + return 'util' in GLOBAL_OBJ && typeof (GLOBAL_OBJ as GlobalObjectWithUtil).util.format === 'function' + ? (GLOBAL_OBJ as GlobalObjectWithUtil).util.format(...values) + : normalizeAndSafeJoin(values, normalizeDepth, normalizeMaxBreadth); +} diff --git a/packages/core/src/utils/string.ts b/packages/core/src/utils/string.ts index ab98c794f681..5e817b2f0c73 100644 --- a/packages/core/src/utils/string.ts +++ b/packages/core/src/utils/string.ts @@ -1,4 +1,5 @@ -import { isRegExp, isString, isVueViewModel } from './is'; +import { isPrimitive, isRegExp, isString, isVueViewModel } from './is'; +import { normalize } from './normalize'; export { escapeStringForRegex } from '../vendor/escapeStringForRegex'; @@ -60,7 +61,10 @@ export function snipLine(line: string, colno: number): string { } /** - * Join values in array + * Join values in array. + * + * We recommend using {@link normalizeAndSafeJoin} instead. + * * @param input array of values to be joined together * @param delimiter string to be placed in-between values * @returns Joined values @@ -93,6 +97,24 @@ export function safeJoin(input: unknown[], delimiter?: string): string { return output.join(delimiter); } +/** + * Turn an array of values into a string by normalizing and joining them. + * + * A more robust version of {@link safeJoin}. + * + * @param values - The values to join. + * @param normalizeDepth - The depth to normalize the values. + * @param normalizeMaxBreadth - The maximum breadth to normalize the values. + * @returns The joined values. + */ +export function normalizeAndSafeJoin(values: unknown[], normalizeDepth: number, normalizeMaxBreadth: number): string { + return values + .map(value => + isPrimitive(value) ? String(value) : JSON.stringify(normalize(value, normalizeDepth, normalizeMaxBreadth)), + ) + .join(' '); +} + /** * Checks if the given value matches a regex or string *