Skip to content

Commit 830f455

Browse files
authored
fix(json): flatten view command JSON output for --fields filtering (#495)
## Summary `issue view`, `event view`, and `trace view` commands yielded wrapper objects (e.g., `{ issue, event, trace }`) as their `CommandOutput` data. When `--fields` was used, `filterFields` looked for field names at the top level of this wrapper, so `--fields shortId,title` returned `{}` because those fields lived inside the nested `issue` object. ## Fix Add `jsonTransform` to all three view commands that spreads the primary entity as the top-level object: - **issue view**: `{ ...issue, event, trace }` - **event view**: `{ ...event, trace }` - **trace view**: `{ ...summary, spans }` This makes `--fields shortId,title` work directly on issue properties, while enrichment data remains accessible via `--fields event.id` or `--fields trace.traceId`. ## Test plan - `bun test test/commands/trace/view.func.test.ts` — updated assertions for flattened JSON structure - Manual: `sentry issue view <id> --json --fields shortId,title,count` should return filtered fields instead of `{}`
1 parent da74739 commit 830f455

File tree

6 files changed

+116
-50
lines changed

6 files changed

+116
-50
lines changed

AGENTS.md

Lines changed: 39 additions & 44 deletions
Large diffs are not rendered by default.

src/commands/event/view.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { openInBrowser } from "../../lib/browser.js";
2424
import { buildCommand } from "../../lib/command.js";
2525
import { ApiError, ContextError, ResolutionError } from "../../lib/errors.js";
2626
import { formatEventDetails } from "../../lib/formatters/index.js";
27+
import { filterFields } from "../../lib/formatters/json.js";
2728
import { CommandOutput } from "../../lib/formatters/output.js";
2829
import {
2930
applyFreshFlag,
@@ -77,6 +78,27 @@ function formatEventView(data: EventViewData): string {
7778
return parts.join("\n");
7879
}
7980

81+
/**
82+
* Transform event view data for JSON output.
83+
*
84+
* Flattens the event as the primary object so that `--fields eventID,title`
85+
* works directly on event properties. The `trace` enrichment data is
86+
* attached as a nested key, accessible via `--fields trace.traceId`.
87+
*
88+
* Without this transform, `--fields eventID` would return `{}` because
89+
* the raw yield shape is `{ event, trace }` and `eventID` lives inside `event`.
90+
*/
91+
function jsonTransformEventView(
92+
data: EventViewData,
93+
fields?: string[]
94+
): unknown {
95+
const { event, trace } = data;
96+
const result: Record<string, unknown> = { ...event, trace };
97+
if (fields && fields.length > 0) {
98+
return filterFields(result, fields);
99+
}
100+
return result;
101+
}
80102
/** Usage hint for ContextError messages */
81103
const USAGE_HINT = "sentry event view <org>/<project> <event-id>";
82104

@@ -387,7 +409,7 @@ export const viewCommand = buildCommand({
387409
},
388410
output: {
389411
human: formatEventView,
390-
jsonExclude: ["spanTreeLines"],
412+
jsonTransform: jsonTransformEventView,
391413
},
392414
parameters: {
393415
positional: {

src/commands/issue/view.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
isPlainOutput,
1616
muted,
1717
} from "../../lib/formatters/index.js";
18+
import { filterFields } from "../../lib/formatters/json.js";
1819
import { CommandOutput } from "../../lib/formatters/output.js";
1920
import {
2021
applyFreshFlag,
@@ -84,6 +85,30 @@ function formatIssueView(data: IssueViewData): string {
8485
return parts.join("\n");
8586
}
8687

88+
/**
89+
* Transform issue view data for JSON output.
90+
*
91+
* Flattens the issue as the primary object so that `--fields shortId,title`
92+
* works directly on issue properties. The `event` and `trace` enrichment
93+
* data are attached as nested keys, accessible via `--fields event.id`
94+
* or `--fields trace.traceId`.
95+
*
96+
* Without this transform, `--fields shortId` would return `{}` because
97+
* the raw yield shape is `{ issue, event, trace }` and `shortId` lives
98+
* inside `issue`.
99+
*/
100+
function jsonTransformIssueView(
101+
data: IssueViewData,
102+
fields?: string[]
103+
): unknown {
104+
const { issue, event, trace } = data;
105+
const result: Record<string, unknown> = { ...issue, event, trace };
106+
if (fields && fields.length > 0) {
107+
return filterFields(result, fields);
108+
}
109+
return result;
110+
}
111+
87112
export const viewCommand = buildCommand({
88113
docs: {
89114
brief: "View details of a specific issue",
@@ -104,7 +129,7 @@ export const viewCommand = buildCommand({
104129
},
105130
output: {
106131
human: formatIssueView,
107-
jsonExclude: ["spanTreeLines"],
132+
jsonTransform: jsonTransformIssueView,
108133
},
109134
parameters: {
110135
positional: issueIdPositional,

src/commands/trace/view.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
formatSimpleSpanTree,
2020
formatTraceSummary,
2121
} from "../../lib/formatters/index.js";
22+
import { filterFields } from "../../lib/formatters/json.js";
2223
import { CommandOutput } from "../../lib/formatters/output.js";
2324
import {
2425
applyFreshFlag,
@@ -117,6 +118,28 @@ export function formatTraceView(data: TraceViewData): string {
117118
return parts.join("\n");
118119
}
119120

121+
/**
122+
* Transform trace view data for JSON output.
123+
*
124+
* Flattens the summary as the primary object so that `--fields traceId,duration`
125+
* works directly on summary properties. The raw `spans` array is preserved as
126+
* a nested key, accessible via `--fields spans`.
127+
*
128+
* Without this transform, `--fields traceId` would return `{}` because
129+
* the raw yield shape is `{ summary, spans }` and `traceId` lives inside `summary`.
130+
*/
131+
function jsonTransformTraceView(
132+
data: TraceViewData,
133+
fields?: string[]
134+
): unknown {
135+
const { summary, spans } = data;
136+
const result: Record<string, unknown> = { ...summary, spans };
137+
if (fields && fields.length > 0) {
138+
return filterFields(result, fields);
139+
}
140+
return result;
141+
}
142+
120143
export const viewCommand = buildCommand({
121144
docs: {
122145
brief: "View details of a specific trace",
@@ -130,7 +153,7 @@ export const viewCommand = buildCommand({
130153
},
131154
output: {
132155
human: formatTraceView,
133-
jsonExclude: ["spanTreeLines"],
156+
jsonTransform: jsonTransformTraceView,
134157
},
135158
parameters: {
136159
positional: {

test/commands/trace/view.func.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,9 +159,9 @@ describe("viewCommand.func", () => {
159159

160160
const output = stdoutWrite.mock.calls.map((c) => c[0]).join("");
161161
const parsed = JSON.parse(output);
162-
expect(parsed).toHaveProperty("summary");
162+
expect(parsed).toHaveProperty("traceId");
163163
expect(parsed).toHaveProperty("spans");
164-
expect(parsed.summary.traceId).toBe("aaaa1111bbbb2222cccc3333dddd4444");
164+
expect(parsed.traceId).toBe("aaaa1111bbbb2222cccc3333dddd4444");
165165
expect(parsed.spans).toHaveLength(1);
166166
});
167167

test/e2e/trace.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,8 @@ describe("sentry trace view", () => {
175175

176176
expect(result.exitCode).toBe(0);
177177
const data = JSON.parse(result.stdout);
178-
expect(data).toHaveProperty("summary");
178+
// jsonTransform flattens summary to top level (no wrapper)
179+
expect(data).toHaveProperty("traceId");
179180
expect(data).toHaveProperty("spans");
180181
});
181182

0 commit comments

Comments
 (0)