Skip to content

Commit 9da8df4

Browse files
committed
refactor: extract resolveProjectBySlug into shared utility
Deduplicate resolveFromProjectSearch() which was identically implemented in event/view, trace/view, log/view, and project/view. The shared resolveProjectBySlug() in resolve-target.ts handles project slug lookup with consistent error messages and disambiguation prompts.
1 parent cace029 commit 9da8df4

File tree

9 files changed

+174
-241
lines changed

9 files changed

+174
-241
lines changed

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ sentry project list <org-slug>
158158
sentry project list --platform javascript
159159
```
160160

161-
#### `sentry project view [<org>/<project>]`
161+
#### `sentry project view <target>`
162162

163163
View details of a project
164164

@@ -173,12 +173,13 @@ View details of a project
173173
sentry project view
174174

175175
# Explicit org and project
176-
sentry project view my-org/frontend
176+
sentry project view <org>/<project>
177177

178178
# Find project across all orgs
179-
sentry project view frontend
179+
sentry project view <project>
180+
181+
sentry project view my-org/frontend
180182

181-
# Open in browser
182183
sentry project view my-org/frontend -w
183184
```
184185

src/commands/event/view.ts

Lines changed: 19 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,20 @@
55
*/
66

77
import type { SentryContext } from "../../context.js";
8-
import { findProjectsBySlug, getEvent } from "../../lib/api-client.js";
8+
import { getEvent } from "../../lib/api-client.js";
99
import {
1010
ProjectSpecificationType,
1111
parseOrgProjectArg,
1212
spansFlag,
1313
} from "../../lib/arg-parsing.js";
1414
import { openInBrowser } from "../../lib/browser.js";
1515
import { buildCommand } from "../../lib/command.js";
16-
import { ContextError, ValidationError } from "../../lib/errors.js";
16+
import { ContextError } from "../../lib/errors.js";
1717
import { formatEventDetails, writeJson } from "../../lib/formatters/index.js";
18-
import { resolveOrgAndProject } from "../../lib/resolve-target.js";
18+
import {
19+
resolveOrgAndProject,
20+
resolveProjectBySlug,
21+
} from "../../lib/resolve-target.js";
1922
import { buildEventSearchUrl } from "../../lib/sentry-urls.js";
2023
import { getSpanTreeLines } from "../../lib/span-tree.js";
2124
import type { SentryEvent, Writer } from "../../types/index.js";
@@ -105,48 +108,6 @@ export type ResolvedEventTarget = {
105108
detectedFrom?: string;
106109
};
107110

108-
/**
109-
* Resolve target from a project search result.
110-
*
111-
* Searches for a project by slug across all accessible organizations.
112-
* Throws if no project found or if multiple projects found in different orgs.
113-
*
114-
* @param projectSlug - Project slug to search for
115-
* @param eventId - Event ID (used in error messages)
116-
* @returns Resolved target with org and project info
117-
* @throws {ContextError} If no project found
118-
* @throws {ValidationError} If project exists in multiple organizations
119-
*
120-
* @internal Exported for testing
121-
*/
122-
export async function resolveFromProjectSearch(
123-
projectSlug: string,
124-
eventId: string
125-
): Promise<ResolvedEventTarget> {
126-
const found = await findProjectsBySlug(projectSlug);
127-
if (found.length === 0) {
128-
throw new ContextError(`Project "${projectSlug}"`, USAGE_HINT, [
129-
"Check that you have access to a project with this slug",
130-
]);
131-
}
132-
if (found.length > 1) {
133-
const orgList = found.map((p) => ` ${p.orgSlug}/${p.slug}`).join("\n");
134-
throw new ValidationError(
135-
`Project "${projectSlug}" exists in multiple organizations.\n\n` +
136-
`Specify the organization:\n${orgList}\n\n` +
137-
`Example: sentry event view <org>/${projectSlug} ${eventId}`
138-
);
139-
}
140-
// Safe assertion: length is exactly 1 after the checks above
141-
const foundProject = found[0] as (typeof found)[0];
142-
return {
143-
org: foundProject.orgSlug,
144-
project: foundProject.slug,
145-
orgDisplay: foundProject.orgSlug,
146-
projectDisplay: foundProject.slug,
147-
};
148-
}
149-
150111
export const viewCommand = buildCommand({
151112
docs: {
152113
brief: "View details of a specific event",
@@ -205,9 +166,19 @@ export const viewCommand = buildCommand({
205166
};
206167
break;
207168

208-
case ProjectSpecificationType.ProjectSearch:
209-
target = await resolveFromProjectSearch(parsed.projectSlug, eventId);
169+
case ProjectSpecificationType.ProjectSearch: {
170+
const resolved = await resolveProjectBySlug(
171+
parsed.projectSlug,
172+
USAGE_HINT,
173+
`sentry event view <org>/${parsed.projectSlug} ${eventId}`
174+
);
175+
target = {
176+
...resolved,
177+
orgDisplay: resolved.org,
178+
projectDisplay: resolved.project,
179+
};
210180
break;
181+
}
211182

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

219190
default:
220191
// Exhaustive check - should never reach here
221-
throw new ValidationError("Invalid target specification");
192+
throw new ContextError("Organization and project", USAGE_HINT);
222193
}
223194

224195
if (!target) {

src/commands/log/view.ts

Lines changed: 10 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66

77
import { buildCommand } from "@stricli/core";
88
import type { SentryContext } from "../../context.js";
9-
import { findProjectsBySlug, getLog } from "../../lib/api-client.js";
9+
import { getLog } from "../../lib/api-client.js";
1010
import { parseOrgProjectArg } from "../../lib/arg-parsing.js";
1111
import { openInBrowser } from "../../lib/browser.js";
1212
import { ContextError, ValidationError } from "../../lib/errors.js";
1313
import { formatLogDetails, writeJson } from "../../lib/formatters/index.js";
14-
import { resolveOrgAndProject } from "../../lib/resolve-target.js";
14+
import {
15+
resolveOrgAndProject,
16+
resolveProjectBySlug,
17+
} from "../../lib/resolve-target.js";
1518
import { buildLogsUrl } from "../../lib/sentry-urls.js";
1619
import type { DetailedSentryLog, Writer } from "../../types/index.js";
1720

@@ -68,46 +71,6 @@ export type ResolvedLogTarget = {
6871
detectedFrom?: string;
6972
};
7073

71-
/**
72-
* Resolve target from a project search result.
73-
*
74-
* Searches for a project by slug across all accessible organizations.
75-
* Throws if no project found or if multiple projects found in different orgs.
76-
*
77-
* @param projectSlug - Project slug to search for
78-
* @param logId - Log ID (used in error messages)
79-
* @returns Resolved target with org and project info
80-
* @throws {ContextError} If no project found
81-
* @throws {ValidationError} If project exists in multiple organizations
82-
*
83-
* @internal Exported for testing
84-
*/
85-
export async function resolveFromProjectSearch(
86-
projectSlug: string,
87-
logId: string
88-
): Promise<ResolvedLogTarget> {
89-
const found = await findProjectsBySlug(projectSlug);
90-
if (found.length === 0) {
91-
throw new ContextError(`Project "${projectSlug}"`, USAGE_HINT, [
92-
"Check that you have access to a project with this slug",
93-
]);
94-
}
95-
if (found.length > 1) {
96-
const orgList = found.map((p) => ` ${p.orgSlug}/${p.slug}`).join("\n");
97-
throw new ValidationError(
98-
`Project "${projectSlug}" exists in multiple organizations.\n\n` +
99-
`Specify the organization:\n${orgList}\n\n` +
100-
`Example: sentry log view <org>/${projectSlug} ${logId}`
101-
);
102-
}
103-
// Safe assertion: length is exactly 1 after the checks above
104-
const foundProject = found[0] as (typeof found)[0];
105-
return {
106-
org: foundProject.orgSlug,
107-
project: foundProject.slug,
108-
};
109-
}
110-
11174
/**
11275
* Write human-readable log output to stdout.
11376
*
@@ -187,7 +150,11 @@ export const viewCommand = buildCommand({
187150
break;
188151

189152
case "project-search":
190-
target = await resolveFromProjectSearch(parsed.projectSlug, logId);
153+
target = await resolveProjectBySlug(
154+
parsed.projectSlug,
155+
USAGE_HINT,
156+
`sentry log view <org>/${parsed.projectSlug} ${logId}`
157+
);
191158
break;
192159

193160
case "org-all":

src/commands/project/view.ts

Lines changed: 18 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,14 @@
66
*/
77

88
import type { SentryContext } from "../../context.js";
9-
import {
10-
findProjectsBySlug,
11-
getProject,
12-
getProjectKeys,
13-
} from "../../lib/api-client.js";
9+
import { getProject, getProjectKeys } from "../../lib/api-client.js";
1410
import {
1511
ProjectSpecificationType,
1612
parseOrgProjectArg,
1713
} from "../../lib/arg-parsing.js";
1814
import { openInBrowser } from "../../lib/browser.js";
1915
import { buildCommand } from "../../lib/command.js";
20-
import { AuthError, ContextError, ValidationError } from "../../lib/errors.js";
16+
import { AuthError, ContextError } from "../../lib/errors.js";
2117
import {
2218
divider,
2319
formatProjectDetails,
@@ -27,6 +23,7 @@ import {
2723
import {
2824
type ResolvedTarget,
2925
resolveAllTargets,
26+
resolveProjectBySlug,
3027
} from "../../lib/resolve-target.js";
3128
import { buildProjectUrl } from "../../lib/sentry-urls.js";
3229
import type { ProjectKey, SentryProject } from "../../types/index.js";
@@ -199,44 +196,6 @@ function writeMultipleProjects(
199196
}
200197
}
201198

202-
/**
203-
* Resolve target from a project search result.
204-
*
205-
* Searches for a project by slug across all accessible organizations.
206-
* Throws if no project found or if multiple projects found in different orgs.
207-
*
208-
* @param projectSlug - Project slug to search for
209-
* @returns Resolved target with org and project info
210-
* @throws {ContextError} If no project found
211-
* @throws {ValidationError} If project exists in multiple organizations
212-
*/
213-
async function resolveFromProjectSearch(
214-
projectSlug: string
215-
): Promise<ResolvedTarget> {
216-
const found = await findProjectsBySlug(projectSlug);
217-
if (found.length === 0) {
218-
throw new ContextError(`Project "${projectSlug}"`, USAGE_HINT, [
219-
"Check that you have access to a project with this slug",
220-
]);
221-
}
222-
if (found.length > 1) {
223-
const orgList = found.map((p) => ` ${p.orgSlug}/${p.slug}`).join("\n");
224-
throw new ValidationError(
225-
`Project "${projectSlug}" exists in multiple organizations.\n\n` +
226-
`Specify the organization:\n${orgList}\n\n` +
227-
`Example: sentry project view <org>/${projectSlug}`
228-
);
229-
}
230-
// Safe assertion: length is exactly 1 after the checks above
231-
const foundProject = found[0] as (typeof found)[0];
232-
return {
233-
org: foundProject.orgSlug,
234-
project: foundProject.slug,
235-
orgDisplay: foundProject.orgSlug,
236-
projectDisplay: foundProject.slug,
237-
};
238-
}
239-
240199
export const viewCommand = buildCommand({
241200
docs: {
242201
brief: "View details of a project",
@@ -299,10 +258,22 @@ export const viewCommand = buildCommand({
299258
];
300259
break;
301260

302-
case ProjectSpecificationType.ProjectSearch:
261+
case ProjectSpecificationType.ProjectSearch: {
303262
// Search for project across all orgs - single target
304-
resolvedTargets = [await resolveFromProjectSearch(parsed.projectSlug)];
263+
const resolved = await resolveProjectBySlug(
264+
parsed.projectSlug,
265+
USAGE_HINT,
266+
`sentry project view <org>/${parsed.projectSlug}`
267+
);
268+
resolvedTargets = [
269+
{
270+
...resolved,
271+
orgDisplay: resolved.org,
272+
projectDisplay: resolved.project,
273+
},
274+
];
305275
break;
276+
}
306277

307278
case ProjectSpecificationType.OrgAll:
308279
throw new ContextError(
@@ -325,7 +296,7 @@ export const viewCommand = buildCommand({
325296
}
326297

327298
default:
328-
throw new ValidationError("Invalid target specification");
299+
throw new ContextError("Organization and project", USAGE_HINT);
329300
}
330301

331302
if (flags.web) {

src/commands/trace/view.ts

Lines changed: 10 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import { buildCommand } from "@stricli/core";
88
import type { SentryContext } from "../../context.js";
9-
import { findProjectsBySlug, getDetailedTrace } from "../../lib/api-client.js";
9+
import { getDetailedTrace } from "../../lib/api-client.js";
1010
import { parseOrgProjectArg, spansFlag } from "../../lib/arg-parsing.js";
1111
import { openInBrowser } from "../../lib/browser.js";
1212
import { ContextError, ValidationError } from "../../lib/errors.js";
@@ -17,7 +17,10 @@ import {
1717
writeFooter,
1818
writeJson,
1919
} from "../../lib/formatters/index.js";
20-
import { resolveOrgAndProject } from "../../lib/resolve-target.js";
20+
import {
21+
resolveOrgAndProject,
22+
resolveProjectBySlug,
23+
} from "../../lib/resolve-target.js";
2124
import { buildTraceUrl } from "../../lib/sentry-urls.js";
2225
import type { Writer } from "../../types/index.js";
2326

@@ -75,46 +78,6 @@ export type ResolvedTraceTarget = {
7578
detectedFrom?: string;
7679
};
7780

78-
/**
79-
* Resolve target from a project search result.
80-
*
81-
* Searches for a project by slug across all accessible organizations.
82-
* Throws if no project found or if multiple projects found in different orgs.
83-
*
84-
* @param projectSlug - Project slug to search for
85-
* @param traceId - Trace ID (used in error messages)
86-
* @returns Resolved target with org and project info
87-
* @throws {ContextError} If no project found
88-
* @throws {ValidationError} If project exists in multiple organizations
89-
*
90-
* @internal Exported for testing
91-
*/
92-
export async function resolveFromProjectSearch(
93-
projectSlug: string,
94-
traceId: string
95-
): Promise<ResolvedTraceTarget> {
96-
const found = await findProjectsBySlug(projectSlug);
97-
if (found.length === 0) {
98-
throw new ContextError(`Project "${projectSlug}"`, USAGE_HINT, [
99-
"Check that you have access to a project with this slug",
100-
]);
101-
}
102-
if (found.length > 1) {
103-
const orgList = found.map((p) => ` ${p.orgSlug}/${p.slug}`).join("\n");
104-
throw new ValidationError(
105-
`Project "${projectSlug}" exists in multiple organizations.\n\n` +
106-
`Specify the organization:\n${orgList}\n\n` +
107-
`Example: sentry trace view <org>/${projectSlug} ${traceId}`
108-
);
109-
}
110-
// Safe assertion: length is exactly 1 after the checks above
111-
const foundProject = found[0] as (typeof found)[0];
112-
return {
113-
org: foundProject.orgSlug,
114-
project: foundProject.slug,
115-
};
116-
}
117-
11881
/**
11982
* Write human-readable trace output to stdout.
12083
*
@@ -196,7 +159,11 @@ export const viewCommand = buildCommand({
196159
break;
197160

198161
case "project-search":
199-
target = await resolveFromProjectSearch(parsed.projectSlug, traceId);
162+
target = await resolveProjectBySlug(
163+
parsed.projectSlug,
164+
USAGE_HINT,
165+
`sentry trace view <org>/${parsed.projectSlug} ${traceId}`
166+
);
200167
break;
201168

202169
case "org-all":

0 commit comments

Comments
 (0)