Skip to content

Commit d262a03

Browse files
committed
refactor(formatters): extract mdRow/mdKvTable helpers, simplify column defs
- Add mdRow() helper: shared streaming row renderer used by both formatLogRow and formatTraceRow (eliminates duplicated isPlainOutput branching) - Add mdKvTable() helper: builds key-value detail tables from [label, value] tuples (replaces manual string concatenation in formatLogDetails sections) - Simplify mdTableHeader() column alignment: use ':' suffix convention (e.g. 'Duration:') instead of [name, 'right'] tuples - Make formatLogsHeader/formatTracesHeader consistent: both modes now emit markdown table rows via mdRow() instead of diverging formats
1 parent 7a63c0e commit d262a03

File tree

5 files changed

+101
-106
lines changed

5 files changed

+101
-106
lines changed

src/lib/formatters/log.ts

Lines changed: 35 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import { cyan, muted, red, yellow } from "./colors.js";
1010
import {
1111
divider,
1212
escapeMarkdownCell,
13-
isPlainOutput,
13+
mdKvTable,
14+
mdRow,
1415
mdTableHeader,
15-
renderInlineMarkdown,
1616
renderMarkdown,
1717
} from "./markdown.js";
1818

@@ -77,13 +77,7 @@ export function formatLogRow(log: SentryLog): string {
7777
const level = `**${(log.severity ?? "info").toUpperCase()}**`;
7878
const message = escapeMarkdownCell(log.message ?? "");
7979
const trace = log.trace ? ` \`[${log.trace.slice(0, 8)}]\`` : "";
80-
const cells = [timestamp, level, `${message}${trace}`];
81-
82-
if (isPlainOutput()) {
83-
return `| ${cells.join(" | ")} |\n`;
84-
}
85-
86-
return `| ${cells.map((c) => renderInlineMarkdown(c)).join(" | ")} |\n`;
80+
return mdRow([timestamp, level, `${message}${trace}`]);
8781
}
8882

8983
/**
@@ -95,13 +89,7 @@ export function formatLogRow(log: SentryLog): string {
9589
* @returns Header string (includes trailing newline)
9690
*/
9791
export function formatLogsHeader(): string {
98-
if (isPlainOutput()) {
99-
return `${mdTableHeader(LOG_TABLE_COLS)}\n`;
100-
}
101-
const header = renderInlineMarkdown(
102-
LOG_TABLE_COLS.map((c) => `**${c}**`).join(" ")
103-
);
104-
return `${header}\n${divider(80)}\n`;
92+
return `${mdRow(LOG_TABLE_COLS.map((c) => `**${c}**`))}${divider(80)}\n`;
10593
}
10694

10795
/**
@@ -158,14 +146,13 @@ export function formatLogDetails(
158146
lines.push("");
159147

160148
// Core fields table
161-
const rows: string[] = [];
162-
rows.push(`| **ID** | \`${logId}\` |`);
163-
rows.push(`| **Timestamp** | ${formatTimestamp(log.timestamp)} |`);
164-
rows.push(`| **Severity** | ${formatSeverityLabel(log.severity)} |`);
165-
166-
lines.push("| | |");
167-
lines.push("|---|---|");
168-
lines.push(...rows);
149+
lines.push(
150+
mdKvTable([
151+
["ID", `\`${logId}\``],
152+
["Timestamp", formatTimestamp(log.timestamp)],
153+
["Severity", formatSeverityLabel(log.severity)],
154+
])
155+
);
169156

170157
if (log.message) {
171158
lines.push("");
@@ -176,101 +163,80 @@ export function formatLogDetails(
176163

177164
// Context section
178165
if (log.project || log.environment || log.release) {
179-
lines.push("");
180-
lines.push("### Context");
181-
lines.push("");
182-
const ctxRows: string[] = [];
166+
const ctxRows: [string, string][] = [];
183167
if (log.project) {
184-
ctxRows.push(`| **Project** | ${log.project} |`);
168+
ctxRows.push(["Project", log.project]);
185169
}
186170
if (log.environment) {
187-
ctxRows.push(`| **Environment** | ${log.environment} |`);
171+
ctxRows.push(["Environment", log.environment]);
188172
}
189173
if (log.release) {
190-
ctxRows.push(`| **Release** | ${log.release} |`);
174+
ctxRows.push(["Release", log.release]);
191175
}
192-
lines.push("| | |");
193-
lines.push("|---|---|");
194-
lines.push(...ctxRows);
176+
lines.push("");
177+
lines.push(mdKvTable(ctxRows, "Context"));
195178
}
196179

197180
// SDK section
198181
const sdkName = log["sdk.name"];
199182
const sdkVersion = log["sdk.version"];
200183
if (sdkName || sdkVersion) {
201-
lines.push("");
202-
lines.push("### SDK");
203-
lines.push("");
204184
// Wrap in backticks to prevent markdown from interpreting underscores/dashes
205185
const sdkInfo =
206186
sdkName && sdkVersion
207187
? `\`${sdkName} ${sdkVersion}\``
208188
: `\`${sdkName ?? sdkVersion}\``;
209-
lines.push("| | |");
210-
lines.push("|---|---|");
211-
lines.push(`| **SDK** | ${sdkInfo} |`);
189+
lines.push("");
190+
lines.push(mdKvTable([["SDK", sdkInfo]], "SDK"));
212191
}
213192

214193
// Trace section
215194
if (log.trace) {
216-
lines.push("");
217-
lines.push("### Trace");
218-
lines.push("");
219-
const traceRows: string[] = [];
220-
traceRows.push(`| **Trace ID** | \`${log.trace}\` |`);
195+
const traceRows: [string, string][] = [["Trace ID", `\`${log.trace}\``]];
221196
if (log.span_id) {
222-
traceRows.push(`| **Span ID** | \`${log.span_id}\` |`);
197+
traceRows.push(["Span ID", `\`${log.span_id}\``]);
223198
}
224-
traceRows.push(`| **Link** | ${buildTraceUrl(orgSlug, log.trace)} |`);
225-
lines.push("| | |");
226-
lines.push("|---|---|");
227-
lines.push(...traceRows);
199+
traceRows.push(["Link", buildTraceUrl(orgSlug, log.trace)]);
200+
lines.push("");
201+
lines.push(mdKvTable(traceRows, "Trace"));
228202
}
229203

230204
// Source location section (OTel code attributes)
231205
const codeFunction = log["code.function"];
232206
const codeFilePath = log["code.file.path"];
233207
const codeLineNumber = log["code.line.number"];
234208
if (codeFunction || codeFilePath) {
235-
lines.push("");
236-
lines.push("### Source Location");
237-
lines.push("");
238-
const srcRows: string[] = [];
209+
const srcRows: [string, string][] = [];
239210
if (codeFunction) {
240-
srcRows.push(`| **Function** | \`${codeFunction}\` |`);
211+
srcRows.push(["Function", `\`${codeFunction}\``]);
241212
}
242213
if (codeFilePath) {
243214
const location = codeLineNumber
244215
? `${codeFilePath}:${codeLineNumber}`
245216
: codeFilePath;
246-
srcRows.push(`| **File** | \`${location}\` |`);
217+
srcRows.push(["File", `\`${location}\``]);
247218
}
248-
lines.push("| | |");
249-
lines.push("|---|---|");
250-
lines.push(...srcRows);
219+
lines.push("");
220+
lines.push(mdKvTable(srcRows, "Source Location"));
251221
}
252222

253223
// OpenTelemetry section
254224
const otelKind = log["sentry.otel.kind"];
255225
const otelStatus = log["sentry.otel.status_code"];
256226
const otelScope = log["sentry.otel.instrumentation_scope.name"];
257227
if (otelKind || otelStatus || otelScope) {
258-
lines.push("");
259-
lines.push("### OpenTelemetry");
260-
lines.push("");
261-
const otelRows: string[] = [];
228+
const otelRows: [string, string][] = [];
262229
if (otelKind) {
263-
otelRows.push(`| **Kind** | ${otelKind} |`);
230+
otelRows.push(["Kind", otelKind]);
264231
}
265232
if (otelStatus) {
266-
otelRows.push(`| **Status** | ${otelStatus} |`);
233+
otelRows.push(["Status", otelStatus]);
267234
}
268235
if (otelScope) {
269-
otelRows.push(`| **Scope** | ${otelScope} |`);
236+
otelRows.push(["Scope", otelScope]);
270237
}
271-
lines.push("| | |");
272-
lines.push("|---|---|");
273-
lines.push(...otelRows);
238+
lines.push("");
239+
lines.push(mdKvTable(otelRows, "OpenTelemetry"));
274240
}
275241

276242
return renderMarkdown(lines.join("\n"));

src/lib/formatters/markdown.ts

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -129,21 +129,67 @@ export function escapeMarkdownCell(value: string): string {
129129
}
130130

131131
/**
132-
* Build a markdown table header row + separator from column definitions.
132+
* Build a raw markdown table header row + separator from column names.
133133
*
134-
* @param cols - Column definitions: `[name]` or `[name, "right"]` for right-align
134+
* Column names ending with `:` are right-aligned (the `:` is stripped from
135+
* the displayed name and a `---:` separator is emitted instead of `---`).
136+
*
137+
* Used by batch-rendered tables that pipe the result through `renderMarkdown()`.
138+
* For streaming table rows use {@link mdRow}.
139+
*
140+
* @param cols - Column names (append `:` for right-align, e.g. `"Duration:"`)
135141
* @returns Two-line string: `| A | B |\n| --- | ---: |`
136142
*/
137-
export function mdTableHeader(
138-
cols: ReadonlyArray<string | readonly [string, "right"]>
139-
): string {
140-
const names = cols.map((c) => (typeof c === "string" ? c : c[0]));
141-
const seps = cols.map((c) =>
142-
typeof c !== "string" && c[1] === "right" ? "---:" : "---"
143-
);
143+
export function mdTableHeader(cols: readonly string[]): string {
144+
const names = cols.map((c) => (c.endsWith(":") ? c.slice(0, -1) : c));
145+
const seps = cols.map((c) => (c.endsWith(":") ? "---:" : "---"));
144146
return `| ${names.join(" | ")} |\n| ${seps.join(" | ")} |`;
145147
}
146148

149+
/**
150+
* Build a markdown table row from cell values.
151+
*
152+
* In plain mode the cells are emitted as-is (raw CommonMark).
153+
* In rendered mode each cell is passed through `renderInlineMarkdown()`
154+
* so inline constructs like `**bold**` and `` `code` `` become ANSI-styled.
155+
*
156+
* @param cells - Cell values (may contain inline markdown)
157+
* @returns `| a | b |\n`
158+
*/
159+
export function mdRow(cells: readonly string[]): string {
160+
const out = isPlainOutput()
161+
? cells
162+
: cells.map((c) => renderInlineMarkdown(c));
163+
return `| ${out.join(" | ")} |\n`;
164+
}
165+
166+
/**
167+
* Build a key-value markdown table section with an optional heading.
168+
*
169+
* Each entry is rendered as `| **Label** | value |`.
170+
* Uses the blank-header-row pattern required by marked-terminal.
171+
*
172+
* @param rows - `[label, value]` tuples
173+
* @param heading - Optional `### Heading` text (omit the `###` prefix)
174+
* @returns Raw markdown string (not rendered)
175+
*/
176+
export function mdKvTable(
177+
rows: ReadonlyArray<readonly [string, string]>,
178+
heading?: string
179+
): string {
180+
const lines: string[] = [];
181+
if (heading) {
182+
lines.push(`### ${heading}`);
183+
lines.push("");
184+
}
185+
lines.push("| | |");
186+
lines.push("|---|---|");
187+
for (const [label, value] of rows) {
188+
lines.push(`| **${label}** | ${value} |`);
189+
}
190+
return lines.join("\n");
191+
}
192+
147193
/**
148194
* Render a muted horizontal rule for streaming header separators.
149195
*

src/lib/formatters/trace.ts

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ import { formatRelativeTime } from "./human.js";
99
import {
1010
divider,
1111
escapeMarkdownCell,
12-
isPlainOutput,
12+
mdRow,
1313
mdTableHeader,
14-
renderInlineMarkdown,
1514
renderMarkdown,
1615
} from "./markdown.js";
1716

@@ -47,13 +46,8 @@ export function formatTraceDuration(ms: number): string {
4746
return `${mins}m ${secs}s`;
4847
}
4948

50-
/** Column headers for the streaming trace table (Duration is right-aligned) */
51-
const TRACE_TABLE_COLS = [
52-
"Trace ID",
53-
"Transaction",
54-
["Duration", "right"] as const,
55-
"When",
56-
] as const;
49+
/** Column headers for the streaming trace table (`:` suffix = right-aligned) */
50+
const TRACE_TABLE_COLS = ["Trace ID", "Transaction", "Duration:", "When"];
5751

5852
/**
5953
* Format column header for traces list (used before per-row output).
@@ -64,15 +58,10 @@ const TRACE_TABLE_COLS = [
6458
* @returns Header string (includes trailing newline)
6559
*/
6660
export function formatTracesHeader(): string {
67-
if (isPlainOutput()) {
68-
return `${mdTableHeader(TRACE_TABLE_COLS)}\n`;
69-
}
70-
const header = renderInlineMarkdown(
71-
TRACE_TABLE_COLS.map((c) => `**${typeof c === "string" ? c : c[0]}**`).join(
72-
" "
73-
)
61+
const names = TRACE_TABLE_COLS.map((c) =>
62+
c.endsWith(":") ? c.slice(0, -1) : c
7463
);
75-
return `${header}\n${divider(96)}\n`;
64+
return `${mdRow(names.map((n) => `**${n}**`))}${divider(96)}\n`;
7665
}
7766

7867
/**
@@ -90,13 +79,7 @@ export function formatTraceRow(item: TransactionListItem): string {
9079
const transaction = escapeMarkdownCell(item.transaction || "unknown");
9180
const duration = formatTraceDuration(item["transaction.duration"]);
9281
const when = formatRelativeTime(item.timestamp).trim();
93-
const cells = [traceId, transaction, duration, when];
94-
95-
if (isPlainOutput()) {
96-
return `| ${cells.join(" | ")} |\n`;
97-
}
98-
99-
return `| ${cells.map((c) => renderInlineMarkdown(c)).join(" | ")} |\n`;
82+
return mdRow([traceId, transaction, duration, when]);
10083
}
10184

10285
/**

test/lib/formatters/log.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,7 @@ describe("formatLogsHeader (plain mode)", () => {
224224

225225
test("emits markdown table header and separator", () => {
226226
const result = formatLogsHeader();
227-
expect(result).toContain("| Timestamp | Level | Message |");
228-
expect(result).toContain("| --- | --- | --- |");
227+
expect(result).toContain("| **Timestamp** | **Level** | **Message** |");
229228
});
230229

231230
test("ends with newline", () => {

test/lib/formatters/trace.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,9 @@ describe("formatTracesHeader (plain mode)", () => {
187187

188188
test("emits markdown table header and separator", () => {
189189
const result = formatTracesHeader();
190-
expect(result).toContain("| Trace ID | Transaction | Duration | When |");
191-
expect(result).toContain("| --- | --- | ---: | --- |");
190+
expect(result).toContain(
191+
"| **Trace ID** | **Transaction** | **Duration** | **When** |"
192+
);
192193
});
193194

194195
test("ends with newline", () => {

0 commit comments

Comments
 (0)