Skip to content

Commit 744801c

Browse files
betegonclaudeBYK
authored
feat(dashboard): add dashboard list, view, and create commands (#406)
## Summary - Add `sentry dashboard list` — list dashboards with `--web` and `--json` support - Add `sentry dashboard view` — view dashboard detail by numeric ID or title (case-insensitive match) - Add `sentry dashboard create` — create dashboards with optional inline `--widget-*` flags for quick widget creation - Add dashboard API client functions (`listDashboards`, `getDashboard`, `createDashboard`, `updateDashboard`) - Add Zod-validated aggregate function and search filter constants for spans/discover datasets - Add `parseAggregate()` / `parseSortExpression()` shorthand for CLI-friendly aggregate syntax (e.g. `count` → `count()`, `p95:span.duration` → `p95(span.duration)`) - Add auto-layout engine that packs widgets into the 6-column grid Split from #401. Widget commands (add/edit/delete) follow in a stacked PR. ## Test plan - [x] `bun run typecheck` — no new type errors - [x] `bun run lint` — passes - [x] `bun test test/types/dashboard.test.ts` — 53 tests pass (enum constants, schema validation, parseAggregate, parseSortExpression, prepareWidgetQueries) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Burak Yigit Kaya <byk@sentry.io>
1 parent dfc2100 commit 744801c

20 files changed

Lines changed: 2669 additions & 45 deletions

File tree

AGENTS.md

Lines changed: 38 additions & 42 deletions
Large diffs are not rendered by default.

plugins/sentry-cli/skills/sentry-cli/SKILL.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,39 @@ Update the Sentry CLI to the latest version
505505
- `--json - Output as JSON`
506506
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
507507

508+
### Dashboard
509+
510+
Manage Sentry dashboards
511+
512+
#### `sentry dashboard list <org/project>`
513+
514+
List dashboards
515+
516+
**Flags:**
517+
- `-w, --web - Open in browser`
518+
- `-n, --limit <value> - Maximum number of dashboards to list - (default: "30")`
519+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
520+
- `--json - Output as JSON`
521+
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
522+
523+
#### `sentry dashboard view <args...>`
524+
525+
View a dashboard
526+
527+
**Flags:**
528+
- `-w, --web - Open in browser`
529+
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
530+
- `--json - Output as JSON`
531+
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
532+
533+
#### `sentry dashboard create <args...>`
534+
535+
Create a dashboard
536+
537+
**Flags:**
538+
- `--json - Output as JSON`
539+
- `--fields <value> - Comma-separated fields to include in JSON output (dot.notation supported)`
540+
508541
### Repo
509542

510543
Work with Sentry repositories

src/app.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { apiCommand } from "./commands/api.js";
1111
import { authRoute } from "./commands/auth/index.js";
1212
import { whoamiCommand } from "./commands/auth/whoami.js";
1313
import { cliRoute } from "./commands/cli/index.js";
14+
import { dashboardRoute } from "./commands/dashboard/index.js";
15+
import { listCommand as dashboardListCommand } from "./commands/dashboard/list.js";
1416
import { eventRoute } from "./commands/event/index.js";
1517
import { helpCommand } from "./commands/help.js";
1618
import { initCommand } from "./commands/init.js";
@@ -47,6 +49,7 @@ import { error as errorColor, warning } from "./lib/formatters/colors.js";
4749
* Used to suggest the correct command when users type e.g. `sentry projects view cli`.
4850
*/
4951
const PLURAL_TO_SINGULAR: Record<string, string> = {
52+
dashboards: "dashboard",
5053
issues: "issue",
5154
orgs: "org",
5255
projects: "project",
@@ -64,6 +67,7 @@ export const routes = buildRouteMap({
6467
help: helpCommand,
6568
auth: authRoute,
6669
cli: cliRoute,
70+
dashboard: dashboardRoute,
6771
org: orgRoute,
6872
project: projectRoute,
6973
repo: repoRoute,
@@ -77,6 +81,7 @@ export const routes = buildRouteMap({
7781
init: initCommand,
7882
api: apiCommand,
7983
schema: schemaCommand,
84+
dashboards: dashboardListCommand,
8085
issues: issueListCommand,
8186
orgs: orgListCommand,
8287
projects: projectListCommand,
@@ -95,6 +100,7 @@ export const routes = buildRouteMap({
95100
"sentry is a command-line interface for interacting with Sentry. " +
96101
"It provides commands for authentication, viewing issues, and making API calls.",
97102
hideRoute: {
103+
dashboards: true,
98104
issues: true,
99105
orgs: true,
100106
projects: true,

src/commands/dashboard/create.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/**
2+
* sentry dashboard create
3+
*
4+
* Create a new dashboard in a Sentry organization.
5+
*/
6+
7+
import type { SentryContext } from "../../context.js";
8+
import { createDashboard, getProject } from "../../lib/api-client.js";
9+
import {
10+
type ParsedOrgProject,
11+
parseOrgProjectArg,
12+
} from "../../lib/arg-parsing.js";
13+
import { buildCommand } from "../../lib/command.js";
14+
import { ContextError, ValidationError } from "../../lib/errors.js";
15+
import { formatDashboardCreated } from "../../lib/formatters/human.js";
16+
import { CommandOutput } from "../../lib/formatters/output.js";
17+
import {
18+
fetchProjectId,
19+
resolveAllTargets,
20+
resolveOrg,
21+
resolveProjectBySlug,
22+
toNumericId,
23+
} from "../../lib/resolve-target.js";
24+
import { buildDashboardUrl } from "../../lib/sentry-urls.js";
25+
import type { DashboardDetail } from "../../types/dashboard.js";
26+
27+
type CreateFlags = {
28+
readonly json: boolean;
29+
readonly fields?: string[];
30+
};
31+
32+
type CreateResult = DashboardDetail & { url: string };
33+
34+
/**
35+
* Parse array positional args for `dashboard create`.
36+
*
37+
* Handles:
38+
* - `<title>` — title only (auto-detect org/project)
39+
* - `<target> <title>` — explicit target + title
40+
*/
41+
function parsePositionalArgs(args: string[]): {
42+
title: string;
43+
targetArg: string | undefined;
44+
} {
45+
if (args.length === 0) {
46+
throw new ValidationError("Dashboard title is required.", "title");
47+
}
48+
if (args.length === 1) {
49+
return { title: args[0] as string, targetArg: undefined };
50+
}
51+
// Two args: first is target, second is title
52+
return { title: args[1] as string, targetArg: args[0] as string };
53+
}
54+
55+
/** Result of resolving org + project IDs from the parsed target */
56+
type ResolvedDashboardTarget = {
57+
orgSlug: string;
58+
projectIds: number[];
59+
};
60+
61+
/** Enrich targets that lack a projectId by calling the project API */
62+
async function enrichTargetProjectIds(
63+
targets: { org: string; project: string; projectId?: number }[]
64+
): Promise<number[]> {
65+
const enriched = await Promise.all(
66+
targets.map(async (t) => {
67+
if (t.projectId !== undefined) {
68+
return t.projectId;
69+
}
70+
try {
71+
const info = await getProject(t.org, t.project);
72+
return toNumericId(info.id);
73+
} catch {
74+
return;
75+
}
76+
})
77+
);
78+
return enriched.filter((id): id is number => id !== undefined);
79+
}
80+
81+
/** Resolve org and project IDs from the parsed target argument */
82+
async function resolveDashboardTarget(
83+
parsed: ParsedOrgProject,
84+
cwd: string
85+
): Promise<ResolvedDashboardTarget> {
86+
switch (parsed.type) {
87+
case "explicit": {
88+
const pid = await fetchProjectId(parsed.org, parsed.project);
89+
return {
90+
orgSlug: parsed.org,
91+
projectIds: pid !== undefined ? [pid] : [],
92+
};
93+
}
94+
case "org-all":
95+
return { orgSlug: parsed.org, projectIds: [] };
96+
97+
case "project-search": {
98+
const found = await resolveProjectBySlug(
99+
parsed.projectSlug,
100+
"sentry dashboard create <org>/<project> <title>"
101+
);
102+
const pid = await fetchProjectId(found.org, found.project);
103+
return {
104+
orgSlug: found.org,
105+
projectIds: pid !== undefined ? [pid] : [],
106+
};
107+
}
108+
case "auto-detect": {
109+
const result = await resolveAllTargets({ cwd });
110+
if (result.targets.length === 0) {
111+
const resolved = await resolveOrg({ cwd });
112+
if (!resolved) {
113+
throw new ContextError(
114+
"Organization",
115+
"sentry dashboard create <org>/ <title>"
116+
);
117+
}
118+
return { orgSlug: resolved.org, projectIds: [] };
119+
}
120+
const orgSlug = (result.targets[0] as (typeof result.targets)[0]).org;
121+
const projectIds = await enrichTargetProjectIds(result.targets);
122+
return { orgSlug, projectIds };
123+
}
124+
default: {
125+
const _exhaustive: never = parsed;
126+
throw new Error(
127+
`Unexpected parsed type: ${(_exhaustive as { type: string }).type}`
128+
);
129+
}
130+
}
131+
}
132+
133+
export const createCommand = buildCommand({
134+
docs: {
135+
brief: "Create a dashboard",
136+
fullDescription:
137+
"Create a new Sentry dashboard.\n\n" +
138+
"Examples:\n" +
139+
" sentry dashboard create 'My Dashboard'\n" +
140+
" sentry dashboard create my-org/ 'My Dashboard'\n" +
141+
" sentry dashboard create my-org/my-project 'My Dashboard'",
142+
},
143+
output: {
144+
human: formatDashboardCreated,
145+
},
146+
parameters: {
147+
positional: {
148+
kind: "array",
149+
parameter: {
150+
brief: "[<org/project>] <title>",
151+
parse: String,
152+
},
153+
},
154+
flags: {},
155+
},
156+
async *func(this: SentryContext, _flags: CreateFlags, ...args: string[]) {
157+
const { cwd } = this;
158+
159+
const { title, targetArg } = parsePositionalArgs(args);
160+
const parsed = parseOrgProjectArg(targetArg);
161+
const { orgSlug, projectIds } = await resolveDashboardTarget(parsed, cwd);
162+
163+
const dashboard = await createDashboard(orgSlug, {
164+
title,
165+
projects: projectIds.length > 0 ? projectIds : undefined,
166+
});
167+
const url = buildDashboardUrl(orgSlug, dashboard.id);
168+
169+
yield new CommandOutput({ ...dashboard, url } as CreateResult);
170+
},
171+
});

src/commands/dashboard/index.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { buildRouteMap } from "@stricli/core";
2+
import { createCommand } from "./create.js";
3+
import { listCommand } from "./list.js";
4+
import { viewCommand } from "./view.js";
5+
6+
export const dashboardRoute = buildRouteMap({
7+
routes: {
8+
list: listCommand,
9+
view: viewCommand,
10+
create: createCommand,
11+
},
12+
docs: {
13+
brief: "Manage Sentry dashboards",
14+
fullDescription:
15+
"View and manage dashboards in your Sentry organization.\n\n" +
16+
"Commands:\n" +
17+
" list List dashboards\n" +
18+
" view View a dashboard\n" +
19+
" create Create a dashboard",
20+
hideRoute: {},
21+
},
22+
});

0 commit comments

Comments
 (0)