diff --git a/packages/core/src/integrations/consola.ts b/packages/core/src/integrations/consola.ts index 7cc2d2395416..26ca7b71ab4e 100644 --- a/packages/core/src/integrations/consola.ts +++ b/packages/core/src/integrations/consola.ts @@ -3,6 +3,7 @@ import { getClient } from '../currentScopes'; import { _INTERNAL_captureLog } from '../logs/internal'; import { formatConsoleArgs } from '../logs/utils'; import type { LogSeverityLevel } from '../types-hoist/log'; +import { normalize } from '../utils/normalize'; /** * Options for the Sentry Consola reporter. @@ -61,8 +62,9 @@ export interface ConsolaReporter { */ export interface ConsolaLogObject { /** - * Allows additional custom properties to be set on the log object. - * These properties will be captured as log attributes with a 'consola.' prefix. + * Allows additional custom properties to be set on the log object. These properties will be captured as log attributes. + * + * Additional properties are set when passing a single object with a `message` (`consola.[type]({ message: '', ... })`) or if the reporter is called directly * * @example * ```ts @@ -73,7 +75,7 @@ export interface ConsolaLogObject { * userId: 123, * sessionId: 'abc-123' * }); - * // Will create attributes: consola.userId and consola.sessionId + * // Will create attributes: `userId` and `sessionId` * ``` */ [key: string]: unknown; @@ -152,6 +154,10 @@ export interface ConsolaLogObject { * * When provided, this is the final formatted message. When not provided, * the message should be constructed from the `args` array. + * + * Note: In reporters, `message` is typically undefined. It is primarily for + * `consola.[type]({ message: 'xxx' })` usage and is normalized into `args` before + * reporters receive the log object. See: https://github.com/unjs/consola/issues/406#issuecomment-3684792551 */ message?: string; } @@ -194,8 +200,9 @@ export function createConsolaReporter(options: ConsolaReporterOptions = {}): Con return { log(logObj: ConsolaLogObject) { + // We need to exclude certain known properties from being added as additional attributes // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { type, level, message: consolaMessage, args, tag, date: _date, ...attributes } = logObj; + const { type, level, message: consolaMessage, args, tag, date: _date, ...rest } = logObj; // Get client - use provided client or current client const client = providedClient || getClient(); @@ -223,7 +230,13 @@ export function createConsolaReporter(options: ConsolaReporterOptions = {}): Con } const message = messageParts.join(' '); + const attributes: Record = {}; + // Build attributes + for (const [key, value] of Object.entries(rest)) { + attributes[key] = normalize(value, normalizeDepth, normalizeMaxBreadth); + } + attributes['sentry.origin'] = 'auto.log.consola'; if (tag) { diff --git a/packages/core/test/lib/integrations/consola.test.ts b/packages/core/test/lib/integrations/consola.test.ts index 2b9ea96edf75..e1a32b775e54 100644 --- a/packages/core/test/lib/integrations/consola.test.ts +++ b/packages/core/test/lib/integrations/consola.test.ts @@ -62,7 +62,7 @@ describe('createConsolaReporter', () => { }); describe('message and args handling', () => { - it('should format message from args when message is not provided', () => { + it('should format message from args', () => { const logObj = { type: 'info', args: ['Hello', 'world', 123, { key: 'value' }], @@ -101,6 +101,51 @@ describe('createConsolaReporter', () => { }, }); }); + + it('consola-merged: args=[message] with extra keys on log object', () => { + sentryReporter.log({ + type: 'log', + level: 2, + args: ['Hello', 'world', { some: 'obj' }], + userId: 123, + action: 'login', + time: '2026-02-24T10:24:04.477Z', + smallObj: { firstLevel: { secondLevel: { thirdLevel: { fourthLevel: 'deep' } } } }, + tag: '', + }); + + const call = vi.mocked(_INTERNAL_captureLog).mock.calls[0]![0]; + + // Message from args + expect(call.message).toBe('Hello world {"some":"obj"}'); + expect(call.attributes).toMatchObject({ + 'consola.type': 'log', + 'consola.level': 2, + userId: 123, + smallObj: { firstLevel: { secondLevel: { thirdLevel: '[Object]' } } }, // Object is normalized + action: 'login', + time: '2026-02-24T10:24:04.477Z', + 'sentry.origin': 'auto.log.consola', + }); + expect(call.attributes?.['sentry.message.parameter.0']).toBeUndefined(); + }); + + it('capturing custom keys mimicking direct reporter.log({ type, message, userId, sessionId })', () => { + sentryReporter.log({ + type: 'info', + message: 'User action', + userId: 123, + sessionId: 'abc-123', + }); + + const call = vi.mocked(_INTERNAL_captureLog).mock.calls[0]![0]; + expect(call.message).toBe('User action'); + expect(call.attributes).toMatchObject({ + 'consola.type': 'info', + userId: 123, + sessionId: 'abc-123', + }); + }); }); describe('level mapping', () => {