🧹 fix: Preserve Unconsumed Primitive Splat Args in Debug Traversal

The previous round dropped every primitive SPLAT[0] value to avoid
duplicating consumed %s args, but that removed useful context from
calls like \`logger.debug('prefix:', detail)\` where the primitive was
never interpolated — users lost the \`detail\` value.

Refine the heuristic: skip a primitive splat value only when it already
appears inside the (post-interpolation) \`info.message\`; otherwise
surface it. Arrays and objects continue to surface unconditionally.

Regression test covers the 'prefix:', detail case.

Reviewed-by: Codex (P2 finding on PR #12737, commit 6bf9548f).
This commit is contained in:
Danny Avila 2026-04-18 12:09:11 -04:00
parent 6bf9548ff2
commit c29c18e8a1
2 changed files with 34 additions and 8 deletions

View file

@ -256,6 +256,17 @@ describe('debugTraverse', () => {
expect(tenantMatches.length).toBe(1);
});
it('surfaces unconsumed primitive SPLAT[0] (no %s in message) for debug level', () => {
const info = {
level: 'debug',
message: 'prefix:',
timestamp: 'ts',
[SPLAT_SYMBOL]: ['detailValueXYZ'],
};
const out = runFormatter(info);
expect(out).toContain('detailValueXYZ');
});
it('still surfaces array metadata in SPLAT[0] when no object is extracted', () => {
const info = {
level: 'debug',

View file

@ -198,17 +198,32 @@ const debugTraverse = winston.format.printf(({ level, message, timestamp, ...met
/*
* Prefer the structured metadata object (which winston merges into info)
* over `SPLAT_SYMBOL[0]`. The first splat entry may be a *consumed*
* printf arg e.g. `logger.warn('failed for %s', tenant)` leaves
* `tenant` in `SPLAT[0]` after interpolation, and appending it again
* would emit `failed for tenant tenant`. Only fall back to the splat
* entry when it's a non-primitive (array or plain object); a string
* or number there is almost certainly a consumed %s/%d arg.
* over `SPLAT_SYMBOL[0]`. For primitive splat values, skip only if the
* value already appears in the interpolated message (i.e. `%s`/`%d`
* consumed it) `logger.warn('failed for %s', tenant)` leaves `tenant`
* in `SPLAT[0]` after interpolation and would otherwise be appended a
* second time. Unconsumed primitives like
* `logger.debug('prefix:', detail)` still surface.
*/
const extracted = extractMetaObject(metadata);
const splatFirst = metadata[SPLAT_SYMBOL]?.[0];
const splatUsable =
Array.isArray(splatFirst) || (splatFirst != null && typeof splatFirst === 'object');
const splatUsable = (() => {
if (splatFirst == null) {
return false;
}
if (Array.isArray(splatFirst) || typeof splatFirst === 'object') {
return true;
}
if (
typeof splatFirst === 'string' ||
typeof splatFirst === 'number' ||
typeof splatFirst === 'boolean'
) {
const splatStr = String(splatFirst);
return splatStr !== '' && !message.includes(splatStr);
}
return false;
})();
const debugValue = extracted ?? (splatUsable ? splatFirst : undefined);
if (!debugValue) {