diff --git a/src/commands/dashboard/create.ts b/src/commands/dashboard/create.ts index 517d0387b..50082a961 100644 --- a/src/commands/dashboard/create.ts +++ b/src/commands/dashboard/create.ts @@ -99,7 +99,7 @@ async function resolveDashboardTarget( parsed.projectSlug, "sentry dashboard create / " ); - const pid = await fetchProjectId(found.org, found.project); + const pid = toNumericId(found.projectData.id); return { orgSlug: found.org, projectIds: pid !== undefined ? [pid] : [], diff --git a/src/commands/event/view.ts b/src/commands/event/view.ts index ed1dace45..1a5d5ad67 100644 --- a/src/commands/event/view.ts +++ b/src/commands/event/view.ts @@ -229,7 +229,8 @@ export async function resolveEventTarget( `sentry event view <org>/${parsed.projectSlug} ${eventId}` ); return { - ...resolved, + org: resolved.org, + project: resolved.project, orgDisplay: resolved.org, projectDisplay: resolved.project, }; diff --git a/src/commands/project/delete.ts b/src/commands/project/delete.ts index d18971abb..b44742b61 100644 --- a/src/commands/project/delete.ts +++ b/src/commands/project/delete.ts @@ -241,11 +241,13 @@ export const deleteCommand = buildCommand({ ); } - const { org: orgSlug, project: projectSlug } = - await resolveOrgProjectTarget(parsed, cwd, COMMAND_NAME); + const resolved = await resolveOrgProjectTarget(parsed, cwd, COMMAND_NAME); + const { org: orgSlug, project: projectSlug } = resolved; - // Verify project exists before prompting — also used to display the project name - const project = await getProject(orgSlug, projectSlug); + // Use already-fetched project data from project-search, or fetch for + // explicit/auto-detect paths (also verifies the project exists) + const project = + resolved.projectData ?? (await getProject(orgSlug, projectSlug)); // Dry-run mode: show what would be deleted without deleting it if (flags["dry-run"]) { diff --git a/src/commands/project/view.ts b/src/commands/project/view.ts index ae28ee8c9..1e8743031 100644 --- a/src/commands/project/view.ts +++ b/src/commands/project/view.ts @@ -99,9 +99,11 @@ async function fetchProjectDetails( target: ResolvedTarget ): Promise<ProjectWithDsn | null> { const result = await withAuthGuard(async () => { - // Fetch project and DSN in parallel + // Fetch project (skip if already fetched during resolution) and DSN in parallel const [project, dsn] = await Promise.all([ - getProject(target.org, target.project), + target.projectData + ? Promise.resolve(target.projectData) + : getProject(target.org, target.project), tryGetPrimaryDsn(target.org, target.project), ]); return { project, dsn }; @@ -239,9 +241,11 @@ export const viewCommand = buildCommand({ ); resolvedTargets = [ { - ...resolved, + org: resolved.org, + project: resolved.project, orgDisplay: resolved.org, projectDisplay: resolved.project, + projectData: resolved.projectData, }, ]; break; diff --git a/src/lib/resolve-target.ts b/src/lib/resolve-target.ts index 11e0f3513..e00ff7085 100644 --- a/src/lib/resolve-target.ts +++ b/src/lib/resolve-target.ts @@ -15,6 +15,7 @@ import { basename } from "node:path"; import pLimit from "p-limit"; +import type { SentryProject } from "../types/index.js"; import { findProjectByDsnKey, findProjectsByPattern, @@ -86,6 +87,8 @@ export type ResolvedTarget = { detectedFrom?: string; /** Package path in monorepo (e.g., "packages/frontend") */ packagePath?: string; + /** Full project data when already fetched (avoids redundant getProject re-fetch) */ + projectData?: SentryProject; }; /** @@ -947,7 +950,7 @@ export async function resolveOrg( * @param projectSlug - Project slug to search for * @param usageHint - Usage example shown in error messages * @param disambiguationExample - Example command for multi-org disambiguation (e.g., "sentry event view <org>/frontend abc123") - * @returns Resolved org and project slugs + * @returns Resolved org, project slugs, and the full project data (avoids redundant re-fetch) * @throws {ContextError} If no project found * @throws {ValidationError} If project exists in multiple organizations */ @@ -955,7 +958,7 @@ export async function resolveProjectBySlug( projectSlug: string, usageHint: string, disambiguationExample?: string -): Promise<{ org: string; project: string }> { +): Promise<{ org: string; project: string; projectData: SentryProject }> { const { projects, orgs } = await findProjectsBySlug(projectSlug); if (projects.length === 0) { // Check if the slug matches an organization — common mistake @@ -1003,9 +1006,13 @@ export async function resolveProjectBySlug( ); } + // Strip orgSlug (from ProjectWithOrg) so projectData is a clean SentryProject + // — prevents leaking the extra field into JSON output when callers spread it. + const { orgSlug: _org, ...projectData } = foundProject; return { org: foundProject.orgSlug, project: foundProject.slug, + projectData, }; } @@ -1072,6 +1079,8 @@ export type ResolvedOrgProject = { org: string; /** Project slug */ project: string; + /** Full project data when resolved via project-search (avoids redundant re-fetch) */ + projectData?: SentryProject; }; /** @@ -1148,7 +1157,12 @@ export async function resolveOrgProjectTarget( } const match = projects[0] as (typeof projects)[number]; - return { org: match.orgSlug, project: match.slug }; + const { orgSlug: _org, ...matchData } = match; + return { + org: match.orgSlug, + project: match.slug, + projectData: matchData, + }; } case "auto-detect": { diff --git a/test/commands/event/view.test.ts b/test/commands/event/view.test.ts index 73ac67769..0820e399d 100644 --- a/test/commands/event/view.test.ts +++ b/test/commands/event/view.test.ts @@ -373,10 +373,11 @@ describe("resolveProjectBySlug", () => { const result = await resolveProjectBySlug("backend", HINT); - expect(result).toEqual({ + expect(result).toMatchObject({ org: "my-company", project: "backend", }); + expect(result.projectData).toBeDefined(); }); test("uses orgSlug from project result", async () => { @@ -447,7 +448,8 @@ describe("resolveProjectBySlug", () => { }); const result = await resolveProjectBySlug("7275560680", HINT); - expect(result).toEqual({ org: "acme", project: "my-frontend" }); + expect(result).toMatchObject({ org: "acme", project: "my-frontend" }); + expect(result.projectData).toBeDefined(); }); }); }); diff --git a/test/commands/log/view.test.ts b/test/commands/log/view.test.ts index 31cdd8025..3c0fd867d 100644 --- a/test/commands/log/view.test.ts +++ b/test/commands/log/view.test.ts @@ -354,10 +354,11 @@ describe("resolveProjectBySlug", () => { const result = await resolveProjectBySlug("backend", HINT); - expect(result).toEqual({ + expect(result).toMatchObject({ org: "my-company", project: "backend", }); + expect(result.projectData).toBeDefined(); }); test("uses orgSlug from project result", async () => { diff --git a/test/commands/trace/list.test.ts b/test/commands/trace/list.test.ts index 796014ce9..741f8a31b 100644 --- a/test/commands/trace/list.test.ts +++ b/test/commands/trace/list.test.ts @@ -146,7 +146,8 @@ describe("resolveOrgProjectFromArg", () => { "/tmp", "trace list" ); - expect(result).toEqual({ org: "acme", project: "frontend" }); + expect(result).toMatchObject({ org: "acme", project: "frontend" }); + expect(result.projectData).toBeDefined(); }); test("throws when no project found", async () => { diff --git a/test/commands/trace/view.test.ts b/test/commands/trace/view.test.ts index 9f52453ac..ce817bfef 100644 --- a/test/commands/trace/view.test.ts +++ b/test/commands/trace/view.test.ts @@ -158,10 +158,11 @@ describe("resolveProjectBySlug", () => { const result = await resolveProjectBySlug("backend", HINT); - expect(result).toEqual({ + expect(result).toMatchObject({ org: "my-company", project: "backend", }); + expect(result.projectData).toBeDefined(); }); test("uses orgSlug from project result", async () => { diff --git a/test/lib/resolve-target-listing.test.ts b/test/lib/resolve-target-listing.test.ts index 37945ce50..ccbd3442c 100644 --- a/test/lib/resolve-target-listing.test.ts +++ b/test/lib/resolve-target-listing.test.ts @@ -165,7 +165,8 @@ describe("resolveOrgProjectTarget", () => { const parsed = { type: "project-search" as const, projectSlug: "my-proj" }; const result = await resolveOrgProjectTarget(parsed, CWD, "trace list"); - expect(result).toEqual({ org: "found-org", project: "my-proj" }); + expect(result).toMatchObject({ org: "found-org", project: "my-proj" }); + expect(result.projectData).toBeDefined(); }); test("throws ResolutionError for project-search when no match", async () => { @@ -297,7 +298,8 @@ describe("resolveOrgProjectFromArg", () => { }); const result = await resolveOrgProjectFromArg("my-proj", CWD, "log list"); - expect(result).toEqual({ org: "found-org", project: "my-proj" }); + expect(result).toMatchObject({ org: "found-org", project: "my-proj" }); + expect(result.projectData).toBeDefined(); }); test("throws ContextError for 'org/' (org-all) string", async () => {