Skip to content

Commit c9796b2

Browse files
betegonclaude
andcommitted
refactor(init): scope numeric org ID fix to sentry init only
The previous fix to resolveOrgFromDsn was too broad — returning null broke DSN org auto-detection for read commands like `sentry issue list` that work fine with numeric org IDs. Revert resolveOrgFromDsn to its original behavior. Instead, handle the numeric ID resolution in resolveOrgSlug (local-ops.ts), which is only used by sentry init's project creation path. When the prefetched org is a raw numeric string, attempt to resolve it to a slug via the org regions cache (getOrgByNumericId). On cache miss — empty cache or org from a different Sentry account — fall through to listOrganizations() so the user can select from their accessible orgs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 83cb197 commit c9796b2

3 files changed

Lines changed: 20 additions & 46 deletions

File tree

src/lib/init/local-ops.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -675,7 +675,19 @@ async function resolveOrgSlug(
675675
): Promise<string | LocalOpResult> {
676676
const resolved = await resolveOrgPrefetched(cwd);
677677
if (resolved) {
678-
return resolved.org;
678+
// If the detected org is a raw numeric ID (extracted from a DSN), try to
679+
// resolve it to a real slug. Numeric IDs can fail for write operations like
680+
// project/team creation, and may belong to a different Sentry account.
681+
if (/^\d+$/.test(resolved.org)) {
682+
const { getOrgByNumericId } = await import("../db/regions.js");
683+
const match = await getOrgByNumericId(resolved.org);
684+
if (match) {
685+
return match.slug;
686+
}
687+
// Cache miss — fall through to listOrganizations() for proper selection
688+
} else {
689+
return resolved.org;
690+
}
679691
}
680692

681693
// Fallback: list user's organizations (SQLite-cached after login/first call)

src/lib/resolve-target.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -231,18 +231,11 @@ export async function resolveOrgFromDsn(
231231
}
232232
}
233233

234-
// Check org regions cache for a slug mapped to this numeric ID.
235-
// The cache is populated by `sentry auth login` and `org list`.
236-
const { getOrgByNumericId } = await import("./db/regions.js");
237-
const orgMatch = await getOrgByNumericId(dsn.orgId);
238-
if (orgMatch) {
239-
return { org: orgMatch.slug, detectedFrom };
240-
}
241-
242-
// Can't resolve to slug (empty cache or org belongs to a different account).
243-
// Return null so the caller falls through to listOrganizations() for proper
244-
// interactive org selection instead of passing a raw numeric ID to the API.
245-
return null;
234+
// Fall back to numeric org ID (API accepts both slug and numeric ID)
235+
return {
236+
org: dsn.orgId,
237+
detectedFrom,
238+
};
246239
}
247240

248241
/**

test/isolated/resolve-target.test.ts

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ const mockGetProject = mock(() =>
5353
);
5454
const mockFindProjectByDsnKey = mock(() => Promise.resolve(null));
5555
const mockFindProjectsByPattern = mock(() => Promise.resolve([]));
56-
const mockGetOrgByNumericId = mock(() => Promise.resolve(undefined));
5756

5857
// Mock all dependency modules
5958
mock.module("../../src/lib/db/defaults.js", () => ({
@@ -92,10 +91,6 @@ mock.module("../../src/lib/api-client.js", () => ({
9291
findProjectsByPattern: mockFindProjectsByPattern,
9392
}));
9493

95-
mock.module("../../src/lib/db/regions.js", () => ({
96-
getOrgByNumericId: mockGetOrgByNumericId,
97-
}));
98-
9994
import { ContextError } from "../../src/lib/errors.js";
10095
// Now import the module under test (after mocks are set up)
10196
import {
@@ -123,7 +118,6 @@ function resetAllMocks() {
123118
mockGetProject.mockReset();
124119
mockFindProjectByDsnKey.mockReset();
125120
mockFindProjectsByPattern.mockReset();
126-
mockGetOrgByNumericId.mockReset();
127121

128122
// Set sensible defaults
129123
mockGetDefaultOrganization.mockResolvedValue(null);
@@ -145,7 +139,6 @@ function resetAllMocks() {
145139
mockGetCachedProject.mockResolvedValue(null);
146140
mockGetCachedProjectByDsnKey.mockResolvedValue(null);
147141
mockGetCachedDsn.mockResolvedValue(null);
148-
mockGetOrgByNumericId.mockResolvedValue(undefined);
149142
mockFindProjectsByPattern.mockResolvedValue([]);
150143
}
151144

@@ -205,26 +198,7 @@ describe("resolveOrg", () => {
205198
expect(mockDetectDsn).toHaveBeenCalled();
206199
});
207200

208-
test("returns null when DSN detected but no project/org cache", async () => {
209-
mockGetDefaultOrganization.mockResolvedValue(null);
210-
mockDetectDsn.mockResolvedValue({
211-
raw: "https://abc@o123.ingest.sentry.io/456",
212-
protocol: "https",
213-
publicKey: "abc",
214-
host: "o123.ingest.sentry.io",
215-
projectId: "456",
216-
orgId: "123",
217-
source: "env",
218-
});
219-
mockGetCachedProject.mockResolvedValue(null);
220-
mockGetOrgByNumericId.mockResolvedValue(undefined);
221-
222-
const result = await resolveOrg({ cwd: "/test" });
223-
224-
expect(result).toBeNull();
225-
});
226-
227-
test("resolves org slug from regions cache when project cache misses", async () => {
201+
test("returns numeric orgId when DSN detected but no cache", async () => {
228202
mockGetDefaultOrganization.mockResolvedValue(null);
229203
mockDetectDsn.mockResolvedValue({
230204
raw: "https://abc@o123.ingest.sentry.io/456",
@@ -236,16 +210,11 @@ describe("resolveOrg", () => {
236210
source: "env",
237211
});
238212
mockGetCachedProject.mockResolvedValue(null);
239-
mockGetOrgByNumericId.mockResolvedValue({
240-
slug: "my-org",
241-
regionUrl: "https://sentry.io",
242-
});
243213

244214
const result = await resolveOrg({ cwd: "/test" });
245215

246216
expect(result).not.toBeNull();
247-
expect(result?.org).toBe("my-org");
248-
expect(mockGetOrgByNumericId).toHaveBeenCalledWith("123");
217+
expect(result?.org).toBe("123");
249218
});
250219

251220
test("returns null when no org found from any source", async () => {

0 commit comments

Comments
 (0)