Skip to content

Commit e3ca859

Browse files
authored
fix(event-view): validate event ID format before API call (CLI-156) (#751)
## Summary - Add `validateHexId()` to event view command, matching trace/log view behavior — malformed event IDs are now caught before making API calls - Add flag-like string detection in `validateHexId()` — inputs starting with `-` (e.g., `--h`, `--verbose`) produce a targeted hint instead of the generic "doesn't look like a hex ID" message - Extend `maybeRecoverWithHelp` to recognize `--h` and `-help` as help-seeking tokens, showing actual help output on error Fixes [CLI-156](https://sentry.sentry.io/issues/7410782826/) ## Context When a user types `sentry event --h` (a common typo for `--help` or `-h`), Stricli doesn't recognize `--h` as a help flag — it only handles `--help` and `-h`. Its long-flag regex also requires 2+ chars after `--`, so `--h` isn't treated as a flag at all. It falls through as a positional argument to `event view` (the default command). Unlike `trace view` and `log view` which call `validateHexId()`, event view had no format validation before API calls, so `--h` was sent as an event ID, producing a confusing `ResolutionError: Event '--h' not found`. ## Changes | File | Change | |------|--------| | `src/lib/hex-id.ts` | New `FLAG_LIKE_RE`/`HELP_FLAG_RE` regexes; flag-like detection branch before existing span-ID and slug hints | | `src/commands/event/view.ts` | `validateHexId(eventId, "event ID")` in `func()` after parsing, before resolution; skips sentinels from issue URL/short ID paths | | `src/lib/command.ts` | `maybeRecoverWithHelp` now also matches `"--h"` and `"-help"` in positional args | | `test/lib/hex-id.test.ts` | 4 new tests for flag-like detection (help hint, generic flag hint, precedence over slug hint) | | `test/commands/event/view.test.ts` | 2 new `viewCommand.func` tests for `--h` and non-hex IDs; updated existing func tests to use valid 32-char hex IDs |
1 parent dff7cad commit e3ca859

File tree

6 files changed

+138
-14
lines changed

6 files changed

+138
-14
lines changed

src/commands/event/view.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import {
3232
import { formatEventDetails } from "../../lib/formatters/index.js";
3333
import { filterFields } from "../../lib/formatters/json.js";
3434
import { CommandOutput } from "../../lib/formatters/output.js";
35-
import { HEX_ID_RE, normalizeHexId } from "../../lib/hex-id.js";
35+
import { HEX_ID_RE, normalizeHexId, validateHexId } from "../../lib/hex-id.js";
3636
import {
3737
applyFreshFlag,
3838
FRESH_ALIASES,
@@ -737,11 +737,20 @@ export const viewCommand = buildCommand({
737737
const log = logger.withTag("event.view");
738738

739739
// Parse positional args
740-
const { eventId, targetArg, warning, issueId, issueShortId } =
740+
let { eventId, targetArg, warning, issueId, issueShortId } =
741741
parsePositionalArgs(args);
742742
if (warning) {
743743
log.warn(warning);
744744
}
745+
746+
// Validate event ID format early (before API calls) when the ID came
747+
// from user input. Skip when the ID is a sentinel from issue URL/short
748+
// ID detection — those paths resolve the event through issue lookup.
749+
// Capture the normalized return value (lowercased, UUID dashes stripped).
750+
if (eventId !== LATEST_EVENT_SENTINEL && !issueId && !issueShortId) {
751+
eventId = validateHexId(eventId, "event ID");
752+
}
753+
745754
const parsed = parseOrgProjectArg(targetArg);
746755

747756
// Handle issue-based shortcuts (issue URLs and short IDs) before

src/lib/command.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -485,8 +485,8 @@ export function buildCommand<
485485

486486
/**
487487
* When a command throws a {@link CliError} and a positional arg was
488-
* `"help"`, the user likely intended `--help`. Show the command's
489-
* help instead of the confusing error.
488+
* `"help"`, `"--h"`, or `"-help"`, the user likely intended `--help`.
489+
* Show the command's help instead of the confusing error.
490490
*
491491
* Only fires as **error recovery** — if the command succeeds with a
492492
* legitimate value like a project named "help", this never runs.
@@ -511,7 +511,10 @@ export function buildCommand<
511511
if (!(err instanceof CliError) || err instanceof OutputError) {
512512
return false;
513513
}
514-
if (args.length === 0 || !args.some((a) => a === "help")) {
514+
if (
515+
args.length === 0 ||
516+
!args.some((a) => a === "help" || a === "--h" || a === "-help")
517+
) {
515518
return false;
516519
}
517520
if (!ctx.commandPrefix) {

src/lib/hex-id.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ const MAX_DISPLAY_LENGTH = 40;
2727
/** Matches any character that is NOT a lowercase hex digit (used for slug detection in error hints) */
2828
const NON_HEX_RE = /[^0-9a-f]/;
2929

30+
/** Matches strings starting with a dash — likely CLI flags that Stricli didn't recognize */
31+
const FLAG_LIKE_RE = /^-/;
32+
33+
/** Matches common help flag typos (e.g., "--h", "-h", "--help", "-help") */
34+
const HELP_FLAG_RE = /^--?h(elp)?$/;
35+
3036
/**
3137
* Normalize a potential hex ID: trim, lowercase, strip UUID dashes.
3238
* Does NOT validate — call this before checking {@link HEX_ID_RE}.
@@ -77,8 +83,18 @@ export function validateHexId(value: string, label: string): string {
7783
`Invalid ${label} "${display}". Expected a 32-character hexadecimal string.\n\n` +
7884
"Example: abc123def456abc123def456abc123de";
7985

80-
// Detect common misidentified entity types and add helpful hints
81-
if (SPAN_ID_RE.test(normalized)) {
86+
// Detect common misidentified entity types and add helpful hints.
87+
// Flag-like check first — strings starting with "-" are almost certainly
88+
// CLI flags that Stricli didn't recognize (e.g., "--h" instead of "-h").
89+
if (FLAG_LIKE_RE.test(normalized)) {
90+
if (HELP_FLAG_RE.test(normalized)) {
91+
message +=
92+
"\n\nThis looks like a help flag. Use --help or -h for help.";
93+
} else {
94+
message +=
95+
"\n\nThis looks like a CLI flag, not a hex ID. Check flag syntax with --help.";
96+
}
97+
} else if (SPAN_ID_RE.test(normalized)) {
8298
// 16-char hex looks like a span ID
8399
message +=
84100
"\n\nThis looks like a span ID (16 characters). " +

test/commands/event/view.test.ts

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -787,8 +787,9 @@ describe("viewCommand.func", () => {
787787
let openInBrowserSpy: ReturnType<typeof spyOn>;
788788
let resolveProjectBySlugSpy: ReturnType<typeof spyOn>;
789789

790+
const VALID_EVENT_ID = "abc123def456abc123def456abc123de";
790791
const sampleEvent: SentryEvent = {
791-
eventID: "abc123def456",
792+
eventID: VALID_EVENT_ID,
792793
title: "Error: test",
793794
metadata: {},
794795
contexts: {},
@@ -833,11 +834,11 @@ describe("viewCommand.func", () => {
833834

834835
const { context } = createMockContext();
835836
const func = await viewCommand.loader();
836-
// "abc123def456" has no slash, "test-org/test-proj" has slash → swap detected
837+
// Valid 32-char hex has no slash, "test-org/test-proj" has slash → swap detected
837838
await func.call(
838839
context,
839840
{ json: true, web: false, spans: 0 },
840-
"abc123def456",
841+
VALID_EVENT_ID,
841842
"test-org/test-proj"
842843
);
843844

@@ -904,13 +905,45 @@ describe("viewCommand.func", () => {
904905
context,
905906
{ json: true, web: false, spans: 0 },
906907
"test_org/test_proj",
907-
"abc123def456"
908+
VALID_EVENT_ID
908909
);
909910

910911
// parseOrgProjectArg normalizes "test_org/test_proj" → "test-org/test-proj"
911912
// and sets normalized=true, triggering the warning path (line 343-345)
912913
expect(getEventSpy).toHaveBeenCalled();
913914
});
915+
916+
test("throws ValidationError for flag-like event ID (--h)", async () => {
917+
const { context } = createMockContext();
918+
const func = await viewCommand.loader();
919+
920+
try {
921+
await func.call(context, { json: false, web: false, spans: 0 }, "--h");
922+
expect.unreachable("Should have thrown");
923+
} catch (error) {
924+
expect(error).toBeInstanceOf(ValidationError);
925+
const msg = (error as ValidationError).message;
926+
expect(msg).toContain("event ID");
927+
expect(msg).toContain("looks like a help flag");
928+
}
929+
});
930+
931+
test("throws ValidationError for non-hex event ID", async () => {
932+
const { context } = createMockContext();
933+
const func = await viewCommand.loader();
934+
935+
try {
936+
await func.call(
937+
context,
938+
{ json: false, web: false, spans: 0 },
939+
"not-a-hex-id"
940+
);
941+
expect.unreachable("Should have thrown");
942+
} catch (error) {
943+
expect(error).toBeInstanceOf(ValidationError);
944+
expect((error as ValidationError).message).toContain("event ID");
945+
}
946+
});
914947
});
915948

916949
describe("fetchEventWithContext", () => {

test/e2e/event.test.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ describe("sentry event view", () => {
5252
"event",
5353
"view",
5454
`${TEST_ORG}/${TEST_PROJECT}`,
55-
"abc123",
55+
"abc123def456abc123def456abc123de",
5656
]);
5757

5858
expect(result.exitCode).toBe(1);
@@ -62,12 +62,27 @@ describe("sentry event view", () => {
6262
test("requires org and project without DSN", async () => {
6363
await ctx.setAuthToken(TEST_TOKEN);
6464

65-
const result = await ctx.run(["event", "view", "abc123"]);
65+
const result = await ctx.run([
66+
"event",
67+
"view",
68+
"abc123def456abc123def456abc123de",
69+
]);
6670

6771
expect(result.exitCode).toBe(1);
6872
expect(result.stderr + result.stdout).toMatch(/organization|project/i);
6973
});
7074

75+
test("rejects invalid event ID format", async () => {
76+
await ctx.setAuthToken(TEST_TOKEN);
77+
78+
const result = await ctx.run(["event", "view", "abc123"]);
79+
80+
expect(result.exitCode).toBe(1);
81+
expect(result.stderr + result.stdout).toMatch(
82+
/invalid event id|32-character hexadecimal/i
83+
);
84+
});
85+
7186
test("handles non-existent event", async () => {
7287
await ctx.setAuthToken(TEST_TOKEN);
7388

@@ -76,7 +91,7 @@ describe("sentry event view", () => {
7691
"event",
7792
"view",
7893
`${TEST_ORG}/${TEST_PROJECT}`,
79-
"nonexistent123",
94+
"abc123def456abc123def456abc123de",
8095
]);
8196

8297
expect(result.exitCode).toBe(1);

test/lib/hex-id.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,54 @@ describe("validateHexId", () => {
136136
}
137137
});
138138

139+
test("error hints help flag for --h input", () => {
140+
try {
141+
validateHexId("--h", "event ID");
142+
expect.unreachable("Should have thrown");
143+
} catch (error) {
144+
expect(error).toBeInstanceOf(ValidationError);
145+
const msg = (error as ValidationError).message;
146+
expect(msg).toContain("looks like a help flag");
147+
expect(msg).toContain("--help or -h");
148+
}
149+
});
150+
151+
test("error hints help flag for -help input", () => {
152+
try {
153+
validateHexId("-help", "trace ID");
154+
expect.unreachable("Should have thrown");
155+
} catch (error) {
156+
expect(error).toBeInstanceOf(ValidationError);
157+
const msg = (error as ValidationError).message;
158+
expect(msg).toContain("looks like a help flag");
159+
}
160+
});
161+
162+
test("error hints generic flag for --verbose input", () => {
163+
try {
164+
validateHexId("--verbose", "log ID");
165+
expect.unreachable("Should have thrown");
166+
} catch (error) {
167+
expect(error).toBeInstanceOf(ValidationError);
168+
const msg = (error as ValidationError).message;
169+
expect(msg).toContain("looks like a CLI flag");
170+
expect(msg).toContain("--help");
171+
}
172+
});
173+
174+
test("flag-like detection takes precedence over slug hint", () => {
175+
try {
176+
validateHexId("--my-flag", "event ID");
177+
expect.unreachable("Should have thrown");
178+
} catch (error) {
179+
expect(error).toBeInstanceOf(ValidationError);
180+
const msg = (error as ValidationError).message;
181+
expect(msg).toContain("looks like a CLI flag");
182+
// Should NOT suggest project slug
183+
expect(msg).not.toContain("doesn't look like a hex ID");
184+
}
185+
});
186+
139187
test("no extra hint for random-length hex (not a span ID)", () => {
140188
try {
141189
validateHexId("abc123", "log ID");

0 commit comments

Comments
 (0)