diff --git a/apps/cloud/src/api/error-response.ts b/apps/cloud/src/api/error-response.ts index c122371d4..a076c93ba 100644 --- a/apps/cloud/src/api/error-response.ts +++ b/apps/cloud/src/api/error-response.ts @@ -48,11 +48,26 @@ const toHttpResponseError = (error: unknown): HttpResponseError => { }); }; +// Sentry's `captureException` can't extract a real Error from an Effect +// `Cause` — it logs a `'CauseImpl' captured as exception` warning. Squash +// to a plain value and stash the pretty-printed cause as an extra. +const captureSentryError = (error: unknown): void => { + if (Cause.isCause(error)) { + const pretty = Cause.pretty(error); + Sentry.captureException(Cause.squash(error), (scope) => { + scope.setExtra("cause", pretty); + return scope; + }); + } else { + Sentry.captureException(error); + } +}; + export const isServerError = (error: unknown): boolean => toHttpResponseError(error).status >= 500; export const toErrorResponse = (error: unknown): Response => { const mapped = toHttpResponseError(error); - if (mapped.status >= 500) Sentry.captureException(error); + if (mapped.status >= 500) captureSentryError(error); return Response.json({ error: mapped.message, code: mapped.code }, { status: mapped.status }); }; @@ -61,9 +76,9 @@ export const toErrorServerResponse = (error: unknown): HttpServerResponse.HttpSe if (mapped.status >= 500) { console.error( "[api] toErrorServerResponse error:", - error instanceof Error ? error.stack : error, + Cause.isCause(error) ? Cause.pretty(error) : error instanceof Error ? error.stack : error, ); - Sentry.captureException(error); + captureSentryError(error); } return HttpServerResponse.jsonUnsafe( { error: mapped.message, code: mapped.code }, diff --git a/apps/cloud/src/mcp-session.ts b/apps/cloud/src/mcp-session.ts index 26a457952..92163f408 100644 --- a/apps/cloud/src/mcp-session.ts +++ b/apps/cloud/src/mcp-session.ts @@ -4,7 +4,7 @@ import { DurableObject, env } from "cloudflare:workers"; import { createTraceState } from "@opentelemetry/api"; -import { Data, Effect, Layer } from "effect"; +import { Cause, Data, Effect, Layer } from "effect"; import * as OtelTracer from "@effect/opentelemetry/Tracer"; import type * as Tracer from "effect/Tracer"; import * as Sentry from "@sentry/cloudflare"; @@ -624,8 +624,12 @@ export class McpSessionDO extends DurableObject { }).pipe( Effect.catchCause((cause) => Effect.sync(() => { - console.error("[mcp-session] handleRequest error:", cause); - Sentry.captureException(cause); + const pretty = Cause.pretty(cause); + console.error("[mcp-session] handleRequest error:", pretty); + Sentry.captureException(Cause.squash(cause), (scope) => { + scope.setExtra("cause", pretty); + return scope; + }); return jsonRpcError(500, -32603, "Internal error"); }), ), diff --git a/apps/cloud/src/mcp.ts b/apps/cloud/src/mcp.ts index 958fed84f..3d69dc1a3 100644 --- a/apps/cloud/src/mcp.ts +++ b/apps/cloud/src/mcp.ts @@ -17,7 +17,7 @@ import { env } from "cloudflare:workers"; import { HttpEffect, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"; import * as Sentry from "@sentry/cloudflare"; -import { Context, Effect, Layer, Option, Schema } from "effect"; +import { Cause, Context, Effect, Layer, Option, Schema } from "effect"; import { createCachedRemoteJWKSet } from "./jwks-cache"; import { TelemetryLive } from "./services/telemetry"; @@ -697,8 +697,12 @@ export const mcpApp: Effect.Effect< Effect.withSpan("mcp.request"), Effect.catchCause((cause) => Effect.sync(() => { - console.error("[mcp] request failed:", cause); - Sentry.captureException(cause); + const pretty = Cause.pretty(cause); + console.error("[mcp] request failed:", pretty); + Sentry.captureException(Cause.squash(cause), (scope) => { + scope.setExtra("cause", pretty); + return scope; + }); return jsonRpcError(500, -32603, "Internal server error"); }), ),