Skip to content

Commit 0516332

Browse files
betegonclaude
andcommitted
perf(issue): skip org listing when DSN or cache resolves project
issue view with project-search format (e.g., `buybridge-5BS`) was making 5 sequential HTTP calls totaling ~2.4s. The biggest overhead was listOrganizations() (getUserRegions + listOrganizationsInRegion = ~800ms) inside findProjectsBySlug, even when DSN detection already identified the project. Two optimizations: 1. DSN-aware shortcut in resolveProjectSearch: after alias cache miss, check resolveFromDsn() for a matching project before falling back to findProjectsBySlug. Saves ~1200ms on cached runs, ~800ms first run. 2. Cached-orgs fast path in findProjectsBySlug: check org_regions cache before calling listOrganizations(). Saves ~800ms when org data is cached from any previous command. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9ed32c7 commit 0516332

File tree

2 files changed

+70
-25
lines changed

2 files changed

+70
-25
lines changed

src/commands/issue/utils.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ import { expandToFullShortId, isShortSuffix } from "../../lib/issue-id.js";
2323
import { logger } from "../../lib/logger.js";
2424
import { poll } from "../../lib/polling.js";
2525
import { resolveEffectiveOrg } from "../../lib/region.js";
26-
import { resolveOrg, resolveOrgAndProject } from "../../lib/resolve-target.js";
26+
import {
27+
resolveFromDsn,
28+
resolveOrg,
29+
resolveOrgAndProject,
30+
} from "../../lib/resolve-target.js";
2731
import { parseSentryUrl } from "../../lib/sentry-url-parser.js";
2832
import { isAllDigits } from "../../lib/utils.js";
2933
import type { SentryIssue } from "../../types/index.js";
@@ -148,7 +152,25 @@ async function resolveProjectSearch(
148152
return aliasResult;
149153
}
150154

151-
// 2. Search for project across all accessible orgs
155+
// 2. Check if DSN detection already resolved this project.
156+
// resolveFromDsn() reads from the DSN cache (populated by detectAllDsns
157+
// in tryResolveFromAlias above) + project cache. This avoids the expensive
158+
// listOrganizations() fan-out when the DSN matches the target project.
159+
try {
160+
const dsnTarget = await resolveFromDsn(cwd);
161+
if (
162+
dsnTarget &&
163+
dsnTarget.project.toLowerCase() === projectSlug.toLowerCase()
164+
) {
165+
const fullShortId = expandToFullShortId(suffix, dsnTarget.project);
166+
const issue = await getIssueByShortId(dsnTarget.org, fullShortId);
167+
return { org: dsnTarget.org, issue };
168+
}
169+
} catch {
170+
// DSN resolution failed — fall through to full search
171+
}
172+
173+
// 3. Search for project across all accessible orgs
152174
const { projects } = await findProjectsBySlug(projectSlug.toLowerCase());
153175

154176
if (projects.length === 0) {

src/lib/api/projects.ts

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
SentryProject,
1919
} from "../../types/index.js";
2020

21+
import { getAllOrgRegions } from "../db/regions.js";
2122
import { type AuthGuardSuccess, withAuthGuard } from "../errors.js";
2223
import { logger } from "../logger.js";
2324
import { getApiBaseUrl } from "../sentry-client.js";
@@ -176,34 +177,56 @@ export type ProjectSearchResult = {
176177
export async function findProjectsBySlug(
177178
projectSlug: string
178179
): Promise<ProjectSearchResult> {
179-
const orgs = await listOrganizations();
180180
const isNumericId = isAllDigits(projectSlug);
181181

182-
// Direct lookup in parallel — one API call per org instead of paginating all projects
183-
const searchResults = await Promise.all(
184-
orgs.map((org) =>
185-
withAuthGuard(async () => {
186-
const project = await getProject(org.slug, projectSlug);
187-
// The API accepts project_id_or_slug, so a numeric input could
188-
// resolve by ID instead of slug. When the input is all digits,
189-
// accept the match (the user passed a numeric project ID).
190-
// For non-numeric inputs, verify the slug actually matches to
191-
// avoid false positives from coincidental ID collisions.
192-
// Note: Sentry enforces that project slugs must start with a letter,
193-
// so an all-digits input can only ever be a numeric ID, never a slug.
194-
if (!isNumericId && project.slug !== projectSlug) {
195-
return null;
196-
}
197-
return { ...project, orgSlug: org.slug };
198-
})
199-
)
200-
);
182+
/** Search for the project in a list of org slugs (parallel getProject per org). */
183+
const searchOrgs = (orgSlugs: string[]) =>
184+
Promise.all(
185+
orgSlugs.map((orgSlug) =>
186+
withAuthGuard(async () => {
187+
const project = await getProject(orgSlug, projectSlug);
188+
// The API accepts project_id_or_slug, so a numeric input could
189+
// resolve by ID instead of slug. When the input is all digits,
190+
// accept the match (the user passed a numeric project ID).
191+
// For non-numeric inputs, verify the slug actually matches to
192+
// avoid false positives from coincidental ID collisions.
193+
// Note: Sentry enforces that project slugs must start with a letter,
194+
// so an all-digits input can only ever be a numeric ID, never a slug.
195+
if (!isNumericId && project.slug !== projectSlug) {
196+
return null;
197+
}
198+
return { ...project, orgSlug };
199+
})
200+
)
201+
);
201202

202-
return {
203-
projects: searchResults
203+
const extractProjects = (
204+
results: Awaited<ReturnType<typeof searchOrgs>>
205+
): ProjectWithOrg[] =>
206+
results
204207
.filter((r): r is AuthGuardSuccess<ProjectWithOrg | null> => r.ok)
205208
.map((r) => r.value)
206-
.filter((v): v is ProjectWithOrg => v !== null),
209+
.filter((v): v is ProjectWithOrg => v !== null);
210+
211+
// Fast path: use cached org slugs to skip the expensive listOrganizations()
212+
// round-trip (getUserRegions + listOrganizationsInRegion).
213+
const cachedOrgRegions = await getAllOrgRegions();
214+
if (cachedOrgRegions.size > 0) {
215+
const cachedSlugs = [...cachedOrgRegions.keys()];
216+
const cachedResults = await searchOrgs(cachedSlugs);
217+
const cachedProjects = extractProjects(cachedResults);
218+
if (cachedProjects.length > 0) {
219+
return { projects: cachedProjects, orgs: [] };
220+
}
221+
// Fall through: project might be in a new org not yet cached
222+
}
223+
224+
// Full listing: fetch all orgs from API, then search each
225+
const orgs = await listOrganizations();
226+
const searchResults = await searchOrgs(orgs.map((o) => o.slug));
227+
228+
return {
229+
projects: extractProjects(searchResults),
207230
orgs,
208231
};
209232
}

0 commit comments

Comments
 (0)