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
4 changes: 2 additions & 2 deletions .agents/skills/logging-observability/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Enforce correct logging and error handling patterns in the sentry-mcp codebase.

## Error Hierarchy

### API Errors (`packages/mcp-core/src/api-client/errors.ts`)
### API Errors (`packages/toolkit-core/src/api-client/errors.ts`)

```
ApiError (base class)
Expand Down Expand Up @@ -299,7 +299,7 @@ When reviewing code for logging correctness, verify each item:
## Reference Files

- Logging functions: `packages/mcp-core/src/telem/logging.ts`
- Error hierarchy: `packages/mcp-core/src/api-client/errors.ts`
- Error hierarchy: `packages/toolkit-core/src/api-client/errors.ts`
- Error formatting: `packages/mcp-core/src/internal/error-handling.ts`
- Agent tool wrapper: `packages/mcp-core/src/internal/agents/tools/utils.ts`
- Full error handling guide: `docs/error-handling.md`
183 changes: 183 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,189 @@
"rules": {
"no-bitwise": "off"
}
},
{
"files": ["packages/toolkit-core/src/**/*.ts"],
"rules": {
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "assert",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "async_hooks",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "buffer",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "child_process",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "crypto",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "dns",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "events",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "fs",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "fs/promises",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "http",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "https",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "module",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "net",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "os",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "path",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "perf_hooks",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "process",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "readline",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "stream",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "stream/promises",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "timers",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "tls",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "url",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "util",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "worker_threads",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "zlib",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "zod",
"message": "Shared toolkit-core modules validate through the Standard Schema interface; the host passes Zod or valibot schemas in."
},
{
"name": "valibot",
"message": "Shared toolkit-core modules validate through the Standard Schema interface; the host passes Zod or valibot schemas in."
}
],
"patterns": [
{
"group": ["node:*"],
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"group": ["zod/*", "@sentry/api/zod", "@sentry/api/valibot"],
"message": "Shared toolkit-core modules validate through the Standard Schema interface; the host passes Zod or valibot schemas in."
}
]
}
],
"no-restricted-globals": [
"error",
{
"name": "Buffer",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "process",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "global",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "require",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "module",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "__dirname",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "__filename",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "setImmediate",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
},
{
"name": "clearImmediate",
"message": "Shared toolkit-core modules move into @sentry/api and must run in browsers. Use web-standard APIs, or inject the host-specific piece."
}
]
}
},
{
"files": [
"packages/toolkit-core/src/**/*.test.ts",
"packages/toolkit-core/src/response-cache/codec.ts"
],
"rules": {
"no-restricted-imports": "off",
"no-restricted-globals": "off"
}
}
]
}
7 changes: 7 additions & 0 deletions docs/contributing/api-patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,13 @@ try {

See error patterns in [common-patterns.md](common-patterns.md#error-handling).

`ApiError` and its subclasses live in the shared client,
`@sentry/toolkit-core/api-client/errors`, and `SentryApiService` builds them
from `readErrorResponse` in `@sentry/toolkit-core/api-client/response`. The
CLI reads raw error responses with the same helper and maps them to its own
exit-coded `ApiError`. See
[`packages/toolkit-core/README.md`](../../packages/toolkit-core/README.md#api-client).

### Automatic Retries

`SentryApiService` sends every request, including `@sentry/api` operations,
Expand Down
2 changes: 1 addition & 1 deletion docs/contributing/error-handling.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This document describes how errors are handled throughout the MCP server tool sy

## Error Types and Hierarchy

### API Error Classes (from api-client/errors.ts)
### API Error Classes (from `@sentry/toolkit-core/api-client/errors`)

```
ApiError (base class)
Expand Down
30 changes: 13 additions & 17 deletions packages/cli/src/lib/api/infrastructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
type SdkResult,
SentryApiError,
} from "@sentry/api";
import { readErrorResponse } from "@sentry/toolkit-core/api-client/response";
import { buildSentryApiUrl } from "@sentry/toolkit-core/api-request";
import {
API_MAX_PER_PAGE,
Expand Down Expand Up @@ -702,25 +703,20 @@ async function throwRawApiError(
response: Response,
endpoint: string,
): Promise<never> {
const body = await readErrorResponse(response);
let detail: string | undefined;
try {
const text = await response.text();
try {
const parsed = JSON.parse(text) as { detail?: string };
// Enriched statuses (403, 401) pass undefined when there is no
// usable string detail so the enrichment renders without a noisy
// `{"detail":null}` prefix. Other statuses get the full JSON as
// a debug aid.
if (typeof parsed.detail === "string") {
detail = parsed.detail;
} else if (response.status !== 403 && response.status !== 401) {
detail = JSON.stringify(parsed);
}
} catch {
detail = text || undefined;
}
} catch {
if (body.text === undefined) {
detail = response.statusText;
} else if (body.parseError !== undefined) {
detail = body.text || undefined;
} else if (body.detail !== undefined) {
detail = body.detail;
} else if (response.status !== 403 && response.status !== 401) {
// Enriched statuses (403, 401) pass undefined when there is no
// usable string detail so the enrichment renders without a noisy
// `{"detail":null}` prefix. Other statuses get the full JSON as
// a debug aid.
detail = JSON.stringify(body.json);
}
// Attach a small allowlisted subset of response headers to the Sentry
// event as context. This lets us distinguish Sentry-app 4xx/5xx (which
Expand Down
9 changes: 4 additions & 5 deletions packages/mcp-core/scripts/check-build-imports.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { readFileSync } from "node:fs";

const expectedImport = "@sentry/core/server";
const path = "dist/server.js";

for (const path of ["dist/server.js", "dist/server.cjs"]) {
const source = readFileSync(path, "utf8");
if (!source.includes(expectedImport)) {
throw new Error(`${path} must preserve the ${expectedImport} import`);
}
const source = readFileSync(path, "utf8");
if (!source.includes(expectedImport)) {
throw new Error(`${path} must preserve the ${expectedImport} import`);
}
2 changes: 1 addition & 1 deletion packages/mcp-core/src/api-client/client-event.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { http, HttpResponse } from "msw";
import { afterEach, describe, expect, it, vi } from "vitest";
import { testEvents } from "../internal/test-fixtures";
import { SentryApiService } from "./client";
import { ApiValidationError } from "./errors";
import { ApiValidationError } from "@sentry/toolkit-core/api-client/errors";

const request = {
organizationSlug: "test-org",
Expand Down
71 changes: 71 additions & 0 deletions packages/mcp-core/src/api-client/client-request-paths.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";

/**
* The slug-only fix in VULN-848 hardened the parameters that were vulnerable at the
* time and left the resource IDs raw, which is how VULN-2159/2450 happened ten months
* later. Enumerating parameters does not hold; enforcing the invariant does.
*
* This fails on any new request path that interpolates a value without the `apiPath`
* tag, so the class cannot be reintroduced by a tool author unaware of it.
*/
describe("client.ts request path invariant", () => {
const source = readFileSync(new URL("./client.ts", import.meta.url), "utf8");

/**
* Returns the path portion of a template literal body, stopping at the query
* separator. Only interpolation before the `?` needs the tag; query values are
* already encoded by `URLSearchParams` and must keep their separators literal.
*/
function pathPortion(body: string): string {
let depth = 0;
for (let i = 0; i < body.length; i++) {
if (body[i] === "$" && body[i + 1] === "{") {
depth++;
i++;
} else if (body[i] === "}" && depth > 0) {
depth--;
} else if (body[i] === "?" && depth === 0) {
return body.slice(0, i);
}
}
return body;
}

it("interpolates every request path through apiPath", () => {
const offenders: string[] = [];

source.split("\n").forEach((line, index) => {
const trimmed = line.trim();
if (trimmed.startsWith("//") || trimmed.startsWith("*")) return;

// A template literal opening with a slash is an API request path.
for (let i = 0; i < line.length - 1; i++) {
if (line[i] !== "`" || line[i + 1] !== "/") continue;

const closing = line.indexOf("`", i + 1);
const body = line.slice(i + 1, closing === -1 ? undefined : closing);

if (
!line.slice(0, i).endsWith("apiPath") &&
/\$\{/.test(pathPortion(body))
) {
offenders.push(`${index + 1}: ${trimmed}`);
}
}
});

expect(
offenders,
`Request paths must be built with the apiPath tag so interpolated values cannot escape their segment:\n${offenders.join("\n")}`,
).toEqual([]);
});

it("does not double-encode inside an apiPath template", () => {
const doubleEncoded = [...source.matchAll(/apiPath`[^`]*`/g)]
.map((match) => match[0])
.filter((template) => template.includes("encodeURIComponent"));

expect(doubleEncoded).toEqual([]);
});
});
2 changes: 1 addition & 1 deletion packages/mcp-core/src/api-client/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
ApiRateLimitError,
ApiServerError,
ApiValidationError,
} from "./errors";
} from "@sentry/toolkit-core/api-client/errors";
import { z } from "zod";

describe("validateEvents error bodies", () => {
Expand Down
Loading
Loading