diff --git a/bun.lock b/bun.lock index 88f617c34..7a4fae277 100644 --- a/bun.lock +++ b/bun.lock @@ -9,7 +9,7 @@ "@biomejs/biome": "2.3.8", "@clack/prompts": "^0.11.0", "@mastra/client-js": "^1.4.0", - "@sentry/api": "^0.133.0", + "@sentry/api": "^0.141.0", "@sentry/node-core": "10.50.0", "@sentry/sqlish": "^1.0.0", "@stricli/auto-complete": "^1.2.4", @@ -175,7 +175,7 @@ "@peggyjs/from-mem": ["@peggyjs/from-mem@3.1.3", "", { "dependencies": { "semver": "7.7.4" } }, "sha512-LLlgtfXIaeYXoOYovOI0spLM8ZXaqkAlmcRRrLzHJzLMqkU6Sw0R4KMoCoHx1PjaP815pSCBlS+BN6aD8t1Jgg=="], - "@sentry/api": ["@sentry/api@0.133.0", "", {}, "sha512-flfRUm9T9xgyNEWQqCiNv8wX4QlqOt63tM8dRMbeo26zD4+xEaL4KiaEnPC/W5rXCKg/03FBJysw7rEIuhiedQ=="], + "@sentry/api": ["@sentry/api@0.141.0", "", { "peerDependencies": { "zod": "^3.24.0" }, "optionalPeers": ["zod"] }, "sha512-6DAEAhHnE/bkiUsCIGY4V9fPWVg2sc0Wn0ualQ4xEEurKQgtbafhJyZuuwCfTwT/nYldHosjGfLoWFNdXj9tWA=="], "@sentry/core": ["@sentry/core@10.50.0", "", {}, "sha512-J4A+vzUO3adl0TkFCjaN1+4miamrjHiEIYuLHiuu1lmAjq5WIVw32ObvAh4yMwNtxyaEMosTrrh5M6f12XSJFg=="], diff --git a/package.json b/package.json index 3fef5672d..f258ed173 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "@biomejs/biome": "2.3.8", "@clack/prompts": "^0.11.0", "@mastra/client-js": "^1.4.0", - "@sentry/api": "^0.133.0", + "@sentry/api": "^0.141.0", "@sentry/node-core": "10.50.0", "@sentry/sqlish": "^1.0.0", "@stricli/auto-complete": "^1.2.4", diff --git a/plugins/sentry-cli/skills/sentry-cli/references/event.md b/plugins/sentry-cli/skills/sentry-cli/references/event.md index 705dbf667..5624250f9 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/event.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/event.md @@ -59,7 +59,7 @@ List events for an issue | `platform` | string \| null | Platform (python, javascript, etc.) | | `dateCreated` | string | ISO 8601 creation timestamp | | `crashFile` | string \| null | Crash file URL | -| `metadata` | unknown \| null | Event metadata | +| `metadata` | object \| null | Event metadata | **Examples:** diff --git a/plugins/sentry-cli/skills/sentry-cli/references/issue.md b/plugins/sentry-cli/skills/sentry-cli/references/issue.md index 8e82ed596..17ae5d935 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/issue.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/issue.md @@ -34,16 +34,16 @@ List issues in a project | `culprit` | string | Culprit string | | `count` | string | Total event count | | `userCount` | number | Number of affected users | -| `firstSeen` | string \| null | First occurrence (ISO 8601) | -| `lastSeen` | string \| null | Most recent occurrence (ISO 8601) | +| `firstSeen` | string | First occurrence (ISO 8601) | +| `lastSeen` | string | Most recent occurrence (ISO 8601) | | `level` | string | Severity level | | `status` | string | Issue status | -| `priority` | string | Triage priority | -| `platform` | string | Platform | | `permalink` | string | URL to the issue in Sentry | | `project` | object | Project info | | `metadata` | object | Issue metadata | -| `assignedTo` | unknown \| null | Assigned user or team | +| `assignedTo` | object \| null | Assigned user or team | +| `priority` | string | Triage priority | +| `platform` | string | Platform | | `substatus` | string \| null | Issue substatus | | `isUnhandled` | boolean | Whether the issue is unhandled | | `seerFixabilityScore` | number \| null | Seer AI fixability score (0-1) | @@ -109,7 +109,7 @@ List events for a specific issue | `platform` | string \| null | Platform (python, javascript, etc.) | | `dateCreated` | string | ISO 8601 creation timestamp | | `crashFile` | string \| null | Crash file URL | -| `metadata` | unknown \| null | Event metadata | +| `metadata` | object \| null | Event metadata | ### `sentry issue explain ` diff --git a/plugins/sentry-cli/skills/sentry-cli/references/replay.md b/plugins/sentry-cli/skills/sentry-cli/references/replay.md index 32154321d..71f1b35cd 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/replay.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/replay.md @@ -54,7 +54,7 @@ List recent Session Replays | `releases` | array | Associated releases | | `sdk` | object \| null | SDK metadata | | `started_at` | string \| null | Replay start timestamp | -| `tags` | unknown | Replay tags | +| `tags` | object | Replay tags | | `trace_ids` | array | Linked trace IDs | | `urls` | array | Visited URLs | | `user` | object \| null | User metadata | @@ -118,7 +118,7 @@ View a Session Replay | `releases` | array | Associated releases | | `sdk` | object \| null | SDK metadata | | `started_at` | string \| null | Replay start timestamp | -| `tags` | unknown | Replay tags | +| `tags` | object | Replay tags | | `trace_ids` | array | Linked trace IDs | | `urls` | array | Visited URLs | | `user` | object \| null | User metadata | diff --git a/plugins/sentry-cli/skills/sentry-cli/references/team.md b/plugins/sentry-cli/skills/sentry-cli/references/team.md index 6daeea055..9baf0bb40 100644 --- a/plugins/sentry-cli/skills/sentry-cli/references/team.md +++ b/plugins/sentry-cli/skills/sentry-cli/references/team.md @@ -27,7 +27,7 @@ List teams | `id` | string | Team ID | | `slug` | string | Team slug | | `name` | string | Team name | -| `dateCreated` | string | Creation date (ISO 8601) | +| `dateCreated` | string \| null | Creation date (ISO 8601) | | `isMember` | boolean | Whether you are a member | | `teamRole` | string \| null | Your role in the team | | `memberCount` | number | Number of members | diff --git a/src/lib/formatters/output.ts b/src/lib/formatters/output.ts index 8702810de..46766271d 100644 --- a/src/lib/formatters/output.ts +++ b/src/lib/formatters/output.ts @@ -340,11 +340,51 @@ export type SchemaFieldInfo = { optional: boolean; }; +/** Leaf-type name mapping for {@link zodTypeToString} */ +const ZOD_TYPE_MAP: Record = { + ZodString: "string", + ZodNumber: "number", + ZodBoolean: "boolean", + ZodObject: "object", + ZodArray: "array", + ZodRecord: "object", + ZodNull: "null", + ZodUnknown: "unknown", + ZodAny: "any", + ZodEnum: "string", + ZodLiteral: "string", +}; + +/** + * Resolve a `ZodUnion` into a deduplicated `" | "`-joined type string. + * + * Used by auto-generated `@sentry/api/zod` schemas that represent nullable + * fields as `z.union([z.string(), z.null()])` instead of `z.string().nullable()`. + */ +function resolveZodUnion(options: ZodType[]): { + type: string; + optional: boolean; +} { + let optional = false; + const parts: string[] = []; + for (const opt of options) { + const resolved = zodTypeToString(opt); + if (resolved.optional) { + optional = true; + } + if (!parts.includes(resolved.type)) { + parts.push(resolved.type); + } + } + return { type: parts.join(" | "), optional }; +} + /** * Map a Zod type's internal `typeName` to a human-readable string. * * Unwraps wrapper types (Optional, Nullable, Default) and builds a * composite type string (e.g. "string | null" for ZodNullable). + * Delegates ZodUnion handling to {@link resolveZodUnion}. */ function zodTypeToString(schema: ZodType): { type: string; optional: boolean } { const def = (schema as { _def?: { typeName?: string; innerType?: ZodType } }) @@ -368,19 +408,14 @@ function zodTypeToString(schema: ZodType): { type: string; optional: boolean } { if (def.typeName === "ZodDefault" && def.innerType) { return zodTypeToString(def.innerType); } + if (def.typeName === "ZodUnion") { + const options = (def as { options?: ZodType[] }).options; + if (options?.length) { + return resolveZodUnion(options); + } + } - const TYPE_MAP: Record = { - ZodString: "string", - ZodNumber: "number", - ZodBoolean: "boolean", - ZodObject: "object", - ZodArray: "array", - ZodUnknown: "unknown", - ZodAny: "any", - ZodEnum: "string", - }; - - return { type: TYPE_MAP[def.typeName] ?? "unknown", optional: false }; + return { type: ZOD_TYPE_MAP[def.typeName] ?? "unknown", optional: false }; } /** diff --git a/src/types/sentry.ts b/src/types/sentry.ts index 888e0069e..132146dd8 100644 --- a/src/types/sentry.ts +++ b/src/types/sentry.ts @@ -23,6 +23,11 @@ import type { OrgReleaseResponse as SdkReleaseResponse, BaseTeam as SdkTeam, } from "@sentry/api"; +import { + zBaseTeam, + zGroupEventsResponseDict, + zRetrieveAnIssueResponse, +} from "@sentry/api/zod"; import { z } from "zod"; // SDK-derived types @@ -175,51 +180,72 @@ export type SentryIssue = Omit, "metadata"> & { * SDK-derived fields not listed here. Fields listed as optional may still be * present in most responses; optionality reflects the TypeScript type. */ -export const SentryIssueSchema = z - .object({ +/** + * Derived from the auto-generated `zRetrieveAnIssueResponse` schema. + * + * The generated schema makes all API-documented fields required. We widen it + * with `.partial()` so only the core identifiers (id, shortId, title) are + * required — matching how the CLI uses partial API responses and test mocks. + * Extra fields not in the OpenAPI spec (substatus, priority, isUnhandled, + * seerFixabilityScore) are added via `.extend()`. + */ +export const SentryIssueSchema = zRetrieveAnIssueResponse + .pick({ + id: true, + shortId: true, + title: true, + culprit: true, + count: true, + userCount: true, + firstSeen: true, + lastSeen: true, + level: true, + status: true, + permalink: true, + project: true, + metadata: true, + assignedTo: true, + }) + .partial() + .extend({ id: z.string().describe("Numeric issue ID"), shortId: z.string().describe("Human-readable short ID (e.g. PROJ-ABC)"), title: z.string().describe("Issue title"), - culprit: z.string().optional().describe("Culprit string"), - count: z.string().optional().describe("Total event count"), - userCount: z.number().optional().describe("Number of affected users"), - firstSeen: z - .string() - .nullable() + culprit: zRetrieveAnIssueResponse.shape.culprit + .optional() + .describe("Culprit string"), + count: zRetrieveAnIssueResponse.shape.count + .optional() + .describe("Total event count"), + userCount: zRetrieveAnIssueResponse.shape.userCount + .optional() + .describe("Number of affected users"), + firstSeen: zRetrieveAnIssueResponse.shape.firstSeen .optional() .describe("First occurrence (ISO 8601)"), - lastSeen: z - .string() - .nullable() + lastSeen: zRetrieveAnIssueResponse.shape.lastSeen .optional() .describe("Most recent occurrence (ISO 8601)"), - level: z.string().optional().describe("Severity level"), - status: z.string().optional().describe("Issue status"), - priority: z.string().optional().describe("Triage priority"), - platform: z.string().optional().describe("Platform"), - permalink: z.string().optional().describe("URL to the issue in Sentry"), - project: z - .object({ - id: z.string(), - name: z.string(), - slug: z.string(), - }) + level: zRetrieveAnIssueResponse.shape.level + .optional() + .describe("Severity level"), + status: zRetrieveAnIssueResponse.shape.status + .optional() + .describe("Issue status"), + permalink: zRetrieveAnIssueResponse.shape.permalink + .optional() + .describe("URL to the issue in Sentry"), + project: zRetrieveAnIssueResponse.shape.project .optional() .describe("Project info"), - metadata: z - .object({ - type: z.string().optional(), - value: z.string().optional(), - filename: z.string().optional(), - function: z.string().optional(), - }) + metadata: zRetrieveAnIssueResponse.shape.metadata .optional() .describe("Issue metadata"), - assignedTo: z - .unknown() - .nullable() + assignedTo: zRetrieveAnIssueResponse.shape.assignedTo .optional() .describe("Assigned user or team"), + priority: z.string().optional().describe("Triage priority"), + platform: z.string().optional().describe("Platform"), substatus: z.string().nullable().optional().describe("Issue substatus"), isUnhandled: z .boolean() @@ -231,6 +257,7 @@ export const SentryIssueSchema = z .optional() .describe("Seer AI fixability score (0-1)"), }) + .passthrough() .describe("Sentry issue"); // Event @@ -333,41 +360,52 @@ export type IssueEvent = { /** * Zod schema for {@link IssueEvent} — used for `--fields` documentation in `--help`. + * + * Derived from the auto-generated `zGroupEventsResponseDict` element schema. + * All generated fields are widened to optional via `.partial()`, then the core + * identifiers (id, event.type, eventID) are re-required via `.extend()`. */ -export const IssueEventSchema = z - .object({ +const _IssueEventElement = zGroupEventsResponseDict.element; +export const IssueEventSchema = _IssueEventElement + .partial() + .extend({ id: z.string().describe("Internal event ID"), "event.type": z .string() .describe("Event type (error, default, transaction)"), - groupID: z.string().nullable().describe("Group (issue) ID"), + groupID: _IssueEventElement.shape.groupID + .optional() + .describe("Group (issue) ID"), eventID: z.string().describe("UUID-format event ID"), - projectID: z.string().describe("Project ID"), - message: z.string().describe("Event message"), - title: z.string().describe("Event title"), - location: z.string().nullable().describe("Source location (file:line)"), - culprit: z.string().nullable().describe("Culprit function/module"), - user: z - .object({ - id: z.string().nullish().describe("User ID"), - email: z.string().nullish().describe("User email"), - username: z.string().nullish().describe("Username"), - ip_address: z.string().nullish().describe("IP address"), - name: z.string().nullish().describe("User display name"), - }) - .nullable() - .describe("User context"), - tags: z - .array(z.object({ key: z.string(), value: z.string() })) - .describe("Event tags"), - platform: z - .string() - .nullable() + projectID: _IssueEventElement.shape.projectID + .optional() + .describe("Project ID"), + message: _IssueEventElement.shape.message + .optional() + .describe("Event message"), + title: _IssueEventElement.shape.title.optional().describe("Event title"), + location: _IssueEventElement.shape.location + .optional() + .describe("Source location (file:line)"), + culprit: _IssueEventElement.shape.culprit + .optional() + .describe("Culprit function/module"), + user: _IssueEventElement.shape.user.optional().describe("User context"), + tags: _IssueEventElement.shape.tags.optional().describe("Event tags"), + platform: _IssueEventElement.shape.platform + .optional() .describe("Platform (python, javascript, etc.)"), - dateCreated: z.string().describe("ISO 8601 creation timestamp"), - crashFile: z.string().nullable().describe("Crash file URL"), - metadata: z.record(z.unknown()).nullable().describe("Event metadata"), + dateCreated: _IssueEventElement.shape.dateCreated + .optional() + .describe("ISO 8601 creation timestamp"), + crashFile: _IssueEventElement.shape.crashFile + .optional() + .describe("Crash file URL"), + metadata: _IssueEventElement.shape.metadata + .optional() + .describe("Event metadata"), }) + .passthrough() .describe("Issue event (list endpoint)"); // Project Keys (DSN) @@ -1040,22 +1078,40 @@ export type SentryRepository = z.infer; // Team -/** A team in a Sentry organization */ -export const SentryTeamSchema = z - .object({ - // Core identifiers (required) +/** + * A team in a Sentry organization. + * + * Derived from the auto-generated `zBaseTeam` schema, picking only the + * fields used in CLI output. All picked fields are widened to optional via + * `.partial()`, then core identifiers (id, slug, name) are re-required. + */ +export const SentryTeamSchema = zBaseTeam + .pick({ + id: true, + slug: true, + name: true, + dateCreated: true, + isMember: true, + teamRole: true, + memberCount: true, + }) + .partial() + .extend({ id: z.string().describe("Team ID"), slug: z.string().describe("Team slug"), name: z.string().describe("Team name"), - // Optional metadata - dateCreated: z.string().optional().describe("Creation date (ISO 8601)"), - isMember: z.boolean().optional().describe("Whether you are a member"), - teamRole: z - .string() - .nullable() + dateCreated: zBaseTeam.shape.dateCreated + .optional() + .describe("Creation date (ISO 8601)"), + isMember: zBaseTeam.shape.isMember + .optional() + .describe("Whether you are a member"), + teamRole: zBaseTeam.shape.teamRole .optional() .describe("Your role in the team"), - memberCount: z.number().optional().describe("Number of members"), + memberCount: zBaseTeam.shape.memberCount + .optional() + .describe("Number of members"), }) .passthrough();