diff --git a/src/bin.ts b/src/bin.ts index 29ab5375..2b9621b3 100755 --- a/src/bin.ts +++ b/src/bin.ts @@ -14,6 +14,20 @@ import { shouldSuppressNotification, } from "./lib/version-check.js"; +// Exit cleanly when downstream pipe consumer closes (e.g., `sentry issue list | head`). +// EPIPE (errno -32) is normal Unix behavior — not an error. Node.js/Bun ignore SIGPIPE +// at the process level, so pipe write failures surface as async 'error' events on the +// stream. Without this handler they become uncaught exceptions. +function handleStreamError(err: NodeJS.ErrnoException): void { + if (err.code === "EPIPE") { + process.exit(0); + } + throw err; +} + +process.stdout.on("error", handleStreamError); +process.stderr.on("error", handleStreamError); + /** Run CLI command with telemetry wrapper */ async function runCommand(args: string[]): Promise { await withTelemetry(async (span) => diff --git a/src/lib/telemetry.ts b/src/lib/telemetry.ts index bc16a016..0a69bc0a 100644 --- a/src/lib/telemetry.ts +++ b/src/lib/telemetry.ts @@ -155,6 +155,41 @@ export function createBeforeExitHandler(client: Sentry.BunClient): () => void { }; } +/** + * Check if a Sentry event represents an EPIPE error. + * + * EPIPE (errno -32) occurs when writing to a pipe whose reading end has been + * closed. This is normal Unix behavior when CLI output is piped through + * commands like `head`, `less`, or `grep -m1`. These errors are not bugs + * and should be silently dropped from telemetry. + * + * Detects both Bun-style ("EPIPE: broken pipe, write") and Node.js-style + * ("write EPIPE") error messages, plus the structured `node_system_error` context. + * + * @internal Exported for testing + */ +export function isEpipeError(event: Sentry.ErrorEvent): boolean { + // Check exception message for EPIPE + const exceptions = event.exception?.values; + if (exceptions) { + for (const ex of exceptions) { + if (ex.value?.includes("EPIPE")) { + return true; + } + } + } + + // Check Node.js system error context (set by the SDK for system errors) + const systemError = event.contexts?.node_system_error as + | { code?: string } + | undefined; + if (systemError?.code === "EPIPE") { + return true; + } + + return false; +} + /** * Integrations to exclude for CLI. * These add overhead without benefit for short-lived CLI processes. @@ -206,6 +241,13 @@ export function initSentry(enabled: boolean): Sentry.BunClient | undefined { beforeSend: (event) => { // Remove server_name which may contain hostname (PII) event.server_name = undefined; + + // EPIPE errors are expected when stdout is piped and the consumer closes + // early (e.g., `sentry issue list | head`). Not actionable — drop them. + if (isEpipeError(event)) { + return null; + } + return event; }, });