Skip to content

feat(cli): add attribute list command - #1559

Draft
lzhao-sentry wants to merge 5 commits into
mainfrom
lzhao/feat/add-attribute-list-command
Draft

lzhao-sentry wants to merge 5 commits into
mainfrom
lzhao/feat/add-attribute-list-command

Conversation

@lzhao-sentry

Copy link
Copy Markdown
Member

Changes

This PR adds a top level attribute list command to CLI as part of an effort to clean up and expose attributes for both users and agents.

Some design choices:

  • --with instead of --query. The query param for attribute API doesn't use Sentry query syntax, rather it's just searching attribute keys that appear with the queried keys on same spans, logs, etc
  • No --cursor for now as the attribute API pagination works differently from other APIs (see comments in list.ts), so it wouldn't be consistent to add pagination as is. Instead attribute filtering is encouraged with --with/--search/--type

@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
sentry-local Skipped Skipped Oct 22, 2026 7:45pm UTC

Request Review

@BYK BYK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pre-emptively blocking to do a quick design review (mostly UX)

# Attributes seen on the same spans as the specified attribute
sentry attribute list --with gen_ai.request.model

# Add descriptions, example values, and deprecation info

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Add descriptions, example values, and deprecation info
# Show descriptions, example values, and deprecation info

# Attributes seen on the same spans as the specified attribute
sentry attribute list --with gen_ai.request.model

# Add descriptions, example values, and deprecation info

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Add descriptions, example values, and deprecation info
# Show descriptions, example values, and deprecation info

Comment thread packages/cli/test/commands/attribute/list.test.ts

@BYK BYK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a bit too much slop in this PR, not sure if the agent followed the repo conventions. It also duplicated a lot of existing functionality.

Finally, I'd like to understand why this API is such a snowflake and how can we align its query and pagination system with the rest. CLI can (and should) normalize the pagination so provide a unified and consistent interface but ideally this should already be done on the API side.

const DEFAULT_DATASET = "spans";

/** Default window — matches the MCP server's attribute discovery. */
const DEFAULT_PERIOD = "14d";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should match the default period for the rest of the CLI endpoints

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that 7d? I

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, 7d. The shared default is LIST_PERIOD_FLAG in packages/cli/src/lib/list-command.ts (default: "7d"). span list, trace list, replay list, the event list commands, and agent-conversation list all use it.

Simplest fix: use the shared flag and drop the local constant:

import { LIST_PERIOD_FLAG, PERIOD_ALIASES, /* ... */ } from "../../lib/list-command.js";

// flags:
period: LIST_PERIOD_FLAG,

That way, if the CLI-wide default changes, this command changes with it.

Not every command uses 7d. log list defaults to 30d (14d in trace mode), feedback list and trace logs use 14d, issue list and release list use 90d, and explore uses 24h. Still, span list and trace list are the closest siblings to attribute discovery, and they're 7d. Anyone who wants the MCP-style 14d window can pass --period 14d.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nvm ignore my comment, I found it.

const DEFAULT_PERIOD = "14d";

/** User-facing dataset names (and aliases) that map to API dataset names. */
const DATASET_ALIASES: Record<string, TraceItemAttributeDataset> = {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we had this mapping somewhere else already? Probably in explore? We should use a single, unified list.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue with using the explore mapping (and by extension some of the helpers) is that errors and replays are included. They don't use attributes, but tags

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point. explore's map shouldn't be reused as-is, since errors and replays use tags, not attributes. That's an argument for sharing it differently, not for keeping a second copy, though. The two copies have already drifted: this PR accepts metric and tracemetrics as aliases, and explore doesn't.

Suggestion: move the dataset aliases into a shared module (e.g. lib/datasets.ts). It would hold one alias table plus a list of which datasets are trace-item datasets, and both commands would build their parsing and validation from it:

// lib/datasets.ts
const DATASET_ALIASES = {
  errors: "errors", error: "errors",
  spans: "spans", span: "spans",
  logs: "logs", log: "logs",
  metrics: "tracemetrics", metric: "tracemetrics", tracemetrics: "tracemetrics",
  replays: "replays", replay: "replays",
} as const;

/** Datasets backed by trace items, which have attributes rather than tags. */
export const TRACE_ITEM_DATASETS = ["spans", "logs", "metrics"] as const;

export const EXPLORE_DATASETS = ["errors", ...TRACE_ITEM_DATASETS, "replays"] as const;

export function buildDatasetParser<T extends readonly string[]>(allowed: T) { /* resolve alias, validate against allowed, throw ValidationError */ }
export function toUserDataset(apiName: string): string { /* reverse map for hints */ }

Then attribute list uses buildDatasetParser(TRACE_ITEM_DATASETS) and explore uses buildDatasetParser(EXPLORE_DATASETS). Both get the same aliases, the same error wording, and the same reverse mapping for hints. explore would also pick up the metric and tracemetrics aliases for free.

The other duplicated pieces I flagged (the type list, the parse/validate helpers) can live in the same module, or in a sibling module next to the existing trace-item API types.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could consolidate tags into this command? But I think the endgoal is to eventually only use attributes, and for the scope of the project this command falls under, we're focusing on the datasets that currently use attributes


/** API dataset name → canonical user-facing name, for hints. */
const API_TO_USER_DATASET: Record<string, string> = {
spans: "spans",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

* Parse and validate `--dataset`. Accepts canonical names and singular aliases.
* @internal Exported for testing
*/
export function parseDataset(value: string): TraceItemAttributeDataset {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, all this is probably shared stuff between explore

* Split a comma-separated key list, keeping commas inside brackets so typed
* keys like `tags[cart.total,number]` stay whole.
*/
function splitKeyList(value: string): string[] {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, pretty sure we do this somewhere else.

* deduplicated list of attribute keys.
* @internal Exported for testing
*/
export function parseWithKeys(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really understand the --with flag. It feels like a filter? We should standardise these. Also I think a better way might be to adopt what we are doing in #1575

We tend to stay away from flags and prefer positional arguments when it makes sense and I think here sentry attribute list org/ <filter> or just sentry attribute list <filter> feels more natural rather than having a --with or even --query flag.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For --with, it takes attribute keys only and ignores values. So passing span.op will get all other attributes that occur with span.op, given that that span.op had any value. I think it's unique to attributes, so not sure how it would be best to handle it

I do think --search would fit better in sentry attribute list <filter> since it's substring matches for attribute keys

}

/** Shell-quote a key for a copyable command (zsh globs `tags[...]`). */
function shellKey(key: string): string {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, this is existing functionality.

}
const { key } = example;
if (dataset === "tracemetrics") {
return `Tip: Group or filter metrics with 'sentry explore ${org}/ --dataset metrics' (-F to group, -q to filter)`;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No idea what this tip is about?

Comment on lines +277 to +288
"Target specification:\n" +
" sentry attribute list # auto-detect org from DSN or config\n" +
" sentry attribute list <org>/ # all projects in the organization\n" +
" sentry attribute list <org>/<project> # one project\n" +
" sentry attribute list <project> # find project across all orgs\n\n" +
"Examples:\n" +
" sentry attribute list --dataset logs --with http.method # Attributes seen on logs with http.method\n" +
" sentry attribute list --type string --search http # Attributes containing http and are type string\n" +
" sentry attribute list --period 24h # Get attributes that appeared in the last 24 hours\n" +
" sentry attribute list --context # Display attribute metadata information\n" +
" sentry attribute list --json --fields key,type # Output in JSON format with specified fields\n\n" +
"Alias: `sentry attributes` → `sentry attribute list`",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not useful help. We need to tell what "attributes" are, where and how they might be useful. All this information, alias, examples etc has dedicated fields and the description/help/skills get auto generated from those. This is hacking and bypassing all that structured way.

@sentry-junior sentry-junior Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-reviewed at 2ebde76 against the earlier threads. Good progress. Fixed so far:

  • --period now uses LIST_PERIOD_FLAG (7d).
  • splitKeyList is gone in favor of parseFieldsList, which now keeps commas inside brackets and has a test. shellKey is gone in favor of shellQuote.
  • The metrics tip is now a concrete sentry explore … -m <metric> -F <key> command, and the help now starts by saying what attributes are.

Still open:

  • Examples are still hand-written. fullDescription has its own Examples: block, and fragments/commands/attribute.md has a ## Examples section. The "Generated Docs & Skills" section of packages/cli/AGENTS.md says examples belong in docs.examples ({ description, command }[], see agent-conversation/list.ts) so they show up in --help, the docs and the skill references. It also says fragments are only for prose the command metadata can't hold. Move both example lists into docs.examples and keep only the "Using Attribute Keys" prose in the fragment.
  • Dataset handling is still a copy of explore's. DATASET_ALIASES, VALID_DATASETS, API_TO_USER_DATASET and parseDataset all exist in explore.ts too (around L92–122 and L168). The copies have already drifted: metric/tracemetrics work here but not in explore. I suggested a shared module with a trace-item subset in the L57 thread. That keeps this command limited to spans/logs/metrics without keeping a second copy.
  • --with and --search are unchanged. That discussion is still going in the L145 thread. One more data point: buildWithQuery already turns --with k into has:k. Accepting has:<key> in a positional filter would therefore send the API the same query without a new flag. A plain positional term could replace --search, as long as it matches the way #1575's positional name filter matches.

On pagination: leaving out --cursor matches log list (noCursorFlag), and the comment explains why. Still, the per-type per_page plus trimming the extra look-ahead rows (dropLookAheadRows) works around the API's behavior. That's worth filing against the endpoint so the CLI can eventually page like the other list commands.

I didn't run the tests locally. The latest checks on this commit are green.

@sentry-junior

sentry-junior Bot commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Correction to my review above: I said the checks on 2ebde76 were green, but some were still running when I posted. Lint has since failed, in "Repository quality" and in "sentry". The test job only fails because those two did.

packages/cli/test/lib/api/trace-item-attributes.test.ts:49:9: Unexpected `await` of a non-Promise (non-"Thenable") value.

setAuthToken (src/lib/db/auth.ts) is synchronous now, so drop the await in that beforeEach, and the async along with it.

@BYK BYK left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please address Junior's comments.

This branch was previously deployed

1 inactive deployment
Preview — 2ebde766 Deployed Oct 22, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants