Skip to content

Commit a57c13f

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/repo-list-command
# Conflicts: # src/app.ts # src/types/sentry.ts
2 parents 3d30fb5 + 8aa1a82 commit a57c13f

File tree

21 files changed

+2889
-5
lines changed

21 files changed

+2889
-5
lines changed

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,29 @@ sentry log view my-org/backend 968c763c740cfda8b6728f27fb9e9b01
514514
sentry log list --json | jq '.[] | select(.level == "error")'
515515
```
516516

517+
### Trace
518+
519+
View distributed traces
520+
521+
#### `sentry trace list <target>`
522+
523+
List recent traces in a project
524+
525+
**Flags:**
526+
- `-n, --limit <value> - Number of traces (1-1000) - (default: "20")`
527+
- `-q, --query <value> - Search query (Sentry search syntax)`
528+
- `-s, --sort <value> - Sort by: date, duration - (default: "date")`
529+
- `--json - Output as JSON`
530+
531+
#### `sentry trace view <args...>`
532+
533+
View details of a specific trace
534+
535+
**Flags:**
536+
- `--json - Output as JSON`
537+
- `-w, --web - Open in browser`
538+
- `--spans <value> - Span tree depth limit (number, "all" for unlimited, "no" to disable) - (default: "3")`
539+
517540
### Issues
518541

519542
List issues in a project
@@ -579,6 +602,20 @@ List logs from a project
579602
- `-f, --follow <value> - Stream logs (optionally specify poll interval in seconds)`
580603
- `--json - Output as JSON`
581604

605+
### Traces
606+
607+
List recent traces in a project
608+
609+
#### `sentry traces <target>`
610+
611+
List recent traces in a project
612+
613+
**Flags:**
614+
- `-n, --limit <value> - Number of traces (1-1000) - (default: "20")`
615+
- `-q, --query <value> - Search query (Sentry search syntax)`
616+
- `-s, --sort <value> - Sort by: date, duration - (default: "date")`
617+
- `--json - Output as JSON`
618+
582619
## Output Formats
583620

584621
### JSON Output

src/app.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import { projectRoute } from "./commands/project/index.js";
2121
import { listCommand as projectListCommand } from "./commands/project/list.js";
2222
import { repoRoute } from "./commands/repo/index.js";
2323
import { listCommand as repoListCommand } from "./commands/repo/list.js";
24+
import { traceRoute } from "./commands/trace/index.js";
25+
import { listCommand as traceListCommand } from "./commands/trace/list.js";
2426
import { CLI_VERSION } from "./lib/constants.js";
2527
import { AuthError, CliError, getExitCode } from "./lib/errors.js";
2628
import { error as errorColor } from "./lib/formatters/colors.js";
@@ -37,12 +39,14 @@ export const routes = buildRouteMap({
3739
issue: issueRoute,
3840
event: eventRoute,
3941
log: logRoute,
42+
trace: traceRoute,
4043
api: apiCommand,
4144
issues: issueListCommand,
4245
orgs: orgListCommand,
4346
projects: projectListCommand,
4447
repos: repoListCommand,
4548
logs: logListCommand,
49+
traces: traceListCommand,
4650
},
4751
defaultCommand: "help",
4852
docs: {

src/commands/trace/index.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* sentry trace
3+
*
4+
* View and explore distributed traces from Sentry projects.
5+
*/
6+
7+
import { buildRouteMap } from "@stricli/core";
8+
import { listCommand } from "./list.js";
9+
import { viewCommand } from "./view.js";
10+
11+
export const traceRoute = buildRouteMap({
12+
routes: {
13+
list: listCommand,
14+
view: viewCommand,
15+
},
16+
docs: {
17+
brief: "View distributed traces",
18+
fullDescription:
19+
"View and explore distributed traces from your Sentry projects.\n\n" +
20+
"Commands:\n" +
21+
" list List recent traces in a project\n" +
22+
" view View details of a specific trace",
23+
hideRoute: {},
24+
},
25+
});

src/commands/trace/list.ts

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/**
2+
* sentry trace list
3+
*
4+
* List recent traces from Sentry projects.
5+
*/
6+
7+
import { buildCommand } from "@stricli/core";
8+
import type { SentryContext } from "../../context.js";
9+
import { findProjectsBySlug, listTransactions } from "../../lib/api-client.js";
10+
import { parseOrgProjectArg } from "../../lib/arg-parsing.js";
11+
import { ContextError } from "../../lib/errors.js";
12+
import {
13+
formatTraceRow,
14+
formatTracesHeader,
15+
writeFooter,
16+
writeJson,
17+
} from "../../lib/formatters/index.js";
18+
import { resolveOrgAndProject } from "../../lib/resolve-target.js";
19+
20+
type ListFlags = {
21+
readonly limit: number;
22+
readonly query?: string;
23+
readonly sort: "date" | "duration";
24+
readonly json: boolean;
25+
};
26+
27+
type SortValue = "date" | "duration";
28+
29+
/** Accepted values for the --sort flag */
30+
const VALID_SORT_VALUES: SortValue[] = ["date", "duration"];
31+
32+
/** Usage hint for ContextError messages */
33+
const USAGE_HINT = "sentry trace list <org>/<project>";
34+
35+
/** Maximum allowed value for --limit flag */
36+
const MAX_LIMIT = 1000;
37+
38+
/** Minimum allowed value for --limit flag */
39+
const MIN_LIMIT = 1;
40+
41+
/** Default number of traces to show */
42+
const DEFAULT_LIMIT = 20;
43+
44+
/**
45+
* Validate that --limit value is within allowed range.
46+
*
47+
* @throws Error if value is outside MIN_LIMIT..MAX_LIMIT range
48+
* @internal Exported for testing
49+
*/
50+
export function validateLimit(value: string): number {
51+
const num = Number.parseInt(value, 10);
52+
if (Number.isNaN(num) || num < MIN_LIMIT || num > MAX_LIMIT) {
53+
throw new Error(`--limit must be between ${MIN_LIMIT} and ${MAX_LIMIT}`);
54+
}
55+
return num;
56+
}
57+
58+
/**
59+
* Parse and validate sort flag value.
60+
*
61+
* @throws Error if value is not "date" or "duration"
62+
* @internal Exported for testing
63+
*/
64+
export function parseSort(value: string): SortValue {
65+
if (!VALID_SORT_VALUES.includes(value as SortValue)) {
66+
throw new Error(
67+
`Invalid sort value. Must be one of: ${VALID_SORT_VALUES.join(", ")}`
68+
);
69+
}
70+
return value as SortValue;
71+
}
72+
73+
/** Resolved org and project for trace commands */
74+
type ResolvedTraceTarget = {
75+
org: string;
76+
project: string;
77+
};
78+
79+
/**
80+
* Resolve org/project from parsed argument or auto-detection.
81+
*
82+
* Handles:
83+
* - explicit: "org/project" -> use directly
84+
* - project-search: "project" -> find project across all orgs
85+
* - auto-detect: no input -> use DSN detection or config defaults
86+
*
87+
* @throws {ContextError} When target cannot be resolved
88+
* @internal Exported for testing
89+
*/
90+
export async function resolveTraceTarget(
91+
target: string | undefined,
92+
cwd: string
93+
): Promise<ResolvedTraceTarget> {
94+
const parsed = parseOrgProjectArg(target);
95+
96+
switch (parsed.type) {
97+
case "explicit":
98+
return { org: parsed.org, project: parsed.project };
99+
100+
case "org-all":
101+
throw new ContextError(
102+
"Project",
103+
`Please specify a project: sentry trace list ${parsed.org}/<project>`
104+
);
105+
106+
case "project-search": {
107+
const matches = await findProjectsBySlug(parsed.projectSlug);
108+
109+
if (matches.length === 0) {
110+
throw new ContextError(
111+
"Project",
112+
`No project '${parsed.projectSlug}' found in any accessible organization.\n\n` +
113+
`Try: sentry trace list <org>/${parsed.projectSlug}`
114+
);
115+
}
116+
117+
if (matches.length > 1) {
118+
const options = matches
119+
.map((m) => ` sentry trace list ${m.orgSlug}/${m.slug}`)
120+
.join("\n");
121+
throw new ContextError(
122+
"Project",
123+
`Found '${parsed.projectSlug}' in ${matches.length} organizations. Please specify:\n${options}`
124+
);
125+
}
126+
127+
// Safe: we checked matches.length === 1 above, so first element exists
128+
const match = matches[0] as (typeof matches)[number];
129+
return { org: match.orgSlug, project: match.slug };
130+
}
131+
132+
case "auto-detect": {
133+
const resolved = await resolveOrgAndProject({
134+
cwd,
135+
usageHint: USAGE_HINT,
136+
});
137+
if (!resolved) {
138+
throw new ContextError("Organization and project", USAGE_HINT);
139+
}
140+
return { org: resolved.org, project: resolved.project };
141+
}
142+
143+
default: {
144+
const _exhaustiveCheck: never = parsed;
145+
throw new Error(`Unexpected parsed type: ${_exhaustiveCheck}`);
146+
}
147+
}
148+
}
149+
150+
export const listCommand = buildCommand({
151+
docs: {
152+
brief: "List recent traces in a project",
153+
fullDescription:
154+
"List recent traces from Sentry projects.\n\n" +
155+
"Target specification:\n" +
156+
" sentry trace list # auto-detect from DSN or config\n" +
157+
" sentry trace list <org>/<proj> # explicit org and project\n" +
158+
" sentry trace list <project> # find project across all orgs\n\n" +
159+
"Examples:\n" +
160+
" sentry trace list # List last 10 traces\n" +
161+
" sentry trace list --limit 50 # Show more traces\n" +
162+
" sentry trace list --sort duration # Sort by slowest first\n" +
163+
' sentry trace list -q "transaction:GET /api/users" # Filter by transaction',
164+
},
165+
parameters: {
166+
positional: {
167+
kind: "tuple",
168+
parameters: [
169+
{
170+
placeholder: "target",
171+
brief: "Target: <org>/<project> or <project>",
172+
parse: String,
173+
optional: true,
174+
},
175+
],
176+
},
177+
flags: {
178+
limit: {
179+
kind: "parsed",
180+
parse: validateLimit,
181+
brief: `Number of traces (${MIN_LIMIT}-${MAX_LIMIT})`,
182+
default: String(DEFAULT_LIMIT),
183+
},
184+
query: {
185+
kind: "parsed",
186+
parse: String,
187+
brief: "Search query (Sentry search syntax)",
188+
optional: true,
189+
},
190+
sort: {
191+
kind: "parsed",
192+
parse: parseSort,
193+
brief: "Sort by: date, duration",
194+
default: "date" as const,
195+
},
196+
json: {
197+
kind: "boolean",
198+
brief: "Output as JSON",
199+
default: false,
200+
},
201+
},
202+
aliases: { n: "limit", q: "query", s: "sort" },
203+
},
204+
async func(
205+
this: SentryContext,
206+
flags: ListFlags,
207+
target?: string
208+
): Promise<void> {
209+
const { stdout, cwd, setContext } = this;
210+
211+
// Resolve org/project from positional arg, config, or DSN auto-detection
212+
const { org, project } = await resolveTraceTarget(target, cwd);
213+
setContext([org], [project]);
214+
215+
const traces = await listTransactions(org, project, {
216+
query: flags.query,
217+
limit: flags.limit,
218+
sort: flags.sort,
219+
});
220+
221+
if (flags.json) {
222+
writeJson(stdout, traces);
223+
return;
224+
}
225+
226+
if (traces.length === 0) {
227+
stdout.write("No traces found.\n");
228+
return;
229+
}
230+
231+
stdout.write(`Recent traces in ${org}/${project}:\n\n`);
232+
stdout.write(formatTracesHeader());
233+
for (const trace of traces) {
234+
stdout.write(formatTraceRow(trace));
235+
}
236+
237+
// Show footer with tip
238+
const hasMore = traces.length >= flags.limit;
239+
const countText = `Showing ${traces.length} trace${traces.length === 1 ? "" : "s"}.`;
240+
const tip = hasMore ? " Use --limit to show more." : "";
241+
writeFooter(
242+
stdout,
243+
`${countText}${tip} Use 'sentry trace view <TRACE_ID>' to view the full span tree.`
244+
);
245+
},
246+
});

0 commit comments

Comments
 (0)