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
3 changes: 0 additions & 3 deletions src/commands/event/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -344,9 +344,6 @@ export const viewCommand = buildCommand({
log.warn(suggestion);
}
const parsed = parseOrgProjectArg(targetArg);
if (parsed.type !== "auto-detect" && parsed.normalized) {
log.warn("Normalized slug (Sentry slugs use dashes, not underscores)");
}

const target = await resolveEventTarget({
parsed,
Expand Down
3 changes: 0 additions & 3 deletions src/commands/log/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,9 +358,6 @@ export const viewCommand = buildCommand({
cmdLog.warn(suggestion);
}
const parsed = parseOrgProjectArg(targetArg);
if (parsed.type !== "auto-detect" && parsed.normalized) {
cmdLog.warn("Normalized slug (Sentry slugs use dashes, not underscores)");
}

const target = await resolveTarget(parsed, logIds, cwd);

Expand Down
5 changes: 0 additions & 5 deletions src/commands/span/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import {
FRESH_FLAG,
LIST_CURSOR_FLAG,
} from "../../lib/list-command.js";
import { logger } from "../../lib/logger.js";
import {
resolveOrgAndProject,
resolveProjectBySlug,
Expand Down Expand Up @@ -281,14 +280,10 @@ export const listCommand = buildCommand({
async *func(this: SentryContext, flags: ListFlags, ...args: string[]) {
applyFreshFlag(flags);
const { cwd, setContext } = this;
const log = logger.withTag("span.list");

// Parse positional args
const { traceId, targetArg } = parsePositionalArgs(args);
const parsed = parseOrgProjectArg(targetArg);
if (parsed.type !== "auto-detect" && parsed.normalized) {
log.warn("Normalized slug (Sentry slugs use dashes, not underscores)");
}

// Resolve target
let target: { org: string; project: string } | null = null;
Expand Down
4 changes: 0 additions & 4 deletions src/commands/span/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,14 +284,10 @@ export const viewCommand = buildCommand({
async *func(this: SentryContext, flags: ViewFlags, ...args: string[]) {
applyFreshFlag(flags);
const { cwd, setContext } = this;
const cmdLog = logger.withTag("span.view");

// Parse positional args: first is trace ID (with optional target), rest are span IDs
const { traceId, spanIds, targetArg } = parsePositionalArgs(args);
const parsed = parseOrgProjectArg(targetArg);
if (parsed.type !== "auto-detect" && parsed.normalized) {
cmdLog.warn("Normalized slug (Sentry slugs use dashes, not underscores)");
}

const target = await resolveTarget(parsed, traceId, cwd);

Expand Down
3 changes: 0 additions & 3 deletions src/commands/trace/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,6 @@ export const viewCommand = buildCommand({
log.warn(suggestion);
}
const parsed = parseOrgProjectArg(targetArg);
if (parsed.type !== "auto-detect" && parsed.normalized) {
log.warn("Normalized slug (Sentry slugs use dashes, not underscores)");
}

let target: ResolvedTraceTarget | null = null;

Expand Down
56 changes: 47 additions & 9 deletions src/lib/arg-parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import { ContextError, ValidationError } from "./errors.js";
import { validateResourceId } from "./input-validation.js";
import { logger } from "./logger.js";
import type { ParsedSentryUrl } from "./sentry-url-parser.js";
import { applySentryUrlContext, parseSentryUrl } from "./sentry-url-parser.js";
import { isAllDigits } from "./utils.js";
Expand Down Expand Up @@ -40,6 +41,36 @@ export function normalizeSlug(slug: string): {
return { slug, normalized: false };
}

const log = logger.withTag("arg-parsing");

/**
* Emit a warning when slug normalization replaced underscores with dashes.
* Called internally by {@link parseOrgProjectArg} — callers do not need to
* check `parsed.normalized` themselves.
*/
function warnNormalized(
parsed: Exclude<ParsedOrgProject, { type: "auto-detect" }>
): void {
let slug: string;
switch (parsed.type) {
case "explicit":
slug = `${parsed.org}/${parsed.project}`;
break;
case "org-all":
slug = `${parsed.org}/`;
break;
case "project-search":
slug = parsed.projectSlug;
break;
default:
return;
}

log.warn(
`Normalized slug to '${slug}' (Sentry slugs use dashes, never underscores)`
);
}

// ---------------------------------------------------------------------------
// Issue short ID detection
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -420,18 +451,25 @@ export function parseOrgProjectArg(arg: string | undefined): ParsedOrgProject {
return orgProjectFromUrl(urlParsed);
}

let parsed: ParsedOrgProject;
if (trimmed.includes("/")) {
return parseSlashOrgProject(trimmed);
parsed = parseSlashOrgProject(trimmed);
} else {
// No slash → search for project across all orgs
validateResourceId(trimmed, "project slug");
const np = normalizeSlug(trimmed);
parsed = {
type: "project-search",
projectSlug: np.slug,
...(np.normalized && { normalized: true }),
};
}

// No slash → search for project across all orgs
validateResourceId(trimmed, "project slug");
const np = normalizeSlug(trimmed);
return {
type: "project-search",
projectSlug: np.slug,
...(np.normalized && { normalized: true }),
};
if (parsed.type !== "auto-detect" && parsed.normalized) {
warnNormalized(parsed);
}

return parsed;
}

/**
Expand Down
76 changes: 75 additions & 1 deletion test/lib/arg-parsing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* error messages and edge cases.
*/

import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
import {
detectSwappedTrialArgs,
detectSwappedViewArgs,
Expand Down Expand Up @@ -177,6 +177,80 @@ describe("parseOrgProjectArg", () => {
});
});
});

describe("slug normalization warning", () => {
let stderrSpy: ReturnType<typeof spyOn>;
let stderrOutput: string;

beforeEach(() => {
stderrOutput = "";
stderrSpy = spyOn(process.stderr, "write").mockImplementation(
(chunk: string | Uint8Array) => {
stderrOutput += typeof chunk === "string" ? chunk : "";
return true;
}
);
});

afterEach(() => {
stderrSpy.mockRestore();
});

test("emits warning for underscored project slug", () => {
const result = parseOrgProjectArg("my_project");
expect(result).toEqual({
type: "project-search",
projectSlug: "my-project",
normalized: true,
});
expect(stderrOutput).toContain("Normalized slug to 'my-project'");
expect(stderrOutput).toContain(
"Sentry slugs use dashes, never underscores"
);
});

test("emits warning for underscored org in explicit mode", () => {
const result = parseOrgProjectArg("my_org/cli");
expect(result).toEqual({
type: "explicit",
org: "my-org",
project: "cli",
normalized: true,
});
expect(stderrOutput).toContain("Normalized slug to 'my-org/cli'");
});

test("emits warning for underscored project in explicit mode", () => {
const result = parseOrgProjectArg("sentry/my_project");
expect(result).toEqual({
type: "explicit",
org: "sentry",
project: "my-project",
normalized: true,
});
expect(stderrOutput).toContain("Normalized slug to 'sentry/my-project'");
});

test("emits warning for underscored org in org-all mode", () => {
const result = parseOrgProjectArg("my_org/");
expect(result).toEqual({
type: "org-all",
org: "my-org",
normalized: true,
});
expect(stderrOutput).toContain("Normalized slug to 'my-org/'");
});

test("does not emit warning for auto-detect", () => {
parseOrgProjectArg(undefined);
expect(stderrOutput).not.toContain("Normalized slug");
});

test("does not emit warning when no underscores present", () => {
parseOrgProjectArg("sentry/cli");
expect(stderrOutput).not.toContain("Normalized slug");
});
});
});

describe("parseIssueArg", () => {
Expand Down
Loading