From d6ceae7182a872b1074094f36961f4f3e3962452 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Beteg=C3=B3n?= Date: Mon, 6 Apr 2026 16:49:02 +0200 Subject: [PATCH 1/2] refactor: extract createProjectWithDsn to deduplicate project creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "create project → fetch DSN → build URL" sequence was duplicated between sentry init (local-ops.ts) and sentry project create (create.ts). Extract createProjectWithDsn into api/projects.ts as the shared core, so future changes to the creation flow only need updating in one place. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/commands/project/create.ts | 29 +++++++++++++---------------- src/lib/api-client.ts | 2 ++ src/lib/api/projects.ts | 25 +++++++++++++++++++++++++ src/lib/init/local-ops.ts | 19 +++++++------------ 4 files changed, 47 insertions(+), 28 deletions(-) diff --git a/src/commands/project/create.ts b/src/commands/project/create.ts index 344388ca1..46d23bb9d 100644 --- a/src/commands/project/create.ts +++ b/src/commands/project/create.ts @@ -9,8 +9,8 @@ * 1. Parse name arg → extract org prefix if present (e.g., "acme/my-app") * 2. Resolve org → CLI flag > env vars > config defaults > DSN auto-detection * 3. Resolve team → `--team` flag > auto-select single team > auto-create if empty - * 4. Call `createProject` API - * 5. Fetch DSN (best-effort) and display results + * 4. Call `createProjectWithDsn` (creates project, fetches DSN, builds URL) + * 5. Display results * * When the team is auto-selected or auto-created, the output includes a note * so the user knows which team was used and how to change it. @@ -18,9 +18,9 @@ import type { SentryContext } from "../../context.js"; import { - createProject, + type CreatedProjectDetails, + createProjectWithDsn, listTeams, - tryGetPrimaryDsn, } from "../../lib/api-client.js"; import { parseOrgProjectArg } from "../../lib/arg-parsing.js"; import { buildCommand } from "../../lib/command.js"; @@ -52,9 +52,7 @@ import { type ResolvedTeam, resolveOrCreateTeam, } from "../../lib/resolve-team.js"; -import { buildProjectUrl } from "../../lib/sentry-urls.js"; import { slugify } from "../../lib/utils.js"; -import type { SentryProject } from "../../types/index.js"; const log = logger.withTag("project.create"); @@ -227,7 +225,7 @@ async function handleCreateProject404(opts: { } /** - * Create a project with user-friendly error handling. + * Create a project (with DSN + URL) with user-friendly error handling. * Wraps API errors with actionable messages instead of raw HTTP status codes. */ async function createProjectWithErrors(opts: { @@ -236,10 +234,10 @@ async function createProjectWithErrors(opts: { name: string; platform: string; detectedFrom?: string; -}): Promise { +}): Promise { const { orgSlug, teamSlug, name, platform } = opts; try { - return await createProject(orgSlug, teamSlug, { name, platform }); + return await createProjectWithDsn(orgSlug, teamSlug, { name, platform }); } catch (error) { if (error instanceof ApiError) { if (error.status === 409) { @@ -253,7 +251,9 @@ async function createProjectWithErrors(opts: { throw new CliError(buildPlatformError(`${orgSlug}/${name}`, platform)); } if (error.status === 404) { - return await handleCreateProject404(opts); + // handleCreateProject404 always throws — cast needed because + // createProjectWithDsn's return type differs from SentryProject + return await (handleCreateProject404(opts) as never); } throw new CliError( `Failed to create project '${name}' in ${orgSlug}.\n\n` + @@ -407,8 +407,8 @@ export const createCommand = buildCommand({ return yield new CommandOutput(result); } - // Create the project - const project = await createProjectWithErrors({ + // Create the project, fetch DSN, and build URL + const { project, dsn, url } = await createProjectWithErrors({ orgSlug, teamSlug: team.slug, name, @@ -416,9 +416,6 @@ export const createCommand = buildCommand({ detectedFrom: resolved.detectedFrom, }); - // Fetch DSN (best-effort) - const dsn = await tryGetPrimaryDsn(orgSlug, project.slug); - const result: ProjectCreatedResult = { project, orgSlug, @@ -426,7 +423,7 @@ export const createCommand = buildCommand({ teamSource: team.source, requestedPlatform: platform, dsn, - url: buildProjectUrl(orgSlug, project.slug), + url, slugDiverged: project.slug !== expectedSlug, expectedSlug, }; diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index 71649db13..427e543d8 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -74,6 +74,8 @@ export { } from "./api/organizations.js"; export { createProject, + type CreatedProjectDetails, + createProjectWithDsn, deleteProject, findProjectByDsnKey, findProjectsByPattern, diff --git a/src/lib/api/projects.ts b/src/lib/api/projects.ts index b7ee5ce7b..af3017702 100644 --- a/src/lib/api/projects.ts +++ b/src/lib/api/projects.ts @@ -26,6 +26,7 @@ import { getCachedOrganizations } from "../db/regions.js"; import { type AuthGuardSuccess, withAuthGuard } from "../errors.js"; import { logger } from "../logger.js"; import { getApiBaseUrl } from "../sentry-client.js"; +import { buildProjectUrl } from "../sentry-urls.js"; import { isAllDigits } from "../utils.js"; import { @@ -168,6 +169,30 @@ export async function createProject( return data as unknown as SentryProject; } +/** Result of creating a project and fetching its DSN + dashboard URL. */ +export type CreatedProjectDetails = { + project: SentryProject; + dsn: string | null; + url: string; +}; + +/** + * Create a project, fetch its DSN, and build its dashboard URL. + * + * Shared core used by both `sentry project create` and `sentry init`. + * Callers handle their own error wrapping and team resolution. + */ +export async function createProjectWithDsn( + orgSlug: string, + teamSlug: string, + body: CreateProjectBody +): Promise { + const project = await createProject(orgSlug, teamSlug, body); + const dsn = await tryGetPrimaryDsn(orgSlug, project.slug); + const url = buildProjectUrl(orgSlug, project.slug); + return { project, dsn, url }; +} + /** * Delete a project from an organization. * diff --git a/src/lib/init/local-ops.ts b/src/lib/init/local-ops.ts index 92760e692..69dcf79f8 100644 --- a/src/lib/init/local-ops.ts +++ b/src/lib/init/local-ops.ts @@ -10,7 +10,7 @@ import fs from "node:fs"; import path from "node:path"; import { isCancel, select } from "@clack/prompts"; import { - createProject, + createProjectWithDsn, getProject, listOrganizations, tryGetPrimaryDsn, @@ -919,17 +919,12 @@ async function createSentryProject( usageHint: "sentry init", }); - // 5. Create project - const project = await createProject(orgSlug, team.slug, { - name, - platform, - }); - - // 6. Get DSN (best-effort) - const dsn = await tryGetPrimaryDsn(orgSlug, project.slug); - - // 7. Build URL - const url = buildProjectUrl(orgSlug, project.slug); + // 5. Create project, fetch DSN, and build URL + const { project, dsn, url } = await createProjectWithDsn( + orgSlug, + team.slug, + { name, platform } + ); return { ok: true, From e3c0c670d570b95838060131b6fd74d2bbf005f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Beteg=C3=B3n?= Date: Mon, 6 Apr 2026 16:57:42 +0200 Subject: [PATCH 2/2] fix: sort imports in api-client.ts to satisfy biome Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/api-client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/api-client.ts b/src/lib/api-client.ts index 427e543d8..bf025a422 100644 --- a/src/lib/api-client.ts +++ b/src/lib/api-client.ts @@ -73,8 +73,8 @@ export { listOrganizationsUncached, } from "./api/organizations.js"; export { - createProject, type CreatedProjectDetails, + createProject, createProjectWithDsn, deleteProject, findProjectByDsnKey,