Skip to content
Draft
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
2 changes: 1 addition & 1 deletion apps/cli-docs/src/content/docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ toolkit/
│ │ │ ├── event/ # list, send, view
│ │ │ ├── feedback/ # list, resolve, spam, unresolve, view
│ │ │ ├── integration/ # delete, list
│ │ │ ├── issue/ # archive, events, explain, link, list, merge, plan, resolve, unlink, unresolve, view
│ │ │ ├── issue/ # archive, events, explain, link, list, merge, plan, resolve, tags, unlink, unresolve, view
│ │ │ ├── local/ # close, create, list, reset, restart, retention, run, serve, start, status, stop, view
│ │ │ ├── log/ # list, view
│ │ │ ├── monitor/ # list, run
Expand Down
25 changes: 25 additions & 0 deletions apps/cli-docs/src/fragments/commands/issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,31 @@ Use `@latest` and `@most_frequent` to target issues without knowing their ID.
- **`org/project` targets use slugs, not numeric IDs** — `sentry issue list my-org/6775615880` fails when `6775615880` is a project ID copied from a URL. Run `sentry project list my-org/` to find the slug (e.g. `frontend`).
- **Prefer auto-detect over guessing slugs** — omit the target when possible; the CLI resolves org/project from DSNs, `.env` files, and your working directory.

## Inspect tag distributions

After opening an issue, compare which releases, servers, or browsers account for
its events. `issue view` shows tags on the latest event; `issue tags` aggregates
across the issue's retained events.

The result contains up to **9 top values**, ordered by count. Human output shows
each value's count and percentage of **all retained events**, including events
with a missing value. Any remainder appears as "Other values (not shown)";
percentages are never calculated from just the displayed rows. Retention limits
apply, so this total can differ from the issue's lifetime event count.

`--environment` selects one environment; omitting it includes all environments.
This aggregate does **not** support `--period`, `--query`, `--limit`, or cursor
pagination. Use `issue events` when you need filtered event records. For example,
after identifying a release, inspect recent events with
`sentry issue events FRONT-ABC --query 'release:1.2.3' --period 7d`.

JSON preserves `key`, `name`, `totalValues`, and `topValues`, including counts and
first/last seen timestamps. Values and counts may be `null`; missing data is not
zero. `--fields` selects top-level fields. To discover tag keys, inspect the
latest event with `sentry issue view <issue>`; it may not contain every tag used
by earlier events. A tag with no retained values, or an unknown environment,
can return a not-found error.

## Short IDs and organizations

Full short IDs such as `FRONT-ABC` use your configured organization: `SENTRY_ORG`,
Expand Down
1 change: 1 addition & 0 deletions packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ Manage Sentry issues

- `sentry issue list [<org/project>]` — List issues in a project
- `sentry issue events <issue>` — List events for a specific issue
- `sentry issue tags <issue> <key>` — Show the top values of an issue tag
- `sentry issue explain <issue...>` — Analyze one or more issues using Seer AI
- `sentry issue plan <issue>` — Generate a solution plan using Seer AI
- `sentry issue view <issue...>` — View details of one or more issues
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,40 @@ sentry issue events FRONT-ABC -c next
sentry issue events FRONT-ABC --json
```

### `sentry issue tags <issue> <key>`

Show the top values of an issue tag

**Flags:**
- `-e, --environment <value> - Filter to one environment (default: all environments)`
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`

**JSON Fields** (use `--json --fields` to select specific fields):

| Field | Type | Description |
|-------|------|-------------|
| `uniqueValues` | number \| null | |
| `totalValues` | number \| null | Total retained events, including missing tag values; not a lifetime count |
| `topValues` | array \| null | Up to 9 values ordered by count, with nullable counts and first/last seen timestamps |
| `key` | string | |
| `name` | string | |

**Examples:**

```bash
# Show which releases concentrate an issue's events
sentry issue tags my-org/FRONT-ABC release

# Inspect server distribution for the latest issue in production
sentry issue tags @latest server_name --environment production

# Get browser counts and timestamps as JSON
sentry issue tags FRONT-ABC browser.name --json

# Select the aggregate fields for a release distribution
sentry issue tags FRONT-ABC release --json --fields key,totalValues,topValues
```

### `sentry issue explain <issue...>`

Analyze one or more issues using Seer AI
Expand Down
5 changes: 4 additions & 1 deletion packages/cli/src/commands/issue/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { listCommand } from "./list.js";
import { mergeCommand } from "./merge.js";
import { planCommand } from "./plan.js";
import { resolveCommand } from "./resolve.js";
import { tagsCommand } from "./tags.js";
import { unlinkCommand } from "./unlink.js";
import { unresolveCommand } from "./unresolve.js";
import { viewCommand } from "./view.js";
Expand All @@ -15,6 +16,7 @@ export const issueRoute = buildRouteMap({
routes: {
list: listCommand,
events: eventsCommand,
tags: tagsCommand,
explain: explainCommand,
plan: planCommand,
view: viewCommand,
Expand All @@ -35,6 +37,7 @@ export const issueRoute = buildRouteMap({
"Commands:\n" +
" list List issues in a project\n" +
" events List events for a specific issue\n" +
" tags Show the top values of an issue tag\n" +
" view View details of one or more issues\n" +
" explain Analyze one or more issues using Seer AI\n" +
" plan Generate a solution plan using Seer AI\n" +
Expand All @@ -44,7 +47,7 @@ export const issueRoute = buildRouteMap({
" merge Merge 2+ issues into a single group\n" +
" link Link an existing external issue\n" +
" unlink Remove an external issue link\n\n" +
"Magic selectors (available for view, events, explain, plan, resolve, unresolve, archive):\n" +
"Magic selectors (available for view, events, tags, explain, plan, resolve, unresolve, archive):\n" +
" @latest Most recent unresolved issue\n" +
" @most_frequent Issue with the highest event frequency\n\n" +
"Alias: `sentry issues` → `sentry issue list`",
Expand Down
142 changes: 142 additions & 0 deletions packages/cli/src/commands/issue/tags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* Inspect an issue's tag distribution across retained events.
* Uses the public tag details endpoint: a top-values aggregate, not a
* paginated list. Time and search filters are intentionally not exposed
* because that endpoint ignores them.
*/

import { vGetOrganizationIssueTagResponse } from "@sentry/api/valibot";
import { description, object, pipe } from "valibot";
import type { SentryContext } from "../../context.js";
import { getIssueTag } from "../../lib/api-client.js";
import { buildCommand } from "../../lib/command.js";
import { ContextError } from "../../lib/errors.js";
import { formatIssueTags } from "../../lib/formatters/issue-tags.js";
import { CommandOutput } from "../../lib/formatters/output.js";
import {
applyFreshFlag,
FRESH_ALIASES,
FRESH_FLAG,
} from "../../lib/list-command.js";
import { withProgress } from "../../lib/polling.js";
import { buildCommandHint, issueIdPositional, resolveIssue } from "./utils.js";

type TagsFlags = {
/** Emit the API response as JSON instead of a human table; defaults to false. */
readonly json: boolean;
/** Bypass cached API responses and project detection results; defaults to false. */
readonly fresh: boolean;
/** One literal environment name; undefined includes all environments. */
readonly environment?: string;
};

export const tagsCommand = buildCommand({
docs: {
brief: "Show the top values of an issue tag",
fullDescription:
"Find which releases, servers, browsers, or other tag values concentrate an issue's events.\n\n" +
"Returns up to 9 top values across retained events, ordered by count.\n" +
"Percentages use the total retained event count, including missing values,\n" +
"not just the sum of the displayed rows. This is not a lifetime count.\n" +
"Use --environment to select one environment; the default includes all.\n" +
"Time ranges, search queries, and pagination are not supported.\n\n" +
"Use 'sentry issue view <issue>' to inspect tag keys on the latest event.\n" +
"JSON preserves the API's key, name, totalValues, and topValues (including\n" +
"counts and first/last seen timestamps). --fields selects top-level fields.",
examples: [
{
description: "Show which releases concentrate an issue's events",
command: "sentry issue tags my-org/FRONT-ABC release",
},
{
description:
"Inspect server distribution for the latest issue in production",
command:
"sentry issue tags @latest server_name --environment production",
},
{
description: "Get browser counts and timestamps as JSON",
command: "sentry issue tags FRONT-ABC browser.name --json",
},
{
description: "Select the aggregate fields for a release distribution",
command:
"sentry issue tags FRONT-ABC release --json --fields key,totalValues,topValues",
},
],
},
output: {
human: formatIssueTags,
schema: object({
...vGetOrganizationIssueTagResponse.entries,
totalValues: pipe(
vGetOrganizationIssueTagResponse.entries.totalValues,
description(
"Total retained events, including missing tag values; not a lifetime count",
),
),
topValues: pipe(
vGetOrganizationIssueTagResponse.entries.topValues,
description(
"Up to 9 values ordered by count, with nullable counts and first/last seen timestamps",
),
),
}),
},
parameters: {
positional: {
kind: "tuple",
parameters: [
...issueIdPositional.parameters,
{
placeholder: "key",
brief: "Tag key, e.g. release, server_name, browser.name",
parse: String,
},
],
},
flags: {
environment: {
kind: "parsed",
parse: String,
brief: "Filter to one environment (default: all environments)",
optional: true,
},
fresh: FRESH_FLAG,
},
aliases: { ...FRESH_ALIASES, e: "environment" },
},
async *func(
this: SentryContext,
flags: TagsFlags,
issueArg: string,
key: string,
) {
applyFreshFlag(flags);
const trailingArgs =
flags.environment === undefined
? [key]
: [key, "--environment", flags.environment];
const { org, issue } = await resolveIssue({
issueArg,
cwd: this.cwd,
command: "tags",
trailingArgs,
});
if (!org) {
throw new ContextError(
"Organization",
buildCommandHint("tags", issueArg, undefined, trailingArgs),
);
}

const tag = await withProgress(
{ message: "Fetching tag distribution...", json: flags.json },
() => getIssueTag(org, issue.id, key, flags.environment),
);
yield new CommandOutput(tag);
return {
hint: `Issue: ${issue.shortId}. Environment: ${flags.environment ?? "all environments"}. Scope: retained events.`,
};
},
});
Loading
Loading