Skip to content

Commit 2866f6d

Browse files
committed
fix(telemetry): handle EPIPE errors from piped stdout gracefully
When CLI output is piped through commands like `head` or `jq`, closing the pipe causes EPIPE (errno -32) write errors. These surfaced as fatal uncaught exceptions in Sentry (14 issues, ~34 events) because there was no error handler on stdout/stderr streams. - Register error handlers on process.stdout/stderr that exit cleanly (code 0) on EPIPE, matching standard CLI behavior (gh, npm, etc.) - Add isEpipeError() helper and wire it into beforeSend to drop any EPIPE events that still reach Sentry as a safety net - Resolve all 14 existing EPIPE issues on Sentry
1 parent 805804e commit 2866f6d

File tree

2 files changed

+56
-0
lines changed

2 files changed

+56
-0
lines changed

src/bin.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ import {
1414
shouldSuppressNotification,
1515
} from "./lib/version-check.js";
1616

17+
// Exit cleanly when downstream pipe consumer closes (e.g., `sentry issue list | head`).
18+
// EPIPE (errno -32) is normal Unix behavior — not an error. Node.js/Bun ignore SIGPIPE
19+
// at the process level, so pipe write failures surface as async 'error' events on the
20+
// stream. Without this handler they become uncaught exceptions.
21+
function handleStreamError(err: NodeJS.ErrnoException): void {
22+
if (err.code === "EPIPE") {
23+
process.exit(0);
24+
}
25+
throw err;
26+
}
27+
28+
process.stdout.on("error", handleStreamError);
29+
process.stderr.on("error", handleStreamError);
30+
1731
/** Run CLI command with telemetry wrapper */
1832
async function runCommand(args: string[]): Promise<void> {
1933
await withTelemetry(async (span) =>

src/lib/telemetry.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,41 @@ export function createBeforeExitHandler(client: Sentry.BunClient): () => void {
155155
};
156156
}
157157

158+
/**
159+
* Check if a Sentry event represents an EPIPE error.
160+
*
161+
* EPIPE (errno -32) occurs when writing to a pipe whose reading end has been
162+
* closed. This is normal Unix behavior when CLI output is piped through
163+
* commands like `head`, `less`, or `grep -m1`. These errors are not bugs
164+
* and should be silently dropped from telemetry.
165+
*
166+
* Detects both Bun-style ("EPIPE: broken pipe, write") and Node.js-style
167+
* ("write EPIPE") error messages, plus the structured `node_system_error` context.
168+
*
169+
* @internal Exported for testing
170+
*/
171+
export function isEpipeError(event: Sentry.ErrorEvent): boolean {
172+
// Check exception message for EPIPE
173+
const exceptions = event.exception?.values;
174+
if (exceptions) {
175+
for (const ex of exceptions) {
176+
if (ex.value?.includes("EPIPE")) {
177+
return true;
178+
}
179+
}
180+
}
181+
182+
// Check Node.js system error context (set by the SDK for system errors)
183+
const systemError = event.contexts?.node_system_error as
184+
| { code?: string }
185+
| undefined;
186+
if (systemError?.code === "EPIPE") {
187+
return true;
188+
}
189+
190+
return false;
191+
}
192+
158193
/**
159194
* Integrations to exclude for CLI.
160195
* These add overhead without benefit for short-lived CLI processes.
@@ -206,6 +241,13 @@ export function initSentry(enabled: boolean): Sentry.BunClient | undefined {
206241
beforeSend: (event) => {
207242
// Remove server_name which may contain hostname (PII)
208243
event.server_name = undefined;
244+
245+
// EPIPE errors are expected when stdout is piped and the consumer closes
246+
// early (e.g., `sentry issue list | head`). Not actionable — drop them.
247+
if (isEpipeError(event)) {
248+
return null;
249+
}
250+
209251
return event;
210252
},
211253
});

0 commit comments

Comments
 (0)