Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
await withTelemetry(async (span) =>
Expand Down
42 changes: 42 additions & 0 deletions src/lib/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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;
},
});
Expand Down
Loading