Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/cli-docs/src/fragments/commands/log.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions packages/cli/src/lib/formatters/log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -141,15 +143,15 @@ 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)
: formatSeverityLabel(log.severity);
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];
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -271,7 +273,7 @@ export function formatLogTable(
renderInlineMarkdown(c),
),
);
return renderTextTable(headers, rows);
return renderTextTable(headers, rows, { shrinkable: [false] });
}

/**
Expand Down
55 changes: 48 additions & 7 deletions packages/cli/test/lib/formatters/log.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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}/);
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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", () => {
Expand All @@ -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", () => {
Expand Down
Loading