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
16 changes: 11 additions & 5 deletions docs/src/content/docs/commands/project.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,27 +49,33 @@ my-org mobile-ios cocoa mobile-team
View details of a specific project.

```bash
sentry project view <project-slug>
# Auto-detect from DSN or config
sentry project view

# Explicit org and project
sentry project view <org>/<project>

# Find project across all orgs
sentry project view <project>
```

**Arguments:**

| Argument | Description |
|----------|-------------|
| `<project-slug>` | The project slug |
| `[target]` | Optional: `<org>/<project>`, `<project>`, or omit for auto-detect |

**Options:**

| Option | Description |
|--------|-------------|
| `--org <org-slug>` | Organization slug (if not specified, uses default) |
| `-w, --web` | Open in browser |
| `--json` | Output as JSON |

**Example:**

```bash
sentry project view frontend --org my-org
sentry project view my-org/frontend
```

```
Expand All @@ -83,5 +89,5 @@ DSN: https://abc123@sentry.io/123456
**Open in browser:**

```bash
sentry project view frontend -w
sentry project view my-org/frontend -w
```
16 changes: 11 additions & 5 deletions plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,23 +158,29 @@ sentry project list <org-slug>
sentry project list --platform javascript
```

#### `sentry project view <project>`
#### `sentry project view <target>`

View details of a project

**Flags:**
- `--org <value> - Organization slug`
- `--json - Output as JSON`
- `-w, --web - Open in browser`

**Examples:**

```bash
sentry project view <project-slug>
# Auto-detect from DSN or config
sentry project view

# Explicit org and project
sentry project view <org>/<project>

# Find project across all orgs
sentry project view <project>

sentry project view frontend --org my-org
sentry project view my-org/frontend

sentry project view frontend -w
sentry project view my-org/frontend -w
```

### Issue
Expand Down
67 changes: 19 additions & 48 deletions src/commands/event/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@
*/

import type { SentryContext } from "../../context.js";
import { findProjectsBySlug, getEvent } from "../../lib/api-client.js";
import { getEvent } from "../../lib/api-client.js";
import {
ProjectSpecificationType,
parseOrgProjectArg,
spansFlag,
} from "../../lib/arg-parsing.js";
import { openInBrowser } from "../../lib/browser.js";
import { buildCommand } from "../../lib/command.js";
import { ContextError, ValidationError } from "../../lib/errors.js";
import { ContextError } from "../../lib/errors.js";
import { formatEventDetails, writeJson } from "../../lib/formatters/index.js";
import { resolveOrgAndProject } from "../../lib/resolve-target.js";
import {
resolveOrgAndProject,
resolveProjectBySlug,
} 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";
Expand Down Expand Up @@ -105,48 +108,6 @@ export type ResolvedEventTarget = {
detectedFrom?: string;
};

/**
* Resolve target from a project search result.
*
* Searches for a project by slug across all accessible organizations.
* Throws if no project found or if multiple projects found in different orgs.
*
* @param projectSlug - Project slug to search for
* @param eventId - Event ID (used in error messages)
* @returns Resolved target with org and project info
* @throws {ContextError} If no project found
* @throws {ValidationError} If project exists in multiple organizations
*
* @internal Exported for testing
*/
export 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",
Expand Down Expand Up @@ -205,9 +166,19 @@ export const viewCommand = buildCommand({
};
break;

case ProjectSpecificationType.ProjectSearch:
target = await resolveFromProjectSearch(parsed.projectSlug, eventId);
case ProjectSpecificationType.ProjectSearch: {
const resolved = await resolveProjectBySlug(
parsed.projectSlug,
USAGE_HINT,
`sentry event view <org>/${parsed.projectSlug} ${eventId}`
);
target = {
...resolved,
orgDisplay: resolved.org,
projectDisplay: resolved.project,
};
break;
}

case ProjectSpecificationType.OrgAll:
throw new ContextError("Specific project", USAGE_HINT);
Expand All @@ -218,7 +189,7 @@ export const viewCommand = buildCommand({

default:
// Exhaustive check - should never reach here
throw new ValidationError("Invalid target specification");
throw new ContextError("Organization and project", USAGE_HINT);
}

if (!target) {
Expand Down
53 changes: 10 additions & 43 deletions src/commands/log/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@

import { buildCommand } from "@stricli/core";
import type { SentryContext } from "../../context.js";
import { findProjectsBySlug, getLog } from "../../lib/api-client.js";
import { getLog } from "../../lib/api-client.js";
import { parseOrgProjectArg } from "../../lib/arg-parsing.js";
import { openInBrowser } from "../../lib/browser.js";
import { ContextError, ValidationError } from "../../lib/errors.js";
import { formatLogDetails, writeJson } from "../../lib/formatters/index.js";
import { resolveOrgAndProject } from "../../lib/resolve-target.js";
import {
resolveOrgAndProject,
resolveProjectBySlug,
} from "../../lib/resolve-target.js";
import { buildLogsUrl } from "../../lib/sentry-urls.js";
import type { DetailedSentryLog, Writer } from "../../types/index.js";

Expand Down Expand Up @@ -68,46 +71,6 @@ export type ResolvedLogTarget = {
detectedFrom?: string;
};

/**
* Resolve target from a project search result.
*
* Searches for a project by slug across all accessible organizations.
* Throws if no project found or if multiple projects found in different orgs.
*
* @param projectSlug - Project slug to search for
* @param logId - Log ID (used in error messages)
* @returns Resolved target with org and project info
* @throws {ContextError} If no project found
* @throws {ValidationError} If project exists in multiple organizations
*
* @internal Exported for testing
*/
export async function resolveFromProjectSearch(
projectSlug: string,
logId: string
): Promise<ResolvedLogTarget> {
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 log view <org>/${projectSlug} ${logId}`
);
}
// 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,
};
}

/**
* Write human-readable log output to stdout.
*
Expand Down Expand Up @@ -187,7 +150,11 @@ export const viewCommand = buildCommand({
break;

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

case "org-all":
Expand Down
4 changes: 2 additions & 2 deletions src/commands/project/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ async function resolveOrgsToFetch(
orgFlag: string | undefined,
cwd: string
): Promise<OrgResolution> {
// 1. If --org flag provided, use it directly
// 1. If org positional provided, use it directly
if (orgFlag) {
return { orgs: [orgFlag] };
}
Expand Down Expand Up @@ -309,7 +309,7 @@ export const listCommand = buildCommand({

writeFooter(
stdout,
"Tip: Use 'sentry project view <project> --org <org>' for details"
"Tip: Use 'sentry project view <org>/<project>' for details"
);
},
});
Loading
Loading