Skip to content

Commit 89b9790

Browse files
committed
handle Date/Set/Map
1 parent 525d08c commit 89b9790

4 files changed

Lines changed: 252 additions & 12 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
import { consola } from 'consola';
4+
5+
Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
release: '1.0.0',
8+
environment: 'test',
9+
enableLogs: true,
10+
transport: loggingTransport,
11+
});
12+
13+
async function run(): Promise<void> {
14+
consola.level = 5;
15+
16+
const sentryReporter = Sentry.createConsolaReporter();
17+
consola.addReporter(sentryReporter);
18+
19+
// Test Date objects are preserved
20+
consola.info('Current time:', new Date('2023-01-01T00:00:00.000Z'));
21+
22+
// Test Error objects are preserved
23+
consola.error('Error occurred:', new Error('Test error'));
24+
25+
// Test RegExp objects are preserved
26+
consola.info('Pattern:', /test/gi);
27+
28+
// Test Map and Set objects are preserved
29+
consola.info('Collections:', new Map([['key', 'value']]), new Set([1, 2, 3]));
30+
31+
// Test mixed: plain object + special object
32+
consola.info('Mixed data', { userId: 123 }, new Date('2023-06-15T12:00:00.000Z'));
33+
34+
await Sentry.flush();
35+
}
36+
37+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
38+
void run();
39+

dev-packages/node-integration-tests/suites/consola/test.ts

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,7 @@ describe('consola integration', () => {
571571
'server.address': { value: expect.any(String), type: 'string' },
572572
'consola.type': { value: 'debug', type: 'string' },
573573
'consola.level': { value: 4, type: 'integer' },
574-
'consola.context.0': { value: '[1,2,3,4,5]', type: 'string' },
574+
'consola.args.0': { value: '[1,2,3,4,5]', type: 'string' },
575575
},
576576
},
577577
{
@@ -620,4 +620,115 @@ describe('consola integration', () => {
620620

621621
await runner.completed();
622622
});
623+
624+
test('should preserve special objects (Date, Error, RegExp, Map, Set) as context attributes', async () => {
625+
const runner = createRunner(__dirname, 'subject-special-objects.ts')
626+
.expect({
627+
log: {
628+
items: [
629+
{
630+
timestamp: expect.any(Number),
631+
level: 'info',
632+
body: 'Current time:',
633+
severity_number: expect.any(Number),
634+
trace_id: expect.any(String),
635+
attributes: {
636+
'sentry.origin': { value: 'auto.log.consola', type: 'string' },
637+
'sentry.release': { value: '1.0.0', type: 'string' },
638+
'sentry.environment': { value: 'test', type: 'string' },
639+
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
640+
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
641+
'server.address': { value: expect.any(String), type: 'string' },
642+
'consola.type': { value: 'info', type: 'string' },
643+
'consola.level': { value: 3, type: 'integer' },
644+
// Date objects serialize with extra quotes
645+
'consola.args.0': { value: '"2023-01-01T00:00:00.000Z"', type: 'string' },
646+
},
647+
},
648+
{
649+
timestamp: expect.any(Number),
650+
level: 'error',
651+
body: 'Error occurred:',
652+
severity_number: expect.any(Number),
653+
trace_id: expect.any(String),
654+
attributes: {
655+
'sentry.origin': { value: 'auto.log.consola', type: 'string' },
656+
'sentry.release': { value: '1.0.0', type: 'string' },
657+
'sentry.environment': { value: 'test', type: 'string' },
658+
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
659+
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
660+
'server.address': { value: expect.any(String), type: 'string' },
661+
'consola.type': { value: 'error', type: 'string' },
662+
'consola.level': { value: 0, type: 'integer' },
663+
// Error objects serialize as empty object (properties are non-enumerable)
664+
'consola.args.0': { value: '{}', type: 'string' },
665+
},
666+
},
667+
{
668+
timestamp: expect.any(Number),
669+
level: 'info',
670+
body: 'Pattern:',
671+
severity_number: expect.any(Number),
672+
trace_id: expect.any(String),
673+
attributes: {
674+
'sentry.origin': { value: 'auto.log.consola', type: 'string' },
675+
'sentry.release': { value: '1.0.0', type: 'string' },
676+
'sentry.environment': { value: 'test', type: 'string' },
677+
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
678+
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
679+
'server.address': { value: expect.any(String), type: 'string' },
680+
'consola.type': { value: 'info', type: 'string' },
681+
'consola.level': { value: 3, type: 'integer' },
682+
// RegExp objects serialize as empty object
683+
'consola.args.0': { value: '{}', type: 'string' },
684+
},
685+
},
686+
{
687+
timestamp: expect.any(Number),
688+
level: 'info',
689+
body: 'Collections:',
690+
severity_number: expect.any(Number),
691+
trace_id: expect.any(String),
692+
attributes: {
693+
'sentry.origin': { value: 'auto.log.consola', type: 'string' },
694+
'sentry.release': { value: '1.0.0', type: 'string' },
695+
'sentry.environment': { value: 'test', type: 'string' },
696+
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
697+
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
698+
'server.address': { value: expect.any(String), type: 'string' },
699+
'consola.type': { value: 'info', type: 'string' },
700+
'consola.level': { value: 3, type: 'integer' },
701+
// Map converted to object, Set converted to array
702+
'consola.args.0': { value: '{"key":"value"}', type: 'string' },
703+
'consola.args.1': { value: '[1,2,3]', type: 'string' },
704+
},
705+
},
706+
{
707+
timestamp: expect.any(Number),
708+
level: 'info',
709+
body: 'Mixed data',
710+
severity_number: expect.any(Number),
711+
trace_id: expect.any(String),
712+
attributes: {
713+
'sentry.origin': { value: 'auto.log.consola', type: 'string' },
714+
'sentry.release': { value: '1.0.0', type: 'string' },
715+
'sentry.environment': { value: 'test', type: 'string' },
716+
'sentry.sdk.name': { value: 'sentry.javascript.node', type: 'string' },
717+
'sentry.sdk.version': { value: expect.any(String), type: 'string' },
718+
'server.address': { value: expect.any(String), type: 'string' },
719+
'consola.type': { value: 'info', type: 'string' },
720+
'consola.level': { value: 3, type: 'integer' },
721+
// Plain object properties are extracted
722+
userId: { value: 123, type: 'integer' },
723+
// Date is preserved in context
724+
'consola.args.0': { value: '"2023-06-15T12:00:00.000Z"', type: 'string' },
725+
},
726+
},
727+
],
728+
},
729+
})
730+
.start();
731+
732+
await runner.completed();
733+
});
623734
});

packages/core/src/integrations/consola.ts

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { getClient } from '../currentScopes';
33
import { _INTERNAL_captureLog } from '../logs/internal';
44
import { formatConsoleArgs } from '../logs/utils';
55
import type { LogSeverityLevel } from '../types-hoist/log';
6-
import { isPrimitive } from '../utils/is';
6+
import { isPlainObject, isPrimitive } from '../utils/is';
77
import { normalize } from '../utils/normalize';
88

99
/**
@@ -234,26 +234,24 @@ export function createConsolaReporter(options: ConsolaReporterOptions = {}): Con
234234
if (isPrimitive(arg)) {
235235
primitives.push(arg);
236236
} else if (typeof arg === 'object' && arg !== null) {
237-
// Plain objects: extract properties as attributes
238-
if (!Array.isArray(arg)) {
237+
// Plain objects: extract properties as individual attributes
238+
if (isPlainObject(arg)) {
239239
try {
240240
for (const key in arg) {
241241
// Only add if not conflicting with existing or consola-prefixed attributes
242242
if (!(key in attributes) && !(`consola.${key}` in attributes)) {
243243
// Normalize the value to respect normalizeDepth
244-
attributes[key] = normalize(
245-
(arg as Record<string, unknown>)[key],
246-
normalizeDepth,
247-
normalizeMaxBreadth,
248-
);
244+
attributes[key] = normalize(arg[key], normalizeDepth, normalizeMaxBreadth);
249245
}
250246
}
251247
} catch {
252248
// Skip on error
253249
}
254250
} else {
255-
// Arrays: store as context attribute as they don't have meaningful property names, just numeric indices
256-
attributes[`consola.context.${contextIndex++}`] = arg;
251+
// Non-plain objects (Date, Error, Map, Set, etc.) and arrays: Store as args attribute so they get properly serialized
252+
// Special handling for Map and Set to preserve their data
253+
attributes[`consola.args.${contextIndex++}`] =
254+
arg instanceof Map ? Object.fromEntries(arg) : arg instanceof Set ? Array.from(arg) : arg;
257255
}
258256
} else {
259257
primitives.push(arg);

packages/core/test/lib/integrations/consola.test.ts

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ describe('createConsolaReporter', () => {
275275
attributes: {
276276
'sentry.origin': 'auto.log.consola',
277277
'consola.type': 'info',
278-
'consola.context.0': [1, 2, 3],
278+
'consola.args.0': [1, 2, 3],
279279
},
280280
});
281281
});
@@ -346,6 +346,98 @@ describe('createConsolaReporter', () => {
346346
expect(captureCall.attributes.simpleKey).toBe('simple value');
347347
});
348348

349+
it('should store Date objects as context attributes', () => {
350+
const now = new Date('2023-01-01T00:00:00.000Z');
351+
const logObj = {
352+
type: 'info',
353+
args: ['Current time:', now],
354+
};
355+
356+
sentryReporter.log(logObj);
357+
358+
const captureCall = vi.mocked(_INTERNAL_captureLog).mock.calls[0][0];
359+
expect(captureCall.level).toBe('info');
360+
expect(captureCall.message).toBe('Current time:');
361+
expect(captureCall.attributes['sentry.origin']).toBe('auto.log.consola');
362+
expect(captureCall.attributes['consola.args.0']).toBe(now);
363+
});
364+
365+
it('should store Error objects as context attributes', () => {
366+
const error = new Error('Test error');
367+
const logObj = {
368+
type: 'error',
369+
args: ['Error occurred:', error],
370+
};
371+
372+
sentryReporter.log(logObj);
373+
374+
const captureCall = vi.mocked(_INTERNAL_captureLog).mock.calls[0][0];
375+
expect(captureCall.level).toBe('error');
376+
expect(captureCall.message).toBe('Error occurred:');
377+
expect(captureCall.attributes['sentry.origin']).toBe('auto.log.consola');
378+
expect(captureCall.attributes['consola.args.0']).toBe(error);
379+
});
380+
381+
it('should store RegExp objects as context attributes', () => {
382+
const pattern = /test/gi;
383+
const logObj = {
384+
type: 'info',
385+
args: ['Pattern:', pattern],
386+
};
387+
388+
sentryReporter.log(logObj);
389+
390+
const captureCall = vi.mocked(_INTERNAL_captureLog).mock.calls[0][0];
391+
expect(captureCall.level).toBe('info');
392+
expect(captureCall.message).toBe('Pattern:');
393+
expect(captureCall.attributes['sentry.origin']).toBe('auto.log.consola');
394+
expect(captureCall.attributes['consola.args.0']).toBe(pattern);
395+
});
396+
397+
it('should store Map and Set objects as context attributes', () => {
398+
const map = new Map([
399+
['key', 'value'],
400+
['foo', 'bar'],
401+
]);
402+
const set = new Set([1, 2, 3]);
403+
const logObj = {
404+
type: 'info',
405+
args: ['Collections:', map, set],
406+
};
407+
408+
sentryReporter.log(logObj);
409+
410+
const captureCall = vi.mocked(_INTERNAL_captureLog).mock.calls[0][0];
411+
expect(captureCall.level).toBe('info');
412+
expect(captureCall.message).toBe('Collections:');
413+
expect(captureCall.attributes['sentry.origin']).toBe('auto.log.consola');
414+
// Map should be converted to plain object
415+
expect(captureCall.attributes['consola.args.0']).toEqual({ key: 'value', foo: 'bar' });
416+
// Set should be converted to array
417+
expect(captureCall.attributes['consola.args.1']).toEqual([1, 2, 3]);
418+
});
419+
420+
it('should only extract properties from plain objects', () => {
421+
const plainObj = { userId: 123, name: 'test' };
422+
const error = new Error('test');
423+
const logObj = {
424+
type: 'info',
425+
args: ['Mixed:', plainObj, error],
426+
};
427+
428+
sentryReporter.log(logObj);
429+
430+
const captureCall = vi.mocked(_INTERNAL_captureLog).mock.calls[0][0];
431+
expect(captureCall.level).toBe('info');
432+
expect(captureCall.message).toBe('Mixed:');
433+
// Plain object properties should be extracted
434+
expect(captureCall.attributes.userId).toBe(123);
435+
expect(captureCall.attributes.name).toBe('test');
436+
// Error should NOT have properties extracted (stored as context instead)
437+
expect(captureCall.attributes.message).toBeUndefined();
438+
expect(captureCall.attributes['consola.args.0']).toBe(error);
439+
});
440+
349441
it('should map consola levels to sentry levels when type is not provided', () => {
350442
const logObj = {
351443
level: 0, // Fatal level

0 commit comments

Comments
 (0)