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
11 changes: 7 additions & 4 deletions packages/cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,7 @@ takes:
| Comma-separated fixed values (`--scope`, `--features`) | `choiceListParser(values)` (or `(values, parse)`) | the next item of the list, leaving out the items already in it (`org:read,org:w` → `org:read,org:write`) |
| Platform | `parsePlatform` | platform identifiers, common ones first |
| Something that already exists in Sentry, as a flag value (`--environment`, `--release`, `--team`, `--owner`, `--integration`, `--app`, `--dsn`, `--workflow`, `--project`, `issue tags <key>`) | `parseEnvironment`, `parseEnvironmentList`, `parseRelease`, `parseTeam`, `parseOwner` (`team:<id>`), `parseIntegration`, `parseSentryApp`, `parseAppLinkField` (`name=value`), `parseDsn`, `parseWorkflowList`, `parseProjectSlug`, `parseProjectList`, `parseTagKey` | cached entities of the org the command's target names, else of every org, each once; the `*List` parsers complete the last item of a comma-separated list |
| Field or aggregate of a dataset (`explore --field`/`--sort`, `dashboard widget --group-by`/`--query`/`--sort`, `alert metrics --aggregate`) | `fieldParser`, `fieldOrAggregateParser`, `aggregateParser` (`fn(field)`), or `aggregateShorthandParser` (`fn:field`), given a `DatasetCompletion`: what completes on each dataset (start from `DATASET_FIELDS`), the command's `--dataset` aliases, and its default dataset | the fields of the dataset the `--dataset` typed names (else the default; every dataset when there is none): cached attribute keys of spans, logs, and trace metrics, cached issue tag keys for errors, or a fixed set (replays); then its aggregate functions, from `aggregateFunctionsOf` in `types/dashboard.ts`, as `fn()`, and `fn(field)` once a name is typed in full (`p95` → `p95(span.duration)`). A word starting with `-` completes flag names, so a descending sort's `-` is typed after completing |
| Issue as a flag value (`issue merge --into`) | `parseIssueReference` | what `ISSUE_TARGET` completes |
| Git ref, commit, or repository (`--base-ref`, `--head-sha`, `--repo`, `--commit REPO@SHA`, `set-commits --from`) | `parseVcsRef` (branch and tag names as the remote knows them), `parseGitRef` (refs git resolves locally), `parseGitCommit`, `parseGitRepo` (`owner/repo` of the remotes), `parseRepoCommitList` | read from git in the shell's directory; nothing outside a work tree |
| A structured value with its own grammar (`issue archive --until`, `api <endpoint>`) | `parseArchiveUntil`, `parseApiEndpoint` | the grammar: `forever`, `auto`, `10x`, `10x/5m`, …; API schema paths one segment at a time, with cached orgs and projects for their placeholders |
Expand Down Expand Up @@ -451,12 +452,14 @@ its module must not import `completion-target.ts`, or the two would load
in a cycle.
Cached entities (issues, dashboards, environments, releases, teams,
monitors, alert workflows, integrations, Sentry Apps and their issue-link
form fields, DSNs, issue tag keys) come from the `completion_cache` table. The
form fields, DSNs, issue tag keys, attribute keys of each trace item
dataset) come from the `completion_cache` table. The
authenticated fetch in `src/lib/sentry-client.ts` offers every API response to
`src/lib/api/completion-seed.ts`, which stores entities from the `@sentry/api`
operations it lists (by URL template, type-checked against the SDK), so API
functions never seed by hand. To complete a new entity type, add it to
`CompletionEntityKind`, its operations in `completion-seed.ts` (one
operations it lists (by URL template, type-checked against the SDK, and the
query parameters that pick what a response holds, such as the attribute
keys' `dataset`), so API functions never seed by hand. To complete a new
entity type, add it to `CompletionEntityKind`, its operations in `completion-seed.ts` (one
response may seed several kinds), a tagged parser, and an `entityCompleter`
(target positionals) or `flagEntityCompleter` (flag values) entry in
`src/lib/complete.ts`. To add a
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/commands/alert/metrics/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createMetricAlertRule } from "../../../lib/api-client.js";
import type { ParsedOrgProject } from "../../../lib/arg-parsing.js";
import { buildCommand, numberParser } from "../../../lib/command.js";
import {
aggregateParser,
choiceParser,
parseEnvironment,
parseOwner,
Expand All @@ -31,6 +32,7 @@ import {
parseMetricTriggers,
} from "../../../types/metric-alert.js";
import {
METRIC_ALERT_FIELD_COMPLETION,
METRIC_TIME_WINDOW_VALUES,
normalizeProjectList,
parseJsonObjectList,
Expand Down Expand Up @@ -144,7 +146,7 @@ export const createCommand = buildCommand({
},
aggregate: {
kind: "parsed",
parse: parseText,
parse: aggregateParser(METRIC_ALERT_FIELD_COMPLETION),
brief: "Aggregate expression (for example count(), p95(span.duration))",
},
dataset: {
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/commands/alert/metrics/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "../../../lib/api-client.js";
import { buildCommand, numberParser } from "../../../lib/command.js";
import {
aggregateParser,
choiceParser,
parseEnvironment,
parseOwner,
Expand All @@ -28,6 +29,7 @@ import {
parseMetricTriggers,
} from "../../../types/metric-alert.js";
import {
METRIC_ALERT_FIELD_COMPLETION,
METRIC_TIME_WINDOW_VALUES,
normalizeMetricDataset,
normalizeProjectList,
Expand Down Expand Up @@ -243,7 +245,7 @@ export const editCommand = buildCommand({
},
aggregate: {
kind: "parsed",
parse: parseText,
parse: aggregateParser(METRIC_ALERT_FIELD_COMPLETION),
optional: true,
brief: "Aggregate expression",
},
Expand Down
26 changes: 26 additions & 0 deletions packages/cli/src/commands/alert/mutation-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@
* Shared validation and parsing helpers for alert mutation commands (create/edit).
*/

import {
DATASET_FIELDS,
type DatasetCompletion,
type DatasetFields,
} from "../../lib/completion-target.js";
import { ValidationError } from "../../lib/errors.js";
import {
METRIC_ALERT_DATASET_ALIASES,
METRIC_ALERT_DATASET_NAMES,
type MetricAlertDataset,
metricAlertSource,
} from "../../types/metric-alert.js";

Expand All @@ -14,6 +20,26 @@ export const ISSUE_MATCH_MODES = ["all", "any"] as const;
/** Deprecated dataset names that now route to `spans` with `is_transaction:true`. */
const TRANSACTION_DATASET_NAMES = new Set(["transaction", "transactions"]);

/**
* What `--aggregate` completes, by the dataset `--dataset` names. Crash
* rate (`metrics`, `sessions`) aggregates are not fields of a dataset, so
* nothing completes for them; without `--dataset`, every dataset's.
*/
export const METRIC_ALERT_FIELD_COMPLETION: DatasetCompletion = {
datasets: {
errors: DATASET_FIELDS.errors,
events: DATASET_FIELDS.errors,
spans: DATASET_FIELDS.spans,
logs: DATASET_FIELDS.logs,
} satisfies Partial<Record<MetricAlertDataset, DatasetFields>>,
aliases: {
...METRIC_ALERT_DATASET_ALIASES,
...Object.fromEntries(
[...TRANSACTION_DATASET_NAMES].map((name) => [name, "spans"]),
),
},
};

/** Query filter appended when migrating a `transaction(s)` request to `spans`. */
const IS_TRANSACTION_FILTER = "is_transaction:true";

Expand Down
46 changes: 46 additions & 0 deletions packages/cli/src/commands/dashboard/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import {
listDashboardsPaginated,
} from "../../lib/api-client.js";
import type { ParsedOrgProject } from "../../lib/arg-parsing.js";
import {
DATASET_FIELDS,
type DatasetCompletion,
type DatasetFields,
} from "../../lib/completion-target.js";
import {
ApiError,
ResolutionError,
Expand All @@ -24,8 +29,11 @@ import { logger } from "../../lib/logger.js";
import { resolveOrgOnlyTarget } from "../../lib/resolve-target.js";
import { isAllDigits } from "../../lib/utils.js";
import {
AGGREGATE_ALIASES,
aggregateFunctionsOf,
type DashboardWidget,
DISPLAY_TYPES,
mapWidgetTypeToDataset,
parseAggregate,
parseSortExpression,
parseWidgetInput,
Expand Down Expand Up @@ -616,6 +624,44 @@ const DATASET_ALIASES: Record<string, string> = {
metricsenhanced: "tracemetrics",
};

/**
* What `--query`, `--sort`, and `--group-by` complete on a widget type: the
* fields of the dataset it queries, and the aggregate functions
* `validateAggregateNames` accepts on it, with the aliases that resolve to
* them. `undefined` for a widget type that queries no dataset.
*/
function widgetFields(type: string): DatasetFields | undefined {
const dataset = mapWidgetTypeToDataset(type);
if (!(dataset && dataset in DATASET_FIELDS)) {
return;
}
const functions = aggregateFunctionsOf(type);
return {
entity: DATASET_FIELDS[dataset as keyof typeof DATASET_FIELDS].entity,
functions: [
...functions,
...Object.entries(AGGREGATE_ALIASES)
.filter(([, canonical]) => functions.includes(canonical))
.map(([alias]) => alias),
],
};
}

/**
* What `--query`, `--sort`, and `--group-by` complete, by the widget type
* `--dataset` names. Without `--dataset`, `widget edit` keeps the widget's,
* so it completes every type's; `widget add` sets the spans default.
*/
export const WIDGET_FIELD_COMPLETION: DatasetCompletion = {
datasets: Object.fromEntries(
WIDGET_TYPES.flatMap((type) => {
const fields = widgetFields(type);
return fields ? [[type, fields]] : [];
}),
),
aliases: DATASET_ALIASES,
};

/**
* Normalize a user-provided `--dataset` value to the canonical widget type.
*
Expand Down
18 changes: 14 additions & 4 deletions packages/cli/src/commands/dashboard/widget/add.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ import type { SentryContext } from "../../../context.js";
import { getDashboard, updateDashboard } from "../../../lib/api-client.js";
import { buildCommand, numberParser } from "../../../lib/command.js";
import {
aggregateShorthandParser,
choiceParser,
DASHBOARD_TARGET,
type DatasetCompletion,
fieldParser,
parseText,
} from "../../../lib/completion-target.js";
import { ValidationError } from "../../../lib/errors.js";
Expand All @@ -24,6 +27,7 @@ import {
assignDefaultLayout,
type DashboardDetail,
type DashboardWidget,
DEFAULT_WIDGET_TYPE,
DISPLAY_TYPES,
FALLBACK_LAYOUT,
GRID_COLUMN_POSITIONS,
Expand All @@ -34,7 +38,6 @@ import {
WIDGET_TYPES,
type WidgetLayoutFlags,
type WidgetLayoutMode,
AGGREGATE_FUNCTION_NAMES,
} from "../../../types/dashboard.js";
import {
type DashboardInput,
Expand All @@ -47,9 +50,16 @@ import {
withDashboardErrors,
normalizeDataset,
validateWidgetEnums,
WIDGET_FIELD_COMPLETION,
type WidgetQueryFlags,
} from "../resolve.js";

/** What `--query`, `--sort`, and `--group-by` complete: a spans widget's by default. */
const FIELD_COMPLETION: DatasetCompletion = {
...WIDGET_FIELD_COMPLETION,
default: DEFAULT_WIDGET_TYPE,
};

type AddFlags = WidgetQueryFlags &
WidgetLayoutFlags & {
readonly display: string;
Expand Down Expand Up @@ -191,7 +201,7 @@ export const addCommand = buildCommand({
},
query: {
kind: "parsed",
parse: choiceParser(AGGREGATE_FUNCTION_NAMES),
parse: aggregateShorthandParser(FIELD_COMPLETION),
brief: "Aggregate expression (e.g. count, p95:span.duration)",
variadic: true,
optional: true,
Expand All @@ -204,14 +214,14 @@ export const addCommand = buildCommand({
},
"group-by": {
kind: "parsed",
parse: parseText,
parse: fieldParser(FIELD_COMPLETION),
brief: "Group-by column (repeatable)",
variadic: true,
optional: true,
},
sort: {
kind: "parsed",
parse: choiceParser(AGGREGATE_FUNCTION_NAMES),
parse: aggregateShorthandParser(FIELD_COMPLETION),
brief: "Order by (prefix - for desc, e.g. -count)",
optional: true,
},
Expand Down
15 changes: 10 additions & 5 deletions packages/cli/src/commands/dashboard/widget/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@
import type { SentryContext } from "../../../context.js";
import { getDashboard, updateDashboard } from "../../../lib/api-client.js";
import { buildCommand, numberParser } from "../../../lib/command.js";
import { choiceParser, parseText } from "../../../lib/completion-target.js";
import {
aggregateShorthandParser,
choiceParser,
fieldParser,
parseText,
} from "../../../lib/completion-target.js";
import { ValidationError } from "../../../lib/errors.js";
import { formatWidgetEdited } from "../../../lib/formatters/human.js";
import { CommandOutput } from "../../../lib/formatters/output.js";
Expand All @@ -29,7 +34,6 @@ import {
validateWidgetLayout,
WIDGET_TYPES,
type WidgetLayoutFlags,
AGGREGATE_FUNCTION_NAMES,
} from "../../../types/dashboard.js";
import { type DashboardArguments, dashboardArguments } from "../arguments.js";
import {
Expand All @@ -39,6 +43,7 @@ import {
resolveWidgetIndex,
validateSortReferencesAggregate,
validateWidgetEnums,
WIDGET_FIELD_COMPLETION,
type WidgetQueryFlags,
} from "../resolve.js";

Expand Down Expand Up @@ -268,7 +273,7 @@ export const editCommand = buildCommand({
},
query: {
kind: "parsed",
parse: choiceParser(AGGREGATE_FUNCTION_NAMES),
parse: aggregateShorthandParser(WIDGET_FIELD_COMPLETION),
brief: "Aggregate expression (e.g. count, p95:span.duration)",
variadic: true,
optional: true,
Expand All @@ -281,14 +286,14 @@ export const editCommand = buildCommand({
},
"group-by": {
kind: "parsed",
parse: parseText,
parse: fieldParser(WIDGET_FIELD_COMPLETION),
brief: "Group-by column (repeatable)",
variadic: true,
optional: true,
},
sort: {
kind: "parsed",
parse: choiceParser(AGGREGATE_FUNCTION_NAMES),
parse: aggregateShorthandParser(WIDGET_FIELD_COMPLETION),
brief: "Order by (prefix - for desc, e.g. -count)",
optional: true,
},
Expand Down
36 changes: 34 additions & 2 deletions packages/cli/src/commands/explore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@ import type { SentryContext } from "../context.js";
import {
isReplaySortValue,
listReplays,
REPLAY_SORT_FIELDS,
queryEvents,
queryEventsTimeSeries,
queryMetricsMeta,
} from "../lib/api-client.js";
import { validateLimit } from "../lib/arg-parsing.js";
import {
choiceParser,
DATASET_FIELDS,
type DatasetCompletion,
fieldOrAggregateParser,
parseEnvironmentList,
parseText,
textParser,
Expand Down Expand Up @@ -112,6 +116,34 @@ const DATASETS = buildDatasetParser(
[...LEGACY_TRANSACTION_DATASETS],
);

/**
* What `--field` completes on each dataset: attribute or tag keys and
* aggregates, or the replay fields the CLI renders. `transaction(s)`
* queries spans.
*/
const FIELD_COMPLETION: DatasetCompletion = {
datasets: {
...DATASET_FIELDS,
replays: { fields: listSupportedReplayFields() },
},
aliases: {
...DATASETS.aliases,
...Object.fromEntries(
[...LEGACY_TRANSACTION_DATASETS].map((name) => [name, "spans"]),
),
},
default: DEFAULT_DATASET,
};

/** What `--sort` completes: `--field`'s, but replays sort by their own fields. */
const SORT_COMPLETION: DatasetCompletion = {
...FIELD_COMPLETION,
datasets: {
...FIELD_COMPLETION.datasets,
replays: { fields: REPLAY_SORT_FIELDS },
},
};

// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -773,7 +805,7 @@ export const exploreCommand = buildListCommand("explore", {
flags: {
field: {
kind: "parsed",
parse: parseText,
parse: fieldOrAggregateParser(FIELD_COMPLETION),
brief:
'API field or aggregate (repeatable). E.g., title, "count()", "p50(span.duration)"',
variadic: true,
Expand Down Expand Up @@ -806,7 +838,7 @@ export const exploreCommand = buildListCommand("explore", {
},
sort: {
kind: "parsed",
parse: parseText,
parse: fieldOrAggregateParser(SORT_COMPLETION),
brief: 'Sort field (prefix with - for desc, e.g., "-count()")',
optional: true,
},
Expand Down
Loading
Loading