Skip to content

Commit d0dd84e

Browse files
betegonclaude
andcommitted
test(log): add coverage for custom attributes, detail fetching, and getSpanDetails
Covers the three uncovered areas: - formatLogDetails: custom attributes rendering, REDUNDANT_LOG_DETAIL_ATTRS filtering, --fields filter, fallback path (extraFields without allAttributes), and empty allAttributes - view.func: getLogItemDetail called for traced logs, skipped for traceless logs, graceful degradation on failure, custom attributes appear in output - traces: getSpanDetails delegates to trace-items endpoint with item_type=spans Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 3aa7aed commit d0dd84e

3 files changed

Lines changed: 237 additions & 2 deletions

File tree

test/commands/log/view.func.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,4 +372,72 @@ describe("viewCommand.func", () => {
372372
}
373373
});
374374
});
375+
376+
describe("detail attribute fetching", () => {
377+
test("calls getLogItemDetail for logs that have a trace", async () => {
378+
const log = makeSampleLog(ID1); // makeSampleLog sets trace: "abc123..."
379+
getLogsSpy.mockResolvedValue([log]);
380+
381+
const { context } = createMockContext();
382+
const func = await viewCommand.loader();
383+
await func.call(context, { json: false, web: false }, "my-org/proj", ID1);
384+
385+
expect(getLogItemDetailSpy).toHaveBeenCalledWith(
386+
"my-org",
387+
"proj",
388+
ID1,
389+
log.trace
390+
);
391+
});
392+
393+
test("does not call getLogItemDetail for logs without a trace", async () => {
394+
const log = makeSampleLog(ID1, "no trace log");
395+
log.trace = null;
396+
getLogsSpy.mockResolvedValue([log]);
397+
398+
const { context } = createMockContext();
399+
const func = await viewCommand.loader();
400+
await func.call(context, { json: false, web: false }, "my-org/proj", ID1);
401+
402+
expect(getLogItemDetailSpy).not.toHaveBeenCalled();
403+
});
404+
405+
test("still renders output when getLogItemDetail fails", async () => {
406+
const log = makeSampleLog(ID1);
407+
getLogsSpy.mockResolvedValue([log]);
408+
getLogItemDetailSpy.mockRejectedValue(new Error("network error"));
409+
410+
const { context, stdoutWrite } = createMockContext();
411+
const func = await viewCommand.loader();
412+
await func.call(context, { json: false, web: false }, "my-org/proj", ID1);
413+
414+
// Should still render the log with standard fields despite detail failure
415+
const output = stdoutWrite.mock.calls.map((c) => c[0]).join("");
416+
expect(output).toContain(ID1);
417+
});
418+
419+
test("renders custom attributes in human output when detail available", async () => {
420+
const log = makeSampleLog(ID1);
421+
getLogsSpy.mockResolvedValue([log]);
422+
getLogItemDetailSpy.mockResolvedValue({
423+
itemId: ID1,
424+
timestamp: log.timestamp,
425+
attributes: [
426+
{ name: "user.id", type: "str", value: "u_42" },
427+
{ name: "order.status", type: "str", value: "shipped" },
428+
],
429+
});
430+
431+
const { context, stdoutWrite } = createMockContext();
432+
const func = await viewCommand.loader();
433+
await func.call(context, { json: false, web: false }, "my-org/proj", ID1);
434+
435+
const output = stdoutWrite.mock.calls.map((c) => c[0]).join("");
436+
expect(output).toContain("Custom Attributes");
437+
expect(output).toContain("user.id");
438+
expect(output).toContain("u_42");
439+
expect(output).toContain("order.status");
440+
expect(output).toContain("shipped");
441+
});
442+
});
375443
});

test/lib/api/traces.test.ts

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@
66
*/
77

88
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
9-
import { listSpans, listTransactions } from "../../../src/lib/api/traces.js";
9+
import {
10+
getSpanDetails,
11+
listSpans,
12+
listTransactions,
13+
} from "../../../src/lib/api/traces.js";
1014
import { mockFetch, useTestConfigDir } from "../../helpers.js";
1115

1216
// ---------------------------------------------------------------------------
@@ -634,3 +638,80 @@ describe("listSpans", () => {
634638
expect(result.nextCursor).toBeUndefined();
635639
});
636640
});
641+
642+
// ---------------------------------------------------------------------------
643+
// getSpanDetails
644+
// ---------------------------------------------------------------------------
645+
646+
describe("getSpanDetails", () => {
647+
useTestConfigDir("traces-span-details-test-");
648+
649+
let originalFetch: typeof globalThis.fetch;
650+
let capturedUrl = "";
651+
let capturedParams: Record<string, string> = {};
652+
653+
beforeEach(() => {
654+
originalFetch = globalThis.fetch;
655+
capturedUrl = "";
656+
capturedParams = {};
657+
});
658+
659+
afterEach(() => {
660+
globalThis.fetch = originalFetch;
661+
});
662+
663+
function mockOk(body: unknown) {
664+
globalThis.fetch = mockFetch(async (input, init) => {
665+
const req = new Request(input!, init);
666+
capturedUrl = req.url;
667+
const url = new URL(capturedUrl);
668+
url.searchParams.forEach((v, k) => {
669+
capturedParams[k] = v;
670+
});
671+
return new Response(JSON.stringify(body), {
672+
status: 200,
673+
headers: { "Content-Type": "application/json" },
674+
});
675+
});
676+
}
677+
678+
const DETAIL_RESPONSE = {
679+
itemId: "abc123",
680+
timestamp: "2026-01-01T00:00:00Z",
681+
attributes: [
682+
{ name: "span.op", type: "str", value: "db.query" },
683+
{ name: "user.id", type: "str", value: "u_42" },
684+
],
685+
};
686+
687+
test("calls trace-items endpoint with item_type=spans", async () => {
688+
mockOk(DETAIL_RESPONSE);
689+
690+
await getSpanDetails("my-org", "my-project", "span-id-abc", "trace-id-xyz");
691+
692+
expect(capturedUrl).toContain(
693+
"/projects/my-org/my-project/trace-items/span-id-abc/"
694+
);
695+
expect(capturedParams.item_type).toBe("spans");
696+
expect(capturedParams.trace_id).toBe("trace-id-xyz");
697+
});
698+
699+
test("returns parsed attributes", async () => {
700+
mockOk(DETAIL_RESPONSE);
701+
702+
const result = await getSpanDetails(
703+
"my-org",
704+
"my-project",
705+
"span-id-abc",
706+
"trace-id-xyz"
707+
);
708+
709+
expect(result.itemId).toBe("abc123");
710+
expect(result.attributes).toHaveLength(2);
711+
expect(result.attributes[0]).toEqual({
712+
name: "span.op",
713+
type: "str",
714+
value: "db.query",
715+
});
716+
});
717+
});

test/lib/formatters/log.test.ts

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ import {
1212
formatLogTable,
1313
getLogId,
1414
} from "../../../src/lib/formatters/log.js";
15-
import type { DetailedSentryLog, SentryLog } from "../../../src/types/index.js";
15+
import type {
16+
DetailedSentryLog,
17+
SentryLog,
18+
TraceItemAttribute,
19+
} from "../../../src/types/index.js";
1620

1721
/** Force rendered (TTY) mode for a describe block */
1822
function useRenderedMode() {
@@ -441,6 +445,88 @@ describe("formatLogDetails", () => {
441445
expect(result).not.toContain("SDK");
442446
expect(result).not.toContain("Trace");
443447
});
448+
449+
describe("Custom Attributes section", () => {
450+
const customAttrs: TraceItemAttribute[] = [
451+
{ name: "user.id", type: "str", value: "u_42" },
452+
{ name: "order.total", type: "float", value: 99.9 },
453+
{ name: "retry.count", type: "int", value: 3 },
454+
{ name: "is_premium", type: "bool", value: true },
455+
];
456+
457+
test("renders custom attributes when allAttributes provided", () => {
458+
const log = createDetailedTestLog();
459+
const result = stripAnsi(formatLogDetails(log, "test-org", customAttrs));
460+
461+
expect(result).toContain("Custom Attributes");
462+
expect(result).toContain("user.id");
463+
expect(result).toContain("u_42");
464+
expect(result).toContain("order.total");
465+
expect(result).toContain("99.9");
466+
expect(result).toContain("retry.count");
467+
expect(result).toContain("3");
468+
expect(result).toContain("is_premium");
469+
expect(result).toContain("true");
470+
});
471+
472+
test("filters out REDUNDANT_LOG_DETAIL_ATTRS from custom attributes", () => {
473+
const attrsWithRedundant: TraceItemAttribute[] = [
474+
{ name: "user.id", type: "str", value: "u_42" },
475+
// these are in REDUNDANT_LOG_DETAIL_ATTRS and should be suppressed
476+
{ name: "severity_number", type: "int", value: 9 },
477+
{ name: "project.id", type: "str", value: "123" },
478+
];
479+
const log = createDetailedTestLog();
480+
const result = stripAnsi(
481+
formatLogDetails(log, "test-org", attrsWithRedundant)
482+
);
483+
484+
expect(result).toContain("user.id");
485+
expect(result).not.toContain("severity_number");
486+
expect(result).not.toContain("project.id");
487+
});
488+
489+
test("extraFields limits which custom attributes are shown", () => {
490+
const log = createDetailedTestLog();
491+
const result = stripAnsi(
492+
formatLogDetails(log, "test-org", customAttrs, ["user.id"])
493+
);
494+
495+
expect(result).toContain("Custom Attributes");
496+
expect(result).toContain("user.id");
497+
expect(result).not.toContain("order.total");
498+
expect(result).not.toContain("retry.count");
499+
});
500+
501+
test("shows no Custom Attributes section when all are filtered by extraFields", () => {
502+
const log = createDetailedTestLog();
503+
const result = stripAnsi(
504+
formatLogDetails(log, "test-org", customAttrs, ["nonexistent.field"])
505+
);
506+
507+
expect(result).not.toContain("Custom Attributes");
508+
});
509+
510+
test("fallback: shows extraFields from log when allAttributes absent", () => {
511+
const log = createDetailedTestLog({
512+
"user.id": "u_99",
513+
} as DetailedSentryLog);
514+
const result = stripAnsi(
515+
formatLogDetails(log, "test-org", undefined, ["user.id"])
516+
);
517+
518+
expect(result).toContain("Custom Attributes");
519+
expect(result).toContain("user.id");
520+
expect(result).toContain("u_99");
521+
});
522+
523+
test("no Custom Attributes section when allAttributes is empty array", () => {
524+
const log = createDetailedTestLog();
525+
const result = stripAnsi(formatLogDetails(log, "test-org", []));
526+
527+
expect(result).not.toContain("Custom Attributes");
528+
});
529+
});
444530
});
445531

446532
describe("getLogId", () => {

0 commit comments

Comments
 (0)