History

feat(cloud): wire Sentry error capture for HTTP + MCP DO

HTTP path: TanStack Start + our Effect error handlers swallow throws and return 500 responses, so `Sentry.withSentry` (which only sees uncaught exceptions) never captures them. Adds explicit `Sentry.captureException` in the places that turn errors into 500s: - `toErrorServerResponse` / `toErrorResponse` for the Effect API paths - `handleMcpRequest_POST`'s catch for the MCP proxy path DO path: wraps McpSessionDO with `Sentry.instrumentDurableObjectWithSentry`. Unlike otel-cf-workers' `instrumentDO` (which broke `this` binding on WorkerTransport's stream primitives), Sentry's wrapper uses `Reflect.apply` and is safe. Sets `instrumentPrototypeMethods: true` because the DO's `init` / `handleRequest` live on the prototype — the default auto-wrap only visits own properties, which misses them, and errors thrown inside `init()` never reach Sentry without this flag. Verified end-to-end: debug smoke tests captured both an explicit `captureException` call (HTTP path) and a DO init() throw, both landing in Sentry issues on the node-cloudflare-workers project. Debug endpoints removed before merge.

Rhys Sullivan committed 7 months ago

Commit ef5eb1b1 parent c0e64f93 files · +48 −30Browse files

Showing the changes to apps/cloud/src/api (1 of 3 files). Show all

Unified · Split

apps/cloud/src/api/error-response.ts+6−1
@@ -1,3 +1,4 @@
1import * as Sentry from "@sentry/cloudflare";
1import { Data } from "effect";2import { Data } from "effect";
2import { HttpServerResponse } from "@effect/platform";3import { HttpServerResponse } from "@effect/platform";
3 4
@@ -20,12 +21,16 @@ export const isServerError = (error: unknown): boolean => toHttpResponseError(er · 16 unmodified lines
20 21
21export const toErrorResponse = (error: unknown): Response => {22export const toErrorResponse = (error: unknown): Response => {
22 const mapped = toHttpResponseError(error);23 const mapped = toHttpResponseError(error);
24 if (mapped.status >= 500) Sentry.captureException(error);
23 return Response.json({ error: mapped.message, code: mapped.code }, { status: mapped.status });25 return Response.json({ error: mapped.message, code: mapped.code }, { status: mapped.status });
24};26};
25 27
26export const toErrorServerResponse = (error: unknown): HttpServerResponse.HttpServerResponse => {28export const toErrorServerResponse = (error: unknown): HttpServerResponse.HttpServerResponse => {
27 console.error("[api] toErrorServerResponse error:", error instanceof Error ? error.stack : error);
28 const mapped = toHttpResponseError(error);29 const mapped = toHttpResponseError(error);
30 if (mapped.status >= 500) {
31 console.error("[api] toErrorServerResponse error:", error instanceof Error ? error.stack : error);
32 Sentry.captureException(error);
33 }
29 return HttpServerResponse.unsafeJson(34 return HttpServerResponse.unsafeJson(
30 { error: mapped.message, code: mapped.code },35 { error: mapped.message, code: mapped.code },
31 { status: mapped.status },36 { status: mapped.status },