Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/commands/dashboard/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ async function resolveDashboardTarget(
parsed.projectSlug,
"sentry dashboard create <org>/<project> <title>"
);
const pid = await fetchProjectId(found.org, found.project);
const pid = toNumericId(found.projectData.id);
return {
orgSlug: found.org,
projectIds: pid !== undefined ? [pid] : [],
Expand Down
3 changes: 2 additions & 1 deletion src/commands/event/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
10 changes: 6 additions & 4 deletions src/commands/project/delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]) {
Expand Down
10 changes: 7 additions & 3 deletions src/commands/project/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -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;
Expand Down
20 changes: 17 additions & 3 deletions src/lib/resolve-target.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import { basename } from "node:path";
import pLimit from "p-limit";
import type { SentryProject } from "../types/index.js";
import {
findProjectByDsnKey,
findProjectsByPattern,
Expand Down Expand Up @@ -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;
};

/**
Expand Down Expand Up @@ -947,15 +950,15 @@ 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
*/
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
Expand Down Expand Up @@ -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,
};
}

Expand Down Expand Up @@ -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;
};

/**
Expand Down Expand Up @@ -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": {
Expand Down
6 changes: 4 additions & 2 deletions test/commands/event/view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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();
});
});
});
Expand Down
3 changes: 2 additions & 1 deletion test/commands/log/view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
3 changes: 2 additions & 1 deletion test/commands/trace/list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
3 changes: 2 additions & 1 deletion test/commands/trace/view.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
6 changes: 4 additions & 2 deletions test/lib/resolve-target-listing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 () => {
Expand Down
Loading