Skip to content

Commit 499cee3

Browse files
authored
Merge pull request #14 from betegon/feat/event-command
feat(event): add event get command and refactor target resolution
2 parents a4007a1 + e690d3b commit 499cee3

File tree

9 files changed

+528
-172
lines changed

9 files changed

+528
-172
lines changed

packages/cli/src/app.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { buildApplication, buildRouteMap } from "@stricli/core";
22
import { apiCommand } from "./commands/api.js";
33
import { authRoute } from "./commands/auth/index.js";
4+
import { eventRoute } from "./commands/event/index.js";
45
import { issueRoute } from "./commands/issue/index.js";
56
import { orgRoute } from "./commands/org/index.js";
67
import { projectRoute } from "./commands/project/index.js";
@@ -11,6 +12,7 @@ const routes = buildRouteMap({
1112
org: orgRoute,
1213
project: projectRoute,
1314
issue: issueRoute,
15+
event: eventRoute,
1416
api: apiCommand,
1517
},
1618
docs: {
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/**
2+
* sentry event get
3+
*
4+
* Get detailed information about a Sentry event.
5+
*/
6+
7+
import { buildCommand } from "@stricli/core";
8+
import type { SentryContext } from "../../context.js";
9+
import { getEvent } from "../../lib/api-client.js";
10+
import { formatEventDetails } from "../../lib/formatters/human.js";
11+
import { writeJson } from "../../lib/formatters/json.js";
12+
import { resolveOrgAndProject } from "../../lib/resolve-target.js";
13+
import type { SentryEvent } from "../../types/index.js";
14+
15+
type GetFlags = {
16+
readonly org?: string;
17+
readonly project?: string;
18+
readonly json: boolean;
19+
};
20+
21+
/**
22+
* Write human-readable event output to stdout.
23+
*
24+
* @param stdout - Output stream
25+
* @param event - The event to display
26+
* @param detectedFrom - Optional source description for auto-detection
27+
*/
28+
function writeHumanOutput(
29+
stdout: NodeJS.WriteStream,
30+
event: SentryEvent,
31+
detectedFrom?: string
32+
): void {
33+
const lines = formatEventDetails(event, `Event ${event.eventID}`);
34+
35+
// Skip leading empty line for standalone display
36+
const output = lines.slice(1);
37+
stdout.write(`${output.join("\n")}\n`);
38+
39+
if (detectedFrom) {
40+
stdout.write(`\nDetected from ${detectedFrom}\n`);
41+
}
42+
}
43+
44+
export const getCommand = buildCommand({
45+
docs: {
46+
brief: "Get details of a specific event",
47+
fullDescription:
48+
"Retrieve detailed information about a Sentry event by its ID.\n\n" +
49+
"The organization and project are resolved from:\n" +
50+
" 1. --org and --project flags\n" +
51+
" 2. Config defaults\n" +
52+
" 3. SENTRY_DSN environment variable or source code detection",
53+
},
54+
parameters: {
55+
positional: {
56+
kind: "tuple",
57+
parameters: [
58+
{
59+
brief:
60+
"Event ID (hexadecimal, e.g., 9999aaaaca8b46d797c23c6077c6ff01)",
61+
parse: String,
62+
},
63+
],
64+
},
65+
flags: {
66+
org: {
67+
kind: "parsed",
68+
parse: String,
69+
brief: "Organization slug",
70+
optional: true,
71+
},
72+
project: {
73+
kind: "parsed",
74+
parse: String,
75+
brief: "Project slug",
76+
optional: true,
77+
},
78+
json: {
79+
kind: "boolean",
80+
brief: "Output as JSON",
81+
default: false,
82+
},
83+
},
84+
},
85+
async func(
86+
this: SentryContext,
87+
flags: GetFlags,
88+
eventId: string
89+
): Promise<void> {
90+
const { process, cwd } = this;
91+
const { stdout } = process;
92+
93+
const target = await resolveOrgAndProject({
94+
org: flags.org,
95+
project: flags.project,
96+
cwd,
97+
});
98+
99+
if (!target) {
100+
throw new Error(
101+
"Organization and project are required to fetch an event.\n\n" +
102+
"Please specify them using:\n" +
103+
` sentry event get ${eventId} --org <org-slug> --project <project-slug>\n\n` +
104+
"Or set SENTRY_DSN environment variable for automatic detection."
105+
);
106+
}
107+
108+
const event = await getEvent(target.org, target.project, eventId);
109+
110+
if (flags.json) {
111+
writeJson(stdout, event);
112+
return;
113+
}
114+
115+
writeHumanOutput(stdout, event, target.detectedFrom);
116+
},
117+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { buildRouteMap } from "@stricli/core";
2+
import { getCommand } from "./get.js";
3+
4+
export const eventRoute = buildRouteMap({
5+
routes: {
6+
get: getCommand,
7+
},
8+
docs: {
9+
brief: "View Sentry events",
10+
fullDescription:
11+
"View detailed event data from Sentry. " +
12+
"Use 'sentry event get <event-id>' to view a specific event.",
13+
hideRoute: {},
14+
},
15+
});

packages/cli/src/commands/issue/get.ts

Lines changed: 9 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -12,72 +12,33 @@ import {
1212
getLatestEvent,
1313
isShortId,
1414
} from "../../lib/api-client.js";
15-
import { getCachedProject, getDefaultOrganization } from "../../lib/config.js";
16-
import { detectDsn } from "../../lib/dsn/index.js";
1715
import {
1816
formatEventDetails,
1917
formatIssueDetails,
2018
} from "../../lib/formatters/human.js";
2119
import { writeJson } from "../../lib/formatters/json.js";
20+
import { resolveOrg } from "../../lib/resolve-target.js";
2221
import type { SentryEvent, SentryIssue } from "../../types/index.js";
2322

2423
type GetFlags = {
2524
readonly org?: string;
2625
readonly json: boolean;
27-
readonly event: boolean;
2826
};
2927

3028
/**
31-
* Try to fetch the latest event for an issue
29+
* Try to fetch the latest event for an issue.
30+
* Returns undefined if the fetch fails (non-blocking).
3231
*/
3332
async function tryGetLatestEvent(
3433
issueId: string
3534
): Promise<SentryEvent | undefined> {
3635
try {
3736
return await getLatestEvent(issueId);
3837
} catch {
39-
// Event fetch might fail, continue without it
4038
return;
4139
}
4240
}
4341

44-
/**
45-
* Resolve organization from various sources for short ID lookup
46-
*/
47-
async function resolveOrg(
48-
flagOrg: string | undefined,
49-
cwd: string
50-
): Promise<string | null> {
51-
// 1. Check CLI flag
52-
if (flagOrg) {
53-
return flagOrg;
54-
}
55-
56-
// 2. Check config defaults
57-
const defaultOrg = await getDefaultOrganization();
58-
if (defaultOrg) {
59-
return defaultOrg;
60-
}
61-
62-
// 3. Try DSN auto-detection
63-
try {
64-
const dsn = await detectDsn(cwd);
65-
if (dsn?.orgId) {
66-
// Check cache for org slug
67-
const cached = await getCachedProject(dsn.orgId, dsn.projectId);
68-
if (cached) {
69-
return cached.orgSlug;
70-
}
71-
// Fall back to numeric org ID (API accepts both)
72-
return dsn.orgId;
73-
}
74-
} catch {
75-
// Detection failed
76-
}
77-
78-
return null;
79-
}
80-
8142
/**
8243
* Write human-readable issue output
8344
*/
@@ -100,7 +61,7 @@ export const getCommand = buildCommand({
10061
brief: "Get details of a specific issue",
10162
fullDescription:
10263
"Retrieve detailed information about a Sentry issue by its ID or short ID. " +
103-
"Use --event to also fetch the latest event details.\n\n" +
64+
"The latest event is automatically included for full context.\n\n" +
10465
"For short IDs (e.g., SPOTLIGHT-ELECTRON-4D), the organization is resolved from:\n" +
10566
" 1. --org flag\n" +
10667
" 2. Config defaults\n" +
@@ -129,11 +90,6 @@ export const getCommand = buildCommand({
12990
brief: "Output as JSON",
13091
default: false,
13192
},
132-
event: {
133-
kind: "boolean",
134-
brief: "Also fetch the latest event",
135-
default: false,
136-
},
13793
},
13894
},
13995
async func(
@@ -149,22 +105,23 @@ export const getCommand = buildCommand({
149105
// Check if it's a short ID (contains letters) vs numeric ID
150106
if (isShortId(issueId)) {
151107
// Short ID requires organization context
152-
const org = await resolveOrg(flags.org, cwd);
153-
if (!org) {
108+
const resolved = await resolveOrg({ org: flags.org, cwd });
109+
if (!resolved) {
154110
throw new Error(
155111
"Organization is required for short ID lookup.\n\n" +
156112
"Please specify it using:\n" +
157113
` sentry issue get ${issueId} --org <org-slug>\n\n` +
158114
"Or set SENTRY_DSN environment variable for automatic detection."
159115
);
160116
}
161-
issue = await getIssueByShortId(org, issueId);
117+
issue = await getIssueByShortId(resolved.org, issueId);
162118
} else {
163119
// Numeric ID can be fetched directly
164120
issue = await getIssue(issueId);
165121
}
166122

167-
const event = flags.event ? await tryGetLatestEvent(issue.id) : undefined;
123+
// Always fetch the latest event for full context
124+
const event = await tryGetLatestEvent(issue.id);
168125

169126
if (flags.json) {
170127
const output = event ? { issue, event } : { issue };

0 commit comments

Comments
 (0)