Skip to content
Merged
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
6 changes: 4 additions & 2 deletions packages/cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export const myCommand = buildCommand({
},
output: {
human: formatMyData, // (data: T) => string
jsonTransform: jsonTransformMyData, // optional: (data: T, fields?) => unknown
jsonTransform: jsonTransformMyData, // optional: (data: T, fields?) => typed JSON
jsonExclude: ["humanOnlyField"], // optional: strip keys from JSON
},
parameters: {
Expand Down Expand Up @@ -227,7 +227,9 @@ Lint enforcement: `stderr.write()` is banned in command files (GritQL rule). Use
- **API fields** keep their `@sentry/api` type and come from the generated `@sentry/api/valibot` schema (`vXxxResponse.entries`, wrapped in `pipe(field, description(…))` when it needs a description).
- **Fields the CLI adds** (target context such as `org`/`orgSlug`, flags such as `dryRun`/`deleted`, computed values such as `url`, `trace`, `period`) are written once as a valibot `object` of output fields. Spread its `entries` into the schema, and get the type with `InferOutput<typeof Fields>`, or `WithOutputFields<ApiType, typeof Fields>` (`src/types/output.ts`) when the fields sit on (or override) an API object.
- Type the `human` formatter's parameter (and `jsonTransform`'s, when set) with the derived type. `buildCommand`, `buildListCommand`, and `buildDeleteCommand` infer the output type from `output` and check every `CommandOutput` that `func` yields against it. Annotate a yield (`new CommandOutput<Output>(…)`) only when it builds a discriminated-union literal (`kind: "endpoints"`) that would otherwise widen to `string`.
- Without `jsonTransform`, the data (each item, for array data) must also satisfy `output.schema`: a field the schema documents but the command doesn't produce, or documents with another type, is a compile error. With `jsonTransform`, the schema describes the transform's item, which the types can't see, so it isn't checked. Keep the data typed: an API response typed `Record<string, unknown>` can't satisfy a schema built from `@sentry/api/valibot`, so type it with its `@sentry/api` response type.
- Without `jsonTransform`, the data (each item, for array data) must also satisfy `output.schema`: a field the schema documents but the command doesn't produce, or documents with another type, is a compile error. Keep the data typed: an API response typed `Record<string, unknown>` can't satisfy a schema built from `@sentry/api/valibot`, so type it with its `@sentry/api` response type.
- With `jsonTransform`, the schema describes the transform's item: each item of a `{ data: [...] }` envelope or an array, otherwise the result itself. Every field the schema documents must exist on that item with a matching type; optionality isn't compared, since `--fields` makes items `Partial`. So the transform must return a typed result, not `unknown` or `Record<string, unknown>`: return `ListJson<Item>` from `jsonTransformListResult`, type a hand-built envelope as `ListEnvelope<Item>` (`lib/org-list.ts`), and let a single object's type come from its spread with `filterFieldsIfSet`. The error names the mismatched fields under `~outputMismatch`.
- An `OrgListConfig` checks its entity against `schema` when you pass the schema's type: `OrgListConfig<SentryTeam, TeamWithOrg, typeof SentryTeamSchema>`. Don't annotate an output config as `OutputConfig<T>` or check it with `satisfies OutputConfig<T>`: that erases the schema's type, and the check passes without comparing anything.
- Reuse the shared fields: `DryRunOutputField`, `orgListItemSchema`/`WithOrg` (`lib/org-list.ts`) for org-scoped lists, `opaque<T>()` for a field the docs leave opaque but the code types (a full event payload).
- Data only the human formatter needs (pre-rendered lines, a resolved target) stays out of the schema; keep it outside the derived type or strip it with `jsonExclude`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,15 @@ List spans in a project or trace

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Span ID |
| `parent_span` | string \| null | Parent span ID |
| `span.op` | string \| null | Span operation (e.g. http.client, db) |
| `span_id` | string | Span ID |
| `trace_id` | string | Trace ID |
| `parent_span_id` | string \| null | Parent span ID |
| `op` | string | Span operation (e.g. http.client, db) |
| `description` | string \| null | Span description |
| `span.duration` | number \| null | Duration (ms) |
| `timestamp` | string | Timestamp (ISO 8601) |
| `project` | string | Project slug |
| `transaction` | string \| null | Transaction name |
| `trace` | string | Trace ID |
| `duration_ms` | number | Duration (ms) |
| `start_timestamp` | number | Start time (Unix seconds) |
| `project_slug` | string | Project slug |
| `transaction` | string | Transaction name |

**Examples:**

Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/commands/agent-conversation/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
paginationHint,
targetPatternExplanation,
} from "../../lib/list-command.js";
import type { ListEnvelope } from "../../lib/org-list.js";
import { withProgress } from "../../lib/polling.js";
import { toNumericId } from "../../lib/resolve-target.js";
import {
Expand Down Expand Up @@ -109,13 +110,13 @@ function formatListHuman(result: ConversationListResult): string {
function jsonTransform(
result: ConversationListResult,
fields?: string[],
): unknown {
): ListEnvelope<ConversationListItem> {
const items =
fields && fields.length > 0
? result.conversations.map((c) => filterFields(c, fields))
: result.conversations;

const envelope: Record<string, unknown> = {
const envelope: ListEnvelope<ConversationListItem> = {
data: items,
hasMore: result.hasMore,
hasPrev: !!result.hasPrev,
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/commands/dashboard/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
buildListLimitFlag,
paginationHint,
} from "../../lib/list-command.js";
import type { ListEnvelope } from "../../lib/org-list.js";
import { withProgress } from "../../lib/polling.js";
import {
buildDashboardsListUrl,
Expand Down Expand Up @@ -186,13 +187,13 @@ function formatDashboardListHuman(result: DashboardListResult): string {
function jsonTransformDashboardList(
result: DashboardListResult,
fields?: string[],
): unknown {
): ListEnvelope<DashboardListItem> {
const items =
fields && fields.length > 0
? result.dashboards.map((d) => filterFields(d, fields))
: result.dashboards;

const envelope: Record<string, unknown> = {
const envelope: ListEnvelope<DashboardListItem> = {
data: items,
hasMore: result.hasMore,
hasPrev: !!result.hasPrev,
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/commands/dashboard/revisions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
LIST_CURSOR_FLAG,
paginationHint,
} from "../../lib/list-command.js";
import type { ListEnvelope } from "../../lib/org-list.js";
import { withProgress } from "../../lib/polling.js";
import { buildDashboardUrl } from "../../lib/sentry-web-urls.js";
import {
Expand Down Expand Up @@ -91,13 +92,13 @@ function formatRevisionsHuman(result: RevisionsResult): string {
function jsonTransformRevisions(
result: RevisionsResult,
fields?: string[],
): unknown {
): ListEnvelope<DashboardRevision> {
const items =
fields && fields.length > 0
? result.revisions.map((r) => filterFields(r, fields))
: result.revisions;

const envelope: Record<string, unknown> = {
const envelope: ListEnvelope<DashboardRevision> = {
data: items,
hasMore: result.hasMore,
hasPrev: !!result.hasPrev,
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/commands/event/shared-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
LIST_PERIOD_FLAG,
PERIOD_ALIASES,
} from "../../lib/list-command.js";
import type { ListEnvelope } from "../../lib/org-list.js";
import { sanitizeQuery } from "../../lib/search-query.js";
import { appendPeriodHint, type TimeRange } from "../../lib/time-range.js";
import type { IssueEvent } from "../../types/index.js";
Expand Down Expand Up @@ -154,13 +155,13 @@ export function formatEventsHuman(result: EventsResult): string {
export function jsonTransformEvents(
result: EventsResult,
fields?: string[],
): unknown {
): ListEnvelope<IssueEvent> {
const items =
fields && fields.length > 0
? result.events.map((e) => filterFields(e, fields))
: result.events;

const envelope: Record<string, unknown> = {
const envelope: ListEnvelope<IssueEvent> = {
data: items,
hasMore: result.hasMore,
hasPrev: result.hasPrev,
Expand Down
15 changes: 4 additions & 11 deletions packages/cli/src/commands/event/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import {
loadEventAttachments,
} from "../../lib/event-attachments.js";
import { formatEventDetails } from "../../lib/formatters/index.js";
import { filterFields } from "../../lib/formatters/json.js";
import { filterFieldsIfSet } from "../../lib/formatters/json.js";
import { CommandOutput } from "../../lib/formatters/output.js";
import { validateHexId } from "../../lib/hex-id.js";
import {
Expand Down Expand Up @@ -162,20 +162,13 @@ export function formatEventView(data: EventViewData): string {
* This preserves backward compatibility: single-event callers still get
* a flat object, while multi-event callers get an array.
*/
export function jsonTransformEventView(
data: EventViewData,
fields?: string[],
): unknown {
const transform = (entry: SingleEventViewData): Record<string, unknown> => {
export function jsonTransformEventView(data: EventViewData, fields?: string[]) {
const transform = (entry: SingleEventViewData) => {
const fieldsOut: EventViewFields = {
trace: entry.trace,
attachments: entry.attachments ?? [],
};
const result: Record<string, unknown> = { ...entry.event, ...fieldsOut };
if (fields && fields.length > 0) {
return filterFields(result, fields) as Record<string, unknown>;
}
return result;
return filterFieldsIfSet({ ...entry.event, ...fieldsOut }, fields);
};

// Use requestedCount (not events.length) to decide the shape so that
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/commands/feedback/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
paginationHint,
targetPatternExplanation,
} from "../../lib/list-command.js";
import { jsonTransformListResult } from "../../lib/org-list.js";
import { jsonTransformListResult, type ListJson } from "../../lib/org-list.js";
import { withProgress } from "../../lib/polling.js";
import {
type ResolvedOrgOptionalTarget,
Expand All @@ -53,6 +53,7 @@ import {
} from "../../lib/time-range.js";
import {
type FeedbackListResult,
type SentryFeedback,
SentryFeedbackSchema,
} from "../../types/index.js";

Expand Down Expand Up @@ -121,7 +122,7 @@ function pageHint(
function jsonTransformFeedbackList(
result: FeedbackListResult,
fields?: string[],
): unknown {
): ListJson<SentryFeedback> {
return jsonTransformListResult(
{
items: result.feedback,
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/feedback/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ async function tryListAttachments(
function jsonTransformFeedbackView(
data: FeedbackViewResult,
fields?: string[],
): unknown {
const result: Record<string, unknown> = {
) {
const result = {
...data.feedback,
org: data.org,
event: data.event,
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/commands/integration/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ const INTEGRATION_COLUMNS: Column<IntegrationWithOrg>[] = [
/** Shared config that plugs into the org-list framework. */
const integrationListConfig: OrgListConfig<
SentryIntegration,
IntegrationWithOrg
IntegrationWithOrg,
typeof SentryIntegrationSchema
> = {
paginationKey: PAGINATION_KEY,
entityPlural: "integrations",
Expand Down
10 changes: 4 additions & 6 deletions packages/cli/src/commands/issue/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,7 @@ import {
shouldAutoCompact,
writeIssueTable,
} from "../../lib/formatters/index.js";
import {
CommandOutput,
type OutputConfig,
} from "../../lib/formatters/output.js";
import { CommandOutput } from "../../lib/formatters/output.js";
import {
appendQueryHint,
buildListCommand,
Expand All @@ -74,6 +71,7 @@ import {
type FetchResult as FetchResultOf,
jsonTransformListResult,
type ListCommandMeta,
type ListJson,
type ListResult,
type ModeHandler,
trimWithGroupGuarantee,
Expand Down Expand Up @@ -1604,7 +1602,7 @@ function formatIssueListHuman(result: IssueListResult): string {
function jsonTransformIssueList(
result: IssueListResult,
fields?: string[],
): unknown {
): ListJson<SentryIssue> {
const transformed = jsonTransformListResult(result, fields);
// Only inject into empty paginated envelopes — helps agents discover
// query syntax when their search returned nothing.
Expand All @@ -1623,7 +1621,7 @@ function jsonTransformIssueList(
}

/** Output configuration for the issue list command. */
const issueListOutput: OutputConfig<IssueListResult> = {
const issueListOutput = {
human: formatIssueListHuman,
jsonTransform: jsonTransformIssueList,
schema: SentryIssueSchema,
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/commands/log/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ import {
TARGET_PATTERN_NOTE,
} from "../../lib/list-command.js";
import { logger } from "../../lib/logger.js";
import type { ListEnvelope } from "../../lib/org-list.js";
import { withProgress } from "../../lib/polling.js";
import {
type BoundArguments,
Expand Down Expand Up @@ -737,7 +738,10 @@ function createLogRenderer(): HumanRenderer<LogOutput> {
* {@link LogLike} items (follow mode). Single-fetch yields a JSON envelope
* with `data` and `hasMore`; follow mode yields one JSON object per line (JSONL).
*/
function jsonTransformLogOutput(data: LogOutput, fields?: string[]): unknown {
function jsonTransformLogOutput(
data: LogOutput,
fields?: string[],
): ListEnvelope<LogLike> | Partial<LogLike> {
if (isLogListResult(data)) {
// Batch (single-fetch): return envelope with data + hasMore
const logList = data;
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/commands/monitor/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ const MONITOR_COLUMNS: Column<MonitorWithOrg>[] = [
];

/** Shared config that plugs into the org-list framework. */
const monitorListConfig: OrgListConfig<SentryMonitor, MonitorWithOrg> = {
const monitorListConfig: OrgListConfig<
SentryMonitor,
MonitorWithOrg,
typeof SentryMonitorSchema
> = {
paginationKey: PAGINATION_KEY,
entityPlural: "monitors",
commandPrefix: "sentry monitor list",
Expand Down
10 changes: 4 additions & 6 deletions packages/cli/src/commands/project/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,7 @@ import {
resolveCursor,
} from "../../lib/db/pagination.js";
import { ResolutionError, withAuthGuard } from "../../lib/errors.js";
import {
CommandOutput,
type OutputConfig,
} from "../../lib/formatters/output.js";
import { CommandOutput } from "../../lib/formatters/output.js";
import { type Column, formatTable } from "../../lib/formatters/table.js";
import {
buildListCommand,
Expand Down Expand Up @@ -922,9 +919,10 @@ export const listCommand = buildListCommand("project", {
}
return parts.join("");
},
jsonTransform: jsonTransformListResult,
jsonTransform: (result: ListResult<ProjectWithOrg>, fields?: string[]) =>
jsonTransformListResult(result, fields),
schema: orgListItemSchema(SentryProjectSchema),
} satisfies OutputConfig<ListResult<ProjectWithOrg>>,
},
parameters: {
positional: listTargetPositional({ names: true, multi: true }),
flags: {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/project/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ function hydrateOrganizationName(entry: ProjectViewEntry): ProjectViewEntry {
function jsonTransformProjectView(
entries: ProjectViewEntry[],
fields?: string[],
): unknown {
) {
const hydrated = entries.map((entry) => {
const { detectedFrom: _detectedFrom, ...rest } =
hydrateOrganizationName(entry);
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/commands/replay/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
paginationHint,
targetPatternExplanation,
} from "../../lib/list-command.js";
import type { ListEnvelope } from "../../lib/org-list.js";
import { withProgress } from "../../lib/polling.js";
import {
getReplayUserLabel,
Expand Down Expand Up @@ -231,13 +232,13 @@ function formatReplayListHuman(result: ReplayListResult): string {
function jsonTransformReplayList(
result: ReplayListResult,
fields?: string[],
): unknown {
): ListEnvelope<ReplayListItem> {
const items =
fields && fields.length > 0
? result.replays.map((replay) => filterFields(replay, fields))
: result.replays;

const envelope: Record<string, unknown> = {
const envelope: ListEnvelope<ReplayListItem> = {
data: items,
hasMore: result.hasMore,
hasPrev: result.hasPrev,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/replay/view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,7 @@ export const viewCommand = buildCommand({
human: formatReplayDetails,
jsonTransform: (data: ReplayViewData, fields?: string[]) => {
const { replay, ...outputFields } = data;
const result: Record<string, unknown> = { ...replay, ...outputFields };
const result = { ...replay, ...outputFields };
return fields && fields.length > 0
? filterFields(result, fields)
: result;
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/src/commands/repo/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ const REPO_COLUMNS: Column<RepositoryWithOrg>[] = [
];

/** Shared config that plugs into the org-list framework. */
const repoListConfig: OrgListConfig<SentryRepository, RepositoryWithOrg> = {
const repoListConfig: OrgListConfig<
SentryRepository,
RepositoryWithOrg,
typeof SentryRepositorySchema
> = {
paginationKey: PAGINATION_KEY,
entityPlural: "repositories",
commandPrefix: "sentry repo list",
Expand Down
Loading
Loading