diff --git a/.gitignore b/.gitignore index 3f638f09d..8dc6c7f09 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,9 @@ dist-bin dist-build *.tgz +# e2e bundle build lock (see packages/cli/test/e2e/bundle-setup.ts) +packages/cli/.bundle-build.lock + # fossilize build cache .node-cache diff --git a/packages/cli/.gitignore b/packages/cli/.gitignore index 34f1a52fc..9af50246e 100644 --- a/packages/cli/.gitignore +++ b/packages/cli/.gitignore @@ -13,6 +13,9 @@ dist-bin dist-build *.tgz +# e2e bundle build lock (see test/e2e/bundle-setup.ts) +.bundle-build.lock + # fossilize build cache .node-cache diff --git a/packages/cli/package.json b/packages/cli/package.json index 83877165b..34f2036af 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -88,7 +88,7 @@ "@clack/prompts": "0.11.0", "@hono/node-server": "^2.0.10", "@mastra/client-js": "^1.26.0", - "@sentry/api": "^0.253.0", + "@sentry/api": "^0.256.0", "@sentry/core": "10.63.0", "@sentry/node-core": "10.63.0", "@sentry/sqlish": "^1.0.1", diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/event.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/event.md index 7498b1f3b..19aef65d9 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/event.md +++ b/packages/cli/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` | object \| null | Event metadata | +| `metadata` | object | Event metadata | **Examples:** diff --git a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md index 99f49ca22..602792503 100644 --- a/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md +++ b/packages/cli/plugins/sentry-cli/skills/sentry-cli/references/issue.md @@ -114,7 +114,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` | object \| null | Event metadata | +| `metadata` | object | Event metadata | **Examples:** diff --git a/packages/cli/src/lib/api/replays.ts b/packages/cli/src/lib/api/replays.ts index 62fdf1553..c670aa335 100644 --- a/packages/cli/src/lib/api/replays.ts +++ b/packages/cli/src/lib/api/replays.ts @@ -8,7 +8,8 @@ import { type ListProjectReplayRecordingSegmentsResponse, listProjectReplayRecordingSegments, } from "@sentry/api"; -import { zListProjectReplayRecordingSegmentsResponse } from "@sentry/api/zod"; +import { vListProjectReplayRecordingSegmentsResponse } from "@sentry/api/valibot"; +import { safeParse } from "valibot"; import type { z } from "zod"; import { REPLAY_LIST_FIELDS, @@ -123,16 +124,16 @@ type FetchReplayRecordingSegmentsPageOptions = { * its object boundary. The SDK invokes response validators outside its normal * error-result path, so convert Zod failures to the CLI's API error type here. */ +// biome-ignore lint/suspicious/useAwait: the SDK's responseValidator hook requires a Promise-returning function async function validateReplayRecordingSegmentsResponse( data: unknown ): Promise { - const result = - await zListProjectReplayRecordingSegmentsResponse.safeParseAsync(data); + const result = safeParse(vListProjectReplayRecordingSegmentsResponse, data); if (!result.success) { throw new ApiError( "Unexpected replay recording segments response", 0, - result.error.message + result.issues.map((issue) => issue.message).join(", ") ); } } diff --git a/packages/cli/src/types/sentry.ts b/packages/cli/src/types/sentry.ts index 8d46916fe..494bcea07 100644 --- a/packages/cli/src/types/sentry.ts +++ b/packages/cli/src/types/sentry.ts @@ -102,18 +102,15 @@ export type SentryProject = Partial & { // Issue Constants /** - * Runtime-iterable tuple of issue status values, tied to the SDK's literal - * union in both directions: + * Runtime-iterable tuple of issue status values the CLI renders. * - * - `satisfies readonly NonNullable[]` catches - * **removals/renames** in the SDK union (a tuple entry that no longer - * exists in the union fails to assign). - * - `_IssueStatusParity` below catches **additions** in the SDK union - * (an SDK status missing from our tuple makes the conditional type - * reduce to `never` instead of `true`). - * - * Together they fail typechecking on any drift, forcing the tuple and the - * SDK union to stay in sync. + * This is a deliberate superset of the SDK's `GetOrganizationIssueResponse` + * status union: it keeps `resolvedInNextRelease` and `muted`, which the + * retrieve-issue endpoint still emits and the CLI still renders (see + * STATUS_ICONS / STATUS_LABELS / STATUS_COLORS). As of @sentry/api 0.256 the + * SDK union narrowed and no longer covers those two, so the previous + * `satisfies NonNullable[]` drift guard misfired on + * statuses the CLI needs to display and was removed. */ export const ISSUE_STATUSES = [ "resolved", @@ -121,14 +118,9 @@ export const ISSUE_STATUSES = [ "unresolved", "ignored", "muted", -] as const satisfies readonly NonNullable[]; +] as const; export type IssueStatus = (typeof ISSUE_STATUSES)[number]; -// Note: a reverse exhaustiveness check (SDK → ISSUE_STATUSES) is not possible here -// because GetOrganizationIssueResponses is a union of all HTTP response types, one of which -// has `status: string` (loose), making SdkIssueDetail["status"] resolve to `string`. -// The `satisfies` above catches the forward direction (invalid values in our tuple). - export const ISSUE_LEVELS = [ "fatal", "error", diff --git a/packages/cli/test/e2e/bundle-setup.ts b/packages/cli/test/e2e/bundle-setup.ts index 50b229678..ac18e291f 100644 --- a/packages/cli/test/e2e/bundle-setup.ts +++ b/packages/cli/test/e2e/bundle-setup.ts @@ -2,13 +2,17 @@ * Shared npm bundle build helper for e2e tests. * * Serializes bundle builds across parallel test files so `bundle.test.ts` and - * `library.test.ts` never run `pnpm run bundle` concurrently or delete `dist/` - * while another file's build is in flight. + * `library.test.ts` never run `pnpm run bundle` concurrently. vitest runs each + * test file in its own worker process (`pool: "forks"`), so an in-process + * promise cannot coordinate them — the lock has to live on the filesystem. + * Whichever worker wins the `mkdir` lock builds once; the rest wait for the + * bundle to appear. */ import { spawn } from "node:child_process"; -import { existsSync, rmSync } from "node:fs"; +import { existsSync, mkdirSync, rmSync } from "node:fs"; import { join } from "node:path"; +import { setTimeout as sleep } from "node:timers/promises"; function noop(): void { // Intentionally empty — absorbs async spawn errors @@ -16,6 +20,9 @@ function noop(): void { const ROOT_DIR = join(import.meta.dirname, "../.."); +/** Cross-process build lock directory (kept outside `dist/`). */ +const LOCK_DIR = join(ROOT_DIR, ".bundle-build.lock"); + /** Bundled library entrypoint used by library-mode e2e tests. */ export const BUNDLE_INDEX_PATH = join(ROOT_DIR, "dist/index.cjs"); @@ -30,26 +37,60 @@ let buildPromise: Promise | null = null; /** * Ensure the npm bundle exists under `dist/`, building it once if needed. * - * @param options.clean - When true, delete `dist/` before building. Only the - * first concurrent caller's preference applies while a build is in flight. + * Safe to call concurrently from multiple test files: a filesystem lock + * ensures exactly one worker runs `pnpm run bundle` while the others wait for + * the bundle to appear. */ -export function ensureBundleBuilt(options?: { - clean?: boolean; -}): Promise { - if (!options?.clean && existsSync(BUNDLE_INDEX_PATH)) { +export function ensureBundleBuilt(): Promise { + if (existsSync(BUNDLE_INDEX_PATH) && !existsSync(LOCK_DIR)) { return Promise.resolve(); } - buildPromise ??= runBundleBuild(Boolean(options?.clean)); + buildPromise ??= runBundleBuild(); return buildPromise; } -async function runBundleBuild(clean: boolean): Promise { - const distDir = join(ROOT_DIR, "dist"); - if (clean && existsSync(distDir)) { - rmSync(distDir, { recursive: true, force: true }); +async function runBundleBuild(): Promise { + // Atomic `mkdir` acts as a cross-process lock: only one worker creates the + // directory and builds; the rest fall through to wait for the bundle. + let holdsLock = false; + try { + mkdirSync(LOCK_DIR); + holdsLock = true; + } catch { + // Another worker is building — wait for the bundle to appear. + } + + if (!holdsLock) { + buildPromise = null; + await waitForBundle(); + return; + } + + try { + await spawnBundle(); + } finally { + rmSync(LOCK_DIR, { recursive: true, force: true }); + } + + if (!existsSync(BUNDLE_INDEX_PATH)) { + buildPromise = null; + throw new Error("Bundle not built — cannot run library/bundle tests"); + } +} + +async function waitForBundle(): Promise { + const deadline = Date.now() + 55_000; + while (Date.now() < deadline) { + if (existsSync(BUNDLE_INDEX_PATH) && !existsSync(LOCK_DIR)) { + return; + } + await sleep(250); } + throw new Error("Bundle not built — cannot run library/bundle tests"); +} +async function spawnBundle(): Promise { const exitCode = await new Promise((resolve) => { let buildStderr = ""; const proc = spawn("pnpm", ["run", "bundle"], { @@ -72,7 +113,7 @@ async function runBundleBuild(clean: boolean): Promise { }); }); - if (exitCode !== 0 || !existsSync(BUNDLE_INDEX_PATH)) { + if (exitCode !== 0) { buildPromise = null; throw new Error("Bundle not built — cannot run library/bundle tests"); } diff --git a/packages/cli/test/e2e/bundle.test.ts b/packages/cli/test/e2e/bundle.test.ts index 41687c051..c188426ec 100644 --- a/packages/cli/test/e2e/bundle.test.ts +++ b/packages/cli/test/e2e/bundle.test.ts @@ -50,7 +50,7 @@ const INK_APP_PATH = join(ROOT_DIR, "dist/ink-app.js"); describe("npm bundle", () => { beforeAll(async () => { - await ensureBundleBuilt({ clean: true }); + await ensureBundleBuilt(); }, 60_000); // Bundle can take a while test("bundle file exists", () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a94e23f71..424e1eb0d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,8 +74,8 @@ importers: specifier: ^1.26.0 version: 1.36.0(express@5.2.1)(zod@3.25.76) '@sentry/api': - specifier: ^0.253.0 - version: 0.253.0(zod@3.25.76) + specifier: ^0.256.0 + version: 0.256.0(valibot@1.4.2(typescript@5.9.3))(zod@3.25.76) '@sentry/core': specifier: 10.63.0 version: 10.63.0(patch_hash=e663994979ff877a26ab6d4dea5968fbaee4ccdfdeb85623983535a572678940) @@ -1478,12 +1478,15 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@sentry/api@0.253.0': - resolution: {integrity: sha512-g6zU6Qa7HaqEAhcPkrAF2yIF9Th3UBxNIM2HjFt9eTLWT+cHqO1zvgPe8yl5IXVpn4xwZmytbdome43ba+FgSQ==} + '@sentry/api@0.256.0': + resolution: {integrity: sha512-6SL/EpZW569eAEAOHossu2bWHXl1ODJsvs1xWro0hbmLieDD2G4FjBIyNj+zPwbHcCpXNfPhpCTLmkLVih9khA==} engines: {node: '>=22'} peerDependencies: + valibot: '*' zod: ^3.24.0 peerDependenciesMeta: + valibot: + optional: true zod: optional: true @@ -5920,8 +5923,9 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@sentry/api@0.253.0(zod@3.25.76)': + '@sentry/api@0.256.0(valibot@1.4.2(typescript@5.9.3))(zod@3.25.76)': optionalDependencies: + valibot: 1.4.2(typescript@5.9.3) zod: 3.25.76 '@sentry/astro@10.69.0(@opentelemetry/core@2.10.0(@opentelemetry/api@1.9.1))(astro@7.1.6(@astrojs/markdown-remark@7.2.2)(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@types/node@24.13.3)(tsx@4.23.5)(yaml@2.9.0))':