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
24 changes: 14 additions & 10 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,31 @@ We follow [gh CLI](https://cli.github.com/) conventions for best-in-class develo

### List Commands

List commands use **flags only** for context (no positional arguments).
List commands use **optional positional arguments** for context with smart auto-detection.

```bash
sentry org list [--limit N] [--json]
sentry project list [--org ORG] [--limit N] [--json]
sentry issue list [--org ORG] [--project PROJECT] [--json]
sentry project list [org] [--limit N] [--json]
sentry issue list [<org>/<project>] [--json]
```

**Rationale**: Flags are self-documenting and avoid ambiguity when multiple identifiers are needed.
**Target syntax**:
- `<org>/<project>` - Explicit organization and project (e.g., `my-org/frontend`)
- `<org>/` - All projects in the specified organization
- `<project>` - Search for project by name across all accessible organizations
- *(omit)* - Auto-detect from DSN or config

**Rationale**: Positional arguments follow `gh` CLI conventions and are more concise than flags.

### View Commands

View commands use **optional positional arguments** for the primary identifier, supporting auto-detection when omitted.

```bash
sentry org view [org-slug] [--json] [-w] # works with DSN if no arg
sentry project view [project-slug] [--org ORG] [--json] [-w] # works with DSN if no arg
sentry issue view <issue-id> [--org ORG] [--json] [-w] # issue ID required
sentry event view <event-id> [--org ORG] [--project PROJECT] [--json] [-w]
sentry project view [<org>/<project>] [--json] [-w] # works with DSN if no arg
sentry issue view <issue-id> [--json] [-w] # issue ID required
sentry event view [<org>/<project>] <event-id> [--json] [-w] # event ID required
```

**Key insight**: `org view` and `project view` mirror `gh repo view` - works in context (DSN) or with explicit arg.
Expand All @@ -37,16 +43,14 @@ sentry event view <event-id> [--org ORG] [--project PROJECT] [--json] [-w]

Context (org, project) is resolved in this priority order:

1. **CLI flags** (`--org`, `--project`) - explicit, always wins
1. **Positional arguments** (`<org>/<project>`) - explicit, always wins
2. **Config defaults** - set via `sentry config set`
3. **DSN auto-detection** - from `SENTRY_DSN` env var or source code

## Common Flags

| Flag | Description | Used In |
|------|-------------|---------|
| `--org` | Organization slug | Most commands |
| `--project` | Project slug | Project/issue/event commands |
| `--json` | Output as JSON | All view/list commands |
| `-w`, `--web` | Open in browser | All view commands |
| `--limit` | Max items to return | List commands |
Expand Down
19 changes: 9 additions & 10 deletions docs/src/content/docs/commands/issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,6 @@ The analysis may take a few minutes for new issues.

| Option | Description |
|--------|-------------|
| `--org <org-slug>` | Organization slug (required for short IDs if not auto-detected) |
| `--project <project-slug>` | Project slug (required for short suffixes if not auto-detected) |
| `--force` | Force new analysis even if one already exists |
| `--json` | Output as JSON |

Expand All @@ -180,11 +178,11 @@ The analysis may take a few minutes for new issues.
# By numeric issue ID
sentry issue explain 123456789

# By short ID
sentry issue explain MYPROJECT-ABC --org my-org
# By short ID with org prefix
sentry issue explain my-org/MYPROJECT-ABC

# By short suffix (requires project context)
sentry issue explain G --org my-org --project my-project
# By project-suffix format
sentry issue explain myproject-G

# Force a fresh analysis
sentry issue explain 123456789 --force
Expand Down Expand Up @@ -216,8 +214,6 @@ This command requires that `sentry issue explain` has been run first to identify

| Option | Description |
|--------|-------------|
| `--org <org-slug>` | Organization slug (required for short IDs if not auto-detected) |
| `--project <project-slug>` | Project slug (required for short suffixes if not auto-detected) |
| `--cause <n>` | Root cause ID to plan (required if multiple causes were identified) |
| `--json` | Output as JSON |

Expand All @@ -230,8 +226,11 @@ sentry issue plan 123456789
# Specify which root cause to plan for (if multiple were found)
sentry issue plan 123456789 --cause 0

# By short ID
sentry issue plan MYPROJECT-ABC --org my-org --cause 1
# By short ID with org prefix
sentry issue plan my-org/MYPROJECT-ABC --cause 1

# By project-suffix format
sentry issue plan myproject-G --cause 0
```

**Requirements:**
Expand Down
8 changes: 4 additions & 4 deletions docs/src/content/docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ The Sentry CLI includes several features designed to streamline your workflow, e

## DSN Auto-Detection

The CLI automatically detects your Sentry project from your codebase, eliminating the need to specify `--org` and `--project` flags for every command.
The CLI automatically detects your Sentry project from your codebase, eliminating the need to specify the target for every command.

### How It Works

Expand Down Expand Up @@ -47,7 +47,7 @@ Once your project has a DSN configured, commands automatically use it:

```bash
# Instead of:
sentry issue list --org my-org --project my-project
sentry issue list my-org/my-project

# Just run:
sentry issue list
Expand Down Expand Up @@ -151,10 +151,10 @@ sentry issue explain FRONTEND-XYZ

### Short Suffix

Just the suffix portion when `--project` context is provided:
Just the suffix portion when project context is provided via the `<org>/` prefix:

```bash
sentry issue view ABC --org my-org --project myproject
sentry issue view my-org/myproject-ABC
```

### Alias-Suffix
Expand Down
19 changes: 10 additions & 9 deletions plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,11 +239,11 @@ sentry issue explain <issue-id>
# By numeric issue ID
sentry issue explain 123456789

# By short ID
sentry issue explain MYPROJECT-ABC --org my-org
# By short ID with org prefix
sentry issue explain my-org/MYPROJECT-ABC

# By short suffix (requires project context)
sentry issue explain G --org my-org --project my-project
# By project-suffix format
sentry issue explain myproject-G

# Force a fresh analysis
sentry issue explain 123456789 --force
Expand All @@ -269,8 +269,11 @@ sentry issue plan 123456789
# Specify which root cause to plan for (if multiple were found)
sentry issue plan 123456789 --cause 0

# By short ID
sentry issue plan MYPROJECT-ABC --org my-org --cause 1
# By short ID with org prefix
sentry issue plan my-org/MYPROJECT-ABC --cause 1

# By project-suffix format
sentry issue plan myproject-G --cause 0
```

#### `sentry issue view <issue>`
Expand Down Expand Up @@ -300,13 +303,11 @@ sentry issue view FRONT-ABC -w

View Sentry events

#### `sentry event view <event-id>`
#### `sentry event view <args...>`

View details of a specific event

**Flags:**
- `--org <value> - Organization slug`
- `--project <value> - Project slug`
- `--json - Output as JSON`
- `-w, --web - Open in browser`
- `--spans <value> - Span tree depth limit (number, "all" for unlimited, "no" to disable) - (default: "3")`
Expand Down
169 changes: 128 additions & 41 deletions src/commands/event/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@

import { buildCommand } from "@stricli/core";
import type { SentryContext } from "../../context.js";
import { getEvent } from "../../lib/api-client.js";
import { spansFlag } from "../../lib/arg-parsing.js";
import { findProjectsBySlug, getEvent } from "../../lib/api-client.js";
import {
ProjectSpecificationType,
parseOrgProjectArg,
spansFlag,
} from "../../lib/arg-parsing.js";
import { openInBrowser } from "../../lib/browser.js";
import { ContextError } from "../../lib/errors.js";
import { ContextError, ValidationError } from "../../lib/errors.js";
import { formatEventDetails, writeJson } from "../../lib/formatters/index.js";
import { resolveOrgAndProject } from "../../lib/resolve-target.js";
import { buildEventSearchUrl } from "../../lib/sentry-urls.js";
import { getSpanTreeLines } from "../../lib/span-tree.js";
import type { SentryEvent, Writer } from "../../types/index.js";

type ViewFlags = {
readonly org?: string;
readonly project?: string;
readonly json: boolean;
readonly web: boolean;
readonly spans: number;
Expand Down Expand Up @@ -54,41 +56,104 @@ function writeHumanOutput(stdout: Writer, options: HumanOutputOptions): void {
}
}

/** Usage hint for ContextError messages */
const USAGE_HINT = "sentry event view <org>/<project> <event-id>";

/**
* Parse positional arguments for event view.
* Handles: `<event-id>` or `<target> <event-id>`
*
* @returns Parsed event ID and optional target arg
*/
export function parsePositionalArgs(args: string[]): {
eventId: string;
targetArg: string | undefined;
} {
if (args.length === 0) {
throw new ContextError("Event ID", USAGE_HINT);
}

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

if (args.length === 1) {
// Single arg - must be event ID
return { eventId: first, targetArg: undefined };
}

const second = args[1];
if (second === undefined) {
// Should not happen given length check, but TypeScript needs this
return { eventId: first, targetArg: undefined };
}

// Two or more args - first is target, second is event ID
return { eventId: second, targetArg: first };
}

/** Resolved target type for internal use */
type ResolvedEventTarget = {
org: string;
project: string;
orgDisplay: string;
projectDisplay: string;
detectedFrom?: string;
};

/**
* Resolve target from a project search result.
*/
async function resolveFromProjectSearch(
projectSlug: string,
eventId: string
): Promise<ResolvedEventTarget> {
const found = await findProjectsBySlug(projectSlug);
if (found.length === 0) {
throw new ContextError(`Project "${projectSlug}"`, USAGE_HINT, [
"Check that you have access to a project with this slug",
]);
}
if (found.length > 1) {
const orgList = found.map((p) => ` ${p.orgSlug}/${p.slug}`).join("\n");
throw new ValidationError(
`Project "${projectSlug}" exists in multiple organizations.\n\n` +
`Specify the organization:\n${orgList}\n\n` +
`Example: sentry event view <org>/${projectSlug} ${eventId}`
);
}
// Safe assertion: length is exactly 1 after the checks above
const foundProject = found[0] as (typeof found)[0];
return {
org: foundProject.orgSlug,
project: foundProject.slug,
orgDisplay: foundProject.orgSlug,
projectDisplay: foundProject.slug,
};
}

export const viewCommand = buildCommand({
docs: {
brief: "View details of a specific event",
fullDescription:
"View detailed information about a Sentry event by its ID.\n\n" +
"The organization and project are resolved from:\n" +
" 1. --org and --project flags\n" +
" 2. Config defaults\n" +
" 3. SENTRY_DSN environment variable or source code detection",
"Target specification:\n" +
" sentry event view <event-id> # auto-detect from DSN or config\n" +
" sentry event view <org>/<proj> <event-id> # explicit org and project\n" +
" sentry event view <project> <event-id> # find project across all orgs",
},
parameters: {
positional: {
kind: "tuple",
parameters: [
{
placeholder: "event-id",
brief:
"Event ID (hexadecimal, e.g., 9999aaaaca8b46d797c23c6077c6ff01)",
parse: String,
},
],
},
flags: {
org: {
kind: "parsed",
parse: String,
brief: "Organization slug",
optional: true,
},
project: {
kind: "parsed",
kind: "array",
parameter: {
placeholder: "args",
brief:
"[<org>/<project>] <event-id> - Target (optional) and event ID (required)",
parse: String,
brief: "Project slug",
optional: true,
},
},
flags: {
json: {
kind: "boolean",
brief: "Output as JSON",
Expand All @@ -106,22 +171,44 @@ export const viewCommand = buildCommand({
async func(
this: SentryContext,
flags: ViewFlags,
eventId: string
...args: string[]
): Promise<void> {
const { stdout, cwd } = this;

const target = await resolveOrgAndProject({
org: flags.org,
project: flags.project,
cwd,
usageHint: `sentry event view ${eventId} --org <org> --project <project>`,
});
// Parse positional args
const { eventId, targetArg } = parsePositionalArgs(args);
const parsed = parseOrgProjectArg(targetArg);

let target: ResolvedEventTarget | null = null;

switch (parsed.type) {
case ProjectSpecificationType.Explicit:
target = {
org: parsed.org,
project: parsed.project,
orgDisplay: parsed.org,
projectDisplay: parsed.project,
};
break;

case ProjectSpecificationType.ProjectSearch:
target = await resolveFromProjectSearch(parsed.projectSlug, eventId);
break;

case ProjectSpecificationType.OrgAll:
throw new ContextError("Specific project", USAGE_HINT);

case ProjectSpecificationType.AutoDetect:
target = await resolveOrgAndProject({ cwd, usageHint: USAGE_HINT });
break;

default:
// Exhaustive check - should never reach here
throw new ValidationError("Invalid target specification");
}

if (!target) {
throw new ContextError(
"Organization and project",
`sentry event view ${eventId} --org <org-slug> --project <project-slug>`
);
throw new ContextError("Organization and project", USAGE_HINT);
}

if (flags.web) {
Expand Down
Loading
Loading