Skip to content

Commit e2c9215

Browse files
committed
refactor(span/view): use positional trace ID + add tests + fix SKILL.md
UX improvement: Replace --trace flag with positional argument to match the standardized [<org>/<project>/]<id> pattern used by span list, trace view, and event view. Before: sentry span view <span-id> --trace <trace-id> After: sentry span view [<org>/<project>/]<trace-id> <span-id> This is consistent with span list which already uses: sentry span list [<org>/<project>/]<trace-id> SKILL.md: Update placeholder text from generic '<args...>' to descriptive '<org/project/trace-id...>' and '<trace-id/span-id...>'. Tests: Add comprehensive tests for both span commands: - span/list: parsePositionalArgs, parseSort, and func body tests (API calls, query translation, org/project resolution) → 86.71% coverage - span/view: validateSpanId, parsePositionalArgs, and func body tests (span lookup, JSON output, multi-span, child tree) → 86.89% coverage - formatters/trace: findSpanById (case-insensitive, undefined span_id), computeSpanDurationMs, spanListItemToFlatSpan
1 parent 753acc4 commit e2c9215

File tree

7 files changed

+908
-92
lines changed

7 files changed

+908
-92
lines changed

AGENTS.md

Lines changed: 50 additions & 20 deletions
Large diffs are not rendered by default.

plugins/sentry-cli/skills/sentry-cli/SKILL.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -639,7 +639,7 @@ sentry log list --json | jq '.[] | select(.level == "error")'
639639

640640
View spans in distributed traces
641641

642-
#### `sentry span list <args...>`
642+
#### `sentry span list <org/project/trace-id...>`
643643

644644
List spans in a trace
645645

@@ -651,12 +651,11 @@ List spans in a trace
651651
- `--json - Output as JSON`
652652
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
653653

654-
#### `sentry span view <args...>`
654+
#### `sentry span view <trace-id/span-id...>`
655655

656656
View details of specific spans
657657

658658
**Flags:**
659-
- `-t, --trace <value> - Trace ID containing the span(s) (required)`
660659
- `--spans <value> - Span tree depth limit (number, "all" for unlimited, "no" to disable) - (default: "3")`
661660
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
662661
- `--json - Output as JSON`
@@ -837,7 +836,7 @@ List logs from a project
837836

838837
List spans in a trace
839838

840-
#### `sentry spans <args...>`
839+
#### `sentry spans <org/project/trace-id...>`
841840

842841
List spans in a trace
843842

src/commands/span/list.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,9 @@ export const listCommand = buildCommand({
190190
positional: {
191191
kind: "array",
192192
parameter: {
193-
placeholder: "args",
193+
placeholder: "org/project/trace-id",
194194
brief:
195-
"[<org>/<project>] <trace-id> - Target (optional) and trace ID (required)",
195+
"[<org>/<project>/]<trace-id> - Target (optional) and trace ID (required)",
196196
parse: String,
197197
},
198198
},

src/commands/span/view.ts

Lines changed: 46 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import { validateTraceId } from "../../lib/trace-id.js";
3737
const log = logger.withTag("span.view");
3838

3939
type ViewFlags = {
40-
readonly trace: string;
4140
readonly json: boolean;
4241
readonly spans: number;
4342
readonly fresh: boolean;
@@ -49,7 +48,7 @@ const SPAN_ID_RE = /^[0-9a-f]{16}$/i;
4948

5049
/** Usage hint for ContextError messages */
5150
const USAGE_HINT =
52-
"sentry span view [<org>/<project>] <span-id> [<span-id>...] --trace <trace-id>";
51+
"sentry span view [<org>/<project>/]<trace-id> <span-id> [<span-id>...]";
5352

5453
/**
5554
* Validate that a string is a 16-character hexadecimal span ID.
@@ -69,67 +68,55 @@ export function validateSpanId(value: string): string {
6968
return trimmed;
7069
}
7170

72-
/**
73-
* Check if a string looks like a 16-char hex span ID.
74-
* Used to distinguish span IDs from target args without throwing.
75-
*/
76-
function looksLikeSpanId(value: string): boolean {
77-
return SPAN_ID_RE.test(value.trim());
78-
}
79-
8071
/**
8172
* Parse positional arguments for span view.
82-
* Handles:
83-
* - `<span-id>` — single span ID (auto-detect org/project)
84-
* - `<span-id> <span-id> ...` — multiple span IDs
85-
* - `<target> <span-id> [<span-id>...]` — explicit target + span IDs
8673
*
87-
* The first arg is treated as a target if it contains "/" or doesn't look
88-
* like a 16-char hex span ID.
74+
* Uses the same `[<org>/<project>/]<id>` pattern as other commands.
75+
* The first positional is the trace ID (optionally slash-prefixed with
76+
* org/project), and the remaining positionals are span IDs.
77+
*
78+
* Formats:
79+
* - `<trace-id> <span-id> [...]` — auto-detect org/project
80+
* - `<org>/<project>/<trace-id> <span-id> [...]` — explicit target
81+
* - `<project>/<trace-id> <span-id> [...]` — project search
8982
*
9083
* @param args - Positional arguments from CLI
91-
* @returns Parsed span IDs and optional target arg
92-
* @throws {ContextError} If no arguments provided
93-
* @throws {ValidationError} If any span ID has an invalid format
84+
* @returns Parsed trace ID, span IDs, and optional target arg
85+
* @throws {ContextError} If insufficient arguments
86+
* @throws {ValidationError} If any ID has an invalid format
9487
*/
9588
export function parsePositionalArgs(args: string[]): {
89+
traceId: string;
9690
spanIds: string[];
9791
targetArg: string | undefined;
9892
} {
9993
if (args.length === 0) {
100-
throw new ContextError("Span ID", USAGE_HINT);
94+
throw new ContextError("Trace ID and span ID", USAGE_HINT);
10195
}
10296

10397
const first = args[0];
10498
if (first === undefined) {
105-
throw new ContextError("Span ID", USAGE_HINT);
99+
throw new ContextError("Trace ID and span ID", USAGE_HINT);
106100
}
107101

108-
if (args.length === 1) {
109-
// Single arg — could be slash-separated or a plain span ID
110-
const { id, targetArg } = parseSlashSeparatedArg(
111-
first,
112-
"Span ID",
113-
USAGE_HINT
114-
);
115-
const spanIds = [validateSpanId(id)];
116-
return { spanIds, targetArg };
117-
}
118-
119-
// Multiple args — determine if first is a target or span ID
120-
if (first.includes("/") || !looksLikeSpanId(first)) {
121-
// First arg is a target
122-
const rawIds = args.slice(1);
123-
const spanIds = rawIds.map((v) => validateSpanId(v));
124-
if (spanIds.length === 0) {
125-
throw new ContextError("Span ID", USAGE_HINT);
126-
}
127-
return { spanIds, targetArg: first };
102+
// First arg is trace ID (possibly with org/project prefix)
103+
const { id, targetArg } = parseSlashSeparatedArg(
104+
first,
105+
"Trace ID",
106+
USAGE_HINT
107+
);
108+
const traceId = validateTraceId(id);
109+
110+
// Remaining args are span IDs
111+
const rawSpanIds = args.slice(1);
112+
if (rawSpanIds.length === 0) {
113+
throw new ContextError("Span ID", USAGE_HINT, [
114+
`Use 'sentry span list ${first}' to find span IDs within this trace`,
115+
]);
128116
}
117+
const spanIds = rawSpanIds.map((v) => validateSpanId(v));
129118

130-
// All args are span IDs
131-
const spanIds = args.map((v) => validateSpanId(v));
132-
return { spanIds, targetArg: undefined };
119+
return { traceId, spanIds, targetArg };
133120
}
134121

135122
/**
@@ -159,7 +146,6 @@ type ResolvedSpanTarget = { org: string; project: string };
159146
*/
160147
async function resolveTarget(
161148
parsed: ReturnType<typeof parseOrgProjectArg>,
162-
spanIds: string[],
163149
traceId: string,
164150
cwd: string
165151
): Promise<ResolvedSpanTarget | null> {
@@ -171,7 +157,7 @@ async function resolveTarget(
171157
return await resolveProjectBySlug(
172158
parsed.projectSlug,
173159
USAGE_HINT,
174-
`sentry span view <org>/${parsed.projectSlug} ${spanIds[0]} --trace ${traceId}`
160+
`sentry span view <org>/${parsed.projectSlug}/${traceId} <span-id>`
175161
);
176162

177163
case "org-all":
@@ -287,14 +273,15 @@ export const viewCommand = buildCommand({
287273
fullDescription:
288274
"View detailed information about one or more spans within a trace.\n\n" +
289275
"Target specification:\n" +
290-
" sentry span view <span-id> --trace <trace-id> # auto-detect\n" +
291-
" sentry span view <org>/<proj> <span-id> --trace <trace-id> # explicit\n" +
292-
" sentry span view <project> <span-id> --trace <trace-id> # project search\n\n" +
293-
"The --trace flag is required to identify which trace contains the span(s).\n" +
294-
"Multiple span IDs can be passed as separate arguments.\n\n" +
276+
" sentry span view <trace-id> <span-id> # auto-detect\n" +
277+
" sentry span view <org>/<project>/<trace-id> <span-id> # explicit\n" +
278+
" sentry span view <project>/<trace-id> <span-id> # project search\n\n" +
279+
"The first argument is the trace ID (optionally prefixed with org/project),\n" +
280+
"followed by one or more span IDs.\n\n" +
295281
"Examples:\n" +
296-
" sentry span view a1b2c3d4e5f67890 --trace <trace-id>\n" +
297-
" sentry span view a1b2c3d4e5f67890 b2c3d4e5f6789012 --trace <trace-id>",
282+
" sentry span view <trace-id> a1b2c3d4e5f67890\n" +
283+
" sentry span view <trace-id> a1b2c3d4e5f67890 b2c3d4e5f6789012\n" +
284+
" sentry span view sentry/my-project/<trace-id> a1b2c3d4e5f67890",
298285
},
299286
output: {
300287
human: formatSpanViewHuman,
@@ -304,38 +291,31 @@ export const viewCommand = buildCommand({
304291
positional: {
305292
kind: "array",
306293
parameter: {
307-
placeholder: "args",
294+
placeholder: "trace-id/span-id",
308295
brief:
309-
"[<org>/<project>] <span-id> [<span-id>...] - Target (optional) and one or more span IDs",
296+
"[<org>/<project>/]<trace-id> <span-id> [<span-id>...] - Trace ID and one or more span IDs",
310297
parse: String,
311298
},
312299
},
313300
flags: {
314-
trace: {
315-
kind: "parsed",
316-
parse: validateTraceId,
317-
brief: "Trace ID containing the span(s) (required)",
318-
},
319301
...spansFlag,
320302
fresh: FRESH_FLAG,
321303
},
322-
aliases: { ...FRESH_ALIASES, t: "trace" },
304+
aliases: { ...FRESH_ALIASES },
323305
},
324306
async *func(this: SentryContext, flags: ViewFlags, ...args: string[]) {
325307
applyFreshFlag(flags);
326308
const { cwd, setContext } = this;
327309
const cmdLog = logger.withTag("span.view");
328310

329-
const traceId = flags.trace;
330-
331-
// Parse positional args
332-
const { spanIds, targetArg } = parsePositionalArgs(args);
311+
// Parse positional args: first is trace ID (with optional target), rest are span IDs
312+
const { traceId, spanIds, targetArg } = parsePositionalArgs(args);
333313
const parsed = parseOrgProjectArg(targetArg);
334314
if (parsed.type !== "auto-detect" && parsed.normalized) {
335315
cmdLog.warn("Normalized slug (Sentry slugs use dashes, not underscores)");
336316
}
337317

338-
const target = await resolveTarget(parsed, spanIds, traceId, cwd);
318+
const target = await resolveTarget(parsed, traceId, cwd);
339319

340320
if (!target) {
341321
throw new ContextError("Organization and project", USAGE_HINT);

0 commit comments

Comments
 (0)