From 8db64de8df5d6fd1088e7e2d77272300862dff16 Mon Sep 17 00:00:00 2001 From: "sentry-junior[bot]" <264270552+sentry-junior[bot]@users.noreply.github.com> Date: Thu, 22 Oct 2026 18:25:26 +0000 Subject: [PATCH] fix(cli): Handle malformed sourcemaps and timestamps - `sentry sourcemap inject` crashed with a bare SyntaxError when a companion .map file was not valid JSON. It now throws a ValidationError that names the file, before touching the JS bundle. The dry-run path already tolerated this. - formatRelativeTime rendered the literal text "Invalid Date" for an unparseable timestamp. It now shows the same muted dash as a missing one. Ported from getsentry/cli#1118 and getsentry/cli#1200. Co-Authored-By: BYK --- packages/cli/src/lib/formatters/time-utils.ts | 6 +++++- packages/cli/src/lib/sourcemap/debug-id.ts | 12 +++++++++++- .../cli/test/lib/formatters/human.utils.test.ts | 4 ++++ packages/cli/test/script/debug-id.test.ts | 14 ++++++++++++++ 4 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/lib/formatters/time-utils.ts b/packages/cli/src/lib/formatters/time-utils.ts index f7771ca50..b06954306 100644 --- a/packages/cli/src/lib/formatters/time-utils.ts +++ b/packages/cli/src/lib/formatters/time-utils.ts @@ -19,7 +19,7 @@ import { colorTag } from "./markdown.js"; * - Under 3 days: "2d ago" * - Otherwise: short date like "Jan 18" * - * Returns a muted "—" when the input is undefined. + * Returns a muted "—" when the input is undefined or not a valid date. * * @param dateString - ISO date string or undefined * @returns Human-readable relative time string @@ -30,6 +30,10 @@ export function formatRelativeTime(dateString: string | undefined): string { } const date = new Date(dateString); + // An unparseable timestamp would otherwise render as "Invalid Date". + if (Number.isNaN(date.getTime())) { + return colorTag("muted", "—"); + } const now = Date.now(); // Clamp to >= 0 so clock skew, scheduled/future timestamps, or bad API data // render as "0m ago" instead of a negative duration like "-5m ago". diff --git a/packages/cli/src/lib/sourcemap/debug-id.ts b/packages/cli/src/lib/sourcemap/debug-id.ts index 8864712f4..54dcd5e53 100644 --- a/packages/cli/src/lib/sourcemap/debug-id.ts +++ b/packages/cli/src/lib/sourcemap/debug-id.ts @@ -21,6 +21,7 @@ import { createHash } from "node:crypto"; import { readFile, writeFile } from "node:fs/promises"; +import { ValidationError } from "../errors.js"; import { logger } from "../logger.js"; import { isLikelyBinary } from "../scan/binary.js"; import { @@ -266,7 +267,16 @@ export async function injectDebugId( return { debugId: jsDebugId, wasInjected: false }; } - const map = JSON.parse(mapContent) as SourcemapJson; + let map: SourcemapJson; + try { + map = JSON.parse(mapContent) as SourcemapJson; + } catch (error) { + log.debug(`Failed to parse sourcemap ${mapPath}`, error); + throw new ValidationError( + `Sourcemap '${mapPath}' is not valid JSON.`, + "sourcemap", + ); + } const existingId = jsDebugId ?? readSourcemapDebugId(map); if ( existingId && diff --git a/packages/cli/test/lib/formatters/human.utils.test.ts b/packages/cli/test/lib/formatters/human.utils.test.ts index fa80683c3..c70c86cf8 100644 --- a/packages/cli/test/lib/formatters/human.utils.test.ts +++ b/packages/cli/test/lib/formatters/human.utils.test.ts @@ -99,6 +99,10 @@ describe("formatRelativeTime", () => { expect(stripAnsi(result).trim()).toBe("—"); }); + test("returns em-dash for an unparseable date", () => { + expect(stripAnsi(formatRelativeTime("not-a-date")).trim()).toBe("—"); + }); + test("formats minutes ago for recent times", () => { const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000).toISOString(); const result = stripAnsi(formatRelativeTime(fiveMinutesAgo)); diff --git a/packages/cli/test/script/debug-id.test.ts b/packages/cli/test/script/debug-id.test.ts index 84c3e1e1f..e4733d2c2 100644 --- a/packages/cli/test/script/debug-id.test.ts +++ b/packages/cli/test/script/debug-id.test.ts @@ -145,6 +145,20 @@ describe("injectDebugId", () => { expect(mapResult.debug_id).toBe(debugId); }); + test("rejects a malformed sourcemap with a ValidationError", async () => { + const jsPath = join(tmpDir, "bundle.js"); + const mapPath = join(tmpDir, "bundle.js.map"); + + await writeFile(jsPath, 'console.log("hello");\n'); + await writeFile(mapPath, "{ not json"); + + await expect(injectDebugId(jsPath, mapPath)).rejects.toThrow( + `Sourcemap '${mapPath}' is not valid JSON.`, + ); + // The JS file must not be touched when the map cannot be parsed. + expect(await readFile(jsPath, "utf-8")).toBe('console.log("hello");\n'); + }); + test("prepends one ; to sourcemap mappings", async () => { const jsPath = join(tmpDir, "bundle.js"); const mapPath = join(tmpDir, "bundle.js.map");