Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
91d18a8
fix(cli): preserve valid Explore queries and metric expressions
betegon Oct 25, 2026
95d64f5
fix(cli): explain rejected Explore field expressions
betegon Oct 25, 2026
673fdbf
feat(cli): add attribute list command
lzhao-sentry Oct 20, 2026
b26bfad
make list.ts comment clearer, add cmd to complete.ts
lzhao-sentry Oct 20, 2026
0e6d12f
revert complete.ts change, fix --context example comment
lzhao-sentry Oct 21, 2026
1f46113
remove redundant functions, attempt to make help better
lzhao-sentry Oct 22, 2026
630303f
normalize/add pagination, make dataset module
lzhao-sentry Oct 23, 2026
0d1d854
make --with, --search into positional filters
lzhao-sentry Oct 23, 2026
a626786
fix linting error
lzhao-sentry Oct 23, 2026
30a48b7
ref(cli): Resolve the attribute list target in its positional
sentry-junior[bot] Oct 23, 2026
506389e
ref(cli): Use named @sentry/api types for trace item attribute filters
sentry-junior[bot] Oct 23, 2026
b0d1c56
docs(cli): Mark attribute fragment cross-command example block
sentry-junior[bot] Oct 23, 2026
ad80f6a
fix(cli): preserve attribute pagination and typed field keys
betegon Oct 25, 2026
a6b17b7
clean up test spacing, address jr comments
lzhao-sentry Oct 26, 2026
cffbb85
Merge remote-tracking branch 'origin/main' into lzhao/feat/add-attrib…
sentry-junior[bot] Oct 26, 2026
97ae048
address review comments
lzhao-sentry Oct 28, 2026
735af22
Merge remote-tracking branch 'origin/main' into lzhao/feat/add-attrib…
sentry-junior[bot] Oct 29, 2026
1841530
ref(cli): Tidy attribute list leftovers
sentry-junior[bot] Oct 29, 2026
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
1 change: 1 addition & 0 deletions apps/cli-docs/src/content/docs/contributing.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ toolkit/
│ │ ├── commands/ # CLI commands
│ │ │ ├── agent-conversation/# list, view
│ │ │ ├── alert/ # create, delete, edit, list, view
│ │ │ ├── attribute/ # list
│ │ │ ├── auth/ # list, login, logout, refresh, status, switch, token, whoami
│ │ │ ├── build/ # download, upload
│ │ │ ├── cli/ # completion, defaults, feedback, fix, import, setup, uninstall, upgrade
Expand Down
9 changes: 9 additions & 0 deletions apps/cli-docs/src/fragments/commands/attribute.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@


## Using Attribute Keys

Attributes are the typed key-value data attached to spans, logs, and metric samples. Their keys can be passed to other commands like `explore` to filter, add columns, or group results.

Custom non-string attributes include their type in the key, e.g. `tags[cart.total,number]`. Search for the name (`cart.total`), then use the returned key in queries and fields. Quote these keys in the shell.

The endpoint returns discovery candidates, including built-in and virtual keys even when no matching item was observed. It does not provide an exhaustive inventory or proof that every candidate co-occurs with the `has:` keys.
2 changes: 2 additions & 0 deletions packages/cli/grammar/field-list.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/** Split output fields at top-level commas, preserving nested expressions. */
export declare function parse(input: string): string[];
23 changes: 23 additions & 0 deletions packages/cli/grammar/field-list.pegjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Output fields are paths or API expressions, not search filters. Preserve
// their spelling and split only at commas outside balanced groups or quotes.
fields
= head:field tail:("," field)* {
return [head, ...tail.map((part) => part[1])].filter(Boolean);
}

field
= value:$(part*) { return value.trim(); }

// Unmatched punctuation can be part of a literal JSON key. This grammar
// separates fields without validating the API expressions themselves.
part
= brackets / parentheses / quoted / [^,]

brackets
= "[" (brackets / parentheses / quoted / [^()[\]"])* "]"

parentheses
= "(" (brackets / parentheses / quoted / [^()[\]"])* ")"

quoted
= '"' ("\\" . / [^"\\])* '"'
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@
"@sentry/core": "catalog:",
"@sentry/mcp-server": "workspace:*",
"@sentry/node": "catalog:",
"@sentry/search-syntax": "catalog:",
"@sentry/sqlish": "^1.0.1",
"@sentry/symbolic": "13.7.0",
"@sentry/toolkit-core": "workspace:*",
Expand Down
8 changes: 8 additions & 0 deletions packages/cli/plugins/sentry-cli/skills/sentry-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,14 @@ Manage Sentry alert rules

→ Full flags and examples: `references/alert.md`

### Attribute

Explore span, log, and metric attributes

- `sentry attribute list [<target>] [<filter>]` — List attribute keys for spans, logs, or metrics

→ Full flags and examples: `references/attribute.md`

### Build

Manage mobile build artifacts
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
name: sentry-cli-attribute
version: 0.48.0-dev.0
description: Explore span, log, and metric attributes
requires:
bins: ["sentry"]
auth: true
---

# Attribute Commands

Explore span, log, and metric attributes

### `sentry attribute list [<target>] [<filter>]`

List attribute keys for spans, logs, or metrics

**Arguments:**

| Argument | Description |
|----------|-------------|
| `[<target>]` | &lt;org&gt;/ (all projects), &lt;org&gt;/&lt;project&gt;, or &lt;project&gt; (search) |
| `[<filter>]` | Text in attribute names and/or has:&lt;key&gt; terms, e.g. 'gen_ai has:http.method' |

**Flags:**
- `-d, --dataset <value> - Dataset to list attributes for (spans, logs, metrics) - (default: "spans")`
- `--type <value>... - Only show attributes of these types: string, number, boolean, array. Repeat the flag for each type or pass a comma-separated list (e.g. --type string,number)`
- `-t, --period <value> - Time range: "7d", "2024-01-01..2024-02-01", ">=2024-01-01" - (default: "7d")`
- `-n, --limit <value> - Maximum number of attributes to list - (default: "25")`
- `-f, --fresh - Bypass cache, re-detect projects, and fetch fresh data`
- `-c, --cursor <value> - Navigate pages: "next", "prev", "first" (or raw cursor string)`

**Examples:**

```bash
# List span attribute keys for a project
sentry attribute list my-org/my-project

# List log attribute keys across the whole organization
sentry attribute list my-org/ --dataset logs

# String attributes whose names contain gen_ai (case-sensitive)
sentry attribute list my-org/ gen_ai --type string

# Discover candidates alongside an attribute
sentry attribute list my-org/ has:gen_ai.request.model

# Discover candidates alongside several keys (one quoted argument)
sentry attribute list my-org/ 'has:gen_ai.request.model has:gen_ai.usage.input_tokens'

# Text and has: filters together
sentry attribute list my-org/ 'gen_ai has:http.method'

# Discover candidates using the last 24 hours (includes built-in and virtual keys)
sentry attribute list my-org/ --period 24h

# Show more than the default 25
sentry attribute list my-org/my-project -n 100

# Show the next page of the previous listing (same flags)
sentry attribute list my-org/my-project -n 100 -c next

# Filter spans that have a discovered key
sentry span list my-org/my-project -q 'has:gen_ai.request.model'

# Find a custom numeric attribute by name
sentry attribute list my-org/my-project cart.total --type number

# Show a discovered typed key as a span column
sentry span list my-org/my-project --fields 'tags[cart.total,number]'

# Group a metric by a discovered attribute
sentry explore my-org/ --dataset metrics -m llm.token_usage -F gen_ai.request.model

# Select fields as JSON
sentry attribute list my-org/ --json --fields key,type,source
```

All commands also support `--json`, `--fields`, `--help`, `--log-level`, and `--verbose` flags.
4 changes: 2 additions & 2 deletions packages/cli/script/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ const VERSION: string = pkg.version;
/** Node version for SEA binaries. "lts" resolves to the latest LTS via fossilize. */
const NODE_VERSION = "lts";

/** Files that use _require() for lazy relative imports (circular dep breaking). */
/** Files whose lazy relative imports must be included in the native bundle. */
const REQUIRE_ALIAS_FILTER =
/(?:db[\\/](?:index|schema)|list-command|telemetry)\.ts$/;
/(?:db[\\/](?:index|schema)|formatters[\\/]json|list-command|telemetry)\.ts$/;
const REQUIRE_ALIAS_RE = /\b_require\(/g;

/** Build-time constants injected into the binary */
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/script/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,10 @@ const sentrySourcemapPlugin: Plugin = {
* it stays a runtime `require()` that fails in a real install (the driver is
* a devDependency, not shipped). `node:sqlite` in the same file stays a
* builtin and is left external by esbuild regardless.
* `formatters/json` loads its generated parser only when --fields is parsed.
*/
const REQUIRE_ALIAS_FILTER =
/(?:db[\\/](?:index|schema|sqlite)|list-command|telemetry)\.ts$/;
/(?:db[\\/](?:index|schema|sqlite)|formatters[\\/]json|list-command|telemetry)\.ts$/;
const REQUIRE_ALIAS_RE = /\b_require\(/g;
const IDENTIFIER_FILTER_RE = /^[A-Za-z_$][\w$]*$/;
// Match both the prefixed and bare specifiers — bundled dependencies (e.g.
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/script/ensure-generated.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ const SCHEMA_OUTPUTS = [
const GRAMMAR_OUTPUTS = [
"src/generated/org-project-target-grammar.js",
"src/generated/org-project-target-grammar.d.ts",
"src/generated/field-list-grammar.js",
"src/generated/field-list-grammar.d.ts",
];
/** Produced by `generate:docs`; imported at runtime by `src/`. */
const DOCS_OUTPUTS = [
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { conversationRoute } from "./commands/agent-conversation/index.js";
import { listCommand as conversationListCommand } from "./commands/agent-conversation/list.js";
import { alertRoute } from "./commands/alert/index.js";
import { apiCommand } from "./commands/api.js";
import { attributeRoute } from "./commands/attribute/index.js";
import { listCommand as attributeListCommand } from "./commands/attribute/list.js";
import { authRoute } from "./commands/auth/index.js";
import { whoamiCommand } from "./commands/auth/whoami.js";
import { bashHookCommand } from "./commands/bash-hook.js";
Expand Down Expand Up @@ -97,6 +99,7 @@ import { buildRouteMap } from "./lib/route-map.js";
*/
const PLURAL_TO_SINGULAR: Record<string, string> = {
"agent-conversations": "agent-conversation",
attributes: "attribute",
dashboards: "dashboard",
dsns: "dsn",
events: "event",
Expand All @@ -122,6 +125,7 @@ export const routes = buildRouteMap({
routes: {
help: helpCommand,
alert: alertRoute,
attribute: attributeRoute,
auth: authRoute,
build: buildRoute,
cli: cliRoute,
Expand Down Expand Up @@ -168,6 +172,7 @@ export const routes = buildRouteMap({
"send-envelope": sendEnvelopeCommand,
"bash-hook": bashHookCommand,
"agent-conversations": conversationListCommand,
attributes: attributeListCommand,
dashboards: dashboardListCommand,
dsns: dsnListCommand,
integrations: integrationListCommand,
Expand Down Expand Up @@ -195,6 +200,7 @@ export const routes = buildRouteMap({
hideRoute: {
dsns: true,
"agent-conversations": true,
attributes: true,
dashboards: true,
events: true,
integrations: true,
Expand Down
17 changes: 17 additions & 0 deletions packages/cli/src/commands/attribute/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/** Trace item attribute commands. */

import { buildRouteMap } from "../../lib/route-map.js";
import { listCommand } from "./list.js";

export const attributeRoute = buildRouteMap({
routes: { list: listCommand },
docs: {
brief: "Explore span, log, and metric attributes",
fullDescription:
"List the attribute keys available on spans, logs, and trace metrics.\n\n" +
"Commands:\n" +
" list List attribute keys with their type and source\n\n" +
"Alias: `sentry attributes` → `sentry attribute list`",
hideRoute: {},
},
});
Loading
Loading