Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -679,7 +679,7 @@ List recent traces in a project
- `--json - Output as JSON`
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`

#### `sentry trace view <args...>`
#### `sentry trace view <org/project/trace-id...>`

View details of a specific trace

Expand All @@ -690,14 +690,14 @@ View details of a specific trace
- `--json - Output as JSON`
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`

#### `sentry trace logs <args...>`
#### `sentry trace logs <org/trace-id...>`

View logs associated with a trace

**Flags:**
- `-w, --web - Open trace in browser`
- `-t, --period <value> - Time period to search (e.g., "14d", "7d", "24h"). Default: 14d - (default: "14d")`
- `-n, --limit <value> - Number of log entries (1-1000) - (default: "100")`
- `-n, --limit <value> - Number of log entries (<=1000) - (default: "100")`
- `-q, --query <value> - Additional filter query (Sentry search syntax)`
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
- `--json - Output as JSON`
Expand Down
129 changes: 23 additions & 106 deletions src/commands/span/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,14 @@
import type { SentryContext } from "../../context.js";
import type { SpanSortValue } from "../../lib/api/traces.js";
import { listSpans } from "../../lib/api-client.js";
import {
parseOrgProjectArg,
parseSlashSeparatedArg,
validateLimit,
} from "../../lib/arg-parsing.js";
import { validateLimit } from "../../lib/arg-parsing.js";
import { buildCommand } from "../../lib/command.js";
import {
buildPaginationContextKey,
clearPaginationCursor,
resolveOrgCursor,
setPaginationCursor,
} from "../../lib/db/pagination.js";
import { ContextError, ValidationError } from "../../lib/errors.js";
import {
type FlatSpan,
formatSpanTable,
Expand All @@ -36,10 +31,10 @@ import {
LIST_CURSOR_FLAG,
} from "../../lib/list-command.js";
import {
resolveOrgAndProject,
resolveProjectBySlug,
} from "../../lib/resolve-target.js";
import { validateTraceId } from "../../lib/trace-id.js";
parseTraceTarget,
resolveTraceOrgProject,
warnIfNormalized,
} from "../../lib/trace-target.js";

type ListFlags = {
readonly limit: number;
Expand Down Expand Up @@ -74,49 +69,6 @@ export const PAGINATION_KEY = "span-list";
/** Usage hint for ContextError messages */
const USAGE_HINT = "sentry span list [<org>/<project>/]<trace-id>";

/**
* Parse positional arguments for span list.
* Handles: `<trace-id>` or `<org>/<project>/<trace-id>`
*
* Uses the standard `parseSlashSeparatedArg` pattern: the last `/`-separated
* segment is the trace ID, and everything before it is the org/project target.
*
* @param args - Positional arguments from CLI
* @returns Parsed trace ID and optional target arg
* @throws {ContextError} If no arguments provided
* @throws {ValidationError} If the trace ID format is invalid
*/
export function parsePositionalArgs(args: string[]): {
traceId: string;
targetArg: string | undefined;
} {
if (args.length === 0) {
throw new ContextError("Trace ID", USAGE_HINT);
}

const first = args[0];
if (first === undefined) {
throw new ContextError("Trace ID", USAGE_HINT);
}

if (args.length === 1) {
const { id, targetArg } = parseSlashSeparatedArg(
first,
"Trace ID",
USAGE_HINT
);
return { traceId: validateTraceId(id), targetArg };
}

const second = args[1];
if (second === undefined) {
return { traceId: validateTraceId(first), targetArg: undefined };
}

// Two or more args — first is target, second is trace ID
return { traceId: validateTraceId(second), targetArg: first };
}

/**
* Parse --limit flag, delegating range validation to shared utility.
*/
Expand Down Expand Up @@ -281,46 +233,15 @@ export const listCommand = buildCommand({
applyFreshFlag(flags);
const { cwd, setContext } = this;

// Parse positional args
const { traceId, targetArg } = parsePositionalArgs(args);
const parsed = parseOrgProjectArg(targetArg);

// Resolve target
let target: { org: string; project: string } | null = null;

switch (parsed.type) {
case "explicit":
target = { org: parsed.org, project: parsed.project };
break;

case "project-search":
target = await resolveProjectBySlug(
parsed.projectSlug,
USAGE_HINT,
`sentry span list <org>/${parsed.projectSlug}/${traceId}`
);
break;

case "org-all":
throw new ContextError("Specific project", USAGE_HINT);

case "auto-detect":
target = await resolveOrgAndProject({ cwd, usageHint: USAGE_HINT });
break;

default: {
const _exhaustiveCheck: never = parsed;
throw new ValidationError(
`Invalid target specification: ${_exhaustiveCheck}`
);
}
}

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

setContext([target.org], [target.project]);
// Parse and resolve org/project/trace-id
const parsed = parseTraceTarget(args, USAGE_HINT);
warnIfNormalized(parsed, "span.list");
const { traceId, org, project } = await resolveTraceOrgProject(
parsed,
cwd,
USAGE_HINT
);
setContext([org], [project]);

// Build server-side query
const queryParts = [`trace:${traceId}`];
Expand All @@ -332,22 +253,18 @@ export const listCommand = buildCommand({
// Build context key and resolve cursor for pagination
const contextKey = buildPaginationContextKey(
"span",
`${target.org}/${target.project}/${traceId}`,
`${org}/${project}/${traceId}`,
{ sort: flags.sort, q: flags.query }
);
const cursor = resolveOrgCursor(flags.cursor, PAGINATION_KEY, contextKey);

// Fetch spans from EAP endpoint
const { data: spanItems, nextCursor } = await listSpans(
target.org,
target.project,
{
query: apiQuery,
sort: flags.sort,
limit: flags.limit,
cursor,
}
);
const { data: spanItems, nextCursor } = await listSpans(org, project, {
query: apiQuery,
sort: flags.sort,
limit: flags.limit,
cursor,
});

// Store or clear pagination cursor
if (nextCursor) {
Expand All @@ -362,11 +279,11 @@ export const listCommand = buildCommand({
// Build hint footer
let hint: string | undefined;
if (flatSpans.length === 0 && hasMore) {
hint = `Try the next page: ${nextPageHint(target.org, target.project, traceId, flags)}`;
hint = `Try the next page: ${nextPageHint(org, project, traceId, flags)}`;
} else if (flatSpans.length > 0) {
const countText = `Showing ${flatSpans.length} span${flatSpans.length === 1 ? "" : "s"}.`;
hint = hasMore
? `${countText} Next page: ${nextPageHint(target.org, target.project, traceId, flags)}`
? `${countText} Next page: ${nextPageHint(org, project, traceId, flags)}`
: `${countText} Use 'sentry span view ${traceId} <span-id>' to view span details.`;
}

Expand Down
99 changes: 24 additions & 75 deletions src/commands/span/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,7 @@

import type { SentryContext } from "../../context.js";
import { getDetailedTrace } from "../../lib/api-client.js";
import {
parseOrgProjectArg,
parseSlashSeparatedArg,
spansFlag,
} from "../../lib/arg-parsing.js";
import { spansFlag } from "../../lib/arg-parsing.js";
import { buildCommand } from "../../lib/command.js";
import { ContextError, ValidationError } from "../../lib/errors.js";
import {
Expand All @@ -30,10 +26,10 @@ import {
} from "../../lib/list-command.js";
import { logger } from "../../lib/logger.js";
import {
resolveOrgAndProject,
resolveProjectBySlug,
} from "../../lib/resolve-target.js";
import { validateTraceId } from "../../lib/trace-id.js";
parseSlashSeparatedTraceTarget,
resolveTraceOrgProject,
warnIfNormalized,
} from "../../lib/trace-target.js";

const log = logger.withTag("span.view");

Expand All @@ -51,23 +47,18 @@ const USAGE_HINT =
/**
* Parse positional arguments for span view.
*
* Uses the same `[<org>/<project>/]<id>` pattern as other commands.
* The first positional is the trace ID (optionally slash-prefixed with
* org/project), and the remaining positionals are span IDs.
*
* Formats:
* - `<trace-id> <span-id> [...]` — auto-detect org/project
* - `<org>/<project>/<trace-id> <span-id> [...]` — explicit target
* The first positional is the trace ID (optionally with org/project prefix),
* parsed via the shared `parseSlashSeparatedTraceTarget`. The remaining
* positionals are span IDs.
*
* @param args - Positional arguments from CLI
* @returns Parsed trace ID, span IDs, and optional target arg
* @returns Parsed trace target and span IDs
* @throws {ContextError} If insufficient arguments
* @throws {ValidationError} If any ID has an invalid format
*/
export function parsePositionalArgs(args: string[]): {
traceId: string;
traceTarget: ReturnType<typeof parseSlashSeparatedTraceTarget>;
spanIds: string[];
targetArg: string | undefined;
} {
if (args.length === 0) {
throw new ContextError("Trace ID and span ID", USAGE_HINT);
Expand All @@ -78,13 +69,8 @@ export function parsePositionalArgs(args: string[]): {
throw new ContextError("Trace ID and span ID", USAGE_HINT);
}

// First arg is trace ID (possibly with org/project prefix)
const { id, targetArg } = parseSlashSeparatedArg(
first,
"Trace ID",
USAGE_HINT
);
const traceId = validateTraceId(id);
// First arg is trace target (possibly with org/project prefix)
const traceTarget = parseSlashSeparatedTraceTarget(first, USAGE_HINT);

// Remaining args are span IDs
const rawSpanIds = args.slice(1);
Expand All @@ -95,7 +81,7 @@ export function parsePositionalArgs(args: string[]): {
}
const spanIds = rawSpanIds.map((v) => validateSpanId(v));

return { traceId, spanIds, targetArg };
return { traceTarget, spanIds };
}

/**
Expand All @@ -117,43 +103,6 @@ function warnMissingIds(spanIds: string[], foundIds: Set<string>): void {
}
}

/** Resolved target type for span commands. */
type ResolvedSpanTarget = { org: string; project: string };

/**
* Resolve org/project from the parsed target argument.
*/
async function resolveTarget(
parsed: ReturnType<typeof parseOrgProjectArg>,
traceId: string,
cwd: string
): Promise<ResolvedSpanTarget | null> {
switch (parsed.type) {
case "explicit":
return { org: parsed.org, project: parsed.project };

case "project-search":
return await resolveProjectBySlug(
parsed.projectSlug,
USAGE_HINT,
`sentry span view <org>/${parsed.projectSlug}/${traceId} <span-id>`
);

case "org-all":
throw new ContextError("Specific project", USAGE_HINT);

case "auto-detect":
return await resolveOrgAndProject({ cwd, usageHint: USAGE_HINT });

default: {
const _exhaustiveCheck: never = parsed;
throw new ValidationError(
`Invalid target specification: ${_exhaustiveCheck}`
);
}
}
}

// ---------------------------------------------------------------------------
// Output config types and formatters
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -285,21 +234,21 @@ export const viewCommand = buildCommand({
applyFreshFlag(flags);
const { cwd, setContext } = this;

// Parse positional args: first is trace ID (with optional target), rest are span IDs
const { traceId, spanIds, targetArg } = parsePositionalArgs(args);
const parsed = parseOrgProjectArg(targetArg);

const target = await resolveTarget(parsed, traceId, cwd);
// Parse positional args: first is trace target, rest are span IDs
const { traceTarget, spanIds } = parsePositionalArgs(args);
warnIfNormalized(traceTarget, "span.view");

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

setContext([target.org], [target.project]);
// Resolve org/project
const { traceId, org, project } = await resolveTraceOrgProject(
traceTarget,
cwd,
USAGE_HINT
);
setContext([org], [project]);

// Fetch trace data (single fetch for all span lookups)
const timestamp = Math.floor(Date.now() / 1000);
const spans = await getDetailedTrace(target.org, traceId, timestamp);
const spans = await getDetailedTrace(org, traceId, timestamp);

if (spans.length === 0) {
throw new ValidationError(
Expand Down
Loading
Loading