@@ -12,12 +12,20 @@ import {
1212 getIssueInOrg ,
1313 type IssueSort ,
1414 listIssuesPaginated ,
15+ listOrganizations ,
1516 triggerRootCauseAnalysis ,
17+ tryGetIssueByShortId ,
1618} from "../../lib/api-client.js" ;
1719import { type IssueSelector , parseIssueArg } from "../../lib/arg-parsing.js" ;
1820import { getProjectByAlias } from "../../lib/db/project-aliases.js" ;
1921import { detectAllDsns } from "../../lib/dsn/index.js" ;
20- import { ApiError , ContextError , ResolutionError } from "../../lib/errors.js" ;
22+ import {
23+ ApiError ,
24+ type AuthGuardFailure ,
25+ ContextError ,
26+ ResolutionError ,
27+ withAuthGuard ,
28+ } from "../../lib/errors.js" ;
2129import { getProgressMessage } from "../../lib/formatters/seer.js" ;
2230import { expandToFullShortId , isShortSuffix } from "../../lib/issue-id.js" ;
2331import { logger } from "../../lib/logger.js" ;
@@ -124,12 +132,66 @@ async function tryResolveFromAlias(
124132 return { org : projectEntry . orgSlug , issue } ;
125133}
126134
135+ /**
136+ * Fallback for when the fast shortid fan-out found no matches.
137+ * Uses findProjectsBySlug to give a precise error ("project not found" vs
138+ * "issue not found") and retries the issue lookup on transient failures.
139+ */
140+ async function resolveProjectSearchFallback (
141+ projectSlug : string ,
142+ suffix : string ,
143+ commandHint : string
144+ ) : Promise < StrictResolvedIssue > {
145+ const { projects } = await findProjectsBySlug ( projectSlug . toLowerCase ( ) ) ;
146+
147+ if ( projects . length === 0 ) {
148+ throw new ResolutionError (
149+ `Project '${ projectSlug } '` ,
150+ "not found" ,
151+ commandHint ,
152+ [ "No project with this slug found in any accessible organization" ]
153+ ) ;
154+ }
155+
156+ if ( projects . length > 1 ) {
157+ const orgList = projects . map ( ( p ) => p . orgSlug ) . join ( ", " ) ;
158+ throw new ResolutionError (
159+ `Project '${ projectSlug } '` ,
160+ "is ambiguous" ,
161+ commandHint ,
162+ [
163+ `Found in: ${ orgList } ` ,
164+ `Specify the org: sentry issue ... <org>/${ projectSlug } -${ suffix } ` ,
165+ ]
166+ ) ;
167+ }
168+
169+ // Project exists — retry the issue lookup. The fast path may have failed
170+ // due to a transient error (5xx, timeout); retrying here either succeeds
171+ // or propagates the real error to the user.
172+ const matchedProject = projects [ 0 ] ;
173+ const matchedOrg = matchedProject ?. orgSlug ;
174+ if ( matchedOrg && matchedProject ) {
175+ const retryShortId = expandToFullShortId ( suffix , matchedProject . slug ) ;
176+ const issue = await getIssueByShortId ( matchedOrg , retryShortId ) ;
177+ return { org : matchedOrg , issue } ;
178+ }
179+
180+ throw new ResolutionError (
181+ `Project '${ projectSlug } '` ,
182+ "not found" ,
183+ commandHint
184+ ) ;
185+ }
186+
127187/**
128188 * Resolve project-search type: search for project across orgs, then fetch issue.
129189 *
130190 * Resolution order:
131191 * 1. Try alias cache (fast, local)
132- * 2. Search for project across orgs via API
192+ * 2. Check DSN detection cache
193+ * 3. Try shortid endpoint directly across all orgs (fast path)
194+ * 4. Fall back to findProjectsBySlug for precise error messages
133195 *
134196 * @param projectSlug - Project slug to search for
135197 * @param suffix - Issue suffix (uppercase)
@@ -173,20 +235,33 @@ async function resolveProjectSearch(
173235 return { org : dsnTarget . org , issue } ;
174236 }
175237
176- // 3. Search for project across all accessible orgs
177- const { projects } = await findProjectsBySlug ( projectSlug . toLowerCase ( ) ) ;
238+ // 3. Fast path: try resolving the short ID directly across all orgs.
239+ // The shortid endpoint validates both project existence and issue existence
240+ // in a single call, eliminating the separate getProject() round-trip.
241+ const fullShortId = expandToFullShortId ( suffix , projectSlug ) ;
242+ const orgs = await listOrganizations ( ) ;
178243
179- if ( projects . length === 0 ) {
180- throw new ResolutionError (
181- `Project '${ projectSlug } '` ,
182- "not found" ,
183- commandHint ,
184- [ "No project with this slug found in any accessible organization" ]
185- ) ;
244+ const results = await Promise . all (
245+ orgs . map ( ( org ) =>
246+ withAuthGuard ( ( ) => tryGetIssueByShortId ( org . slug , fullShortId ) )
247+ )
248+ ) ;
249+
250+ const successes : StrictResolvedIssue [ ] = [ ] ;
251+ for ( let i = 0 ; i < results . length ; i ++ ) {
252+ const result = results [ i ] ;
253+ const org = orgs [ i ] ;
254+ if ( result && org && result . ok && result . value ) {
255+ successes . push ( { org : org . slug , issue : result . value } ) ;
256+ }
186257 }
187258
188- if ( projects . length > 1 ) {
189- const orgList = projects . map ( ( p ) => p . orgSlug ) . join ( ", " ) ;
259+ if ( successes . length === 1 && successes [ 0 ] ) {
260+ return successes [ 0 ] ;
261+ }
262+
263+ if ( successes . length > 1 ) {
264+ const orgList = successes . map ( ( s ) => s . org ) . join ( ", " ) ;
190265 throw new ResolutionError (
191266 `Project '${ projectSlug } '` ,
192267 "is ambiguous" ,
@@ -198,18 +273,24 @@ async function resolveProjectSearch(
198273 ) ;
199274 }
200275
201- const project = projects [ 0 ] ;
202- if ( ! project ) {
203- throw new ResolutionError (
204- `Project '${ projectSlug } '` ,
205- "not found" ,
206- commandHint
207- ) ;
276+ // If every org failed with a real error (403, 5xx, network timeout),
277+ // surface it instead of falling through to a misleading "not found".
278+ // Only throw when ALL results are errors — if some orgs returned clean
279+ // 404s ({ok: true, value: null}), fall through to the fallback for a
280+ // precise error message.
281+ const realErrors = results . filter (
282+ ( r ) : r is AuthGuardFailure => r !== undefined && ! r . ok
283+ ) ;
284+ if ( realErrors . length === results . length && realErrors . length > 0 ) {
285+ const firstError = realErrors [ 0 ] ?. error ;
286+ if ( firstError instanceof Error ) {
287+ throw firstError ;
288+ }
208289 }
209290
210- const fullShortId = expandToFullShortId ( suffix , project . slug ) ;
211- const issue = await getIssueByShortId ( project . orgSlug , fullShortId ) ;
212- return { org : project . orgSlug , issue } ;
291+ // 4. Fall back to findProjectsBySlug for precise error messages
292+ // and retry the issue lookup (handles transient failures).
293+ return resolveProjectSearchFallback ( projectSlug , suffix , commandHint ) ;
213294}
214295
215296/**
0 commit comments