Skip to content

Commit 89b237d

Browse files
committed
fix(arg-parsing): strip DSN 'o' prefix from org identifiers (#CLI-AA)
When users or AI agents extract org identifiers from Sentry DSN hosts (e.g., o1081365 from o1081365.ingest.us.sentry.io) and pass them as CLI arguments, the 'o' prefix causes API failures since the Sentry API accepts numeric org IDs but not the o-prefixed DSN form. Add stripDsnOrgPrefix() that normalizes oNNNNN → NNNNN in: - parseOrgProjectArg() for org-all and explicit modes - parseWithSlash() for issue arg parsing The pattern /^o(\d+)$/ only matches 'o' followed by pure digits, so normal slugs like 'organic' or 'o1abc' are unaffected. Fixes CLI-AA
1 parent 63d397e commit 89b237d

File tree

2 files changed

+144
-4
lines changed

2 files changed

+144
-4
lines changed

src/lib/arg-parsing.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,41 @@ import type { ParsedSentryUrl } from "./sentry-url-parser.js";
1111
import { applySentryUrlContext, parseSentryUrl } from "./sentry-url-parser.js";
1212
import { isAllDigits } from "./utils.js";
1313

14+
/**
15+
* Pattern matching DSN-style org identifiers: "o" followed by only digits.
16+
*
17+
* Sentry DSN hosts encode org IDs as `oNNNNN.ingest[.region].sentry.io`
18+
* (e.g., `o1081365.ingest.us.sentry.io`). When users or AI agents extract
19+
* the org portion from a DSN and pass it as a CLI argument (e.g.,
20+
* `sentry event view o1081365/ <event-id>`), the `o` prefix must be
21+
* stripped because the Sentry API accepts numeric org IDs but not the
22+
* `o`-prefixed DSN form.
23+
*/
24+
const DSN_ORG_PREFIX_PATTERN = /^o(\d+)$/;
25+
26+
/**
27+
* Normalize a DSN-style org identifier to a numeric org ID.
28+
*
29+
* DSN hosts encode org IDs as `oNNNNN` (e.g., `o1081365` from
30+
* `o1081365.ingest.us.sentry.io`). The Sentry API accepts numeric IDs
31+
* via `organization_id_or_slug` path parameters, but not the `o`-prefixed
32+
* DSN form.
33+
*
34+
* @param org - Raw org identifier from CLI input
35+
* @returns The numeric org ID if input matches `oNNNNN`, otherwise unchanged
36+
*
37+
* @example
38+
* stripDsnOrgPrefix("o1081365") // "1081365"
39+
* stripDsnOrgPrefix("o123") // "123"
40+
* stripDsnOrgPrefix("sentry") // "sentry" (no change)
41+
* stripDsnOrgPrefix("organic") // "organic" (no change — not all digits after 'o')
42+
*
43+
* @internal Exported for testing
44+
*/
45+
export function stripDsnOrgPrefix(org: string): string {
46+
return org.replace(DSN_ORG_PREFIX_PATTERN, "$1");
47+
}
48+
1449
/**
1550
* Validate that a CLI --limit flag value is within an allowed range.
1651
*
@@ -224,11 +259,11 @@ export function parseOrgProjectArg(arg: string | undefined): ParsedOrgProject {
224259

225260
if (!project) {
226261
// "sentry/" → list all projects in org
227-
return { type: "org-all", org };
262+
return { type: "org-all", org: stripDsnOrgPrefix(org) };
228263
}
229264

230265
// "sentry/cli" → explicit org and project
231-
return { type: "explicit", org, project };
266+
return { type: "explicit", org: stripDsnOrgPrefix(org), project };
232267
}
233268

234269
// No slash → search for project across all orgs
@@ -325,7 +360,7 @@ function parseAfterSlash(
325360
*/
326361
function parseWithSlash(arg: string): ParsedIssueArg {
327362
const slashIdx = arg.indexOf("/");
328-
const org = arg.slice(0, slashIdx);
363+
const rawOrg = arg.slice(0, slashIdx);
329364
const rest = arg.slice(slashIdx + 1);
330365

331366
if (!rest) {
@@ -334,7 +369,7 @@ function parseWithSlash(arg: string): ParsedIssueArg {
334369
);
335370
}
336371

337-
if (!org) {
372+
if (!rawOrg) {
338373
// Leading slash with dash → project-search (e.g., "/cli-G")
339374
if (rest.includes("-")) {
340375
return parseWithDash(rest);
@@ -343,6 +378,7 @@ function parseWithSlash(arg: string): ParsedIssueArg {
343378
return { type: "suffix-only", suffix: rest.toUpperCase() };
344379
}
345380

381+
const org = stripDsnOrgPrefix(rawOrg);
346382
return parseAfterSlash(arg, org, rest);
347383
}
348384

test/lib/arg-parsing.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,42 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
1010
import {
1111
parseIssueArg,
1212
parseOrgProjectArg,
13+
stripDsnOrgPrefix,
1314
} from "../../src/lib/arg-parsing.js";
1415
import { ValidationError } from "../../src/lib/errors.js";
1516

17+
describe("stripDsnOrgPrefix", () => {
18+
test("strips 'o' prefix from DSN-style org IDs", () => {
19+
expect(stripDsnOrgPrefix("o1081365")).toBe("1081365");
20+
expect(stripDsnOrgPrefix("o123")).toBe("123");
21+
expect(stripDsnOrgPrefix("o0")).toBe("0");
22+
expect(stripDsnOrgPrefix("o9999999999")).toBe("9999999999");
23+
});
24+
25+
test("preserves normal org slugs", () => {
26+
expect(stripDsnOrgPrefix("sentry")).toBe("sentry");
27+
expect(stripDsnOrgPrefix("my-org")).toBe("my-org");
28+
expect(stripDsnOrgPrefix("acme-corp")).toBe("acme-corp");
29+
});
30+
31+
test("preserves slugs starting with 'o' that have non-digit chars", () => {
32+
expect(stripDsnOrgPrefix("organic")).toBe("organic");
33+
expect(stripDsnOrgPrefix("org-name")).toBe("org-name");
34+
expect(stripDsnOrgPrefix("o1abc")).toBe("o1abc");
35+
expect(stripDsnOrgPrefix("open123")).toBe("open123");
36+
});
37+
38+
test("preserves pure numeric strings (no 'o' prefix)", () => {
39+
expect(stripDsnOrgPrefix("1081365")).toBe("1081365");
40+
expect(stripDsnOrgPrefix("123")).toBe("123");
41+
});
42+
43+
test("preserves empty string and 'o' alone", () => {
44+
expect(stripDsnOrgPrefix("")).toBe("");
45+
expect(stripDsnOrgPrefix("o")).toBe("o");
46+
});
47+
});
48+
1649
describe("parseOrgProjectArg", () => {
1750
// Representative examples for documentation (invariants covered by property tests)
1851
test("org/project returns explicit", () => {
@@ -38,6 +71,40 @@ describe("parseOrgProjectArg", () => {
3871
);
3972
});
4073

74+
// DSN org prefix normalization (Fixes CLI-AA)
75+
describe("DSN org prefix normalization", () => {
76+
test("strips 'o' prefix in org-all mode", () => {
77+
expect(parseOrgProjectArg("o1081365/")).toEqual({
78+
type: "org-all",
79+
org: "1081365",
80+
});
81+
});
82+
83+
test("strips 'o' prefix in explicit mode", () => {
84+
expect(parseOrgProjectArg("o1081365/myproject")).toEqual({
85+
type: "explicit",
86+
org: "1081365",
87+
project: "myproject",
88+
});
89+
});
90+
91+
test("does not strip from normal org slugs", () => {
92+
expect(parseOrgProjectArg("organic/cli")).toEqual({
93+
type: "explicit",
94+
org: "organic",
95+
project: "cli",
96+
});
97+
});
98+
99+
test("does not strip from slugs with mixed chars after 'o'", () => {
100+
expect(parseOrgProjectArg("o1abc/cli")).toEqual({
101+
type: "explicit",
102+
org: "o1abc",
103+
project: "cli",
104+
});
105+
});
106+
});
107+
41108
// URL integration tests — applySentryUrlContext may set SENTRY_URL as a side effect
42109
describe("Sentry URL inputs", () => {
43110
let savedSentryUrl: string | undefined;
@@ -264,6 +331,43 @@ describe("parseIssueArg", () => {
264331
});
265332
});
266333

334+
// DSN org prefix normalization (Fixes CLI-AA)
335+
describe("DSN org prefix normalization", () => {
336+
test("strips 'o' prefix from DSN-style org in explicit", () => {
337+
expect(parseIssueArg("o1081365/CLI-G")).toEqual({
338+
type: "explicit",
339+
org: "1081365",
340+
project: "CLI",
341+
suffix: "G",
342+
});
343+
});
344+
345+
test("strips 'o' prefix from DSN-style org in explicit-org-numeric", () => {
346+
expect(parseIssueArg("o999/123456789")).toEqual({
347+
type: "explicit-org-numeric",
348+
org: "999",
349+
numericId: "123456789",
350+
});
351+
});
352+
353+
test("strips 'o' prefix from DSN-style org in explicit-org-suffix", () => {
354+
expect(parseIssueArg("o1081365/G")).toEqual({
355+
type: "explicit-org-suffix",
356+
org: "1081365",
357+
suffix: "G",
358+
});
359+
});
360+
361+
test("does not strip from normal org slugs in issue args", () => {
362+
expect(parseIssueArg("organic/cli-G")).toEqual({
363+
type: "explicit",
364+
org: "organic",
365+
project: "cli",
366+
suffix: "G",
367+
});
368+
});
369+
});
370+
267371
// Edge cases - document tricky behaviors
268372
describe("edge cases", () => {
269373
test("/suffix returns suffix-only", () => {

0 commit comments

Comments
 (0)