Dashboard › calm-forest › Session ses_2c1a6943
ses_2c1a69437ffeMZ1zqXuaYE5quvThe following tool was executed by the user
HEAD is now at 0ddb1288 feat: AsyncIterable streaming support for library SDK ! Pull request getsentry/cli#586 was already merged From https://github.com/getsentry/cli * branch main -> FETCH_HEAD 664362ca..6c7223a9 main -> origin/main Updating 664362ca..6c7223a9 Fast-forward AGENTS.md | 3 + docs/src/content/docs/agent-guidance.md | 37 +- plugins/sentry-cli/skills/sentry-cli/SKILL.md | 37 +- src/commands/cli/upgrade.ts | 139 +++-- src/lib/delta-upgrade.ts | 2 + src/lib/formatters/human.ts | 105 +++- src/lib/release-notes.ts | 720 ++++++++++++++++++++++++++ test/lib/release-notes.property.test.ts | 218 ++++++++ test/lib/release-notes.test.ts | 295 +++++++++++ 9 files changed, 1483 insertions(+), 73 deletions(-) create mode 100644 src/lib/release-notes.ts create mode 100644 ...
Let's fix https://github.com/getsentry/cli/issues/539
Let me start by reading the issue details.
title: dashboard widget add: MRI queries silently succeed but render as Internal Error state: OPEN author: sentry-junior labels: comments: 0 assignees: projects: milestone: number: 539 -- ## Summary When using `--dataset metrics` with MRI-style query syntax (e.g. `avg(g:custom/node.runtime.mem.rss@byte)`), the widget is created successfully but displays as "Internal Error" in the dashboard UI. The CLI accepts the input without error, giving no indication that the query format is unsupported. ## Reproduction ```sh sentry dashboard widget add 'My Dashboard' 'Memory Usage' \ --display line \ --dataset metrics \ --query 'avg(g:custom/node.runtime.mem.rss@byte)' ``` Widget is created, but renders as "Internal Error" in the Sentry dashboard UI. ## Expected behavior Either: 1. The CLI validates and rejects unsupported query formats for the given dataset upfront with a clear error, or 2. ...
task_id: ses_2c1a64ce0ffeooCtE7riZBaQ2f (for resuming to continue this task if needed)
<task_result>
I now have all the information needed. Here is the comprehensive analysis:
---
## Complete Analysis: `dashboard widget add` Command
### 1. Command Implementation
**File:** `/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/src/commands/dashboard/widget/add.ts`
- **Command definition:** Lines 76-284 (`addCommand = buildCommand({...})`)
- **`func()` entry point:** Lines 191-283
- **Positional arg parsing:** Lines 51-73 (`parseAddPositionalArgs`)
The command flow:
1. Parses positional args into `dashboardArgs` and `title` (line 194)
2. Validates display/dataset enums early (line 197)
3. Resolves org from target arg (lines 199-206)
4. Resolves dashboard ID by numeric ID or title (line 207)
5. Builds widget from flags via `buildWidgetFromFlags()` (lines 209-218)
6. Validates widget layout flags (line 222)
7. ...
Excellent, thorough exploration. Let me now read the key files to understand the exact code I need to modify.
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/src/types/dashboard.ts</path>
<type>file</type>
<content>440: // Aggregate & sort parsing (quote-free CLI shorthand)
441: // ---------------------------------------------------------------------------
442:
443: /**
444: * Parse a shorthand aggregate expression into Sentry query syntax.
445: * Resolves aliases (spm→epm, tpm→epm, etc.) so widgets render in the dashboard UI.
446: *
447: * Accepts three formats:
448: * "count" → "count()"
449: * "p95:span.duration" → "p95(span.duration)"
450: * "count()" → "count()" (passthrough if already has parens)
451: * "spm" → "epm()" (alias resolved)
452: */
453: export function parseAggregate(input: string): string {
454: if (input.includes("(")) {
455: // Resolve aliases even in paren form: spm() → epm(), tpm(x) → epm(x)
456: const parenIdx = input.indexOf("(");
457: const ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/src/types/dashboard.ts</path>
<type>file</type>
<content>950: | ScalarResult
951: | UnsupportedResult
952: | ErrorResult;
953:
954: // ---------------------------------------------------------------------------
955: // Dataset mapping
956: // ---------------------------------------------------------------------------
957:
958: /**
959: * Maps widget types to API dataset parameter values.
960: *
961: * Widget types that don't map to a dataset (issue, metrics, etc.)
962: * return null and are rendered as "unsupported".
963: */
964: const WIDGET_TYPE_TO_DATASET: Record<string, string> = {
965: spans: "spans",
966: discover: "discover",
967: "error-events": "errors",
968: "transaction-like": "transactions",
969: logs: "logs",
970: };
971:
972: /**
973: * Map a widget's `widgetType` to the API `dataset` parameter. ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/src/commands/dashboard/resolve.ts</path>
<type>file</type>
<content>320: *
321: * Shared between `dashboard widget add` and `dashboard widget edit`.
322: * Parses aggregate shorthand, sort expressions, and validates via Zod schema.
323: *
324: * @param opts - Widget configuration from parsed flags
325: * @returns Validated widget with computed query fields
326: */
327: export function buildWidgetFromFlags(opts: {
328: title: string;
329: display: string;
330: dataset?: string;
331: query?: string[];
332: where?: string;
333: groupBy?: string[];
334: sort?: string;
335: limit?: number;
336: }): DashboardWidget {
337: const aggregates = (opts.query ?? ["count"]).map(parseAggregate);
338: validateAggregateNames(aggregates, opts.dataset);
339:
340: // Issue table widgets need at least one column or the Sentry UI shows "Columns: None". ...
Now let me read the MRI-related portion of the aggregate parsing more carefully, and also look at what the parseAggregate function does with MRI syntax:
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/src/types/dashboard.ts</path>
<type>file</type>
<content>1: /**
2: * Dashboard types and schemas
3: *
4: * Zod schemas and TypeScript types for Sentry Dashboard API responses.
5: * Includes utility functions for stripping server-generated fields
6: * before PUT requests, and strict input validation for user-authored widgets.
7: */
8:
9: import { z } from "zod";
10:
11: import { ValidationError } from "../lib/errors.js";
12: import { logger } from "../lib/logger.js";
13:
14: // ---------------------------------------------------------------------------
15: // Widget type and display type enums
16: //
17: // Source: sentry/src/sentry/models/dashboard_widget.py
18: // Also in: @sentry/api types (cli/node_modules/@sentry/api/dist/types.gen.d.ts)
19: // ---------------------------------------------------------------------------
20:
21: /**
22: * Valid widget types (dataset ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/src/types/dashboard.ts</path>
<type>file</type>
<content>220: * The Sentry events API silently resolves these, but the dashboard widget UI
221: * only understands canonical function names from AggregationKey. Resolving
222: * aliases here ensures widgets render correctly in the UI.
223: *
224: * Source: https://github.com/getsentry/sentry/blob/master/src/sentry/search/events/constants.py
225: * SPAN_FUNCTION_ALIASES: spm→epm, sps→eps
226: * FUNCTION_ALIASES: tpm→epm, tps→eps
227: */
228: export const AGGREGATE_ALIASES: Record<string, string> = {
229: spm: "epm",
230: sps: "eps",
231: tpm: "epm",
232: tps: "eps",
233: };
234:
235: /**
236: * Canonical aggregate functions for the spans dataset (default for dashboard widgets).
237: * These are the function names the dashboard UI can render in the "Visualize" dropdown.
238: *
239: * Source: ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/src/types/dashboard.ts</path> <type>file</type> <content>310: * errors.tsx https://github.com/getsentry/sentry/blob/a42668e87cc8a0b7410ac2acecee6074c52f376f/static/app/views/dashboards/datasetConfig/errors.tsx#L115-L123 311: * transactions.tsx https://github.com/getsentry/sentry/blob/a42668e87cc8a0b7410ac2acecee6074c52f376f/static/app/views/dashboards/datasetConfig/transactions.tsx#L76-L84 312: * releases.tsx https://github.com/getsentry/sentry/blob/a42668e87cc8a0b7410ac2acecee6074c52f376f/static/app/views/dashboards/datasetConfig/releases.tsx#L90-L98 313: * logs.tsx https://github.com/getsentry/sentry/blob/a42668e87cc8a0b7410ac2acecee6074c52f376f/static/app/views/dashboards/datasetConfig/logs.tsx#L201-L209 314: * traceMetrics.tsx ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/src/types/dashboard.ts</path> <type>file</type> <content>360: "table", 361: "top_n", 362: ], 363: metrics: [ 364: "area", 365: "bar", 366: "big_number", 367: "categorical_bar", 368: "line", 369: "stacked_area", 370: "table", 371: "top_n", 372: ], 373: logs: [ 374: "area", 375: "bar", 376: "big_number", 377: "categorical_bar", 378: "line", 379: "stacked_area", 380: "table", 381: "top_n", 382: ], 383: discover: [ 384: "area", 385: "bar", 386: "big_number", 387: "categorical_bar", 388: "line", 389: "stacked_area", 390: "table", 391: "top_n", 392: ], 393: tracemetrics: ["area", "bar", "big_number", "categorical_bar", "line"], 394: "preprod-app-size": ["line"], 395: } as const satisfies Record<WidgetType, readonly string[]>; 396: 397: /** 398: * Valid `is:` filter ...
Now let me look at the existing tests to understand what patterns are covered:
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/types/dashboard.test.ts</path>
<type>file</type>
<content>1: /**
2: * Dashboard Type & Validation Tests
3: *
4: * Tests for enum constants, strict input schema, and parseWidgetInput()
5: * in src/types/dashboard.ts.
6: */
7:
8: import { describe, expect, test } from "bun:test";
9: import { ValidationError } from "../../src/lib/errors.js";
10: import {
11: assignDefaultLayout,
12: type DashboardWidget,
13: DashboardWidgetInputSchema,
14: DEFAULT_WIDGET_TYPE,
15: DISCOVER_AGGREGATE_FUNCTIONS,
16: DISPLAY_TYPES,
17: DiscoverAggregateFunctionSchema,
18: type DisplayType,
19: EventsStatsDataPointSchema,
20: EventsStatsSeriesSchema,
21: EventsTableResponseSchema,
22: GRID_COLUMNS,
23: IS_FILTER_VALUES,
24: IsFilterValueSchema,
25: mapWidgetTypeToDataset,
26: parseAggregate,
27: parseSortExpression,
28: parseWidgetInput,
29: ...
Found 6 matches
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/types/dashboard.test.ts:
Line 59: "tracemetrics",
Line 772: expect(mapWidgetTypeToDataset("tracemetrics")).toBeNull();
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/commands/dashboard/widget/add.test.ts:
Line 328: // tracemetrics: no table or top_n
Line 330: test("throws ValidationError for table with tracemetrics dataset", async () => {
Line 336: { json: false, display: "table", dataset: "tracemetrics" },
Line 342: expect(err.message).toContain('"tracemetrics" dataset supports');
Now let me understand the issue flow better. When someone passes --dataset metrics --query 'avg(g:custom/node.runtime.mem.rss@byte)', parseAggregate sees the ( in avg(g:custom/...) and passes it through. Then validateAggregateNames extracts "avg" which IS valid, so it passes. But the MRI syntax (g:custom/node.runtime.mem.rss@byte) is only valid for the metrics dataset which maps to null (not queryable). The widget gets created but shows "Internal Error".
Let me check how the metrics and tracemetrics datasets are handled:
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/commands/dashboard/widget/add.test.ts</path>
<type>file</type>
<content>1: /**
2: * Dashboard Widget Add Command Tests
3: *
4: * Tests for the widget add command in src/commands/dashboard/widget/add.ts.
5: * Uses spyOn pattern to mock API client and resolve-target.
6: */
7:
8: import {
9: afterEach,
10: beforeEach,
11: describe,
12: expect,
13: mock,
14: spyOn,
15: test,
16: } from "bun:test";
17:
18: import { addCommand } from "../../../../src/commands/dashboard/widget/add.js";
19: // biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking
20: import * as apiClient from "../../../../src/lib/api-client.js";
21: import { ValidationError } from "../../../../src/lib/errors.js";
22: // biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking
23: import * as resolveTarget from "../../../../src/lib/resolve-target.js";
24: import ...
Found 1 matches /home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/src/commands/dashboard/widget/index.ts: Line 23: " metrics, logs, tracemetrics, preprod-app-size\n\n" +
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/commands/dashboard/widget/add.test.ts</path>
<type>file</type>
<content>310: )
311: .catch((e: Error) => e);
312: expect(err).toBeInstanceOf(ValidationError);
313: expect(err.message).toContain('"preprod-app-size" dataset supports');
314: });
315:
316: test("allows line with preprod-app-size dataset", async () => {
317: const { context } = createMockContext();
318: const func = await addCommand.loader();
319: await func.call(
320: context,
321: { json: false, display: "line", dataset: "preprod-app-size" },
322: "123",
323: "App Size"
324: );
325: expect(updateDashboardSpy).toHaveBeenCalledTimes(1);
326: });
327:
328: // tracemetrics: no table or top_n
329: // https://github.com/getsentry/sentry/blob/a42668e/static/app/views/dashboards/datasetConfig/traceMetrics.tsx#L285-L291
330: test("throws ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/commands/dashboard/resolve.test.ts</path>
<type>file</type>
<content>1: /**
2: * Dashboard Resolution Utility Tests
3: *
4: * Tests for positional argument parsing, dashboard ID resolution,
5: * and org resolution in src/commands/dashboard/resolve.ts.
6: */
7:
8: import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
9: import {
10: enrichDashboardError,
11: parseDashboardListArgs,
12: parseDashboardPositionalArgs,
13: resolveDashboardId,
14: resolveOrgFromTarget,
15: } from "../../../src/commands/dashboard/resolve.js";
16: // biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking
17: import * as apiClient from "../../../src/lib/api-client.js";
18: import { parseOrgProjectArg } from "../../../src/lib/arg-parsing.js";
19: import {
20: ApiError,
21: ContextError,
22: ResolutionError,
23: ValidationError,
24: } from ...
Now let me look at the validate and build flow in resolve.ts more carefully, specifically the validateWidgetEnums and buildWidgetFromFlags functions, plus the tests for validateAggregateNames:
No files found
No files found
Found 14 matches
/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/types/dashboard.test.ts:
Line 26: parseAggregate,
Line 130: // sps/spm are aliases resolved in parseAggregate(), not canonical functions
Line 392: // parseAggregate
Line 395: describe("parseAggregate", () => {
Line 397: expect(parseAggregate("count")).toBe("count()");
Line 401: expect(parseAggregate("p95:span.duration")).toBe("p95(span.duration)");
Line 405: expect(parseAggregate("count()")).toBe("count()");
Line 409: expect(parseAggregate("avg(span.self_time)")).toBe("avg(span.self_time)");
Line 413: expect(parseAggregate("avg:span.self_time")).toBe("avg(span.self_time)");
Line 417: expect(parseAggregate("p50")).toBe("p50()");
Line 418: expect(parseAggregate("p75")).toBe("p75()");
Line 419: ...
Now I have a clear picture. Let me plan the fix:
[
{
"content": "Detect MRI-style query syntax and reject with helpful error message",
"status": "pending",
"priority": "high"
},
{
"content": "Validate that `metrics` and `tracemetrics` datasets get appropriate query format guidance",
"status": "pending",
"priority": "high"
},
{
"content": "Add tests for MRI detection and validation",
"status": "pending",
"priority": "high"
},
{
"content": "Run existing tests to ensure no regressions",
"status": "pending",
"priority": "medium"
}
]
Here's my plan:
The problem: When a user passes --dataset metrics --query 'avg(g:custom/node.runtime.mem.rss@byte)', the CLI happily creates the widget because:
parseAggregate sees ( in the input and passes it throughvalidateAggregateNames extracts avg which is a valid function nameg:custom/...) is never validatedmetrics dataset maps to null in WIDGET_TYPE_TO_DATASET (not queryable), but there's no check preventing query creation with this datasetThe fix should:
type:namespace/name@unit) and reject it with a clear error, suggesting --dataset tracemetrics with the correct avg(value,name,type,unit) formatbuildWidgetFromFlags or validateAggregateNamesLet me implement this:
[
{
"content": "Detect MRI-style query syntax and reject with helpful error message",
"status": "in_progress",
"priority": "high"
},
{
"content": "Validate that `metrics` and `tracemetrics` datasets get appropriate query format guidance",
"status": "pending",
"priority": "high"
},
{
"content": "Add tests for MRI detection and validation",
"status": "pending",
"priority": "high"
},
{
"content": "Run existing tests to ensure no regressions",
"status": "pending",
"priority": "medium"
}
]
Let me first understand what MRI syntax looks like to write a proper regex:
MRI format: type_letter:namespace/metric_name@unit
c (counter), d (distribution), g (gauge), s (set), e (extracted)custom or sessions, etc.So in a query like avg(g:custom/node.runtime.mem.rss@byte), the argument g:custom/node.runtime.mem.rss@byte is the MRI.
The correct format for tracemetrics is avg(value,node.runtime.mem.rss,gauge,byte).
Let me look at the existing parseAggregate to understand how it handles the input currently:
When avg(g:custom/node.runtime.mem.rss@byte) is passed:
( so it enters the first branchfn = "avg", alias is undefined"avg(g:custom/node.runtime.mem.rss@byte)" unchangedThe fix: Add an MRI detection function that checks if any aggregate argument contains MRI syntax, and throw a ValidationError with guidance on the correct format.
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/src/types/dashboard.ts</path>
<type>file</type>
<content>440: // Aggregate & sort parsing (quote-free CLI shorthand)
441: // ---------------------------------------------------------------------------
442:
443: /**
444: * Parse a shorthand aggregate expression into Sentry query syntax.
445: * Resolves aliases (spm→epm, tpm→epm, etc.) so widgets render in the dashboard UI.
446: *
447: * Accepts three formats:
448: * "count" → "count()"
449: * "p95:span.duration" → "p95(span.duration)"
450: * "count()" → "count()" (passthrough if already has parens)
451: * "spm" → "epm()" (alias resolved)
452: */
453: export function parseAggregate(input: string): string {
454: if (input.includes("(")) {
455: // Resolve aliases even in paren form: spm() → epm(), tpm(x) → epm(x)
456: const parenIdx = input.indexOf("(");
457: const ...
Now I'll implement the MRI detection. I'll add it to validateAggregateNames since that's where aggregate validation happens, adding a check before the function name validation:
I'm in plan mode and can only write to plan files. Let me create a plan document.
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/.opencode/plans</path> <type>directory</type> <entries> 1774435278023-misty-knight.md 1774450386695-misty-engine.md 1774465506784-swift-falcon.md 1774466167684-gentle-canyon.md 1774814128234-happy-cactus.md sdk-typed-methods.md (6 entries) </entries>
Wrote file successfully.
LSP errors detected in other files:
<diagnostics file="/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/types/dashboard.test.ts">
ERROR [123:50] No overload matches this call.
Overload 1 of 2, '(expected: "count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"): void', gave the following error.
Argument of type 'string' is not assignable to parameter of type '"count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"'.
Overload 2 of 2, '(expected: "count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"): void', gave the following error. ...
The LSP errors are pre-existing and not related to my plan. The plan is written. Let me provide a summary and exit.
The plan is ready at .opencode/plans/fix-mri-query-validation.md. Here's the summary:
Root cause: When avg(g:custom/node.runtime.mem.rss@byte) is passed with --dataset metrics, parseAggregate passes it through unchanged because it already has parens, and validateAggregateNames only checks the function name (avg) which is valid. The MRI argument is never validated, so the widget gets created but renders as "Internal Error" in the Sentry UI.
Fix: Add MRI syntax detection to the aggregate validation pipeline in src/types/dashboard.ts:
detectMri(arg) — regex-based detection of MRI format <type>:<namespace>/<name>@<unit>rejectMriQueries(aggregates, dataset) — checks each aggregate's argument for MRI syntax and throws ValidationError with a helpful suggestion showing the correct tracemetrics formatvalidateAggregateNames — call rejectMriQueries first, before function-name validationError message example:
MRI query syntax is not supported for dashboard widgets: "avg(g:custom/node.runtime.mem.rss@byte)".
Use the tracemetrics query format instead:
--query 'avg(value,node.runtime.mem.rss,gauge,byte)'
Use --dataset tracemetrics instead of --dataset metrics.
Tests: Unit tests for detectMri parsing, validateAggregateNames MRI rejection, and integration test in the addCommand flow.
Files modified: src/types/dashboard.ts, test/types/dashboard.test.ts, test/commands/dashboard/widget/add.test.ts
The plan file at .opencode/plans/1774867540937-gentle-island.md is empty or does not exist. You must write your plan to this file before calling plan_exit. Use the write tool to create the plan file first.
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/.opencode/plans/fix-mri-query-validation.md</path> <type>file</type> <content>1: # Fix: MRI queries silently succeed but render as Internal Error 2: 3: **Issue:** https://github.com/getsentry/cli/issues/539 4: 5: ## Problem 6: 7: When using `--dataset metrics` with MRI-style query syntax (e.g. `avg(g:custom/node.runtime.mem.rss@byte)`), the widget is created successfully but displays as "Internal Error" in the dashboard UI. The CLI accepts the input without error. 8: 9: **Root cause:** `parseAggregate` sees the `(` in `avg(g:custom/...)` and passes it through unchanged. `validateAggregateNames` then extracts `avg` which is a valid function name. The MRI argument (`g:custom/node.runtime.mem.rss@byte`) is never validated. The `metrics` dataset maps to `null` in `WIDGET_TYPE_TO_DATASET` (not queryable), but there's no check preventing this. ...
I need to copy the plan to the expected path:
Wrote file successfully.
LSP errors detected in other files:
<diagnostics file="/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/types/dashboard.test.ts">
ERROR [123:50] No overload matches this call.
Overload 1 of 2, '(expected: "count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"): void', gave the following error.
Argument of type 'string' is not assignable to parameter of type '"count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"'.
Overload 2 of 2, '(expected: "count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"): void', gave the following error. ...
Good, the plan is written. The LSP errors are pre-existing and unrelated.
Should we roll our own parser or look into /home/byk/Code/getsentry/sentry for already written parser for us to utilize?
Good call — let me check the Sentry codebase for an existing MRI parser before writing one from scratch.
task_id: ses_2c1a05229ffeTT1wsDAsyqBw6Q (for resuming to continue this task if needed)
<task_result>
Good - `parse_mri` is re-exported as `sentry.snuba.metrics.parse_mri`. Now I have a complete picture. Here is my summary.
---
## MRI Parsing Code Search Results
### Primary Parser: Python (`parse_mri`)
**File:** `/home/byk/Code/getsentry/sentry/src/sentry/snuba/metrics/naming_layer/mri.py`
**Language:** Python
This is the **single, canonical MRI parser** in the Sentry codebase. There is **no TypeScript/JavaScript MRI parser** anywhere in `static/app/`. The frontend handles metrics through a different abstraction (trace metrics with separate `name`/`type`/`unit` fields rather than a combined MRI string).
#### Regex (lines 52-54)
```python
MRI_SCHEMA_REGEX_STRING = r"(?P<entity>[^:]+):(?P<namespace>[^/]+)/(?P<name>[^@]+)@(?P<unit>.+)"
MRI_SCHEMA_REGEX = re.compile(rf"^{MRI_SCHEMA_REGEX_STRING}$")
MRI_EXPRESSION_REGEX = ...
There's a well-defined parser in the Sentry Python codebase but no TypeScript equivalent — the frontend doesn't parse MRI strings at all (it uses separate name/type/unit fields instead).
The Python parser is minimal — just a regex + a dataclass:
# Regex: ^(?P<entity>[^:]+):(?P<namespace>[^/]+)/(?P<name>[^@]+)@(?P<unit>.+)$
# Result: ParsedMRI { entity, namespace, name, unit }
Key observations:
entity accepts multi-char values like "dist", not just [cdgse]c/d/g/s/e) happens downstream in lookup tables, not in the parserparse_mri_field variant also matches op(mri) format — exactly what we need for detecting avg(g:custom/foo@byte)Given this is ~5 lines of logic (a regex + named groups), I'd say roll our own — it's a trivial port of the Python parser. Pulling in a dependency or wiring a cross-repo import for a single regex would be overkill. We should use the same permissive regex from Sentry's canonical parser ([^:]+ for entity, not just [cdgse]) so we catch all MRI variants, then map the entity to a human-readable type name for the suggestion.
Let me update the plan to use the canonical Sentry regex instead of a restricted one:
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/.opencode/plans/1774867540937-gentle-island.md</path> <type>file</type> <content>1: # Fix: MRI queries silently succeed but render as Internal Error 2: 3: **Issue:** https://github.com/getsentry/cli/issues/539 4: 5: ## Problem 6: 7: When using `--dataset metrics` with MRI-style query syntax (e.g. `avg(g:custom/node.runtime.mem.rss@byte)`), the widget is created successfully but displays as "Internal Error" in the dashboard UI. The CLI accepts the input without error. 8: 9: **Root cause:** `parseAggregate` sees the `(` in `avg(g:custom/...)` and passes it through unchanged. `validateAggregateNames` then extracts `avg` which is a valid function name. The MRI argument (`g:custom/node.runtime.mem.rss@byte`) is never validated. The `metrics` dataset maps to `null` in `WIDGET_TYPE_TO_DATASET` (not queryable), but there's no check preventing this. ...
Wrote file successfully.
LSP errors detected in other files:
<diagnostics file="/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/types/dashboard.test.ts">
ERROR [123:50] No overload matches this call.
Overload 1 of 2, '(expected: "count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"): void', gave the following error.
Argument of type 'string' is not assignable to parameter of type '"count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"'.
Overload 2 of 2, '(expected: "count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"): void', gave the following error. ...
Plan updated. The key changes from the previous version:
[^:]+ for entity (permissive, matches multi-char like "dist") instead of restricting to [cdgse]parseMri to match Sentry's parse_mri naming conventionParsedMri type uses entity field name (matching Python's ParsedMRI.entity) instead of typeThe implementation is a straightforward 5-line TypeScript port of the Python parser. No dependency or cross-repo wiring needed.
User approved switching to build agent. Wait for further instructions.
The plan at .opencode/plans/1774867540937-gentle-island.md has been approved, you can now edit files. Execute the plan <system-reminder> Your operational mode has changed from plan to build. You are no longer in read-only mode. You are permitted to make file changes, run shell commands, and utilize your arsenal of tools as needed. </system-reminder>
A plan file exists at /home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/.opencode/plans/1774867540937-gentle-island.md. You should execute on the plan defined within it
Let me read the plan and get started.
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/.opencode/plans/1774867540937-gentle-island.md</path> <type>file</type> <content>1: # Fix: MRI queries silently succeed but render as Internal Error 2: 3: **Issue:** https://github.com/getsentry/cli/issues/539 4: 5: ## Problem 6: 7: When using `--dataset metrics` with MRI-style query syntax (e.g. `avg(g:custom/node.runtime.mem.rss@byte)`), the widget is created successfully but displays as "Internal Error" in the dashboard UI. The CLI accepts the input without error. 8: 9: **Root cause:** `parseAggregate` sees the `(` in `avg(g:custom/...)` and passes it through unchanged. `validateAggregateNames` then extracts `avg` which is a valid function name. The MRI argument (`g:custom/node.runtime.mem.rss@byte`) is never validated. The `metrics` dataset maps to `null` in `WIDGET_TYPE_TO_DATASET` (not queryable), but there's no check preventing this. ...
[
{
"content": "Add MRI detection and rejection to src/types/dashboard.ts",
"status": "in_progress",
"priority": "high"
},
{
"content": "Add parseMri and MRI rejection tests to test/types/dashboard.test.ts",
"status": "pending",
"priority": "high"
},
{
"content": "Add integration test to test/commands/dashboard/widget/add.test.ts",
"status": "pending",
"priority": "high"
},
{
"content": "Run tests, typecheck, and lint",
"status": "pending",
"priority": "medium"
}
]
Let me read the relevant sections of the files I need to modify:
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/src/types/dashboard.ts</path>
<type>file</type>
<content>470:
471: /**
472: * Extract the function name from a parsed aggregate string.
473: * "count()" → "count"
474: * "p95(span.duration)" → "p95"
475: */
476: function extractFunctionName(aggregate: string): string {
477: const parenIdx = aggregate.indexOf("(");
478: return parenIdx > 0 ? aggregate.slice(0, parenIdx) : aggregate;
479: }
480:
481: /**
482: * Validate that all aggregate function names in a list are known.
483: * Throws a ValidationError listing valid functions if any are invalid.
484: *
485: * @param aggregates - Parsed aggregate strings (e.g. ["count()", "p95(span.duration)"])
486: * @param dataset - Widget dataset, determines which function list to validate against
487: */
488: export function validateAggregateNames(
489: aggregates: string[],
490: dataset?: string
491: ): void ...
Now let me make the edit to src/types/dashboard.ts:
Edit applied successfully. LSP errors detected in this file, please fix: <diagnostics file="/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/src/types/dashboard.ts"> ERROR [541:5] Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'. ERROR [542:5] Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'. ERROR [543:5] Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'. ERROR [544:5] Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'. ERROR [490:3] Block statements are preferred in this position. ERROR [539:3] Block statements are preferred in this position. ERROR [572:5] Block statements are preferred in this position. </diagnostics>
Need to fix the type errors and lint issues:
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/src/types/dashboard.ts</path>
<type>file</type>
<content>485: * "avg(g:custom/foo@b)" → "g:custom/foo@b"
486: */
487: function extractAggregateArg(aggregate: string): string {
488: const openIdx = aggregate.indexOf("(");
489: const closeIdx = aggregate.lastIndexOf(")");
490: if (openIdx < 0 || closeIdx <= openIdx) return "";
491: return aggregate.slice(openIdx + 1, closeIdx);
492: }
493:
494: // ---------------------------------------------------------------------------
495: // MRI (Metric Resource Identifier) detection
496: //
497: // Port of Sentry's canonical Python parser:
498: // sentry/src/sentry/snuba/metrics/naming_layer/mri.py
499: //
500: // MRI format: <entity>:<namespace>/<name>@<unit>
501: // entity: "c" (counter), "d" (distribution), "g" (gauge), "s" (set),
502: // "e" (extracted), or multi-char like "dist"
503: // namespace: "sessions", ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/src/types/dashboard.ts</path>
<type>file</type>
<content>530: /**
531: * Parse an MRI (Metric Resource Identifier) string into its components.
532: *
533: * Port of Sentry's `parse_mri` from `sentry/snuba/metrics/naming_layer/mri.py`.
534: *
535: * @returns Parsed MRI components, or null if the input doesn't match MRI format
536: */
537: export function parseMri(input: string): ParsedMri | null {
538: const match = MRI_RE.exec(input);
539: if (!match?.groups) return null;
540: return {
541: entity: match.groups.entity,
542: namespace: match.groups.namespace,
543: name: match.groups.name,
544: unit: match.groups.unit,
545: };
546: }
547:
548: /** Maps known MRI entity codes to human-readable tracemetrics type names */
549: const MRI_ENTITY_TYPE_NAMES: Record<string, string> = {
550: c: "counter",
551: d: "distribution",
552: g: "gauge",
553: s: ...
I need to fix:
string | undefined not assignable to string — need to assert the groups existEdit applied successfully. LSP errors detected in this file, please fix: <diagnostics file="/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/src/types/dashboard.ts"> ERROR [543:5] Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'. ERROR [544:5] Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'. ERROR [545:5] Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'. ERROR [546:5] Type 'string | undefined' is not assignable to type 'string'. Type 'undefined' is not assignable to type 'string'. ERROR [541:3] Block statements are preferred in this position. ERROR [574:5] Block statements are preferred in this position. </diagnostics>
Edit applied successfully. LSP errors detected in this file, please fix: <diagnostics file="/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/src/types/dashboard.ts"> ERROR [575:5] Block statements are preferred in this position. </diagnostics>
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/src/types/dashboard.ts</path>
<type>file</type>
<content>570: */
571: function rejectMriQueries(aggregates: string[], dataset?: string): void {
572: for (const agg of aggregates) {
573: const arg = extractAggregateArg(agg);
574: const mri = parseMri(arg);
575: if (!mri) continue;
576:
577: const fn = extractFunctionName(agg);
578: const typeName = MRI_ENTITY_TYPE_NAMES[mri.entity] ?? mri.entity;
579: const suggestion = `${fn}(value,${mri.name},${typeName},${mri.unit})`;
580: const datasetNote =
581: dataset === "tracemetrics"
582: ? ""
583: : `\nUse --dataset tracemetrics instead of --dataset ${dataset ?? "spans"}.`;
584:
(Showing lines 570-584 of 1117. Use offset=585 to continue.)
</content>
Edit applied successfully.
No more LSP errors. Now let me update the test files. First, let me read the current test file to see where to add tests:
[
{
"content": "Add MRI detection and rejection to src/types/dashboard.ts",
"status": "completed",
"priority": "high"
},
{
"content": "Add parseMri and MRI rejection tests to test/types/dashboard.test.ts",
"status": "in_progress",
"priority": "high"
},
{
"content": "Add integration test to test/commands/dashboard/widget/add.test.ts",
"status": "pending",
"priority": "high"
},
{
"content": "Run tests, typecheck, and lint",
"status": "pending",
"priority": "medium"
}
]
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/types/dashboard.test.ts</path>
<type>file</type>
<content>1: /**
2: * Dashboard Type & Validation Tests
3: *
4: * Tests for enum constants, strict input schema, and parseWidgetInput()
5: * in src/types/dashboard.ts.
6: */
7:
8: import { describe, expect, test } from "bun:test";
9: import { ValidationError } from "../../src/lib/errors.js";
10: import {
11: assignDefaultLayout,
12: type DashboardWidget,
13: DashboardWidgetInputSchema,
14: DEFAULT_WIDGET_TYPE,
15: DISCOVER_AGGREGATE_FUNCTIONS,
16: DISPLAY_TYPES,
17: DiscoverAggregateFunctionSchema,
18: type DisplayType,
19: EventsStatsDataPointSchema,
20: EventsStatsSeriesSchema,
21: EventsTableResponseSchema,
22: GRID_COLUMNS,
23: IS_FILTER_VALUES,
24: IsFilterValueSchema,
25: mapWidgetTypeToDataset,
26: parseAggregate,
27: parseSortExpression,
28: parseWidgetInput,
29: ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/types/dashboard.test.ts</path>
<type>file</type>
<content>390:
391: // ---------------------------------------------------------------------------
392: // parseAggregate
393: // ---------------------------------------------------------------------------
394:
395: describe("parseAggregate", () => {
396: test("bare name becomes no-arg function call", () => {
397: expect(parseAggregate("count")).toBe("count()");
398: });
399:
400: test("colon syntax becomes function with arg", () => {
401: expect(parseAggregate("p95:span.duration")).toBe("p95(span.duration)");
402: });
403:
404: test("passthrough when already has parens", () => {
405: expect(parseAggregate("count()")).toBe("count()");
406: });
407:
408: test("passthrough for function with args in parens", () => {
409: expect(parseAggregate("avg(span.self_time)")).toBe("avg(span.self_time)");
410: ...
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/types/dashboard.test.ts</path>
<type>file</type>
<content>860: // Valid: x=4 with existing w=2 → 4+2=6 ≤ 6
861: expect(() => validateWidgetLayout({ x: 4 }, existing)).not.toThrow();
862: });
863: });
864:
865: describe("display type sets", () => {
866: test("TIMESERIES_DISPLAY_TYPES contains chart types", () => {
867: expect(TIMESERIES_DISPLAY_TYPES.has("line")).toBe(true);
868: expect(TIMESERIES_DISPLAY_TYPES.has("area")).toBe(true);
869: expect(TIMESERIES_DISPLAY_TYPES.has("stacked_area")).toBe(true);
870: expect(TIMESERIES_DISPLAY_TYPES.has("bar")).toBe(true);
871: expect(TIMESERIES_DISPLAY_TYPES.has("categorical_bar")).toBe(true);
872: expect(TIMESERIES_DISPLAY_TYPES.has("table")).toBe(false);
873: expect(TIMESERIES_DISPLAY_TYPES.has("big_number")).toBe(false);
874: });
875:
876: test("TABLE_DISPLAY_TYPES contains table types", () ...
Now I'll add the imports and tests. First, update the imports:
Edit applied successfully.
LSP errors detected in this file, please fix:
<diagnostics file="/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/types/dashboard.test.ts">
ERROR [123:50] No overload matches this call.
Overload 1 of 2, '(expected: "count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"): void', gave the following error.
Argument of type 'string' is not assignable to parameter of type '"count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"'.
Overload 2 of 2, '(expected: "count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"): void', gave the following error. ...
The LSP errors are pre-existing (not from my changes). Now add the test blocks at the end of the file:
Edit applied successfully.
LSP errors detected in this file, please fix:
<diagnostics file="/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/types/dashboard.test.ts">
ERROR [123:50] No overload matches this call.
Overload 1 of 2, '(expected: "count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"): void', gave the following error.
Argument of type 'string' is not assignable to parameter of type '"count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"'.
Overload 2 of 2, '(expected: "count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"): void', gave the following error. ...
It seems like the import isn't resolving. Let me check if the export is properly available:
521:export type ParsedMri = {
539:export function parseMri(input: string): ParsedMri | null {
606:export function validateAggregateNames(
The exports are correct. The LSP might just need a moment to pick them up. Those Cannot find name 'parseMri' errors are likely stale — the import is now in line 18. Let me now add the integration test in the widget add test file:
[
{
"content": "Add MRI detection and rejection to src/types/dashboard.ts",
"status": "completed",
"priority": "high"
},
{
"content": "Add parseMri and MRI rejection tests to test/types/dashboard.test.ts",
"status": "completed",
"priority": "high"
},
{
"content": "Add integration test to test/commands/dashboard/widget/add.test.ts",
"status": "in_progress",
"priority": "high"
},
{
"content": "Run tests, typecheck, and lint",
"status": "pending",
"priority": "medium"
}
]
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/commands/dashboard/widget/add.test.ts</path>
<type>file</type>
<content>195: expect(err).toBeInstanceOf(ValidationError);
196: expect(err.message).toContain("Invalid --display");
197: });
198:
199: test("throws ValidationError for invalid aggregate function", async () => {
200: const { context } = createMockContext();
201: const func = await addCommand.loader();
202:
203: const err = await func
204: .call(
205: context,
206: { json: false, display: "line", query: ["not_a_function"] },
207: "123",
208: "Bad Widget"
209: )
210: .catch((e: Error) => e);
211: expect(err).toBeInstanceOf(ValidationError);
212: expect(err.message).toContain("Unknown aggregate function");
213: });
214:
215: test("throws ValidationError for big_number with issue dataset", async () => {
216: const { context } = ...
I'll add the MRI rejection integration test right after the existing "throws ValidationError for invalid aggregate function" test:
Edit applied successfully. LSP errors detected in this file, please fix: <diagnostics file="/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/commands/dashboard/widget/add.test.ts"> ERROR [116:16] Property 'call' does not exist on type 'CommandModule<Readonly<Record<string, unknown>>, BaseArgs, SentryContext> | CommandFunction<Readonly<Record<string, unknown>>, BaseArgs, SentryContext>'. Property 'call' does not exist on type 'CommandModule<Readonly<Record<string, unknown>>, BaseArgs, SentryContext>'. ERROR [142:16] Property 'call' does not exist on type 'CommandModule<Readonly<Record<string, unknown>>, BaseArgs, SentryContext> | CommandFunction<Readonly<Record<string, unknown>>, BaseArgs, SentryContext>'. Property 'call' does not exist on type 'CommandModule<Readonly<Record<string, unknown>>, BaseArgs, SentryContext>'. ERROR [149:56] Tuple type '[]' of length '0' has no element at index '0'. ...
All the LSP errors are pre-existing .call() type issues unrelated to my changes. Now let me run the tests:
[
{
"content": "Add MRI detection and rejection to src/types/dashboard.ts",
"status": "completed",
"priority": "high"
},
{
"content": "Add parseMri and MRI rejection tests to test/types/dashboard.test.ts",
"status": "completed",
"priority": "high"
},
{
"content": "Add integration test to test/commands/dashboard/widget/add.test.ts",
"status": "completed",
"priority": "high"
},
{
"content": "Run tests, typecheck, and lint",
"status": "in_progress",
"priority": "medium"
}
]
bun test v1.3.11 (af24e281)
[warn] table widgets support a maximum of 10 rows. Clamping --limit from 25 to 10.
[warn] bar widgets support a maximum of 10 rows. Clamping --limit from 15 to 10.
test/types/dashboard.test.ts:
884 | // parseMri
885 | // ---------------------------------------------------------------------------
886 |
887 | describe("parseMri", () => {
888 | test("parses standard gauge MRI", () => {
889 | expect(parseMri("g:custom/node.runtime.mem.rss@byte")).toEqual({
^
ReferenceError: parseMri is not defined
at <anonymous> (/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/types/dashboard.test.ts:889:12)
(fail) parseMri > parses standard gauge MRI [1.00ms]
893 | unit: "byte",
894 | });
895 | });
896 |
897 | test("parses counter MRI", () => {
898 | expect(parseMri("c:transactions/measurements.db_calls@none")).toEqual({
^
ReferenceError: parseMri is not ...
bun test v1.3.11 (af24e281) 23 pass 0 fail 51 expect() calls Ran 23 tests across 1 file. [384.00ms]
The widget add integration test passes. The dashboard type test has import resolution issues — parseMri and validateAggregateNames are not being found despite being in the import statement. This is likely a Bun caching issue. Let me verify the import line is correct:
<path>/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/types/dashboard.test.ts</path>
<type>file</type>
<content>8: import { describe, expect, test } from "bun:test";
9: import { ValidationError } from "../../src/lib/errors.js";
10: import {
11: assignDefaultLayout,
12: type DashboardWidget,
13: DashboardWidgetInputSchema,
14: DEFAULT_WIDGET_TYPE,
15: DISCOVER_AGGREGATE_FUNCTIONS,
16: DISPLAY_TYPES,
17: DiscoverAggregateFunctionSchema,
18: type DisplayType,
19: EventsStatsDataPointSchema,
20: EventsStatsSeriesSchema,
21: EventsTableResponseSchema,
22: GRID_COLUMNS,
23: IS_FILTER_VALUES,
24: IsFilterValueSchema,
25: mapWidgetTypeToDataset,
26: parseAggregate,
27: parseSortExpression,
28: parseWidgetInput,
29: prepareWidgetQueries,
30: SPAN_AGGREGATE_FUNCTIONS,
31: SpanAggregateFunctionSchema,
32: stripWidgetServerFields,
33: TABLE_DISPLAY_TYPES,
34: TIMESERIES_DISPLAY_TYPES,
35: ...
I see the issue — my edit added parseMri and validateAggregateNames but they're not in the import list. Let me check:
35: validateWidgetLayout,
782:// validateWidgetLayout
785:describe("validateWidgetLayout", () => {
792: validateWidgetLayout({ x: 0, y: 0, width: 3, height: 2 })
795: validateWidgetLayout({ x: 5, y: 10, width: 1, height: 1 })
The imports parseMri and validateAggregateNames are missing from the import statement. My earlier edit added them but they seem not present. Let me check the actual import block:
29
884:// parseMri
887:describe("parseMri", () => {
889: expect(parseMri("g:custom/node.runtime.mem.rss@byte")).toEqual({
898: expect(parseMri("c:transactions/measurements.db_calls@none")).toEqual({
908: parseMri("d:transactions/measurements.stall_longest_time@millisecond")
918: expect(parseMri("s:sessions/error@none")).toEqual({
927: expect(parseMri("e:spans/duration@millisecond")).toEqual({
936: expect(parseMri("dist:my_namespace/foo@none")).toEqual({
945: expect(parseMri("d:transactions/measurements.disk_io@byte/second")).toEqual(
956: expect(parseMri("span.duration")).toBeNull();
The imports weren't actually added to the import statement. My edit specified adding parseMri and validateAggregateNames but they didn't make it in. The alphabetical position was wrong. Let me fix the import:
Edit applied successfully.
LSP errors detected in this file, please fix:
<diagnostics file="/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/types/dashboard.test.ts">
ERROR [124:50] No overload matches this call.
Overload 1 of 2, '(expected: "count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"): void', gave the following error.
Argument of type 'string' is not assignable to parameter of type '"count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"'.
Overload 2 of 2, '(expected: "count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"): void', gave the following error. ...
Now parseMri is imported but validateAggregateNames is still missing. Let me add it:
Edit applied successfully.
LSP errors detected in this file, please fix:
<diagnostics file="/home/byk/.local/share/opencode/worktree/0e8c2f3bbe145a3280d6fb6ed900a5121f382cf0/calm-forest/test/types/dashboard.test.ts">
ERROR [125:50] No overload matches this call.
Overload 1 of 2, '(expected: "count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"): void', gave the following error.
Argument of type 'string' is not assignable to parameter of type '"count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"'.
Overload 2 of 2, '(expected: "count" | "avg" | "sum" | "min" | "max" | "p50" | "p75" | "p95" | "p99" | "count_unique" | "percentile" | "p90" | "p100" | "eps" | "epm" | "any"): void', gave the following error. ...
Now only the pre-existing LSP errors remain. Let me run both test files again:
bun test v1.3.11 (af24e281) [warn] table widgets support a maximum of 10 rows. Clamping --limit from 25 to 10. [warn] bar widgets support a maximum of 10 rows. Clamping --limit from 15 to 10. 102 pass 0 fail 261 expect() calls Ran 102 tests across 1 file. [103.00ms]
All 102 tests pass. Now let me run lint and typecheck:
$ bunx ultracite check
src/lib/formatters/markdown.ts:281:1 suppressions/unused ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
! Suppression comment has no effect. Remove the suppression or make sure you are suppressing the correct rule.
279 │ * Render a single inline token to an ANSI string.
280 │ */
> 281 │ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: inline token switch is inherently branchy
│ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
282 │ function renderOneInline(token: Token): string {
283 │ switch (token.type) {
Checked 468 files in 9s. No fixes applied.
Found 1 warning.
$ bun run generate:sdk && tsc --noEmit $ bun run script/generate-sdk.ts Discovered 44 commands Generated ./src/sdk.generated.ts Generated ./src/sdk.generated.d.cts
Both lint and typecheck pass (the warning is pre-existing and unrelated). Let me also run the full test suite for the widget add tests to make sure the integration test is still good:
bun test v1.3.11 (af24e281) 23 pass 0 fail 51 expect() calls Ran 23 tests across 1 file. [341.00ms]
All tests pass. Let me also run the edit widget tests to ensure no regressions there:
bun test v1.3.11 (af24e281) 124 pass 0 fail 254 expect() calls Ran 124 tests across 6 files. [559.00ms]
All 124 dashboard tests pass. Let me update the todo list:
[
{
"content": "Add MRI detection and rejection to src/types/dashboard.ts",
"status": "completed",
"priority": "high"
},
{
"content": "Add parseMri and MRI rejection tests to test/types/dashboard.test.ts",
"status": "completed",
"priority": "high"
},
{
"content": "Add integration test to test/commands/dashboard/widget/add.test.ts",
"status": "completed",
"priority": "high"
},
{
"content": "Run tests, typecheck, and lint",
"status": "completed",
"priority": "medium"
}
]
All done. Here's a summary of the changes:
Issue: dashboard widget add silently accepted MRI-style queries (e.g. avg(g:custom/node.runtime.mem.rss@byte)) that would render as "Internal Error" in the Sentry dashboard UI.
Fix: Added MRI syntax detection and rejection to validateAggregateNames in src/types/dashboard.ts, which is called during widget creation and editing.
src/types/dashboard.ts — Added:
extractAggregateArg() — extracts the argument from a parsed aggregate stringparseMri() (exported) — port of Sentry's canonical Python MRI parser using the same permissive regex ^(?<entity>[^:]+):(?<namespace>[^/]+)\/(?<name>[^@]+)@(?<unit>.+)$ParsedMri type (exported) — { entity, namespace, name, unit }MRI_ENTITY_TYPE_NAMES — maps entity codes to tracemetrics type names (c→counter, d→distribution, g→gauge, s→set, e→extracted)rejectMriQueries() — detects MRI in aggregate arguments, throws ValidationError with actionable guidancevalidateAggregateNames() to call rejectMriQueries firsttest/types/dashboard.test.ts — Added 17 new tests:
parseMri tests: all type letters, multi-char entities, units with slashes, null cases for non-MRI/partial/empty/malformed stringsvalidateAggregateNames MRI rejection tests: error thrown for MRI queries, correct suggestion format, dataset-aware guidance, no false positives on valid queriestest/commands/dashboard/widget/add.test.ts — Added 1 integration test confirming the full command flow rejects MRI queries
MRI query syntax is not supported for dashboard widgets: "avg(g:custom/node.runtime.mem.rss@byte)".
Use the tracemetrics query format instead:
--query 'avg(value,node.runtime.mem.rss,gauge,byte)'
Use --dataset tracemetrics instead of --dataset metrics.
Tracemetrics format: fn(value,<metric_name>,<type>,<unit>)
Now create a draft PR from these changes. Follow the repo conventions for your branch name and commit title. The PR (commit) description should be based on the implementation plan and the changes summary but not be overly long or detailed.
Add the plan as a commit note using git note feature.
Check if you are already on a relevant branch before creating a new branch.
Let me check the current state and repo conventions first.
main
M AGENTS.md M src/types/dashboard.ts M test/commands/dashboard/widget/add.test.ts M test/types/dashboard.test.ts
6c7223a9 feat(upgrade): show changelog summary during CLI upgrade (#594) bce0f128 fix(skill): avoid unnecessary auth, reinforce auto-detection, fix field examples (#599) 664362ca chore: regenerate skill files and command docs 7fe554d6 meta: Bump new development version 3afb6a38 Merge branch 'release/0.22.0' 1d783b8f chore: regenerate skill files and command docs 3beb37de release: 0.22.0 5dd1e697 chore: remove upstream issue templates for Sentry SDK light exports (#596) 2cdb867d feat(dashboard): add layout/position flags to widget edit and add commands (#591) 3c370feb fix(dashboard): normalize numeric org IDs from DSN auto-detection (#593)
6c7223a9 feat(upgrade): show changelog summary during CLI upgrade (#594) bce0f128 fix(skill): avoid unnecessary auth, reinforce auto-detection, fix field examples (#599) 9a8e66aa feat(upgrade): show changelog summary during CLI upgrade 05325bb7 Notes added by 'git notes add' c4ac5c1d fix(skill): avoid unnecessary auth, reinforce auto-detection, fix field examples 664362ca chore: regenerate skill files and command docs 7fe554d6 meta: Bump new development version 3afb6a38 Merge branch 'release/0.22.0' 1d783b8f chore: regenerate skill files and command docs 3beb37de release: 0.22.0 5dd1e697 chore: remove upstream issue templates for Sentry SDK light exports (#596) 2cdb867d feat(dashboard): add layout/position flags to widget edit and add commands (#591) b8b7d97b fix(dashboard): re-validate layout after merge in widget edit for layoutless widgets 1916eb75 fix(dashboard): re-validate layout after merging with auto-layout defaults 68f28d7e refactor: extract FALLBACK_LAYOUT constant for ...
I see the convention: fix(scope): description for commit messages, and branch names appear to be feature branches. Let me also check what existing branch names look like:
origin/HEAD -> origin/main origin/main origin/feat/upgrade-changelog origin/fix/skill-auth-autodetect-fields origin/feat/dashboard-widget-layout-flags origin/fix/447-dashboard-numeric-org-id origin/fix/dashboard-actionable-errors origin/byk/async-streaming origin/fix/upgrade-spinner-log-collision origin/fix/auth-status-stale-user-info
Good, the branch naming convention is fix/<description> or fix/<issue>-<description>. I'm on main, so I need to create a new branch.
Switched to a new branch 'fix/539-mri-query-validation'
AGENTS.md | 93 ++++++---------- src/types/dashboard.ts | 122 ++++++++++++++++++++ test/commands/dashboard/widget/add.test.ts | 23 ++++ test/types/dashboard.test.ts | 173 +++++++++++++++++++++++++++++ 4 files changed, 352 insertions(+), 59 deletions(-)
AGENTS.md has unstaged changes (lore-managed). Per the repo instructions, I need to include it.
[fix/539-mri-query-validation deadab44] fix(dashboard): reject MRI queries with actionable tracemetrics guidance 4 files changed, 352 insertions(+), 59 deletions(-)
Now add the plan as a git note:
Now push and create the draft PR:
remote: remote: Create a pull request for 'fix/539-mri-query-validation' on GitHub by visiting: remote: https://github.com/getsentry/cli/pull/new/fix/539-mri-query-validation remote: To https://github.com/getsentry/cli.git * [new branch] fix/539-mri-query-validation -> fix/539-mri-query-validation branch 'fix/539-mri-query-validation' set up to track 'origin/fix/539-mri-query-validation'.
To https://github.com/getsentry/cli.git 7d1986d1..f595ae78 refs/notes/commits -> refs/notes/commits
https://github.com/getsentry/cli/pull/601
Draft PR created: https://github.com/getsentry/cli/pull/601
Keep running
gh run view --log-failed --job $(gh pr checks $PR_NO --json state,link -q '.[] | select(.state == "FAILURE").link | split("/")[-1]')'
to get all failing jobs after you push. Make sure to wait for "Sentry Seer" and "Cursor BugBot" jobs to finish. Fix any failing jobs. Address all unresolved comments (both from bots and humans) from the PR by making a todo list. When you address each PR, either respond to them or mark them as resolved. Don't stop until the todo items are all done. Keep repeating this cycle until there are no more CI failures nor unresolved comments from humans or bots.
Use the following command to get the unresolved comments:
gh api graphql -F owner='<REPO_OWNER>' -F name='<REPO_NAME>' -F number=<PR_NUMBER> -f query='query($owner: String!, $name: String!, $number: Int!) { repository(owner: $owner, name: $name) { pullRequest(number: $number) { reviewThreads(first: 100) { nodes { isResolved comments(first: 100) { nodes { diffHunk body startLine line isMinimized } } } } } } }' -q '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false) | .comments.nodes[] | select(.isMinimized == false) | {diff_hunk: .diffHunk, line: .line, start_line: .startLine, body: .body}]'
You'll need to replace <REPO_OWNER>, <REPO_NAME>, and <PR_NUMBER> with relevant
values for the current repo and PR. (You can probably get them by running gh pr status or gh pr view)
Let me start by checking the PR status and waiting for CI.
Analyze (actions) pending 0 https://github.com/getsentry/cli/actions/runs/23741632993/job/69159876607 Analyze (javascript-typescript) pending 0 https://github.com/getsentry/cli/actions/runs/23741632993/job/69159876569 Analyze (python) pending 0 https://github.com/getsentry/cli/actions/runs/23741632993/job/69159876571 Build Docs pending 0 https://github.com/getsentry/cli/actions/runs/23741634635/job/69159955407 Cursor Bugbot pending 0 https://cursor.com/docs/bugbot Seer Code Review pending 0 https://sentry.io Unit Tests pending 0 https://github.com/getsentry/cli/actions/runs/23741634635/job/69159895402 warden pass 23s https://github.com/getsentry/cli/actions/runs/23741634620/job/69159878423 CodeQL skipping 2s https://github.com/getsentry/cli/runs/69159962755 Check skill files pass 14s https://github.com/getsentry/cli/actions/runs/23741634635/job/69159895393 Detect Changes pass 6s https://github.com/getsentry/cli/actions/runs/23741634635/job/69159878545 Lint & ...
Several checks are still pending. Let me wait and check again.
Analyze (actions) pass 50s https://github.com/getsentry/cli/actions/runs/23741632993/job/69159876607 Analyze (javascript-typescript) pass 1m14s https://github.com/getsentry/cli/actions/runs/23741632993/job/69159876569 Analyze (python) pass 59s https://github.com/getsentry/cli/actions/runs/23741632993/job/69159876571 Build Docs pass 22s https://github.com/getsentry/cli/actions/runs/23741634635/job/69159955407 Check skill files pass 14s https://github.com/getsentry/cli/actions/runs/23741634635/job/69159895393 CodeQL pass 2s https://github.com/getsentry/cli/runs/69159962755 Detect Changes pass 6s https://github.com/getsentry/cli/actions/runs/23741634635/job/69159878545 warden pass 23s https://github.com/getsentry/cli/actions/runs/23741634620/job/69159878423 Cursor Bugbot pending 0 https://cursor.com/docs/bugbot Unit Tests pending 0 https://github.com/getsentry/cli/actions/runs/23741634635/job/69159895402 Secret ...
Most checks pass. Still waiting on Unit Tests, Cursor Bugbot, Seer Code Review, and semgrep. Let me wait more.
Analyze (actions) pass 50s https://github.com/getsentry/cli/actions/runs/23741632993/job/69159876607 Analyze (javascript-typescript) pass 1m14s https://github.com/getsentry/cli/actions/runs/23741632993/job/69159876569 Analyze (python) pass 59s https://github.com/getsentry/cli/actions/runs/23741632993/job/69159876571 Build Binary (linux-x64) pass 21s https://github.com/getsentry/cli/actions/runs/23741634635/job/69160126249 Build Docs pass 22s https://github.com/getsentry/cli/actions/runs/23741634635/job/69159955407 Build npm Package (Node 22) pass 16s https://github.com/getsentry/cli/actions/runs/23741634635/job/69160126308 Build npm Package (Node 24) pass 15s https://github.com/getsentry/cli/actions/runs/23741634635/job/69160126319 CI Status pass 3s https://github.com/getsentry/cli/actions/runs/23741634635/job/69160272410 Check skill ...
All CI checks pass. Only Cursor Bugbot and Seer Code Review are still pending. Let me wait for those.
{"name":"Publish Nightly to GHCR","state":"SKIPPED"}
{"name":"Cursor Bugbot","state":"IN_PROGRESS"}
Seer Code Review is done. Just Cursor Bugbot still running. Let me wait a bit more and also check for any comments that have appeared:
[]
No unresolved comments yet. Let me wait for Cursor Bugbot to finish:
All checks complete. Let me verify there are no failures and check for any new comments:
[]
All CI checks pass with zero failures, and there are no unresolved PR comments. The PR is clean:
PR is ready for review at: https://github.com/getsentry/cli/pull/601
Merge the PR
MERGED at 2026-03-30T11:18:18Z
PR #601 has been merged to main via squash merge.