Skip to content

Commit 0714d27

Browse files
authored
fix: suggest similar projects when project not found in org (CLI-C0) (#493)
## Problem When users specify an explicit org/project (e.g., `sentry issue list elide/elide-server`) and the project isn't found, the error only says: ``` Project 'elide-server' not found in organization 'elide'. Try: sentry issue list elide/<project> Or: - Check the project slug at https://sentry.io/organizations/elide/projects/ ``` This affects **36 users** ([CLI-C0](https://sentry.sentry.io/issues/7318131352/)). Users don't know what the correct project slug is. ## Fix Added a `findSimilarProjects()` helper that, on 404, lists available projects in the org and finds similar slugs using case-insensitive prefix/substring matching. Now the error includes suggestions: ``` Project 'elide-server' not found in organization 'elide'. Try: sentry issue list elide/<project> Or: - Similar projects: 'elide-api-server', 'elide-web' - Check the project slug at https://sentry.io/organizations/elide/projects/ ``` ## Design Decisions - **Best-effort**: `findSimilarProjects` catches all errors and returns empty array on failure — the error message still works without suggestions - **Lightweight matching**: Uses case-insensitive prefix/substring scoring (no heavy fuzzy matching library needed). Scores: exact case-insensitive match (3) > prefix match (2) > substring match (1) - **Limited results**: Returns at most 3 similar slugs to keep the error message readable - **Non-blocking**: Only runs on 404 errors, not on every project fetch
1 parent aaabc30 commit 0714d27

File tree

1 file changed

+59
-3
lines changed

1 file changed

+59
-3
lines changed

src/lib/resolve-target.ts

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
findProjectsByPattern,
2222
findProjectsBySlug,
2323
getProject,
24+
listProjects,
2425
} from "./api-client.js";
2526
import { type ParsedOrgProject, parseOrgProjectArg } from "./arg-parsing.js";
2627
import { getDefaultOrganization, getDefaultProject } from "./db/defaults.js";
@@ -551,12 +552,60 @@ function resolveFromEnvVars(): {
551552
return null;
552553
}
553554

555+
/**
556+
* Find project slugs in the org that are similar to the given slug.
557+
*
558+
* Uses case-insensitive prefix/substring matching — lightweight and
559+
* sufficient for the most common typo patterns (wrong casing, partial
560+
* slug, extra/missing hyphens). Falls back gracefully on API errors
561+
* since this is a best-effort hint, not a critical path.
562+
*
563+
* @param org - Organization slug to search in
564+
* @param slug - The project slug that wasn't found
565+
* @returns Up to 3 similar project slugs, or empty array on error
566+
*/
567+
async function findSimilarProjects(
568+
org: string,
569+
slug: string
570+
): Promise<string[]> {
571+
try {
572+
const projects = await listProjects(org);
573+
const lower = slug.toLowerCase();
574+
575+
// Score each project: exact-case-insensitive > prefix > substring > none
576+
const scored = projects
577+
.map((p) => {
578+
const pLower = p.slug.toLowerCase();
579+
if (pLower === lower) {
580+
return { slug: p.slug, score: 3 };
581+
}
582+
if (pLower.startsWith(lower) || lower.startsWith(pLower)) {
583+
return { slug: p.slug, score: 2 };
584+
}
585+
if (pLower.includes(lower) || lower.includes(pLower)) {
586+
return { slug: p.slug, score: 1 };
587+
}
588+
return { slug: p.slug, score: 0 };
589+
})
590+
.filter((s) => s.score > 0)
591+
.sort((a, b) => b.score - a.score);
592+
593+
return scored.slice(0, 3).map((s) => s.slug);
594+
} catch {
595+
// Best-effort — don't let listing failures block the error message
596+
return [];
597+
}
598+
}
599+
554600
/**
555601
* Fetch the numeric project ID for an explicit org/project pair.
556602
*
557603
* Throws on auth errors and 404s (user-actionable). Returns undefined
558604
* for transient failures (network, 500s) so the command can still
559605
* attempt slug-based querying as a fallback.
606+
*
607+
* On 404, attempts to list similar projects in the org to help the
608+
* user find the correct slug (CLI-C0, 36 users).
560609
*/
561610
export async function fetchProjectId(
562611
org: string,
@@ -568,13 +617,20 @@ export async function fetchProjectId(
568617
projectResult.error instanceof ApiError &&
569618
projectResult.error.status === 404
570619
) {
620+
const similar = await findSimilarProjects(org, project);
621+
const suggestions = [
622+
`Check the project slug at https://sentry.io/organizations/${org}/projects/`,
623+
];
624+
if (similar.length > 0) {
625+
suggestions.unshift(
626+
`Similar projects: ${similar.map((s) => `'${s}'`).join(", ")}`
627+
);
628+
}
571629
throw new ResolutionError(
572630
`Project '${project}'`,
573631
`not found in organization '${org}'`,
574632
`sentry issue list ${org}/<project>`,
575-
[
576-
`Check the project slug at https://sentry.io/organizations/${org}/projects/`,
577-
]
633+
suggestions
578634
);
579635
}
580636
return;

0 commit comments

Comments
 (0)