Skip to content

Commit 42c2ca8

Browse files
authored
feat(traces): expose custom span attributes and improve agent guidance (#623)
## Summary Addresses feedback from an AI agent field report about Sentry CLI usability. The critical gap was that custom span attributes (like `gen_ai.usage.input_tokens`, `pi.session.id`) were completely invisible via CLI — this PR fixes that across all three span/trace commands. ### Custom attribute access (critical) - **`span view`**: Fetches full attributes from the `trace-items/{itemId}/` endpoint (same as Sentry frontend's span detail sidebar). All custom OTEL attributes now appear in both JSON and human output with no configuration needed. - **`trace view`**: `--fields` values that aren't standard output fields are forwarded as `additional_attributes` query params to the trace detail API. - **`span list`**: `--fields` now drives API field selection — unknown field names are requested as additional `field` params from the events API. Includes `gen_ai` group alias that expands to common AI observability fields. ### Agent guidance improvements - Added `--since` as an alias for `--period` on all list commands (the most natural flag name for time filtering) - Added "Quick Reference" section to agent guidance docs covering correct patterns for time filtering, org/project scoping, trace ID usage, and valid dataset names - Regenerated SKILL.md ### Quality of life - Filter zero-valued web vitals measurements from `trace view` JSON output (~40% size reduction for typical traces) - Added `EVENTS_API_DATASETS` shared constant and documented valid dataset names in api command docs - Widened `TraceSpan` type to include all fields the trace detail API actually returns (`measurements`, `additional_attributes`, `sdk_name`, `errors`, `occurrences`, etc.)
1 parent 5192f92 commit 42c2ca8

File tree

12 files changed

+691
-52
lines changed

12 files changed

+691
-52
lines changed

docs/src/content/docs/agent-guidance.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,41 @@ sentry dashboard widget add <dashboard> "Top Endpoints" --display table \
150150
--group-by transaction --sort -count --limit 10
151151
```
152152

153+
## Quick Reference
154+
155+
### Time filtering
156+
157+
Use `--period` (alias: `-t`) to filter by time window:
158+
159+
```bash
160+
sentry trace list --period 1h
161+
sentry span list --period 24h
162+
sentry span list -t 7d
163+
```
164+
165+
### Scoping to an org or project
166+
167+
Org and project are positional arguments following `gh` CLI conventions:
168+
169+
```bash
170+
sentry trace list my-org/my-project
171+
sentry issue list my-org/my-project
172+
sentry span list my-org/my-project/abc123def456...
173+
```
174+
175+
### Listing spans in a trace
176+
177+
Pass the trace ID as a positional argument to `span list`:
178+
179+
```bash
180+
sentry span list abc123def456...
181+
sentry span list my-org/my-project/abc123def456...
182+
```
183+
184+
### Dataset names for the Events API
185+
186+
When querying the Events API (directly or via `sentry api`), valid dataset values are: `spans`, `transactions`, `logs`, `errors`, `discover`.
187+
153188
## Common Mistakes
154189

155190
- **Wrong issue ID format**: Use `PROJECT-123` (short ID), not the numeric ID `123456789`. The short ID includes the project prefix.

docs/src/content/docs/commands/api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,8 @@ sentry api organizations/ --verbose
9595
sentry api organizations/ --dry-run
9696
```
9797

98+
### Dataset Names
99+
100+
When querying the Events API (`/events/` endpoint), valid dataset values are: `spans`, `transactions`, `logs`, `errors`, `discover`.
101+
98102
For full API documentation, see the [Sentry API Reference](https://docs.sentry.io/api/).

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,41 @@ sentry dashboard widget add <dashboard> "Top Endpoints" --display table \
160160
--group-by transaction --sort -count --limit 10
161161
```
162162

163+
### Quick Reference
164+
165+
#### Time filtering
166+
167+
Use `--period` (alias: `-t`) to filter by time window:
168+
169+
```bash
170+
sentry trace list --period 1h
171+
sentry span list --period 24h
172+
sentry span list -t 7d
173+
```
174+
175+
#### Scoping to an org or project
176+
177+
Org and project are positional arguments following `gh` CLI conventions:
178+
179+
```bash
180+
sentry trace list my-org/my-project
181+
sentry issue list my-org/my-project
182+
sentry span list my-org/my-project/abc123def456...
183+
```
184+
185+
#### Listing spans in a trace
186+
187+
Pass the trace ID as a positional argument to `span list`:
188+
189+
```bash
190+
sentry span list abc123def456...
191+
sentry span list my-org/my-project/abc123def456...
192+
```
193+
194+
#### Dataset names for the Events API
195+
196+
When querying the Events API (directly or via `sentry api`), valid dataset values are: `spans`, `transactions`, `logs`, `errors`, `discover`.
197+
163198
### Common Mistakes
164199

165200
- **Wrong issue ID format**: Use `PROJECT-123` (short ID), not the numeric ID `123456789`. The short ID includes the project prefix.

src/commands/span/list.ts

Lines changed: 109 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,79 @@ type ListFlags = {
5757
readonly fields?: string[];
5858
};
5959

60+
/**
61+
* All field names already covered by the default SPAN_FIELDS request —
62+
* both the raw API names (e.g., `id`, `span.op`) and their output-side
63+
* aliases (e.g., `span_id`, `op`) produced by `spanListItemToFlatSpan`.
64+
* Any `--fields` value NOT in this set is treated as a custom attribute
65+
* and forwarded to the API as an extra `field` parameter.
66+
*/
67+
const KNOWN_SPAN_FIELDS = new Set([
68+
// API names (in SPAN_FIELDS)
69+
"id",
70+
"parent_span",
71+
"span.op",
72+
"description",
73+
"span.duration",
74+
"timestamp",
75+
"project",
76+
"transaction",
77+
"trace",
78+
// Output aliases (from spanListItemToFlatSpan)
79+
"span_id",
80+
"parent_span_id",
81+
"op",
82+
"duration_ms",
83+
"start_timestamp",
84+
"project_slug",
85+
]);
86+
87+
/** Field group aliases that expand to curated field sets */
88+
const FIELD_GROUP_ALIASES: Record<string, string[]> = {
89+
gen_ai: [
90+
"gen_ai.usage.input_tokens",
91+
"gen_ai.usage.output_tokens",
92+
"gen_ai.request.model",
93+
"gen_ai.system",
94+
],
95+
};
96+
97+
/**
98+
* Extract field names from --fields that need additional API requests.
99+
*
100+
* Expands group aliases (e.g., `gen_ai` → four OTEL attribute fields) and
101+
* filters out names already covered by the default SPAN_FIELDS request.
102+
*
103+
* @param fields - Raw --fields values from the CLI
104+
* @returns Deduplicated extra API field names, or undefined if none are needed
105+
*/
106+
function extractExtraApiFields(
107+
fields: string[] | undefined
108+
): string[] | undefined {
109+
if (!fields?.length) {
110+
return;
111+
}
112+
113+
const expanded = new Set<string>();
114+
for (const f of fields) {
115+
const alias = FIELD_GROUP_ALIASES[f];
116+
if (alias) {
117+
for (const a of alias) {
118+
expanded.add(a);
119+
}
120+
} else {
121+
expanded.add(f);
122+
}
123+
}
124+
125+
// Remove anything already requested by SPAN_FIELDS or its output aliases
126+
for (const known of KNOWN_SPAN_FIELDS) {
127+
expanded.delete(known);
128+
}
129+
130+
return expanded.size > 0 ? Array.from(expanded) : undefined;
131+
}
132+
60133
/** Accepted values for the --sort flag (matches trace list) */
61134
const VALID_SORT_VALUES: SpanSortValue[] = ["date", "duration"];
62135

@@ -208,13 +281,17 @@ type SpanListData = {
208281
org?: string;
209282
/** Project slug for project-mode header */
210283
project?: string;
284+
/** Extra attribute names from --fields for human table columns */
285+
extraAttributes?: string[];
211286
};
212287

213288
/**
214289
* Format span list data for human-readable terminal output.
215290
*
216291
* Uses `renderMarkdown()` for the header and `formatSpanTable()` for the table,
217292
* ensuring proper rendering in both TTY and plain output modes.
293+
* When extra attributes are present (from --fields API expansion), they are
294+
* appended as additional table columns.
218295
*/
219296
function formatSpanListHuman(data: SpanListData): string {
220297
if (data.flatSpans.length === 0) {
@@ -228,7 +305,7 @@ function formatSpanListHuman(data: SpanListData): string {
228305
} else {
229306
parts.push(`Spans in ${data.org}/${data.project}:\n\n`);
230307
}
231-
parts.push(formatSpanTable(data.flatSpans));
308+
parts.push(formatSpanTable(data.flatSpans, data.extraAttributes));
232309
return parts.join("\n");
233310
}
234311

@@ -266,6 +343,8 @@ function jsonTransformSpanList(data: SpanListData, fields?: string[]): unknown {
266343
type ModeContext = {
267344
cwd: string;
268345
flags: ListFlags;
346+
/** Extra API field names derived from --fields (undefined when none needed) */
347+
extraApiFields?: string[];
269348
};
270349

271350
/**
@@ -278,7 +357,7 @@ async function handleTraceMode(
278357
parsed: ParsedTraceTarget,
279358
ctx: ModeContext
280359
): Promise<{ output: SpanListData; hint?: string }> {
281-
const { flags, cwd } = ctx;
360+
const { flags, cwd, extraApiFields } = ctx;
282361
warnIfNormalized(parsed, "span.list");
283362
const { traceId, org, project } = await resolveTraceOrgProject(
284363
parsed,
@@ -311,14 +390,17 @@ async function handleTraceMode(
311390
limit: flags.limit,
312391
cursor,
313392
statsPeriod: flags.period,
393+
extraFields: extraApiFields,
314394
})
315395
);
316396

317397
// Update pagination state (handles both advance and truncation)
318398
advancePaginationState(PAGINATION_KEY, contextKey, direction, nextCursor);
319399
const hasPrev = hasPreviousPage(PAGINATION_KEY, contextKey);
320400

321-
const flatSpans = spanItems.map(spanListItemToFlatSpan);
401+
const flatSpans = spanItems.map((item) =>
402+
spanListItemToFlatSpan(item, extraApiFields)
403+
);
322404
const hasMore = !!nextCursor;
323405

324406
const nav = paginationHint({
@@ -339,7 +421,14 @@ async function handleTraceMode(
339421
}
340422

341423
return {
342-
output: { flatSpans, hasMore, hasPrev, nextCursor, traceId },
424+
output: {
425+
flatSpans,
426+
hasMore,
427+
hasPrev,
428+
nextCursor,
429+
traceId,
430+
extraAttributes: extraApiFields,
431+
},
343432
hint,
344433
};
345434
}
@@ -354,7 +443,7 @@ async function handleProjectMode(
354443
target: string | undefined,
355444
ctx: ModeContext
356445
): Promise<{ output: SpanListData; hint?: string }> {
357-
const { flags, cwd } = ctx;
446+
const { flags, cwd, extraApiFields } = ctx;
358447
const { org, project } = await resolveOrgProjectFromArg(
359448
target,
360449
cwd,
@@ -382,6 +471,7 @@ async function handleProjectMode(
382471
limit: flags.limit,
383472
cursor,
384473
statsPeriod: flags.period,
474+
extraFields: extraApiFields,
385475
})
386476
);
387477

@@ -394,7 +484,9 @@ async function handleProjectMode(
394484
);
395485
const hasPrev = hasPreviousPage(PROJECT_PAGINATION_KEY, contextKey);
396486

397-
const flatSpans = spanItems.map(spanListItemToFlatSpan);
487+
const flatSpans = spanItems.map((item) =>
488+
spanListItemToFlatSpan(item, extraApiFields)
489+
);
398490
const hasMore = !!nextCursor;
399491

400492
const nav = paginationHint({
@@ -415,7 +507,15 @@ async function handleProjectMode(
415507
}
416508

417509
return {
418-
output: { flatSpans, hasMore, hasPrev, nextCursor, org, project },
510+
output: {
511+
flatSpans,
512+
hasMore,
513+
hasPrev,
514+
nextCursor,
515+
org,
516+
project,
517+
extraAttributes: extraApiFields,
518+
},
419519
hint,
420520
};
421521
}
@@ -498,7 +598,8 @@ export const listCommand = buildListCommand("span", {
498598
async *func(this: SentryContext, flags: ListFlags, ...args: string[]) {
499599
const { cwd } = this;
500600
const parsed = parseSpanListArgs(args);
501-
const modeCtx: ModeContext = { cwd, flags };
601+
const extraApiFields = extractExtraApiFields(flags.fields);
602+
const modeCtx: ModeContext = { cwd, flags, extraApiFields };
502603

503604
const { output, hint } =
504605
parsed.mode === "trace"

0 commit comments

Comments
 (0)