From f98ae63645cebb7f1e753072872432d0073bf1b7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 24 Jun 2025 20:18:37 +0000 Subject: [PATCH 1/2] Add Consola logging integration for capturing logs in Sentry --- packages/core/src/index.ts | 1 + packages/core/src/logs/consola-integration.ts | 191 ++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 packages/core/src/logs/consola-integration.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b4f09d89f381..624b3da0f39f 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-integration'; export type { FeatureFlag } from './utils/featureFlags'; export { diff --git a/packages/core/src/logs/consola-integration.ts b/packages/core/src/logs/consola-integration.ts new file mode 100644 index 000000000000..52c81ae8682e --- /dev/null +++ b/packages/core/src/logs/consola-integration.ts @@ -0,0 +1,191 @@ +import { getClient } from '../currentScopes'; +import { DEBUG_BUILD } from '../debug-build'; +import { defineIntegration } from '../integration'; +import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; +import type { IntegrationFn } from '../types-hoist/integration'; +import { isPrimitive } from '../utils/is'; +import { logger } from '../utils/logger'; +import { normalize } from '../utils/normalize'; +import { _INTERNAL_captureLog } from './exports'; + +interface ConsolaIntegrationOptions { + /** + * Consola instances to add the Sentry reporter to. + * These should be existing consola instances from the user's application. + */ + consola: ConsolaLike | ConsolaLike[]; +} + +/** + * Minimal interface for consola-like objects to avoid adding consola as a dependency. + * Users should pass their actual consola instances. + */ +interface ConsolaLike { + addReporter: (reporter: ConsolaReporter) => void; + removeReporter: (reporter: ConsolaReporter) => void; +} + +interface ConsolaReporter { + log: (logObj: ConsolaLogObject, ctx: { options: any }) => void; +} + +interface ConsolaLogObject { + [key: string]: unknown; + level: number; + type: string; + tag?: string; + args: any[]; + date: Date; + message?: string; + additional?: string | string[]; +} + +const INTEGRATION_NAME = 'ConsolaLogs'; + +const DEFAULT_ATTRIBUTES = { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.consola.logging', +}; + +/** + * Map consola log types to Sentry log levels + */ +const CONSOLA_TYPE_TO_SENTRY_LEVEL: Record = { + silent: 'debug', + fatal: 'fatal', + error: 'error', + warn: 'warning', + info: 'info', + debug: 'debug', + trace: 'debug', + log: 'info', + verbose: 'debug', + start: 'info', + success: 'info', + fail: 'error', + ready: 'info', +}; + +/** + * Map consola log levels (numeric) to Sentry levels + */ +function getLogLevelFromNumeric(level: number): string { + if (level <= 0) return 'error'; // Fatal/Error + if (level === 1) return 'warning'; // Warnings + if (level === 2) return 'info'; // Normal logs + if (level === 3) return 'info'; // Informational logs + if (level >= 4) return 'debug'; // Debug/Trace logs + return 'info'; +} + +const _consolaLoggingIntegration = ((options: ConsolaIntegrationOptions) => { + const consolaInstances = Array.isArray(options.consola) ? options.consola : [options.consola]; + + return { + name: INTEGRATION_NAME, + setup(client) { + const { _experiments, normalizeDepth = 3, normalizeMaxBreadth = 1_000 } = client.getOptions(); + if (!_experiments?.enableLogs) { + DEBUG_BUILD && logger.warn('`_experiments.enableLogs` is not enabled, ConsolaLogs integration disabled'); + return; + } + + // Create the Sentry reporter + const sentryReporter: ConsolaReporter = { + log: (logObj: ConsolaLogObject) => { + if (getClient() !== client) { + return; + } + + // 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 = formatConsolaArgs(args, normalizeDepth, normalizeMaxBreadth); + } + + // Build attributes + const attributes: Record = { ...DEFAULT_ATTRIBUTES }; + if (logObj.tag) { + attributes['consola.tag'] = logObj.tag; + } + + _INTERNAL_captureLog({ + level: sentryLevel as any, + message, + attributes, + }); + }, + }; + + // Add the reporter to all consola instances + consolaInstances.forEach(consola => { + try { + consola.addReporter(sentryReporter); + } catch (error) { + DEBUG_BUILD && logger.warn('Failed to add Sentry reporter to consola instance:', error); + } + }); + + // Store reference to remove on cleanup if needed + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + (client as any).__consolaReporter = sentryReporter; + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access + (client as any).__consolaInstances = consolaInstances; + }, + }; +}) satisfies IntegrationFn; + +/** + * Captures logs from consola instances by adding a Sentry reporter. Requires `_experiments.enableLogs` to be enabled. + * + * @experimental This feature is experimental and may be changed or removed in future versions. + * + * This integration works by adding a custom reporter to your existing consola instances. + * The reporter will forward all logs to Sentry while preserving the original consola behavior. + * + * @example + * + * ```ts + * import * as Sentry from '@sentry/browser'; + * import { consola } from 'consola'; + * + * Sentry.init({ + * integrations: [Sentry.consolaLoggingIntegration({ consola })], + * }); + * + * // Now all consola logs will be sent to Sentry + * consola.info('This will be captured by Sentry'); + * consola.error('This error will also be captured'); + * ``` + */ +export const consolaLoggingIntegration = defineIntegration(_consolaLoggingIntegration); + +function formatConsolaArgs(values: unknown[], normalizeDepth: number, normalizeMaxBreadth: number): string { + return values + .map(value => + isPrimitive(value) ? String(value) : JSON.stringify(normalize(value, normalizeDepth, normalizeMaxBreadth)), + ) + .join(' '); +} From fa0725be4e14c4d99d51bd9160aa1e26ffb5fc39 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 24 Jun 2025 17:15:16 -0400 Subject: [PATCH 2/2] more improvements --- packages/core/src/index.ts | 2 +- packages/core/src/logs/consola-integration.ts | 191 ------------- packages/core/src/logs/consola.ts | 255 ++++++++++++++++++ packages/core/src/logs/console-integration.ts | 24 +- packages/core/src/utils/console.ts | 22 ++ packages/core/src/utils/string.ts | 26 +- 6 files changed, 303 insertions(+), 217 deletions(-) delete mode 100644 packages/core/src/logs/consola-integration.ts create mode 100644 packages/core/src/logs/consola.ts create mode 100644 packages/core/src/utils/console.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 624b3da0f39f..060fc8a207fd 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -123,7 +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-integration'; +export { consolaLoggingIntegration } from './logs/consola'; export type { FeatureFlag } from './utils/featureFlags'; export { diff --git a/packages/core/src/logs/consola-integration.ts b/packages/core/src/logs/consola-integration.ts deleted file mode 100644 index 52c81ae8682e..000000000000 --- a/packages/core/src/logs/consola-integration.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { getClient } from '../currentScopes'; -import { DEBUG_BUILD } from '../debug-build'; -import { defineIntegration } from '../integration'; -import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes'; -import type { IntegrationFn } from '../types-hoist/integration'; -import { isPrimitive } from '../utils/is'; -import { logger } from '../utils/logger'; -import { normalize } from '../utils/normalize'; -import { _INTERNAL_captureLog } from './exports'; - -interface ConsolaIntegrationOptions { - /** - * Consola instances to add the Sentry reporter to. - * These should be existing consola instances from the user's application. - */ - consola: ConsolaLike | ConsolaLike[]; -} - -/** - * Minimal interface for consola-like objects to avoid adding consola as a dependency. - * Users should pass their actual consola instances. - */ -interface ConsolaLike { - addReporter: (reporter: ConsolaReporter) => void; - removeReporter: (reporter: ConsolaReporter) => void; -} - -interface ConsolaReporter { - log: (logObj: ConsolaLogObject, ctx: { options: any }) => void; -} - -interface ConsolaLogObject { - [key: string]: unknown; - level: number; - type: string; - tag?: string; - args: any[]; - date: Date; - message?: string; - additional?: string | string[]; -} - -const INTEGRATION_NAME = 'ConsolaLogs'; - -const DEFAULT_ATTRIBUTES = { - [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.consola.logging', -}; - -/** - * Map consola log types to Sentry log levels - */ -const CONSOLA_TYPE_TO_SENTRY_LEVEL: Record = { - silent: 'debug', - fatal: 'fatal', - error: 'error', - warn: 'warning', - info: 'info', - debug: 'debug', - trace: 'debug', - log: 'info', - verbose: 'debug', - start: 'info', - success: 'info', - fail: 'error', - ready: 'info', -}; - -/** - * Map consola log levels (numeric) to Sentry levels - */ -function getLogLevelFromNumeric(level: number): string { - if (level <= 0) return 'error'; // Fatal/Error - if (level === 1) return 'warning'; // Warnings - if (level === 2) return 'info'; // Normal logs - if (level === 3) return 'info'; // Informational logs - if (level >= 4) return 'debug'; // Debug/Trace logs - return 'info'; -} - -const _consolaLoggingIntegration = ((options: ConsolaIntegrationOptions) => { - const consolaInstances = Array.isArray(options.consola) ? options.consola : [options.consola]; - - return { - name: INTEGRATION_NAME, - setup(client) { - const { _experiments, normalizeDepth = 3, normalizeMaxBreadth = 1_000 } = client.getOptions(); - if (!_experiments?.enableLogs) { - DEBUG_BUILD && logger.warn('`_experiments.enableLogs` is not enabled, ConsolaLogs integration disabled'); - return; - } - - // Create the Sentry reporter - const sentryReporter: ConsolaReporter = { - log: (logObj: ConsolaLogObject) => { - if (getClient() !== client) { - return; - } - - // 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 = formatConsolaArgs(args, normalizeDepth, normalizeMaxBreadth); - } - - // Build attributes - const attributes: Record = { ...DEFAULT_ATTRIBUTES }; - if (logObj.tag) { - attributes['consola.tag'] = logObj.tag; - } - - _INTERNAL_captureLog({ - level: sentryLevel as any, - message, - attributes, - }); - }, - }; - - // Add the reporter to all consola instances - consolaInstances.forEach(consola => { - try { - consola.addReporter(sentryReporter); - } catch (error) { - DEBUG_BUILD && logger.warn('Failed to add Sentry reporter to consola instance:', error); - } - }); - - // Store reference to remove on cleanup if needed - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - (client as any).__consolaReporter = sentryReporter; - // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access - (client as any).__consolaInstances = consolaInstances; - }, - }; -}) satisfies IntegrationFn; - -/** - * Captures logs from consola instances by adding a Sentry reporter. Requires `_experiments.enableLogs` to be enabled. - * - * @experimental This feature is experimental and may be changed or removed in future versions. - * - * This integration works by adding a custom reporter to your existing consola instances. - * The reporter will forward all logs to Sentry while preserving the original consola behavior. - * - * @example - * - * ```ts - * import * as Sentry from '@sentry/browser'; - * import { consola } from 'consola'; - * - * Sentry.init({ - * integrations: [Sentry.consolaLoggingIntegration({ consola })], - * }); - * - * // Now all consola logs will be sent to Sentry - * consola.info('This will be captured by Sentry'); - * consola.error('This error will also be captured'); - * ``` - */ -export const consolaLoggingIntegration = defineIntegration(_consolaLoggingIntegration); - -function formatConsolaArgs(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/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 *