Skip to content
1 change: 1 addition & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@
"sentry-cli/no-namespace-import": "error",
"sentry-cli/no-examples-in-full-description": "error",
"sentry-cli/no-regex-markdown-parsing": "error",
"sentry-cli/json-transform-uses-fields": "error",
"no-restricted-imports": [
"error",
{
Expand Down
1 change: 1 addition & 0 deletions packages/cli/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export const myCommand = buildCommand({
- Functions are `async *func()` generators — yield `new CommandOutput(data)`, return `{ hint }`.
- `output.human` receives the same data object that gets serialized to JSON — no divergent-data paths.
- The wrapper auto-injects `--json` and `--fields` flags. Do NOT add your own `json` flag.
- A `jsonTransform` replaces the default `--fields` filtering, so it must apply its `fields` argument (`filterFieldsIfSet` from `lib/formatters/json.ts`, or per item for list envelopes). The `sentry-cli/json-transform-uses-fields` lint rule rejects one that never reads it ([#1808](https://github.com/getsentry/toolkit/issues/1808)). Drop a transform that only returns its input; the default path already filters.
- Do NOT use `stdout.write()` or `if (flags.json)` branching — the wrapper handles it.

### Command File Structure
Expand Down
92 changes: 92 additions & 0 deletions packages/cli/lint-rules/cli-oxlint-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,61 @@ function readsProperty(node, object, property) {
);
}

const FUNCTION_TYPES = new Set([
"ArrowFunctionExpression",
"FunctionDeclaration",
"FunctionExpression",
]);

/**
* Top-level functions of a module by name: `function f() {}` and
* `const f = () => {}`, exported or not.
*/
function moduleFunctions(program) {
const functions = new Map();
for (const statement of program.body) {
const declaration =
statement.type === "ExportNamedDeclaration"
? statement.declaration
: statement;
if (declaration?.type === "FunctionDeclaration" && declaration.id) {
functions.set(declaration.id.name, declaration);
}
if (declaration?.type === "VariableDeclaration") {
for (const declarator of declaration.declarations) {
if (
declarator.id.type === "Identifier" &&
FUNCTION_TYPES.has(declarator.init?.type)
)
functions.set(declarator.id.name, declarator.init);
}
}
}
return functions;
}

/** Whether a function reads its second parameter (the parsed `--fields`). */
function readsFieldsParameter(fn) {
const fields = fn.params[1];
const name =
fields?.type === "AssignmentPattern" ? fields.left?.name : fields?.name;
return Boolean(name) && containsIdentifier(fn.body, name);
}

/** `jsonTransform: ...` properties anywhere under a node. */
function jsonTransformProperties(node) {
const found = [];
someNode(node, (n) => {
if (
n.type === "Property" &&
(n.key?.name ?? n.key?.value) === "jsonTransform"
)
found.push(n);
return false;
});
return found;
}

/**
* Target positional types from `src/lib/target-positional.ts`, and
* `boundArguments` positionals (`BoundArguments` and the commands' aliases).
Expand Down Expand Up @@ -639,6 +694,43 @@ const rules = {
}
: {},
),
// A transform that drops `fields` makes `--fields` a silent no-op:
// https://github.com/getsentry/toolkit/issues/1808
"json-transform-uses-fields": rule(
"A jsonTransform replaces the default --fields filtering, so it must read its fields parameter.",
(_context, file, report) =>
file.includes("/src/")
? {
Program(program) {
const functions = moduleFunctions(program);
// Exported jsonTransform* helpers are shared by several
// commands, so check them where they are defined.
for (const [fnName, fn] of functions) {
if (
fnName.startsWith("jsonTransform") &&
!readsFieldsParameter(fn)
)
report(fn);
}
for (const property of jsonTransformProperties(program)) {
const value = property.value;
if (FUNCTION_TYPES.has(value.type)) {
if (!readsFieldsParameter(value)) report(property);
} else if (value.type === "Identifier") {
// Imported transforms are checked in their own module.
const fn = functions.get(value.name);
if (
fn &&
!value.name.startsWith("jsonTransform") &&
!readsFieldsParameter(fn)
)
report(property);
}
}
},
}
: {},
),
"no-skipped-tests": rule(
"Do not leave disabled tests in the suite.",
(_context, file, report) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,18 @@
- `--owner <value> - Owner (team:user style value accepted by Sentry API)`
- `-n, --dry-run - Show what would happen without making changes`

**JSON Fields** (use `--json --fields` to select specific fields):

| Field | Type | Description |
|-------|------|-------------|
| `org` | string | Organization slug |
| `id` | string | Alert rule ID |
| `name` | string | Alert rule name |
| `status` | string | Rule status (active, disabled) |
| `dryRun` | boolean | True when --dry-run made no change |
| `body` | object | Request body that would be sent (--dry-run) |
| `project` | string | Project slug |

**Examples:**

```bash
Expand Down Expand Up @@ -127,6 +139,17 @@
- `-f, --force - Force the operation without confirmation`
- `-n, --dry-run - Show what would happen without making changes`

**JSON Fields** (use `--json --fields` to select specific fields):

| Field | Type | Description |
|-------|------|-------------|
| `dryRun` | boolean | True when --dry-run made no change |
| `deleted` | boolean | True when the rule was deleted |
| `org` | string | Organization slug |
| `id` | string | Alert rule ID |
| `name` | string | Alert rule name |
| `project` | string | Project slug |

**Examples:**

```bash
Expand Down Expand Up @@ -161,6 +184,28 @@
- `-m, --filter-match <value> - Filter match mode: all or any`
- `--owner <value> - Owner value (pass empty string to clear)`

**JSON Fields** (use `--json --fields` to select specific fields):

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Alert rule ID |
| `name` | string | Alert rule name |
| `organizationId` | string | |
| `createdBy` | string \| null | |
| `dateCreated` | string | |
| `dateUpdated` | string | |
| `triggers` | object \| null | |
| `actionFilters` | array \| null | |
| `environment` | string \| null | |
| `config` | object | |
| `detectorIds` | array \| null | |
| `enabled` | boolean | |
| `lastTriggered` | string \| null | |
| `owner` | string \| null | |
| `org` | string | Organization slug |
| `project` | string | Project slug |
| `status` | string | Rule status (active, disabled) |

**Examples:**

```bash
Expand Down Expand Up @@ -259,6 +304,17 @@
- `--workflow <value>... - Alert (workflow) id to notify (repeatable or comma-separated)`
- `-n, --dry-run - Show what would happen without making changes`

**JSON Fields** (use `--json --fields` to select specific fields):

| Field | Type | Description |
|-------|------|-------------|
| `org` | string | Organization slug |
| `id` | string | Alert rule ID |
| `name` | string | Alert rule name |
| `status` | string | Rule status (active, disabled) |
| `dryRun` | boolean | True when --dry-run made no change |
| `body` | object | Request body that would be sent (--dry-run) |

Check warning on line 316 in packages/cli/plugins/sentry-cli/skills/sentry-cli/references/alert.md

View check run for this annotation

@sentry/warden / warden: code-review

Metric create dry-run body schema does not match JSON output

`--fields` completion and skill docs type dry-run `body` as the nested detector request payload, but JSON prints the flat CLI body (`query`, `aggregate`, `triggers`, …). Nested paths like `body.dataSources` complete then project empty; real flat keys are not offered.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Metric create dry-run body schema does not match JSON output

--fields completion and skill docs type dry-run body as the nested detector request payload, but JSON prints the flat CLI body (query, aggregate, triggers, …). Nested paths like body.dataSources complete then project empty; real flat keys are not offered.

Evidence
  • MetricAlertCreateOutputSchema types body as vCreateOrganizationProjectDetectorData.entries.body with description "Request body that would be sent (--dry-run)" (output-schemas.ts).
  • alert metrics create dry-run yields the flat CLI body (name, query, aggregate, dataset, timeWindow, triggers, projects, …), not buildDetectorBody() (create.ts; asserted in create.test.ts).
  • The transport only builds the nested detector payload inside createMetricAlertRule → buildDetectorBody (alerts.ts).
  • buildFieldsFlag completes with extractSchemaFieldPaths(schema), so nested detector paths under body are offered against the flat output.

Identified by Warden · code-review · 3VV-PEG


**Examples:**

```bash
Expand Down Expand Up @@ -286,6 +342,16 @@
- `-f, --force - Force the operation without confirmation`
- `-n, --dry-run - Show what would happen without making changes`

**JSON Fields** (use `--json --fields` to select specific fields):

| Field | Type | Description |
|-------|------|-------------|
| `dryRun` | boolean | True when --dry-run made no change |
| `deleted` | boolean | True when the rule was deleted |
| `org` | string | Organization slug |
| `id` | string | Alert rule ID |
| `name` | string | Alert rule name |

**Examples:**

```bash
Expand Down Expand Up @@ -319,6 +385,28 @@
- `--owner <value> - Owner value (pass empty string to clear)`
- `--workflow <value>... - Replace connected alert (workflow) ids (repeatable or comma-separated; empty string disconnects all)`

**JSON Fields** (use `--json --fields` to select specific fields):

| Field | Type | Description |
|-------|------|-------------|
| `owner` | object \| null | |
| `createdBy` | string \| null | |
| `latestGroup` | object \| null | |
| `description` | string \| null | |
| `id` | string | Alert rule ID |
| `projectId` | string \| null | |
| `name` | string | Alert rule name |
| `type` | string | |
| `workflowIds` | array \| null | |
| `dateCreated` | string | |
| `dateUpdated` | string | |
| `dataSources` | array \| null | |
| `conditionGroup` | object \| null | |
| `config` | object | |
| `enabled` | boolean | |
| `org` | string | Organization slug |
| `status` | string | Rule status (active, disabled) |

**Examples:**

```bash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,21 @@ Authenticate with Sentry

List stored accounts

**JSON Fields** (use `--json --fields` to select specific fields):

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Account ID |
| `active` | boolean | Whether commands use this account |
| `host` | string | Sentry server URL |
| `userId` | string | User ID |
| `email` | string | User email |
| `username` | string | Username |
| `name` | string | User display name |
| `usable` | boolean | Whether the credential can still be used or refreshed |
| `expiresAt` | number | Token expiry (Unix milliseconds) |
| `envVar` | string | Env var providing the token, if not stored |

**Examples:**

```bash
Expand Down
Loading
Loading