Skip to content

Commit 762369b

Browse files
authored
fix(event): detect SHORT-ID/EVENT-ID format in event view (#574)
## Summary When a user passes `PHP-SYMFONY-HY/7388f6a62b7d436ab77bb5365f97a1ac` to `sentry event view`, the single-slash format was interpreted as `org/project` with a missing event ID, producing: ``` Error: Event ID is required. Specify them using: sentry event view <org>/<project> <event-id> ``` ## Fix `parseSingleArg()` now detects the `SHORT-ID/EVENT-ID` pattern where: - The part **before** the slash matches an issue short ID (uppercase + dashes, e.g., `CLI-G5`, `PHP-SYMFONY-HY`) - The part **after** the slash is a valid 32-char hex event ID (with UUID-dash normalization) When matched, the issue short ID is resolved to find the org/project, then the specified event is fetched. **Priority:** The `org/SHORT-ID` pattern (e.g., `figma/FULLSCREEN-2RN`) is checked first and still takes precedence — org slugs are lowercase while issue short IDs are uppercase, so there's no ambiguity. ## Sentry Issue Fixes CLI-HV (8 users, 10 events in 0.20.0) ## Changes - `src/commands/event/view.ts`: Added SHORT-ID/EVENT-ID detection in `parseSingleArg()` - `test/commands/event/view.test.ts`: Added tests for the new pattern + priority verification
1 parent 3c9950f commit 762369b

2 files changed

Lines changed: 113 additions & 11 deletions

File tree

src/commands/event/view.ts

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { ApiError, ContextError, ResolutionError } from "../../lib/errors.js";
2727
import { formatEventDetails } from "../../lib/formatters/index.js";
2828
import { filterFields } from "../../lib/formatters/json.js";
2929
import { CommandOutput } from "../../lib/formatters/output.js";
30+
import { HEX_ID_RE, normalizeHexId } from "../../lib/hex-id.js";
3031
import {
3132
applyFreshFlag,
3233
FRESH_ALIASES,
@@ -114,20 +115,39 @@ const USAGE_HINT = "sentry event view <org>/<project> <event-id>";
114115
* "org/project" with a missing event ID.
115116
*/
116117
function parseSingleArg(arg: string): ParsedPositionalArgs {
117-
// Detect "org/SHORT-ID" pattern before parseSlashSeparatedArg.
118-
// e.g., "figma/FULLSCREEN-2RN" → auto-redirect to that issue's latest event.
118+
// Detect "org/SHORT-ID" and "SHORT-ID/EVENT-ID" patterns before
119+
// parseSlashSeparatedArg, which throws ContextError for single-slash args.
119120
const slashIdx = arg.indexOf("/");
120121
if (slashIdx !== -1 && arg.indexOf("/", slashIdx + 1) === -1) {
122+
const beforeSlash = arg.slice(0, slashIdx);
121123
const afterSlash = arg.slice(slashIdx + 1);
124+
125+
// "org/SHORT-ID" → auto-redirect to that issue's latest event.
126+
// e.g., "figma/FULLSCREEN-2RN"
122127
if (afterSlash && looksLikeIssueShortId(afterSlash)) {
123128
// Use "org/" (trailing slash) to signal OrgAll mode so downstream
124129
// parseOrgProjectArg interprets this as an org, not a project search.
125130
return {
126131
eventId: "latest",
127-
targetArg: `${arg.slice(0, slashIdx)}/`,
132+
targetArg: `${beforeSlash}/`,
128133
issueShortId: afterSlash,
129134
};
130135
}
136+
137+
// "SHORT-ID/EVENT-ID" → view a specific event identified by issue short ID.
138+
// e.g., "CLI-G5/abc123def456abc123def456abc123de"
139+
if (
140+
beforeSlash &&
141+
looksLikeIssueShortId(beforeSlash) &&
142+
afterSlash &&
143+
HEX_ID_RE.test(normalizeHexId(afterSlash))
144+
) {
145+
return {
146+
eventId: normalizeHexId(afterSlash),
147+
targetArg: undefined,
148+
issueShortId: beforeSlash,
149+
};
150+
}
131151
}
132152

133153
const { id: eventId, targetArg } = parseSlashSeparatedArg(
@@ -497,6 +517,26 @@ async function resolveIssueShortIdEvent(
497517
return fetchLatestEventData(org, issue.id, spans);
498518
}
499519

520+
/**
521+
* Fetch a specific event by ID (not latest) and build full EventViewData
522+
* including optional span tree. Used by the SHORT-ID/EVENT-ID path.
523+
*/
524+
async function fetchSpecificEventData(
525+
org: string,
526+
project: string,
527+
eventId: string,
528+
spans: number
529+
): Promise<EventViewData> {
530+
const event = await getEvent(org, project, eventId);
531+
const spanTreeResult =
532+
spans > 0 ? await getSpanTreeLines(org, event, spans) : undefined;
533+
const trace =
534+
spanTreeResult?.success && spanTreeResult.traceId
535+
? { traceId: spanTreeResult.traceId, spans: spanTreeResult.spans ?? [] }
536+
: null;
537+
return { event, trace, spanTreeLines: spanTreeResult?.lines };
538+
}
539+
500540
/** Result from an issue-based shortcut (URL or short ID) */
501541
type IssueShortcutResult = {
502542
org: string;
@@ -507,6 +547,7 @@ type IssueShortcutResult = {
507547
/** Options for resolving issue-based shortcuts */
508548
type IssueShortcutOptions = {
509549
parsed: ReturnType<typeof parseOrgProjectArg>;
550+
eventId: string;
510551
issueId: string | undefined;
511552
issueShortId: string | undefined;
512553
cwd: string;
@@ -524,7 +565,7 @@ type IssueShortcutOptions = {
524565
async function resolveIssueShortcut(
525566
options: IssueShortcutOptions
526567
): Promise<IssueShortcutResult | null> {
527-
const { parsed, issueId, issueShortId, cwd, spans } = options;
568+
const { parsed, eventId, issueId, issueShortId, cwd, spans } = options;
528569
const log = logger.withTag("event.view");
529570

530571
// Issue URL shortcut: fetch the latest event directly via the issue ID.
@@ -540,13 +581,9 @@ async function resolveIssueShortcut(
540581
}
541582

542583
// Issue short ID auto-redirect: user passed an issue short ID
543-
// (e.g., "BRUNCHIE-APP-29" or "figma/FULLSCREEN-2RN") instead of a hex
544-
// event ID. Resolve the issue and show its latest event.
584+
// (e.g., "BRUNCHIE-APP-29" or "CLI-G5/abc123...") instead of or
585+
// alongside a hex event ID. Resolve the issue to get org/project.
545586
if (issueShortId) {
546-
log.warn(
547-
`'${issueShortId}' is an issue short ID, not an event ID. Showing the latest event.`
548-
);
549-
550587
// Use the explicit org from the parsed target if available (e.g.,
551588
// "figma/" → org-all with org "figma"), otherwise fall back to
552589
// auto-detection via DSN/env/config.
@@ -559,6 +596,35 @@ async function resolveIssueShortcut(
559596
);
560597
}
561598

599+
// When the user specified a specific event ID (SHORT-ID/EVENT-ID),
600+
// resolve the issue to get the project, then fetch the specific event.
601+
if (eventId !== "latest") {
602+
const issue = await getIssueByShortId(resolved.org, issueShortId);
603+
const issueProject = issue.project?.slug;
604+
if (!issueProject) {
605+
throw new ResolutionError(
606+
`Issue '${issueShortId}'`,
607+
"has no associated project",
608+
`sentry event view <org>/<project> ${eventId}`,
609+
["Specify the project explicitly to view this event"]
610+
);
611+
}
612+
const data = await fetchSpecificEventData(
613+
resolved.org,
614+
issueProject,
615+
eventId,
616+
spans
617+
);
618+
return {
619+
org: resolved.org,
620+
data,
621+
hint: `Viewing event ${eventId} for issue ${issueShortId}`,
622+
};
623+
}
624+
625+
log.warn(
626+
`'${issueShortId}' is an issue short ID, not an event ID. Showing the latest event.`
627+
);
562628
const data = await resolveIssueShortIdEvent(
563629
issueShortId,
564630
resolved.org,
@@ -624,9 +690,11 @@ export const viewCommand = buildCommand({
624690
const parsed = parseOrgProjectArg(targetArg);
625691

626692
// Handle issue-based shortcuts (issue URLs and short IDs) before
627-
// normal event resolution. Both paths fetch the latest event.
693+
// normal event resolution. When eventId is "latest", fetches the
694+
// latest event; otherwise fetches the specific event.
628695
const issueShortcut = await resolveIssueShortcut({
629696
parsed,
697+
eventId,
630698
issueId,
631699
issueShortId,
632700
cwd,

test/commands/event/view.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,40 @@ describe("parsePositionalArgs", () => {
9999
expect(result.issueShortId).toBe("CLI-G");
100100
});
101101

102+
test("detects SHORT-ID/EVENT-ID pattern (CLI-HV)", () => {
103+
const result = parsePositionalArgs([
104+
"CLI-G5/abc123def456abc123def456abc123de",
105+
]);
106+
expect(result.eventId).toBe("abc123def456abc123def456abc123de");
107+
expect(result.targetArg).toBeUndefined();
108+
expect(result.issueShortId).toBe("CLI-G5");
109+
});
110+
111+
test("detects multi-dash SHORT-ID/EVENT-ID pattern", () => {
112+
const result = parsePositionalArgs([
113+
"PHP-SYMFONY-HY/7388f6a62b7d436ab77bb5365f97a1ac",
114+
]);
115+
expect(result.eventId).toBe("7388f6a62b7d436ab77bb5365f97a1ac");
116+
expect(result.targetArg).toBeUndefined();
117+
expect(result.issueShortId).toBe("PHP-SYMFONY-HY");
118+
});
119+
120+
test("normalizes UUID-format event ID in SHORT-ID/EVENT-ID", () => {
121+
const result = parsePositionalArgs([
122+
"CLI-G5/7388f6a6-2b7d-436a-b77b-b5365f97a1ac",
123+
]);
124+
expect(result.eventId).toBe("7388f6a62b7d436ab77bb5365f97a1ac");
125+
expect(result.issueShortId).toBe("CLI-G5");
126+
});
127+
128+
test("org/SHORT-ID takes precedence over SHORT-ID/EVENT-ID", () => {
129+
// "figma/FULLSCREEN-2RN" → org + issue, not issue + event
130+
const result = parsePositionalArgs(["figma/FULLSCREEN-2RN"]);
131+
expect(result.eventId).toBe("latest");
132+
expect(result.targetArg).toBe("figma/");
133+
expect(result.issueShortId).toBe("FULLSCREEN-2RN");
134+
});
135+
102136
test("does not detect org/lowercase-slug as issue short ID", () => {
103137
// "my-org/my-project" is a normal org/project target, not an issue short ID.
104138
// parseSlashSeparatedArg will throw ContextError as expected.

0 commit comments

Comments
 (0)