From f8d4a16e09672f2dc7f92ed598cdc42e082be7b8 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Wed, 15 Jul 2026 00:05:28 -0700 Subject: [PATCH 1/2] feat(orb): wire Sentry into the Cloudflare Worker (central ORB broker) The actual central ORB broker server -- the Cloudflare Worker at src/index.ts / src/api/routes.ts, handling every registration, token mint, and relay push/ pull request -- has had zero Sentry error tracking since it was built. initSentry() has only ever been called from src/server.ts (the self-host Node entrypoint); the Worker has no `.onError(` handler anywhere and no Workers-compatible Sentry SDK dependency. Every ORB fix shipped earlier this session was client-side (self-host); this is the corresponding server-side gap, and by volume of traffic the larger one. @sentry/hono + @sentry/cloudflare (peer dep) auto-capture via Hono's onError by default, confirmed via the package's own README. nodejs_compat (needed for AsyncLocalStorage) was already enabled. The real complication: self-host's server.ts imports THIS Worker's own exported `{fetch, queue}` object directly (`import worker from "./index"`) and calls `worker.fetch(request, env, ctx)` for its own HTTP traffic -- server.ts synthesizes a Worker-shaped `env` by spreading `process.env` specifically so it can reuse this handler byte-for-byte. Naming the new DSN var the same as self-host's own SENTRY_DSN would have silently activated the Cloudflare-only SDK inside a self-hoster's own Node process the moment they set their own Sentry DSN. Fixed two ways: a distinctly-named WORKER_SENTRY_DSN/WORKER_SENTRY_ENVIRONMENT (so there is no name collision to begin with), AND gating the middleware's registration on isCloudflareWorkerRuntime() -- checking navigator.userAgent === "Cloudflare- Workers", Cloudflare's own documented idiom, verified true in a real workerd isolate via test/workers and false under plain Node. Registered as the first middleware in createApp() so it wraps every other middleware and route. Scope: HTTP request handling only (every ORB endpoint). The queue() handler's own top-level safety net is a separate, smaller follow-up -- it dispatches ALL background job types, not just ORB's, and @sentry/hono's onError integration doesn't cover it. --- package-lock.json | 285 ++++++------------ package.json | 2 + src/api/routes.ts | 19 ++ src/env.d.ts | 9 + ...loudflare-worker-runtime-detection.test.ts | 15 + test/workers/worker-runtime.test.ts | 11 + 6 files changed, 153 insertions(+), 188 deletions(-) create mode 100644 test/unit/cloudflare-worker-runtime-detection.test.ts diff --git a/package-lock.json b/package-lock.json index add9d54021..922c2aa2ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,8 @@ "@opentelemetry/exporter-trace-otlp-http": "^0.220.0", "@opentelemetry/resources": "^2.9.0", "@opentelemetry/sdk-trace-node": "^2.9.0", + "@sentry/cloudflare": "^10.63.0", + "@sentry/hono": "^10.63.0", "@sentry/node": "^10.63.0", "@sentry/opentelemetry": "^10.63.0", "agents": "^0.17.3", @@ -1045,9 +1047,9 @@ } }, "node_modules/@apm-js-collab/tracing-hooks": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@apm-js-collab/tracing-hooks/-/tracing-hooks-0.10.0.tgz", - "integrity": "sha512-2/Z3NTewJTruUkmsSnBC5bJlLNUd9keuD1OLlTEpim4FyLhm6m2Rnfv+wrFdUvFfhmH8CRdiDZBqBrn+wyaGuA==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@apm-js-collab/tracing-hooks/-/tracing-hooks-0.10.1.tgz", + "integrity": "sha512-w2OWXR7FWrKqSziuE9+QclaZrStxO/8+OwbXM635s/zs0Eez1Qo3ivSPdB2WsaPY/iznKTytONPx/PitD7IXcA==", "license": "Apache-2.0", "dependencies": { "@apm-js-collab/code-transformer": "^0.15.0", @@ -3728,9 +3730,9 @@ } }, "node_modules/@opentelemetry/api-logs": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.214.0.tgz", - "integrity": "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==", + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", + "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", "license": "Apache-2.0", "dependencies": { "@opentelemetry/api": "^1.3.0" @@ -3786,12 +3788,12 @@ } }, "node_modules/@opentelemetry/instrumentation": { - "version": "0.214.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.214.0.tgz", - "integrity": "sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w==", + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.220.0.tgz", + "integrity": "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/api-logs": "0.214.0", + "@opentelemetry/api-logs": "0.220.0", "import-in-the-middle": "^3.0.0", "require-in-the-middle": "^8.0.0" }, @@ -3838,18 +3840,6 @@ "@opentelemetry/api": "^1.3.0" } }, - "node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/api-logs": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", - "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@opentelemetry/resources": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.9.0.tgz", @@ -3884,18 +3874,6 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/api-logs": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", - "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/@opentelemetry/sdk-metrics": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.9.0.tgz", @@ -5798,28 +5776,30 @@ "node": ">=18" } }, - "node_modules/@sentry/browser-utils/node_modules/@sentry/conventions": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.15.1.tgz", - "integrity": "sha512-ZLP8bRdMON3prWE2tJyImuYscCxdcJeIPIhrOs/rgyFm3C1nCh1B6gdvPj3AZ5zW08oSFFCsq7T+tYEW3h8MNA==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/@sentry/browser-utils/node_modules/@sentry/core": { + "node_modules/@sentry/cloudflare": { "version": "10.65.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.65.0.tgz", - "integrity": "sha512-3aqtmM5NgNGo45BNaaBzi0LPQZAw//NEL4HKS5fXm12pJMa4KEkze8DEKnkTEIrGnWaOJKamecHKlnNg/Mqf/Q==", + "resolved": "https://registry.npmjs.org/@sentry/cloudflare/-/cloudflare-10.65.0.tgz", + "integrity": "sha512-IzSLb20r9nYdZg4oeNq5Rz3G125NYFMV9LMv2eQQ1q2I5QJSY4v2iCDmo5VljnlYEA2N7CqS+iM+J755Ni+dHg==", "license": "MIT", "dependencies": { - "@sentry/conventions": "^0.15.1" + "@opentelemetry/api": "^1.9.1", + "@sentry/core": "10.65.0", + "@sentry/node": "10.65.0", + "@sentry/server-utils": "10.65.0" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.x" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } } }, - "node_modules/@sentry/browser/node_modules/@sentry/conventions": { + "node_modules/@sentry/conventions": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.15.1.tgz", "integrity": "sha512-ZLP8bRdMON3prWE2tJyImuYscCxdcJeIPIhrOs/rgyFm3C1nCh1B6gdvPj3AZ5zW08oSFFCsq7T+tYEW3h8MNA==", @@ -5828,7 +5808,7 @@ "node": ">=14" } }, - "node_modules/@sentry/browser/node_modules/@sentry/core": { + "node_modules/@sentry/core": { "version": "10.65.0", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.65.0.tgz", "integrity": "sha512-3aqtmM5NgNGo45BNaaBzi0LPQZAw//NEL4HKS5fXm12pJMa4KEkze8DEKnkTEIrGnWaOJKamecHKlnNg/Mqf/Q==", @@ -5840,24 +5820,6 @@ "node": ">=18" } }, - "node_modules/@sentry/conventions": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.12.0.tgz", - "integrity": "sha512-z1JQrl/1SLY+8wpzvork6vl+fpsg/oCCxM7HWWhUnI/R+OGNyoIzieQuggX3uUMY7NBtp8UWCQx6FeFazzOF9g==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/@sentry/core": { - "version": "10.63.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.63.0.tgz", - "integrity": "sha512-OtUbsrnbEHffOF2S2+M5zXa3HIM0U2b4CDVLKMY1dgS0J3ivRF8XvkjvyIcEG/y8JXnwXbnprLyjhG+AqMdUZQ==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/@sentry/feedback": { "version": "10.65.0", "resolved": "https://registry.npmjs.org/@sentry/feedback/-/feedback-10.65.0.tgz", @@ -5870,42 +5832,62 @@ "node": ">=18" } }, - "node_modules/@sentry/feedback/node_modules/@sentry/conventions": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.15.1.tgz", - "integrity": "sha512-ZLP8bRdMON3prWE2tJyImuYscCxdcJeIPIhrOs/rgyFm3C1nCh1B6gdvPj3AZ5zW08oSFFCsq7T+tYEW3h8MNA==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/@sentry/feedback/node_modules/@sentry/core": { + "node_modules/@sentry/hono": { "version": "10.65.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.65.0.tgz", - "integrity": "sha512-3aqtmM5NgNGo45BNaaBzi0LPQZAw//NEL4HKS5fXm12pJMa4KEkze8DEKnkTEIrGnWaOJKamecHKlnNg/Mqf/Q==", + "resolved": "https://registry.npmjs.org/@sentry/hono/-/hono-10.65.0.tgz", + "integrity": "sha512-rsIYVcxy4VrrwpCF8pip10jVNG8O0khRNUWTfxurNhEQ3I8UCq/YEsZUJWtWCkXxDq8Wr4LHN4xEEa2SnoDLTg==", "license": "MIT", "dependencies": { - "@sentry/conventions": "^0.15.1" + "@opentelemetry/api": "^1.9.1", + "@sentry/core": "10.65.0" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.x", + "@hono/node-server": "^1.x || ^2.x", + "@sentry/bun": "10.65.0", + "@sentry/cloudflare": "10.65.0", + "@sentry/deno": "10.65.0", + "@sentry/node": "10.65.0", + "hono": "^4.x" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + }, + "@hono/node-server": { + "optional": true + }, + "@sentry/bun": { + "optional": true + }, + "@sentry/cloudflare": { + "optional": true + }, + "@sentry/deno": { + "optional": true + }, + "@sentry/node": { + "optional": true + } } }, "node_modules/@sentry/node": { - "version": "10.63.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.63.0.tgz", - "integrity": "sha512-E+JfDTdUDGQPRsAfCTR2YgmQgxYdoxk4ks6niHN+ByW8alEZL+nXlcN9vI57qj1LsS4v2jjfLxJf1/cMMt84YA==", + "version": "10.65.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.65.0.tgz", + "integrity": "sha512-t35dcdyksysVch/m/XdLgGJqGKJhr9eMD30Ctn3TeQ8yMB0wNXySfjPR5Yg93fpjmfaHtzc6iYIXRAvgNVfrvA==", "license": "MIT", "dependencies": { "@opentelemetry/api": "^1.9.1", - "@opentelemetry/instrumentation": "^0.214.0", - "@opentelemetry/sdk-trace-base": "^2.6.1", - "@opentelemetry/semantic-conventions": "^1.40.0", - "@sentry/conventions": "^0.12.0", - "@sentry/core": "10.63.0", - "@sentry/node-core": "10.63.0", - "@sentry/opentelemetry": "10.63.0", - "@sentry/server-utils": "10.63.0", + "@opentelemetry/instrumentation": "^0.220.0", + "@opentelemetry/sdk-trace-base": "^2.9.0", + "@sentry/conventions": "^0.15.1", + "@sentry/core": "10.65.0", + "@sentry/node-core": "10.65.0", + "@sentry/opentelemetry": "10.65.0", + "@sentry/server-utils": "10.65.0", "import-in-the-middle": "^3.0.0" }, "engines": { @@ -5913,14 +5895,14 @@ } }, "node_modules/@sentry/node-core": { - "version": "10.63.0", - "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.63.0.tgz", - "integrity": "sha512-TaNtkGDRNxH3SjOea2PDtaebkNjMbAH8ZFsEcwlqmadpS7nqSR7z6slZy/iu7y1nLiUdbmcM5JmXwxksy52WRQ==", + "version": "10.65.0", + "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.65.0.tgz", + "integrity": "sha512-U01X9mPT+jZnsLPmPWfBU67Ka+t/Sdd9RGAuvGoKdrI6N47a/9PDkM9oCW+kj0fmZwogZHTgSnzJU5oi3pImgA==", "license": "MIT", "dependencies": { - "@sentry/conventions": "^0.12.0", - "@sentry/core": "10.63.0", - "@sentry/opentelemetry": "10.63.0", + "@sentry/conventions": "^0.15.1", + "@sentry/core": "10.65.0", + "@sentry/opentelemetry": "10.65.0", "import-in-the-middle": "^3.0.0" }, "engines": { @@ -5952,13 +5934,13 @@ } }, "node_modules/@sentry/opentelemetry": { - "version": "10.63.0", - "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.63.0.tgz", - "integrity": "sha512-8yqi8+Ej/anmMn82blXA0BNMeAMs4av6nx0DzhxDrFya28ZaYOn19PChd3erMidfU0HnLLFNqWiFlYxBKq+/KA==", + "version": "10.65.0", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.65.0.tgz", + "integrity": "sha512-8C6FPvm3XBvUrkM52dX3Gz0p2H0Ij8t4sahUA+GTiCz0WM0fnyPeQPGC/b6I4jamV9UXyCZRnE1UEEGCoD+c7A==", "license": "MIT", "dependencies": { - "@sentry/conventions": "^0.12.0", - "@sentry/core": "10.63.0" + "@sentry/conventions": "^0.15.1", + "@sentry/core": "10.65.0" }, "engines": { "node": ">=18" @@ -5986,27 +5968,6 @@ "react": "^16.14.0 || 17.x || 18.x || 19.x" } }, - "node_modules/@sentry/react/node_modules/@sentry/conventions": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.15.1.tgz", - "integrity": "sha512-ZLP8bRdMON3prWE2tJyImuYscCxdcJeIPIhrOs/rgyFm3C1nCh1B6gdvPj3AZ5zW08oSFFCsq7T+tYEW3h8MNA==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/@sentry/react/node_modules/@sentry/core": { - "version": "10.65.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.65.0.tgz", - "integrity": "sha512-3aqtmM5NgNGo45BNaaBzi0LPQZAw//NEL4HKS5fXm12pJMa4KEkze8DEKnkTEIrGnWaOJKamecHKlnNg/Mqf/Q==", - "license": "MIT", - "dependencies": { - "@sentry/conventions": "^0.15.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@sentry/replay": { "version": "10.65.0", "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-10.65.0.tgz", @@ -6033,59 +5994,17 @@ "node": ">=18" } }, - "node_modules/@sentry/replay-canvas/node_modules/@sentry/conventions": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.15.1.tgz", - "integrity": "sha512-ZLP8bRdMON3prWE2tJyImuYscCxdcJeIPIhrOs/rgyFm3C1nCh1B6gdvPj3AZ5zW08oSFFCsq7T+tYEW3h8MNA==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/@sentry/replay-canvas/node_modules/@sentry/core": { - "version": "10.65.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.65.0.tgz", - "integrity": "sha512-3aqtmM5NgNGo45BNaaBzi0LPQZAw//NEL4HKS5fXm12pJMa4KEkze8DEKnkTEIrGnWaOJKamecHKlnNg/Mqf/Q==", - "license": "MIT", - "dependencies": { - "@sentry/conventions": "^0.15.1" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@sentry/replay/node_modules/@sentry/conventions": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.15.1.tgz", - "integrity": "sha512-ZLP8bRdMON3prWE2tJyImuYscCxdcJeIPIhrOs/rgyFm3C1nCh1B6gdvPj3AZ5zW08oSFFCsq7T+tYEW3h8MNA==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/@sentry/replay/node_modules/@sentry/core": { - "version": "10.65.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.65.0.tgz", - "integrity": "sha512-3aqtmM5NgNGo45BNaaBzi0LPQZAw//NEL4HKS5fXm12pJMa4KEkze8DEKnkTEIrGnWaOJKamecHKlnNg/Mqf/Q==", - "license": "MIT", - "dependencies": { - "@sentry/conventions": "^0.15.1" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@sentry/server-utils": { - "version": "10.63.0", - "resolved": "https://registry.npmjs.org/@sentry/server-utils/-/server-utils-10.63.0.tgz", - "integrity": "sha512-7NN//DG9Yak8t2+6WiEcNmN269iHRVdtZtZIwucEd0OXyZ3FEBBDaBF+bT9V6H/kPtUvVMkHQ72Bn2Xs5JYGxg==", + "version": "10.65.0", + "resolved": "https://registry.npmjs.org/@sentry/server-utils/-/server-utils-10.65.0.tgz", + "integrity": "sha512-80toEFD6s+0Le7jrYB6pHWLF703WSg0WyavAWqrBGWG8JkREHgedAxzFYgoY5GlMI756qk6Ea7UzhJTHd2zAXA==", "license": "MIT", "dependencies": { "@apm-js-collab/code-transformer": "^0.15.0", "@apm-js-collab/code-transformer-bundler-plugins": "^0.5.0", - "@apm-js-collab/tracing-hooks": "^0.10.0", - "@sentry/conventions": "^0.12.0", - "@sentry/core": "10.63.0", + "@apm-js-collab/tracing-hooks": "^0.10.1", + "@sentry/conventions": "^0.15.1", + "@sentry/core": "10.65.0", "magic-string": "~0.30.0" }, "engines": { @@ -7768,15 +7687,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-import-attributes": { - "version": "1.9.5", - "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", - "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^8" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -9929,9 +9839,9 @@ } }, "node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "license": "MIT" }, "node_modules/es-object-atoms": { @@ -11492,14 +11402,13 @@ } }, "node_modules/import-in-the-middle": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.2.0.tgz", - "integrity": "sha512-vR2B6HKIhaBjcZr2bLpFiJ1VbzOlRQ7aby4/gw5WPIzToLjqpfWw3VJ4sk1uDchoOODEirvO2jyrSPtUSL5CrQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.3.1.tgz", + "integrity": "sha512-0rymlHSFLwZ0ixx8DaQkoIyZojJPY2a0K2nEYslhKJ6jIYO/m0IcCb7iQsFPmS7WmKwISZiIrv5Icstrw/CmqA==", "license": "Apache-2.0", "dependencies": { - "acorn": "^8.15.0", - "acorn-import-attributes": "^1.9.5", "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^2.2.0", "module-details-from-path": "^1.0.4" }, "engines": { diff --git a/package.json b/package.json index c254e7888c..b249d271d8 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,8 @@ "@opentelemetry/exporter-trace-otlp-http": "^0.220.0", "@opentelemetry/resources": "^2.9.0", "@opentelemetry/sdk-trace-node": "^2.9.0", + "@sentry/cloudflare": "^10.63.0", + "@sentry/hono": "^10.63.0", "@sentry/node": "^10.63.0", "@sentry/opentelemetry": "^10.63.0", "agents": "^0.17.3", diff --git a/src/api/routes.ts b/src/api/routes.ts index 01f1acb4c0..581baafe0f 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -1,4 +1,5 @@ import { Hono, type Context } from "hono"; +import { sentry } from "@sentry/hono/cloudflare"; import { z } from "zod"; import { parsePositiveInt } from "../utils/json"; import { analyzePRQueue, type AuthorRole, type ChecksStatus } from "../queue-intelligence"; @@ -914,8 +915,26 @@ function contributorOpenIssueCount(issues: Array<{ repoFullName: string; state: return issues.filter((issue) => issue.repoFullName.toLowerCase() === targetRepo && issue.state === "open").length; } +/** True only inside a genuine Cloudflare Workers isolate (the `global_navigator` compat flag, on by default + * for this project's compatibility_date, sets `navigator.userAgent` to this exact literal -- Cloudflare's own + * documented idiom for this check). Self-host's server.ts calls the SAME exported `worker.fetch` this app + * produces (it synthesizes a Worker-shaped `env` by spreading `process.env` specifically so it can reuse this + * handler byte-for-byte) -- gating the Cloudflare-only Sentry middleware on this, rather than on env var + * presence alone, is what keeps it from ever activating inside a self-hoster's own Node process. */ +export function isCloudflareWorkerRuntime(): boolean { + return typeof navigator !== "undefined" && navigator.userAgent === "Cloudflare-Workers"; +} + export function createApp() { const app = new Hono(); + // Registered FIRST/outermost (Sentry's own guidance) so it wraps every other middleware and route below, + // including a thrown exception from the CORS/rate-limit middleware right after this. No-ops completely + // outside a real Workers isolate (see isCloudflareWorkerRuntime) and when WORKER_SENTRY_DSN is unset -- this + // is the Worker-side counterpart to self-host's own initSentry()/installStructuredLogForwarding(), which + // this Worker has never had any equivalent of despite being the actual central Orb broker server. + if (isCloudflareWorkerRuntime()) { + app.use(sentry(app, (env) => ({ dsn: env.WORKER_SENTRY_DSN, environment: env.WORKER_SENTRY_ENVIRONMENT ?? "production" }))); + } app.use("*", async (c, next) => { const allowedOrigin = allowedCorsOrigin(c.env, c.req.header("origin")); if (allowedOrigin) { diff --git a/src/env.d.ts b/src/env.d.ts index 68b4466dd3..0a0b7bbd11 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -164,6 +164,15 @@ declare global { /** Webhook secret for the central LoopOver Orb GitHub App (#1255) — distinct from the review app's * GITHUB_WEBHOOK_SECRET. Verifies inbound POST /v1/orb/webhook deliveries. Inject as a wrangler secret. */ ORB_GITHUB_WEBHOOK_SECRET?: string; + /** Cloudflare Worker error tracking (@sentry/hono/cloudflare). Deliberately NAMED DIFFERENTLY from + * self-host's own SENTRY_DSN (src/selfhost/sentry.ts): server.ts synthesizes a Worker-shaped `env` by + * spreading `process.env` and calls the SAME shared `worker.fetch` this Worker exports, so reusing the + * self-host var name would silently activate the Cloudflare-only SDK inside a self-hoster's own Node + * process the moment they set their OWN SENTRY_DSN — an unrelated, unintended cross-wire. Opt-in like + * every other Sentry var in this codebase: a complete no-op (routes.ts's isCloudflareWorkerRuntime guard + * AND this being unset) until both are true. Inject as a wrangler secret. */ + WORKER_SENTRY_DSN?: string; + WORKER_SENTRY_ENVIRONMENT?: string; /** The central Orb GitHub App's OWN credentials (separate from the loopover review App above). Inject as * wrangler secrets. Used to mint the Orb App JWT → list installations + mint short-lived installation tokens * (the token-broker). CLIENT_ID/SECRET drive the OAuth onboarding flow. */ diff --git a/test/unit/cloudflare-worker-runtime-detection.test.ts b/test/unit/cloudflare-worker-runtime-detection.test.ts new file mode 100644 index 0000000000..6a7393abe4 --- /dev/null +++ b/test/unit/cloudflare-worker-runtime-detection.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; +import { isCloudflareWorkerRuntime } from "../../src/api/routes"; + +describe("isCloudflareWorkerRuntime (gates the Worker-only Sentry middleware in createApp)", () => { + it("is false under plain Node -- the exact condition self-host's server.ts runs createApp()'s shared handler under", () => { + // No mocking here on purpose: this asserts the REAL behavior of the real test runtime (Node), which is + // also self-host's real runtime -- Node's own native `navigator.userAgent` is "Node.js/", never + // the Workers-specific literal. If this ever started returning true under Node, the Cloudflare-only Sentry + // SDK would activate inside every self-hoster's own process the moment they set WORKER_SENTRY_DSN by + // accident (e.g. copy-pasting the wrong var name) -- a real (Workers-only), test/workers/worker-runtime.test.ts + // proves the true side in an actual workerd isolate. + expect(isCloudflareWorkerRuntime()).toBe(false); + expect(navigator.userAgent).not.toBe("Cloudflare-Workers"); + }); +}); diff --git a/test/workers/worker-runtime.test.ts b/test/workers/worker-runtime.test.ts index 36a132f7bd..b40dca1273 100644 --- a/test/workers/worker-runtime.test.ts +++ b/test/workers/worker-runtime.test.ts @@ -1,6 +1,7 @@ import { createExecutionContext, waitOnExecutionContext } from "cloudflare:test"; import { describe, expect, it } from "vitest"; import worker from "../../src/index"; +import { isCloudflareWorkerRuntime } from "../../src/api/routes"; describe("worker runtime", () => { it("serves public metadata and keeps private routes locked in the Workers runtime", async () => { @@ -17,4 +18,14 @@ describe("worker runtime", () => { const mcp = await worker.fetch(new Request("https://gittensory.test/mcp", { method: "POST" }), {} as Env, createExecutionContext()); expect(mcp.status).toBe(401); }); + + it("REGRESSION: isCloudflareWorkerRuntime() is true in a real Workers isolate (the gate that lets the Sentry middleware register at all)", () => { + expect(isCloudflareWorkerRuntime()).toBe(true); + expect(navigator.userAgent).toBe("Cloudflare-Workers"); + }); + + it("still serves a normal response when WORKER_SENTRY_DSN is unset -- the Sentry middleware being registered must not itself break requests", async () => { + const res = await worker.fetch(new Request("https://gittensory.test/health"), { WORKER_SENTRY_DSN: undefined } as unknown as Env, createExecutionContext()); + expect(res.status).toBe(200); + }); }); From 9597167e152a92c4438308ed27788a32c86279c1 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Wed, 15 Jul 2026 00:26:57 -0700 Subject: [PATCH 2/2] fix(orb): v8-ignore the Worker-runtime-only Sentry middleware branch Codecov flagged the isCloudflareWorkerRuntime() TRUE branch as uncovered patch: it only genuinely executes inside a real Workers isolate, and test:coverage runs under plain Node (--pool=forks). The real coverage for this branch is test/workers/worker-runtime.test.ts, which runs under @cloudflare/vitest-pool-workers as a SEPARATE, non-coverage-instrumented step in test:ci -- Codecov has no visibility into it. --- src/api/routes.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/api/routes.ts b/src/api/routes.ts index 581baafe0f..ed4c5ebc94 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -932,9 +932,14 @@ export function createApp() { // outside a real Workers isolate (see isCloudflareWorkerRuntime) and when WORKER_SENTRY_DSN is unset -- this // is the Worker-side counterpart to self-host's own initSentry()/installStructuredLogForwarding(), which // this Worker has never had any equivalent of despite being the actual central Orb broker server. + /* v8 ignore start -- the TRUE branch only genuinely exercises inside a real Workers isolate (this vitest + * run is Node); covered instead by test/workers/worker-runtime.test.ts, which runs under + * @cloudflare/vitest-pool-workers and is NOT part of this coverage-instrumented run. isCloudflareWorkerRuntime + * itself has its own direct Node-side (false) and real-isolate (true) tests. */ if (isCloudflareWorkerRuntime()) { app.use(sentry(app, (env) => ({ dsn: env.WORKER_SENTRY_DSN, environment: env.WORKER_SENTRY_ENVIRONMENT ?? "production" }))); } + /* v8 ignore stop */ app.use("*", async (c, next) => { const allowedOrigin = allowedCorsOrigin(c.env, c.req.header("origin")); if (allowedOrigin) {