diff --git a/package-lock.json b/package-lock.json index 66a3504e9..3f6a09daf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,8 +23,6 @@ "@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", "agents": "^0.17.3", "aws4fetch": "^1.0.20", "drizzle-orm": "^0.45.2", @@ -6454,29 +6452,6 @@ "node": ">=18" } }, - "node_modules/@sentry/cloudflare": { - "version": "10.65.0", - "resolved": "https://registry.npmjs.org/@sentry/cloudflare/-/cloudflare-10.65.0.tgz", - "integrity": "sha512-IzSLb20r9nYdZg4oeNq5Rz3G125NYFMV9LMv2eQQ1q2I5QJSY4v2iCDmo5VljnlYEA2N7CqS+iM+J755Ni+dHg==", - "license": "MIT", - "dependencies": { - "@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/conventions": { "version": "0.15.1", "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.15.1.tgz", @@ -6510,48 +6485,6 @@ "node": ">=18" } }, - "node_modules/@sentry/hono": { - "version": "10.65.0", - "resolved": "https://registry.npmjs.org/@sentry/hono/-/hono-10.65.0.tgz", - "integrity": "sha512-rsIYVcxy4VrrwpCF8pip10jVNG8O0khRNUWTfxurNhEQ3I8UCq/YEsZUJWtWCkXxDq8Wr4LHN4xEEa2SnoDLTg==", - "license": "MIT", - "dependencies": { - "@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.65.0", "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.65.0.tgz", diff --git a/package.json b/package.json index 68e80bf7c..b4d2207b4 100644 --- a/package.json +++ b/package.json @@ -125,8 +125,6 @@ "@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", "agents": "^0.17.3", "aws4fetch": "^1.0.20", "drizzle-orm": "^0.45.2", diff --git a/src/api/routes.ts b/src/api/routes.ts index 0afcd172a..7908188a1 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -1,5 +1,4 @@ import { Hono, type Context } from "hono"; -import { sentry } from "@sentry/hono/cloudflare"; import { createWorkerPostHogErrorMiddleware } from "./worker-posthog"; import { z } from "zod"; import { parsePositiveInt } from "../utils/json"; @@ -1127,8 +1126,8 @@ function contributorOpenIssueCount(issues: Array<{ repoFullName: string; state: * 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. */ + * handler byte-for-byte) -- gating the Cloudflare-only PostHog error 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"; } @@ -1145,22 +1144,15 @@ function internalOpsAgentConfig(env: Env): OpsAgentConfig { 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. + // Registered FIRST/outermost so it wraps every other middleware and route below, including a thrown + // exception from the CORS/rate-limit middleware right after this. REPLACES the old Sentry middleware + // entirely (2026-07-25 epic #8286 correction: full replacement, not a parallel-run). No-ops completely + // outside a real Workers isolate (see isCloudflareWorkerRuntime) and when WORKER_POSTHOG_API_KEY is unset. /* 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" }))); - // PostHog parallel sink (#8288, epic #8286): registered right after Sentry so it still wraps every route - // below. Independent of Sentry's own middleware -- both observe the same shared Context.error Hono sets - // once a downstream handler throws (see createWorkerPostHogErrorMiddleware's own doc comment for why this - // is a c.error check, not a try/catch), so ordering between the two doesn't matter. No-ops completely when - // WORKER_POSTHOG_API_KEY is unset -- byte-identical request handling until configured. app.use(createWorkerPostHogErrorMiddleware()); } /* v8 ignore stop */ diff --git a/src/api/worker-posthog.ts b/src/api/worker-posthog.ts index 55a9c39dc..edcb2be49 100644 --- a/src/api/worker-posthog.ts +++ b/src/api/worker-posthog.ts @@ -1,6 +1,6 @@ -// Hosted ORB Worker PostHog error tracking (#8288, epic #8286). Parallel-run alongside the existing -// @sentry/hono/cloudflare middleware in routes.ts -- both active simultaneously when configured; Sentry is -// only removed once the gated decommission issue (#8298) says so, exactly like #8287's self-host swap. +// Hosted ORB Worker PostHog error tracking (#8288, epic #8286). REPLACES the old @sentry/hono/cloudflare +// middleware entirely (2026-07-25 epic correction: full replacement, not a parallel-run) -- same treatment +// as #8287's self-host swap. // // Uses posthog-node's own documented captureException(), NOT a hand-built $exception payload. #8288 originally // scoped this as a raw-HTTP capture (mirroring JSONbored/metagraphed#7777's usage-telemetry.ts, which really @@ -33,14 +33,12 @@ const WORKER_ERROR_DISTINCT_ID = "loopover-worker"; let hasherLoaded = false; /** The env slice this module reads. Deliberately NAMED DIFFERENTLY from the dual-purpose POSTHOG_API_KEY/ - * POSTHOG_HOST (MCP telemetry #6228/#6235 + self-host error tracking #8287), for the same reason - * WORKER_SENTRY_DSN is named differently from self-host's SENTRY_DSN (see that var's doc comment): this - * Worker's fetch handler is the SAME one self-host's server.ts calls by synthesizing a Worker-shaped env from - * process.env, so reusing POSTHOG_API_KEY here would silently activate Worker-path exception capture inside a - * self-hoster's own Node process the moment they set POSTHOG_API_KEY for #8287's self-host sink -- an - * unrelated, unintended cross-wire. A second, independent reason on top of the Sentry precedent: keeping this - * separate from POSTHOG_API_KEY also lets an operator toggle Worker-level exception capture without touching - * MCP tool-call telemetry, or vice versa. Opt-in like every other Sentry/PostHog var in this codebase: a + * POSTHOG_HOST (MCP telemetry #6228/#6235 + self-host error tracking #8287): this Worker's fetch handler is + * the SAME one self-host's server.ts calls by synthesizing a Worker-shaped env from process.env, so reusing + * POSTHOG_API_KEY here would silently activate Worker-path exception capture inside a self-hoster's own Node + * process the moment they set POSTHOG_API_KEY for #8287's self-host sink -- an unrelated, unintended + * cross-wire. A second, independent reason: keeping this separate from POSTHOG_API_KEY also lets an operator + * toggle Worker-level exception capture without touching MCP tool-call telemetry, or vice versa. Opt-in: a * complete no-op until BOTH isCloudflareWorkerRuntime() AND this is set. Inject as a wrangler secret. */ export type WorkerPostHogEnv = Pick; @@ -142,9 +140,9 @@ function resolveExecutionCtx(c: Context): ExecutionContext { * * Scope note: this captures errors that propagate through the Hono middleware/handler chain specifically, not * every possible Workers-runtime failure mode (e.g. a crash reachable only via the raw fetch() export outside - * Hono's dispatch). Sentry's middleware (which additionally patches the Worker's fetch export via - * @sentry/cloudflare's withSentry) remains the more exhaustive safety net until the decommission gate -- - * consistent with #8288 being a parallel addition, not a byte-for-byte replacement. */ + * Hono's dispatch). Sentry's old middleware additionally patched the Worker's fetch export via + * @sentry/cloudflare's withSentry for that broader case -- this codebase has no PostHog equivalent, since every + * real route in this app is dispatched through Hono anyway. */ export function createWorkerPostHogErrorMiddleware() { return async (c: Context<{ Bindings: WorkerPostHogEnv }>, next: () => Promise): Promise => { if (!isWorkerPostHogConfigured(c.env)) { diff --git a/src/env.d.ts b/src/env.d.ts index bfb453172..a79e84143 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -164,26 +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; - /** Cloudflare Worker error tracking (PostHog, #8288) -- the parallel-run PostHog counterpart to - * WORKER_SENTRY_DSN above, both active simultaneously when configured until the gated decommission (#8298). - * Deliberately NAMED DIFFERENTLY from the dual-purpose POSTHOG_API_KEY/POSTHOG_HOST (MCP telemetry - * #6228/#6235 + self-host error tracking #8287) for the SAME cross-wire reason WORKER_SENTRY_DSN is named - * differently from self-host's SENTRY_DSN: self-host's server.ts calls this same exported worker.fetch by - * synthesizing a Worker-shaped env from process.env, so reusing POSTHOG_API_KEY here would silently - * activate Worker-path exception capture inside a self-hoster's own Node process the moment they set - * POSTHOG_API_KEY for #8287 -- an unrelated, unintended cross-wire. Also lets an operator toggle - * Worker-level exception capture independently of MCP tool-call telemetry. Opt-in like every other - * Sentry/PostHog var in this codebase: a complete no-op until BOTH isCloudflareWorkerRuntime() (routes.ts) - * AND this is set. See src/api/worker-posthog.ts. Inject as a wrangler secret. */ + /** Cloudflare Worker error tracking (PostHog, #8288). REPLACES the old @sentry/hono/cloudflare middleware + * entirely (2026-07-25 epic #8286 correction). Deliberately NAMED DIFFERENTLY from the dual-purpose + * POSTHOG_API_KEY/POSTHOG_HOST (MCP telemetry #6228/#6235 + self-host error tracking #8287): self-host's + * server.ts calls this same exported worker.fetch by synthesizing a Worker-shaped env from process.env, so + * reusing POSTHOG_API_KEY here would silently activate Worker-path exception capture inside a + * self-hoster's own Node process the moment they set POSTHOG_API_KEY for #8287 -- an unrelated, unintended + * cross-wire. Also lets an operator toggle Worker-level exception capture independently of MCP tool-call + * telemetry. Opt-in: a complete no-op until BOTH isCloudflareWorkerRuntime() (routes.ts) AND this is set. + * See src/api/worker-posthog.ts. Inject as a wrangler secret. */ WORKER_POSTHOG_API_KEY?: string; WORKER_POSTHOG_HOST?: string; WORKER_POSTHOG_ENVIRONMENT?: string; diff --git a/test/unit/cloudflare-worker-runtime-detection.test.ts b/test/unit/cloudflare-worker-runtime-detection.test.ts index 6a7393abe..845fd172d 100644 --- a/test/unit/cloudflare-worker-runtime-detection.test.ts +++ b/test/unit/cloudflare-worker-runtime-detection.test.ts @@ -1,14 +1,14 @@ import { describe, expect, it } from "vitest"; import { isCloudflareWorkerRuntime } from "../../src/api/routes"; -describe("isCloudflareWorkerRuntime (gates the Worker-only Sentry middleware in createApp)", () => { +describe("isCloudflareWorkerRuntime (gates the Worker-only PostHog error 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. + // the Workers-specific literal. If this ever started returning true under Node, the Cloudflare-only PostHog + // Worker capture would activate inside every self-hoster's own process the moment they set + // WORKER_POSTHOG_API_KEY 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 bef2cf62c..7501fff21 100644 --- a/test/workers/worker-runtime.test.ts +++ b/test/workers/worker-runtime.test.ts @@ -19,16 +19,11 @@ describe("worker runtime", () => { 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)", () => { + it("REGRESSION: isCloudflareWorkerRuntime() is true in a real Workers isolate (the gate that lets the PostHog error 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://loopover.test/health"), { WORKER_SENTRY_DSN: undefined } as unknown as Env, createExecutionContext()); - expect(res.status).toBe(200); - }); - it("still serves a normal response when WORKER_POSTHOG_API_KEY is unset -- the PostHog middleware (#8288) being registered must not itself break requests", async () => { const res = await worker.fetch(new Request("https://loopover.test/health"), { WORKER_POSTHOG_API_KEY: undefined } as unknown as Env, createExecutionContext()); expect(res.status).toBe(200);