Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';
import { consola } from 'consola';

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0.0',
environment: 'test',
enableLogs: true,
transport: loggingTransport,
});

async function run(): Promise<void> {
consola.level = 5;

const sentryReporter = Sentry.createConsolaReporter();
consola.addReporter(sentryReporter);

// Object context extraction - objects should become searchable attributes
consola.info('User logged in', { userId: 123, sessionId: 'abc-123' });

// Multiple objects - properties should be merged
consola.warn('Payment processed', { orderId: 456 }, { amount: 99.99, currency: 'USD' });

// Mixed primitives and objects
consola.error('Error occurred', 'in payment module', { errorCode: 'E001', retryable: true });

// Aarrays (should be stored as context attributes)
consola.debug('Processing items', [1, 2, 3, 4, 5]);

// Nested objects
consola.info('Complex data', { user: { id: 789, name: 'Jane' }, metadata: { source: 'api' } });

await Sentry.flush();
}

// eslint-disable-next-line @typescript-eslint/no-floating-promises
void run();
111 changes: 110 additions & 1 deletion dev-packages/node-integration-tests/suites/consola/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ describe('consola integration', () => {
{
timestamp: expect.any(Number),
level: 'info',
body: 'Message with args: hello 123 {"key":"value"} [1,2,3]',
body: 'Message with args: hello 123',
severity_number: expect.any(Number),
trace_id: expect.any(String),
attributes: {
Expand All @@ -291,6 +291,7 @@ describe('consola integration', () => {
'server.address': { value: expect.any(String), type: 'string' },
'consola.type': { value: 'info', type: 'string' },
'consola.level': { value: 3, type: 'integer' },
key: { value: 'value', type: 'string' },
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test missing assertion for array attribute extraction (Bugbot Rules)

Per the review rules: "Check that tests actually test the newly added behaviour. When checking on sent payloads by the SDK, ensure that the newly added data is asserted thoroughly." The test subject at subject-args.ts line 24 includes an array [1, 2, 3] in the args, which per the new feature should be stored as consola.args.0. However, the test's expected attributes at lines 285-295 only assert key: 'value' but not the consola.args.0 attribute for the array, leaving the array extraction behavior unverified in this test case.

Fix in Cursor Fix in Web

},
},
{
Expand Down Expand Up @@ -491,4 +492,112 @@ describe('consola integration', () => {

await runner.completed();
});

test('should extract objects as searchable context attributes', async () => {
const runner = createRunner(__dirname, 'subject-object-context.ts')
.expect({
log: {
items: [
{
timestamp: expect.any(Number),
level: 'info',
body: 'User logged in',
severity_number: expect.any(Number),
trace_id: expect.any(String),
attributes: {
'sentry.origin': { value: 'auto.log.consola', type: 'string' },
'sentry.release': { value: '1.0.0', type: 'string' },
'sentry.environment': { value: 'test', type: 'string' },
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
'server.address': { value: expect.any(String), type: 'string' },
'consola.type': { value: 'info', type: 'string' },
'consola.level': { value: 3, type: 'integer' },
userId: { value: 123, type: 'integer' },
sessionId: { value: 'abc-123', type: 'string' },
},
},
{
timestamp: expect.any(Number),
level: 'warn',
body: 'Payment processed',
severity_number: expect.any(Number),
trace_id: expect.any(String),
attributes: {
'sentry.origin': { value: 'auto.log.consola', type: 'string' },
'sentry.release': { value: '1.0.0', type: 'string' },
'sentry.environment': { value: 'test', type: 'string' },
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
'server.address': { value: expect.any(String), type: 'string' },
'consola.type': { value: 'warn', type: 'string' },
'consola.level': { value: 1, type: 'integer' },
orderId: { value: 456, type: 'integer' },
amount: { value: 99.99, type: 'double' },
currency: { value: 'USD', type: 'string' },
},
},
{
timestamp: expect.any(Number),
level: 'error',
body: 'Error occurred in payment module',
severity_number: expect.any(Number),
trace_id: expect.any(String),
attributes: {
'sentry.origin': { value: 'auto.log.consola', type: 'string' },
'sentry.release': { value: '1.0.0', type: 'string' },
'sentry.environment': { value: 'test', type: 'string' },
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
'server.address': { value: expect.any(String), type: 'string' },
'consola.type': { value: 'error', type: 'string' },
'consola.level': { value: 0, type: 'integer' },
errorCode: { value: 'E001', type: 'string' },
retryable: { value: true, type: 'boolean' },
},
},
{
timestamp: expect.any(Number),
level: 'debug',
body: 'Processing items',
severity_number: expect.any(Number),
trace_id: expect.any(String),
attributes: {
'sentry.origin': { value: 'auto.log.consola', type: 'string' },
'sentry.release': { value: '1.0.0', type: 'string' },
'sentry.environment': { value: 'test', type: 'string' },
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
'server.address': { value: expect.any(String), type: 'string' },
'consola.type': { value: 'debug', type: 'string' },
'consola.level': { value: 4, type: 'integer' },
'consola.context.0': { value: '[1,2,3,4,5]', type: 'string' },
},
},
{
timestamp: expect.any(Number),
level: 'info',
body: 'Complex data',
severity_number: expect.any(Number),
trace_id: expect.any(String),
attributes: {
'sentry.origin': { value: 'auto.log.consola', type: 'string' },
'sentry.release': { value: '1.0.0', type: 'string' },
'sentry.environment': { value: 'test', type: 'string' },
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
'server.address': { value: expect.any(String), type: 'string' },
'consola.type': { value: 'info', type: 'string' },
'consola.level': { value: 3, type: 'integer' },
user: { value: '{"id":789,"name":"Jane"}', type: 'string' },
metadata: { value: '{"source":"api"}', type: 'string' },
},
},
],
},
})
.start();

await runner.completed();
});
});
51 changes: 40 additions & 11 deletions packages/core/src/integrations/consola.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { isPrimitive } from '../utils/is';

/**
* Options for the Sentry Consola reporter.
Expand Down Expand Up @@ -206,17 +207,7 @@ export function createConsolaReporter(options: ConsolaReporterOptions = {}): Con

const { normalizeDepth = 3, normalizeMaxBreadth = 1_000 } = client.getOptions();

// Format the log message using the same approach as consola's basic reporter
const messageParts = [];
if (consolaMessage) {
messageParts.push(consolaMessage);
}
if (args && args.length > 0) {
messageParts.push(formatConsoleArgs(args, normalizeDepth, normalizeMaxBreadth));
}
const message = messageParts.join(' ');

// Build attributes
// Build base attributes first
attributes['sentry.origin'] = 'auto.log.consola';

if (tag) {
Expand All @@ -232,6 +223,44 @@ export function createConsolaReporter(options: ConsolaReporterOptions = {}): Con
attributes['consola.level'] = level;
}

// Process args: separate primitives for message, extract objects as attributes
let message = consolaMessage || '';
if (args?.length) {
const primitives: unknown[] = [];
let contextIndex = 0;

for (const arg of args) {
if (isPrimitive(arg)) {
primitives.push(arg);
} else if (typeof arg === 'object' && arg !== null) {
// Plain objects: extract properties as attributes
if (!Array.isArray(arg)) {
try {
for (const key in arg) {
// Only add if not conflicting with existing or consola-prefixed attributes
if (!(key in attributes) && !(`consola.${key}` in attributes)) {
attributes[key] = (arg as Record<string, unknown>)[key];
}
}
} catch {
// Skip on error
}
} else {
// Arrays: store as context attribute as they don't have meaningful property names, just numeric indices
attributes[`consola.context.${contextIndex++}`] = arg;
}
} else {
primitives.push(arg);
}
}

if (primitives.length) {
message = message
? `${message} ${formatConsoleArgs(primitives, normalizeDepth, normalizeMaxBreadth)}`
: formatConsoleArgs(primitives, normalizeDepth, normalizeMaxBreadth);
}
}

_INTERNAL_captureLog({
level: logSeverityLevel,
message,
Expand Down
108 changes: 105 additions & 3 deletions packages/core/test/lib/integrations/consola.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,13 +184,14 @@ describe('createConsolaReporter', () => {

sentryReporter.log(logObj);

expect(formatConsoleArgs).toHaveBeenCalledWith(['Hello', 'world', 123, { key: 'value' }], 3, 1000);
expect(formatConsoleArgs).toHaveBeenCalledWith(['Hello', 'world', 123], 3, 1000);
expect(_INTERNAL_captureLog).toHaveBeenCalledWith({
level: 'info',
message: 'Hello world 123 {"key":"value"}',
message: 'Hello world 123',
attributes: {
'sentry.origin': 'auto.log.consola',
'consola.type': 'info',
key: 'value',
},
});
});
Expand All @@ -208,14 +209,115 @@ describe('createConsolaReporter', () => {

expect(_INTERNAL_captureLog).toHaveBeenCalledWith({
level: 'info',
message: 'Message {"self":"[Circular ~]"}',
message: 'Message',
attributes: {
'sentry.origin': 'auto.log.consola',
'consola.type': 'info',
self: circular,
},
});
});

it('should extract multiple objects as attributes', () => {
const logObj = {
type: 'info',
message: 'User action',
args: [{ userId: 123 }, { sessionId: 'abc-123' }],
};

sentryReporter.log(logObj);

expect(_INTERNAL_captureLog).toHaveBeenCalledWith({
level: 'info',
message: 'User action',
attributes: {
'sentry.origin': 'auto.log.consola',
'consola.type': 'info',
userId: 123,
sessionId: 'abc-123',
},
});
});

it('should handle mixed primitives and objects in args', () => {
const logObj = {
type: 'info',
args: ['Processing', { userId: 456 }, 'for', { action: 'login' }],
};

sentryReporter.log(logObj);

expect(formatConsoleArgs).toHaveBeenCalledWith(['Processing', 'for'], 3, 1000);
expect(_INTERNAL_captureLog).toHaveBeenCalledWith({
level: 'info',
message: 'Processing for',
attributes: {
'sentry.origin': 'auto.log.consola',
'consola.type': 'info',
userId: 456,
action: 'login',
},
});
});

it('should handle arrays as context attributes', () => {
const logObj = {
type: 'info',
message: 'Array data',
args: [[1, 2, 3]],
};

sentryReporter.log(logObj);

expect(_INTERNAL_captureLog).toHaveBeenCalledWith({
level: 'info',
message: 'Array data',
attributes: {
'sentry.origin': 'auto.log.consola',
'consola.type': 'info',
'consola.context.0': [1, 2, 3],
},
});
});

it('should not override existing attributes with object properties', () => {
const logObj = {
type: 'info',
message: 'Test',
tag: 'api',
args: [{ tag: 'should-not-override' }],
};

sentryReporter.log(logObj);

expect(_INTERNAL_captureLog).toHaveBeenCalledWith({
level: 'info',
message: 'Test',
attributes: {
'sentry.origin': 'auto.log.consola',
'consola.type': 'info',
'consola.tag': 'api',
// tag should not be overridden by the object arg
},
});
});

it('should handle objects with nested properties', () => {
const logObj = {
type: 'info',
args: ['Event', { user: { id: 123, name: 'John' }, timestamp: Date.now() }],
};

sentryReporter.log(logObj);

const captureCall = vi.mocked(_INTERNAL_captureLog).mock.calls[0][0];
expect(captureCall.level).toBe('info');
expect(captureCall.message).toBe('Event');
expect(captureCall.attributes['sentry.origin']).toBe('auto.log.consola');
expect(captureCall.attributes.user).toEqual({ id: 123, name: 'John' });
expect(captureCall.attributes.timestamp).toEqual(expect.any(Number));
});

it('should map consola levels to sentry levels when type is not provided', () => {
const logObj = {
level: 0, // Fatal level
Expand Down
Loading