Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
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,50 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';
import { consola } from 'consola';

Sentry.init({
dsn: 'https://[email protected]/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 });

// Arrays (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' } });

// Deep nesting to test normalizeDepth (should be normalized at depth 3 - default)
consola.info('Deep object', {
level1: {
level2: {
level3: {
level4: { level5: 'should be normalized' },
},
},
},
simpleKey: 'simple value',
});

await Sentry.flush();
}

// eslint-disable-next-line @typescript-eslint/no-floating-promises
void run();
131 changes: 130 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,132 @@ 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' },
},
},
{
timestamp: expect.any(Number),
level: 'info',
body: 'Deep object',
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' },
// Nested objects are extracted and normalized respecting normalizeDepth setting
level1: { value: '{"level2":{"level3":{"level4":"[Object]"}}}', type: 'string' },
simpleKey: { value: 'simple value', type: 'string' },
},
},
],
},
})
.start();

await runner.completed();
});
});
57 changes: 46 additions & 11 deletions packages/core/src/integrations/consola.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ 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';
import { normalize } from '../utils/normalize';

/**
* Options for the Sentry Consola reporter.
Expand Down Expand Up @@ -206,17 +208,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 +224,49 @@ 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)) {
// Normalize the value to respect normalizeDepth
attributes[key] = normalize(
(arg as Record<string, unknown>)[key],
normalizeDepth,
normalizeMaxBreadth,
);
}
}
} 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
Loading
Loading