From 7d02e07a03c5c36fe5270b281d3df015ee526c36 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:10:54 +0000 Subject: [PATCH] fix(logger): Keep falsy causes and literal % in error messages - Render an error's cause whenever the error has one, including `0`, `""`, `false`, `null`, and `undefined`, as `util.inspect` does. Only truthy causes were rendered. - An error passed as the first argument is no longer read as a format string, so `%s` in its message stays literal instead of consuming the next argument, matching `console.log`. - Require Craft 3.5.0, the first release that resolves the workspace placeholders the logger's release config uses. The release workflow already runs 3.5.0; older versions would leave them unresolved. Co-Authored-By: BYK <126780+BYK@users.noreply.github.com> --- .craft.yml | 2 +- packages/logger/src/message.ts | 11 +++++++---- packages/logger/test/logger.test.ts | 27 +++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/.craft.yml b/.craft.yml index cd6d29968..0150c2590 100644 --- a/.craft.yml +++ b/.craft.yml @@ -1,4 +1,4 @@ -minVersion: "2.33.0" +minVersion: "3.5.0" workspaces: packages/cli: changelog: diff --git a/packages/logger/src/message.ts b/packages/logger/src/message.ts index 3cd65f4a2..2577d2aaa 100644 --- a/packages/logger/src/message.ts +++ b/packages/logger/src/message.ts @@ -31,7 +31,7 @@ function formatError(error: ErrorLike, depth: number, stack: boolean): string { let cause = ""; if (isErrorLike(error.cause)) { cause = `${stack ? "\n\n" : " "}${formatError(error.cause, depth + 1, stack)}`; - } else if (error.cause) { + } else if ("cause" in error) { cause = `${stack ? `\n\n${" ".repeat(depth + 1)}` : " "}[cause]: ${formatWithOptions(INSPECT_OPTIONS, error.cause)}`; } if (!stack) { @@ -57,8 +57,11 @@ export function formatMessage( args: readonly unknown[], { stack }: { stack: boolean }, ): string { - return formatWithOptions( - INSPECT_OPTIONS, - ...args.map((arg) => (isErrorLike(arg) ? formatError(arg, 0, stack) : arg)), + const formatted = args.map((arg) => + isErrorLike(arg) ? formatError(arg, 0, stack) : arg, ); + // An error is not a format string, so `%s` in its message stays literal. + return isErrorLike(args[0]) + ? formatWithOptions(INSPECT_OPTIONS, "%s", ...formatted) + : formatWithOptions(INSPECT_OPTIONS, ...formatted); } diff --git a/packages/logger/test/logger.test.ts b/packages/logger/test/logger.test.ts index 0813f24c2..ea0c3c8b8 100644 --- a/packages/logger/test/logger.test.ts +++ b/packages/logger/test/logger.test.ts @@ -468,4 +468,31 @@ describe("sentryReporter", () => { "Upload failed: Request failed [cause]: socket hang up", ); }); + + test("keeps causes that are falsy", () => { + const { calls, sentryLogger } = fakeSentryLogger(); + const logger = createLogger({ reporters: [sentryReporter(sentryLogger)] }); + for (const cause of [0, "", false, null, undefined]) { + logger.error(new Error("Failed", { cause })); + } + logger.error(new Error("No cause")); + expect(calls.map(([, message]) => message)).toEqual([ + "Failed [cause]: 0", + "Failed [cause]: ", + "Failed [cause]: false", + "Failed [cause]: null", + "Failed [cause]: undefined", + "No cause", + ]); + }); + + test("does not read an error's message as a format string", () => { + const { calls, sentryLogger } = fakeSentryLogger(); + createLogger({ reporters: [sentryReporter(sentryLogger)] }).error( + new Error("Bad value %s"), + "extra", + { id: 1 }, + ); + expect(calls[0]?.[1]).toBe("Bad value %s extra { id: 1 }"); + }); });