Skip to content

Commit b02a6d0

Browse files
authored
fix(api): strip api/0/ prefix and exclude NodeSystemError integration (CLI-K1) (#523)
Fixes two bugs surfaced by [CLI-K1](https://sentry.sentry.io/issues/7355494310/): **1. Double `/api/0/` prefix in `sentry api` command** When users include the `/api/0/` prefix in the endpoint argument (e.g. `sentry api /api/0/projects/my-org/my-project/environments/`), the URL construction doubled it to `/api/0/api/0/...`. `normalizeEndpoint()` now strips the prefix automatically. **2. `util.getSystemErrorMap()` crash on Bun** The Sentry SDK's `NodeSystemError` integration calls `util.getSystemErrorMap()` which Bun does not implement, causing the SDK's event processing pipeline to crash with a TypeError. This integration is now excluded from the default integrations set — it only adds extra context for Node.js system errors and is not needed for CLI telemetry.
1 parent a03a9d1 commit b02a6d0

File tree

4 files changed

+109
-4
lines changed

4 files changed

+109
-4
lines changed

src/commands/api.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,15 @@ export function normalizeEndpoint(endpoint: string): string {
8787
validateEndpoint(endpoint);
8888

8989
// Remove leading slash if present (rawApiRequest handles the base URL)
90-
const trimmed = endpoint.startsWith("/") ? endpoint.slice(1) : endpoint;
90+
let trimmed = endpoint.startsWith("/") ? endpoint.slice(1) : endpoint;
91+
92+
// Strip api/0/ prefix if user accidentally included it — the base URL
93+
// already includes /api/0/, so keeping it would produce a doubled path
94+
// like /api/0/api/0/... (see CLI-K1).
95+
// Also strip bare "api/0" to maintain idempotency.
96+
if (trimmed.startsWith("api/0/") || trimmed === "api/0") {
97+
trimmed = trimmed.slice(trimmed.startsWith("api/0/") ? 6 : 5);
98+
}
9199

92100
// Split path and query string
93101
const queryIndex = trimmed.indexOf("?");
@@ -1160,6 +1168,18 @@ export const apiCommand = buildCommand({
11601168
const { stdin } = this;
11611169

11621170
const normalizedEndpoint = normalizeEndpoint(endpoint);
1171+
1172+
// Detect whether normalizeEndpoint stripped the api/0/ prefix (CLI-K1).
1173+
// normalizeEndpoint only adds at most 1 char (trailing slash), so if the
1174+
// normalized result is shorter than the raw input, the prefix was stripped.
1175+
const rawLen = endpoint.startsWith("/")
1176+
? endpoint.length - 1
1177+
: endpoint.length;
1178+
if (normalizedEndpoint.length < rawLen) {
1179+
log.warn(
1180+
"Endpoint includes the /api/0/ prefix which is added automatically — stripping it to avoid a doubled path"
1181+
);
1182+
}
11631183
const { body, params } = await resolveBody(flags, stdin);
11641184

11651185
const headers =

src/lib/telemetry.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,24 @@ const EXCLUDED_INTEGRATIONS = new Set([
262262
"Modules", // Lists all loaded modules - unnecessary for CLI telemetry
263263
]);
264264

265+
/**
266+
* Check whether `util.getSystemErrorMap` exists at setup time.
267+
* Bun does not implement this Node.js API, which the SDK's NodeSystemError
268+
* integration uses in its `processEvent` hook. When missing, the hook crashes
269+
* during event processing instead of sending the error report (CLI-K1).
270+
*
271+
* Checked once at module load so the integration filter is a simple boolean.
272+
*/
273+
const hasGetSystemErrorMap = (() => {
274+
try {
275+
// Dynamic require to avoid bundler issues — the check only matters at runtime
276+
const util = require("node:util") as Record<string, unknown>;
277+
return typeof util.getSystemErrorMap === "function";
278+
} catch {
279+
return false;
280+
}
281+
})();
282+
265283
/** Current beforeExit handler, tracked so it can be replaced on re-init */
266284
let currentBeforeExitHandler: (() => void) | null = null;
267285

@@ -312,11 +330,14 @@ export function initSentry(
312330
const client = Sentry.init({
313331
dsn: SENTRY_CLI_DSN,
314332
enabled,
315-
// Keep default integrations but filter out ones that add overhead without benefit
316-
// Important: Don't use defaultIntegrations: false as it may break debug ID support
333+
// Keep default integrations but filter out ones that add overhead without benefit.
334+
// Important: Don't use defaultIntegrations: false as it may break debug ID support.
335+
// NodeSystemError is excluded on runtimes missing util.getSystemErrorMap (Bun) — CLI-K1.
317336
integrations: (defaults) =>
318337
defaults.filter(
319-
(integration) => !EXCLUDED_INTEGRATIONS.has(integration.name)
338+
(integration) =>
339+
!EXCLUDED_INTEGRATIONS.has(integration.name) &&
340+
(integration.name !== "NodeSystemError" || hasGetSystemErrorMap)
320341
),
321342
environment,
322343
// Enable Sentry structured logs for non-exception telemetry (e.g., unexpected input warnings)

test/commands/api.property.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,31 @@ describe("normalizeEndpoint properties", () => {
211211
{ numRuns: DEFAULT_NUM_RUNS }
212212
);
213213
});
214+
215+
test("api/0/ prefix is always stripped (CLI-K1)", async () => {
216+
await fcAssert(
217+
property(simplePathArb, (path) => {
218+
const withPrefix = `api/0/${path}`;
219+
const withoutPrefix = path;
220+
221+
const resultWith = normalizeEndpoint(withPrefix);
222+
const resultWithout = normalizeEndpoint(withoutPrefix);
223+
224+
expect(resultWith).toBe(resultWithout);
225+
}),
226+
{ numRuns: DEFAULT_NUM_RUNS }
227+
);
228+
});
229+
230+
test("result never starts with api/0/ (CLI-K1)", async () => {
231+
await fcAssert(
232+
property(endpointArb, (endpoint) => {
233+
const result = normalizeEndpoint(endpoint);
234+
expect(result.startsWith("api/0/")).toBe(false);
235+
}),
236+
{ numRuns: DEFAULT_NUM_RUNS }
237+
);
238+
});
214239
});
215240

216241
describe("parseMethod properties", () => {

test/commands/api.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,45 @@ describe("normalizeEndpoint edge cases", () => {
5555
});
5656
});
5757

58+
describe("normalizeEndpoint: api/0/ prefix stripping (CLI-K1)", () => {
59+
test("strips api/0/ prefix to avoid doubled path", () => {
60+
expect(normalizeEndpoint("api/0/projects/my-org/my-project/")).toBe(
61+
"projects/my-org/my-project/"
62+
);
63+
});
64+
65+
test("strips /api/0/ prefix with leading slash", () => {
66+
expect(normalizeEndpoint("/api/0/projects/my-org/my-project/")).toBe(
67+
"projects/my-org/my-project/"
68+
);
69+
});
70+
71+
test("strips api/0/ prefix and adds trailing slash", () => {
72+
expect(normalizeEndpoint("api/0/organizations/my-org/issues")).toBe(
73+
"organizations/my-org/issues/"
74+
);
75+
});
76+
77+
test("strips api/0/ prefix with query string", () => {
78+
expect(
79+
normalizeEndpoint(
80+
"api/0/projects/my-org/my-proj/environments/?visibility=visible"
81+
)
82+
).toBe("projects/my-org/my-proj/environments/?visibility=visible");
83+
});
84+
85+
test("strips bare api/0 for idempotency", () => {
86+
// "api/0" → "/" so that normalizing twice is stable
87+
expect(normalizeEndpoint("api/0")).toBe("/");
88+
});
89+
90+
test("does not strip partial api/ prefix", () => {
91+
expect(normalizeEndpoint("api/1/organizations/")).toBe(
92+
"api/1/organizations/"
93+
);
94+
});
95+
});
96+
5897
describe("normalizeEndpoint: path traversal hardening (#350)", () => {
5998
test("rejects bare .. traversal", () => {
6099
expect(() => normalizeEndpoint("..")).toThrow(/path traversal/);

0 commit comments

Comments
 (0)