From 60ebc5fab0b3762e8a9df7fb30fad09944d4b7e4 Mon Sep 17 00:00:00 2001 From: betegon Date: Fri, 9 Oct 2026 10:08:36 +0200 Subject: [PATCH] fix(cli): preserve full log IDs in human output --- apps/cli-docs/src/fragments/commands/log.md | 4 ++ packages/cli/src/lib/formatters/log.ts | 10 ++-- packages/cli/test/lib/formatters/log.test.ts | 55 +++++++++++++++++--- 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/apps/cli-docs/src/fragments/commands/log.md b/apps/cli-docs/src/fragments/commands/log.md index 5a6fbefb2..aa9356b7f 100644 --- a/apps/cli-docs/src/fragments/commands/log.md +++ b/apps/cli-docs/src/fragments/commands/log.md @@ -46,6 +46,10 @@ sentry log list my-org/backend -f -q 'severity:error' ### View a log entry +Copy the full ID from the `ID` column in `sentry log list` or `sentry trace logs` +into `sentry log view`. An abbreviated prefix can match multiple logs and cannot +be expanded into a unique full ID by itself. + Log ID lookups automatically narrow the search when a usable timestamp can be determined from each ID. Otherwise, they use a 90-day lookup window. If a partial scan leaves IDs missing, the CLI retries those IDs once with the highest diff --git a/packages/cli/src/lib/formatters/log.ts b/packages/cli/src/lib/formatters/log.ts index ba351d230..b7d3040b5 100644 --- a/packages/cli/src/lib/formatters/log.ts +++ b/packages/cli/src/lib/formatters/log.ts @@ -119,6 +119,8 @@ function formatTimestamp(timestamp: string): string { /** * Extract cell values for a log row (shared by streaming and batch paths). + * Preserve the full log ID for copying into `log view`: UUIDv7 timestamp + * prefixes can identify many logs rather than a single entry. * * When `includeTrace` is true (the default), a short trace-ID suffix is * appended to the message cell — useful in Explore/Events lists where rows @@ -141,7 +143,7 @@ export function buildLogRowCells( extraFields?: string[], ): string[] { const logId = getLogId(log); - const shortId = logId ? colorTag("muted", logId.slice(0, 8)) : ""; + const id = logId ? colorTag("muted", logId) : ""; const timestamp = formatTimestamp(log.timestamp); const level = padSeverity ? formatSeverity(log.severity) @@ -149,7 +151,7 @@ export function buildLogRowCells( const message = escapeMarkdownCell(log.message ?? ""); const trace = includeTrace && log.trace ? ` \`[${log.trace.slice(0, 8)}]\`` : ""; - const cells: string[] = [shortId, timestamp, level, `${message}${trace}`]; + const cells: string[] = [id, timestamp, level, `${message}${trace}`]; if (extraFields) { for (const field of extraFields) { const val = log[field]; @@ -183,7 +185,7 @@ export function formatLogRow( /** Hint rows for column width estimation in streaming mode. */ const LOG_HINT_ROWS: string[][] = [ [ - "ace106b2", + "019a0000123470008000000000000001", "2026-01-15 23:59:59", "WARNING", "A typical log message with some detail", @@ -271,7 +273,7 @@ export function formatLogTable( renderInlineMarkdown(c), ), ); - return renderTextTable(headers, rows); + return renderTextTable(headers, rows, { shrinkable: [false] }); } /** diff --git a/packages/cli/test/lib/formatters/log.test.ts b/packages/cli/test/lib/formatters/log.test.ts index b6c811b0a..ccdfa40ef 100644 --- a/packages/cli/test/lib/formatters/log.test.ts +++ b/packages/cli/test/lib/formatters/log.test.ts @@ -11,7 +11,9 @@ import { formatLogsHeader, formatLogTable, getLogId, + type LogLike, } from "../../../src/lib/formatters/log.js"; +import { renderInlineMarkdown } from "../../../src/lib/formatters/markdown.js"; import type { DetailedSentryLog, SentryLog, @@ -226,13 +228,13 @@ describe("formatLogRow (plain mode)", () => { expect(result).toMatch(/^\|.+\|.+\|.+\|.+\|\n$/); }); - test("contains log ID prefix, timestamp, severity, message", () => { + test("contains full log ID, timestamp, severity, message", () => { const log = createTestLog({ severity: "error", message: "connection failed", }); const result = formatLogRow(log); - expect(result).toContain("test-id-"); // first 8 chars of "test-id-123" + expect(result).toContain("test-id-123"); expect(result).toContain("connection failed"); expect(result).toContain("ERROR"); expect(result).toMatch(/\d{4}-\d{2}-\d{2}/); @@ -269,6 +271,47 @@ describe("formatLogRow (plain mode)", () => { }); }); +describe.each(["plain", "rendered"])("copyable log IDs (%s mode)", (mode) => { + if (mode === "plain") { + usePlainMode(); + } else { + useRenderedMode(); + } + + test.each(["sentry.item_id", "id"])( + "preserves distinct UUIDv7 IDs from %s that share a timestamp prefix", + (idField) => { + const ids = [ + "019a0000123470008000000000000001", + "019a0000123470008000000000000002", + ]; + const logs: LogLike[] = ids.map((id) => ({ + [idField]: id, + timestamp: "2025-01-30T14:32:15Z", + severity: "info", + message: + "A log message long enough to require wrapping in a narrow terminal", + })); + const table = createLogStreamingTable({ maxWidth: 80 }); + const outputs = [ + logs.map((log) => formatLogRow(log)).join(""), + formatLogTable(logs), + logs + .map((log) => + table.row(buildLogRowCells(log).map(renderInlineMarkdown)), + ) + .join(""), + ]; + + for (const output of outputs) { + for (const id of ids) { + expect(stripAnsi(output)).toContain(id); + } + } + }, + ); +}); + describe("formatLogsHeader (plain mode)", () => { usePlainMode(); @@ -569,11 +612,10 @@ describe("buildLogRowCells", () => { expect(cells.length).toBe(4); }); - test("first cell contains short log ID", () => { + test("first cell contains full log ID", () => { const log = createTestLog(); const cells = buildLogRowCells(log); - // First 8 chars of "test-id-123" - expect(stripAnsi(cells[0])).toContain("test-id-"); + expect(stripAnsi(cells[0])).toContain("test-id-123"); }); test("appends extra field cells when provided", () => { @@ -599,8 +641,7 @@ describe("formatLogTable", () => { test("includes log ID in output", () => { const result = stripAnsi(formatLogTable([createTestLog()])); - // First 8 chars of "test-id-123" - expect(result).toContain("test-id-"); + expect(result).toContain("test-id-123"); }); test("includes all log messages", () => {