The web client's OTLP exporter now ships in the production build: wrangler.jsonc bakes VITE_PUBLIC_OTLP_TRACES_URL=/v1/traces, the client resolves it against the page origin, and a new worker route (observability/browser-traces.ts) forwards the batch to Axiom with the server-held token — the browser never sees AXIOM_TOKEN. The route runs before the worker's server span so exporter traffic doesn't trace itself, requires a session cookie to be present, caps body size, and never reflects Axiom's response to the caller. Client hardening for prod: browser-only install (the module is also evaluated during SSR), relative exporter URLs resolved against the page origin, and per-SESSION sampling via VITE_PUBLIC_OTLP_SAMPLE_RATIO (a session traces everything or nothing — per-span sampling would shred waterfalls). vite dev strips the baked endpoint unless a local motel is actually listening, so plain bun dev doesn't post spans into a dead proxy. e2e keeps working unchanged (verified: 27 browser entries harvested on a fresh run).
Unified · Split
| @@ -48,6 +48,8 @@ declare global { · 47 unmodified lines | |||
| 48 | 48 | ||
| 49 | // Shared with frontend | 49 | // Shared with frontend |
| 50 | VITE_PUBLIC_SITE_URL?: string; | 50 | VITE_PUBLIC_SITE_URL?: string; |
| 51 | VITE_PUBLIC_OTLP_TRACES_URL?: string; | ||
| 52 | VITE_PUBLIC_OTLP_SAMPLE_RATIO?: string; | ||
| 51 | } | 53 | } |
| 52 | } | 54 | } |
| 53 | } | 55 | } |
| @@ -0,0 +1,85 @@ | |||
| 1 | import { describe, expect, it } from "@effect/vitest"; | ||
| 2 | |||
| 3 | import { browserTracesResponse } from "./browser-traces"; | ||
| 4 | |||
| 5 | const makeRequest = (init?: RequestInit & { path?: string }) => | ||
| 6 | new Request(`https://executor.sh${init?.path ?? "/v1/traces"}`, { | ||
| 7 | method: "POST", | ||
| 8 | body: "{}", | ||
| 9 | ...init, | ||
| 10 | }); | ||
| 11 | |||
| 12 | const baseEnv = { | ||
| 13 | AXIOM_TOKEN: "axiom-secret", | ||
| 14 | AXIOM_DATASET: "executor-cloud", | ||
| 15 | } as Env; | ||
| 16 | |||
| 17 | describe("browserTracesResponse", () => { | ||
| 18 | it("ignores non-/v1/traces requests entirely", () => { | ||
| 19 | expect(browserTracesResponse(makeRequest({ path: "/api/tools" }), baseEnv)).toBeNull(); | ||
| 20 | }); | ||
| 21 | |||
| 22 | it("drops batches silently when Axiom is not configured", async () => { | ||
| 23 | const response = await browserTracesResponse( | ||
| 24 | makeRequest({ headers: { cookie: "wos-session=abc" } }), | ||
| 25 | {} as Env, | ||
| 26 | ); | ||
| 27 | expect(response?.status).toBe(204); | ||
| 28 | }); | ||
| 29 | |||
| 30 | it("rejects anonymous posts", async () => { | ||
| 31 | const response = await browserTracesResponse(makeRequest(), baseEnv); | ||
| 32 | expect(response?.status).toBe(401); | ||
| 33 | }); | ||
| 34 | |||
| 35 | it("forwards to Axiom with server-held credentials and hides the upstream body", async () => { | ||
| 36 | let seen: { url: string; auth: string | null; dataset: string | null } | undefined; | ||
| 37 | const response = await browserTracesResponse( | ||
| 38 | makeRequest({ headers: { cookie: "wos-session=abc" } }), | ||
| 39 | baseEnv, | ||
| 40 | (async (url: RequestInfo | URL, init?: RequestInit) => { | ||
| 41 | seen = { | ||
| 42 | url: String(url), | ||
| 43 | auth: new Headers(init?.headers).get("authorization"), | ||
| 44 | dataset: new Headers(init?.headers).get("x-axiom-dataset"), | ||
| 45 | }; | ||
| 46 | return new Response("axiom-internals", { status: 200 }); | ||
| 47 | }) as typeof fetch, | ||
| 48 | ); | ||
| 49 | expect(seen?.url).toBe("https://api.axiom.co/v1/traces"); | ||
| 50 | expect(seen?.auth).toBe("Bearer axiom-secret"); | ||
| 51 | expect(seen?.dataset).toBe("executor-cloud"); | ||
| 52 | expect(response?.status).toBe(204); | ||
| 53 | expect(await response?.text()).toBe(""); | ||
| 54 | }); | ||
| 55 | |||
| 56 | it("reports upstream failure as 502 without leaking detail", async () => { | ||
| 57 | const response = await browserTracesResponse( | ||
| 58 | makeRequest({ headers: { cookie: "wos-session=abc" } }), | ||
| 59 | baseEnv, | ||
| 60 | (async () => new Response("denied", { status: 403 })) as typeof fetch, | ||
| 61 | ); | ||
| 62 | expect(response?.status).toBe(502); | ||
| 63 | }); | ||
| 64 | |||
| 65 | it("refuses oversized batches", async () => { | ||
| 66 | const response = await browserTracesResponse( | ||
| 67 | makeRequest({ | ||
| 68 | headers: { | ||
| 69 | cookie: "wos-session=abc", | ||
| 70 | "content-length": String(3_000_000), | ||
| 71 | }, | ||
| 72 | }), | ||
| 73 | baseEnv, | ||
| 74 | ); | ||
| 75 | expect(response?.status).toBe(413); | ||
| 76 | }); | ||
| 77 | |||
| 78 | it("only accepts POST", async () => { | ||
| 79 | const response = await browserTracesResponse( | ||
| 80 | new Request("https://executor.sh/v1/traces", { method: "GET" }), | ||
| 81 | baseEnv, | ||
| 82 | ); | ||
| 83 | expect(response?.status).toBe(405); | ||
| 84 | }); | ||
| 85 | }); | ||
| @@ -0,0 +1,52 @@ | |||
| 1 | // Browser → Axiom OTLP ingress. The web client exports its spans to | ||
| 2 | // same-origin /v1/traces (it can never hold AXIOM_TOKEN); this worker route | ||
| 3 | // forwards the batch to Axiom with the server-held credentials. Locally and | ||
| 4 | // in e2e the vite dev server proxies the same path to motel before the | ||
| 5 | // worker ever sees it, so this route only serves deployed workers. | ||
| 6 | // | ||
| 7 | // Guards, not auth: the endpoint is write-only into a server-pinned dataset, | ||
| 8 | // but it should not be an anonymous internet ingest — a session cookie must | ||
| 9 | // at least be present, and bodies are capped. We deliberately do NOT verify | ||
| 10 | // the session (that would put a WorkOS round-trip on every span batch). | ||
| 11 | |||
| 12 | const MAX_BODY_BYTES = 2_000_000; | ||
| 13 | |||
| 14 | export const BROWSER_TRACES_PATH = "/v1/traces"; | ||
| 15 | |||
| 16 | export const browserTracesResponse = ( | ||
| 17 | request: Request, | ||
| 18 | env: Env, | ||
| 19 | fetchImpl: typeof fetch = fetch, | ||
| 20 | ): Promise<Response> | null => { | ||
| 21 | const url = new URL(request.url); | ||
| 22 | if (url.pathname !== BROWSER_TRACES_PATH) return null; | ||
| 23 | if (request.method !== "POST") { | ||
| 24 | return Promise.resolve(new Response(null, { status: 405 })); | ||
| 25 | } | ||
| 26 | // Tracing not configured on this deployment — accept and drop so the | ||
| 27 | // client exporter stays quiet (it would retry/log on errors). | ||
| 28 | if (!env.AXIOM_TOKEN) { | ||
| 29 | return Promise.resolve(new Response(null, { status: 204 })); | ||
| 30 | } | ||
| 31 | if (!(request.headers.get("cookie") ?? "").includes("wos-session=")) { | ||
| 32 | return Promise.resolve(new Response(null, { status: 401 })); | ||
| 33 | } | ||
| 34 | const contentLength = Number(request.headers.get("content-length") ?? 0); | ||
| 35 | if (contentLength > MAX_BODY_BYTES) { | ||
| 36 | return Promise.resolve(new Response(null, { status: 413 })); | ||
| 37 | } | ||
| 38 | return fetchImpl(env.AXIOM_TRACES_URL ?? "https://api.axiom.co/v1/traces", { | ||
| 39 | method: "POST", | ||
| 40 | headers: { | ||
| 41 | "content-type": request.headers.get("content-type") ?? "application/json", | ||
| 42 | authorization: `Bearer ${env.AXIOM_TOKEN}`, | ||
| 43 | "x-axiom-dataset": env.AXIOM_DATASET ?? "executor-cloud", | ||
| 44 | }, | ||
| 45 | body: request.body, | ||
| 46 | }).then( | ||
| 47 | // The exporter only needs success/failure; never reflect Axiom's | ||
| 48 | // response body (or its headers) back to an unauthenticated caller. | ||
| 49 | (upstream) => new Response(null, { status: upstream.ok ? 204 : 502 }), | ||
| 50 | () => new Response(null, { status: 502 }), | ||
| 51 | ); | ||
| 52 | }; | ||
| @@ -10,6 +10,7 @@ import * as Sentry from "@sentry/cloudflare"; · 9 unmodified lines | |||
| 10 | import handler from "@tanstack/react-start/server-entry"; | 10 | import handler from "@tanstack/react-start/server-entry"; |
| 11 | 11 | ||
| 12 | import { McpSessionDO as McpSessionDOBase } from "./mcp/session-durable-object"; | 12 | import { McpSessionDO as McpSessionDOBase } from "./mcp/session-durable-object"; |
| 13 | import { browserTracesResponse } from "./observability/browser-traces"; | ||
| 13 | import { flushTracerProvider, installTracerProvider } from "./observability/telemetry"; | 14 | import { flushTracerProvider, installTracerProvider } from "./observability/telemetry"; |
| 14 | 15 | ||
| 15 | // --------------------------------------------------------------------------- | 16 | // --------------------------------------------------------------------------- |
| @@ -68,6 +69,11 @@ const tracer = trace.getTracer("executor-cloud-worker"); · 52 unmodified lines | |||
| 68 | 69 | ||
| 69 | const cloudflareHandler: ExportedHandler<Env> = { | 70 | const cloudflareHandler: ExportedHandler<Env> = { |
| 70 | fetch: async (request, env, ctx) => { | 71 | fetch: async (request, env, ctx) => { |
| 72 | // Browser OTLP ingress — before the server span opens: exporter traffic | ||
| 73 | // must never trace itself (the browser already excludes /v1/traces from | ||
| 74 | // its own tracing for the same reason). | ||
| 75 | const browserTraces = browserTracesResponse(request, env); | ||
| 76 | if (browserTraces) return browserTraces; | ||
| 71 | if (!installTracerProvider()) { | 77 | if (!installTracerProvider()) { |
| 72 | return fetchHandler(request, env, ctx); | 78 | return fetchHandler(request, env, ctx); |
| 73 | } | 79 | } |
| @@ -49,13 +49,21 @@ const loadWranglerPublicVars = () => { · 48 unmodified lines | |||
| 49 | // proxy isn't routed anyway. | 49 | // proxy isn't routed anyway. |
| 50 | const ANALYTICS_PATH = process.env.VITE_PUBLIC_ANALYTICS_PATH ?? "a"; | 50 | const ANALYTICS_PATH = process.env.VITE_PUBLIC_ANALYTICS_PATH ?? "a"; |
| 51 | 51 | ||
| 52 | export default defineConfig(({ mode }) => { | 52 | export default defineConfig(({ command, mode }) => { |
| 53 | const env = loadEnv(mode, process.cwd(), ""); | 53 | const env = loadEnv(mode, process.cwd(), ""); |
| 54 | const publicEnv = { | 54 | const publicEnv = { |
| 55 | ...loadWranglerPublicVars(), | 55 | ...loadWranglerPublicVars(), |
| 56 | VITE_PUBLIC_ANALYTICS_PATH: ANALYTICS_PATH, | 56 | VITE_PUBLIC_ANALYTICS_PATH: ANALYTICS_PATH, |
| 57 | ...env, | 57 | ...env, |
| 58 | }; | 58 | }; |
| 59 | // The wrangler-declared OTLP endpoint is for DEPLOYED workers (the | ||
| 60 | // /v1/traces forwarding route). Under `vite dev` that path is only the | ||
| 61 | // proxy below — keep the exporter off unless something actually listens | ||
| 62 | // (e2e/dev sets MOTEL_URL or the env var itself), or every dev session | ||
| 63 | // posts spans into a dead proxy once a second. | ||
| 64 | if (command === "serve" && !process.env.MOTEL_URL && !env.VITE_PUBLIC_OTLP_TRACES_URL) { | ||
| 65 | delete (publicEnv as Record<string, string | undefined>).VITE_PUBLIC_OTLP_TRACES_URL; | ||
| 66 | } | ||
| 59 | 67 | ||
| 60 | return { | 68 | return { |
| 61 | define: Object.fromEntries( | 69 | define: Object.fromEntries( |
| @@ -62,5 +62,12 @@ · 61 unmodified lines | |||
| 62 | "vars": { | 62 | "vars": { |
| 63 | "VITE_PUBLIC_SITE_URL": "https://executor.sh", | 63 | "VITE_PUBLIC_SITE_URL": "https://executor.sh", |
| 64 | "VITE_PUBLIC_POSTHOG_KEY": "phc_nNLrNMALpRsfrEkZovUkfMxYbcJvHnsJHeoSPavprgLL", | 64 | "VITE_PUBLIC_POSTHOG_KEY": "phc_nNLrNMALpRsfrEkZovUkfMxYbcJvHnsJHeoSPavprgLL", |
| 65 | // Browser OTLP spans → same-origin, forwarded to Axiom by the worker | ||
| 66 | // (src/observability/browser-traces.ts). Relative on purpose: the | ||
| 67 | // client resolves it against the page origin, so previews work too. | ||
| 68 | "VITE_PUBLIC_OTLP_TRACES_URL": "/v1/traces", | ||
| 69 | // Per-session sampling: 1 = every signed-in session exports. Dial down | ||
| 70 | // (e.g. "0.1") if Axiom volume becomes a cost concern. | ||
| 71 | "VITE_PUBLIC_OTLP_SAMPLE_RATIO": "1", | ||
| 65 | }, | 72 | }, |
| 66 | } | 73 | } |
| @@ -3978,7 +3978,7 @@ · 3977 unmodified lines | |||
| 3978 | 3978 | ||
| 3979 | "jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="], | 3979 | "jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="], |
| 3980 | 3980 | ||
| 3981 | "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], | 3981 | "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], |
| 3982 | 3982 | ||
| 3983 | "jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], | 3983 | "jose": ["jose@5.10.0", "", {}, "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg=="], |
| 3984 | 3984 | ||
| @@ -5788,6 +5788,8 @@ · 1803 unmodified lines | |||
| 5788 | 5788 | ||
| 5789 | "@sentry/react/@sentry/core": ["@sentry/core@10.48.0", "", {}, "sha512-h8F+fXVwYC9ro5ZaO8V+v3vqc0awlXHGblEAuVxSGgh4IV/oFX+QVzXeDTTrFOFS6v/Vn5vAyu240eJrJAS6/g=="], | 5789 | "@sentry/react/@sentry/core": ["@sentry/core@10.48.0", "", {}, "sha512-h8F+fXVwYC9ro5ZaO8V+v3vqc0awlXHGblEAuVxSGgh4IV/oFX+QVzXeDTTrFOFS6v/Vn5vAyu240eJrJAS6/g=="], |
| 5790 | 5790 | ||
| 5791 | "@tailwindcss/node/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], | ||
| 5792 | |||
| 5791 | "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], | 5793 | "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.9.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" }, "bundled": true }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], |
| 5792 | 5794 | ||
| 5793 | "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], | 5795 | "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.9.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], |
| @@ -5858,6 +5860,8 @@ · 64 unmodified lines | |||
| 5858 | 5860 | ||
| 5859 | "app-builder-lib/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], | 5861 | "app-builder-lib/fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], |
| 5860 | 5862 | ||
| 5863 | "app-builder-lib/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], | ||
| 5864 | |||
| 5861 | "app-builder-lib/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], | 5865 | "app-builder-lib/which": ["which@5.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ=="], |
| 5862 | 5866 | ||
| 5863 | "astro/@clack/prompts": ["@clack/prompts@1.2.0", "", { "dependencies": { "@clack/core": "1.2.0", "fast-string-width": "^1.1.0", "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w=="], | 5867 | "astro/@clack/prompts": ["@clack/prompts@1.2.0", "", { "dependencies": { "@clack/core": "1.2.0", "fast-string-width": "^1.1.0", "fast-wrap-ansi": "^0.1.3", "sisteransi": "^1.0.5" } }, "sha512-4jmztR9fMqPMjz6H/UZXj0zEmE43ha1euENwkckKKel4XpSfokExPo5AiVStdHSAlHekz4d0CA/r45Ok1E4D3w=="], |
| @@ -5870,6 +5874,8 @@ · 6 unmodified lines | |||
| 5870 | 5874 | ||
| 5871 | "atmn/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], | 5875 | "atmn/dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], |
| 5872 | 5876 | ||
| 5877 | "atmn/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], | ||
| 5878 | |||
| 5873 | "better-auth/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], | 5879 | "better-auth/jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], |
| 5874 | 5880 | ||
| 5875 | "better-call/rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], | 5881 | "better-call/rou3": ["rou3@0.7.12", "", {}, "sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg=="], |
| @@ -5990,6 +5996,8 @@ · 114 unmodified lines | |||
| 5990 | 5996 | ||
| 5991 | "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], | 5997 | "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], |
| 5992 | 5998 | ||
| 5999 | "knip/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], | ||
| 6000 | |||
| 5993 | "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], | 6001 | "libsql/detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], |
| 5994 | 6002 | ||
| 5995 | "matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], | 6003 | "matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], |
| @@ -6432,8 +6440,6 @@ · 436 unmodified lines | |||
| 6432 | 6440 | ||
| 6433 | "@tanstack/router-cli/@tanstack/router-generator/@tanstack/router-utils": ["@tanstack/router-utils@1.162.2", "", { "dependencies": { "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-hTWqJtqIFFdvuCl8WXNyrodp2L9zo2G37xKRrcVmVRWpAB2h+U1LuRAfS4tsFTiWOIoE/B+WDVFB8JpoEdw6jQ=="], | 6441 | "@tanstack/router-cli/@tanstack/router-generator/@tanstack/router-utils": ["@tanstack/router-utils@1.162.2", "", { "dependencies": { "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-hTWqJtqIFFdvuCl8WXNyrodp2L9zo2G37xKRrcVmVRWpAB2h+U1LuRAfS4tsFTiWOIoE/B+WDVFB8JpoEdw6jQ=="], |
| 6434 | 6442 | ||
| 6435 | "@tanstack/router-cli/@tanstack/router-generator/jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], | ||
| 6436 | |||
| 6437 | "@tanstack/router-cli/@tanstack/router-generator/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], | 6443 | "@tanstack/router-cli/@tanstack/router-generator/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], |
| 6438 | 6444 | ||
| 6439 | "@tanstack/router-cli/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], | 6445 | "@tanstack/router-cli/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], |
| @@ -42,6 +42,11 @@ export const reportApiClientInfrastructureCause = (cause: Cause.Cause<unknown>) · 41 unmodified lines | |||
| 42 | // `import.meta.env.VITE_PUBLIC_OTLP_TRACES_URL`; optional chaining would | 42 | // `import.meta.env.VITE_PUBLIC_OTLP_TRACES_URL`; optional chaining would |
| 43 | // dodge the replacement and always read undefined. | 43 | // dodge the replacement and always read undefined. |
| 44 | const otlpTracesUrl = import.meta.env.VITE_PUBLIC_OTLP_TRACES_URL as string | undefined; | 44 | const otlpTracesUrl = import.meta.env.VITE_PUBLIC_OTLP_TRACES_URL as string | undefined; |
| 45 | // Per-SESSION sampling for production: a page either traces everything it | ||
| 46 | // does or nothing (per-span sampling would shred the waterfalls). Unset = 1. | ||
| 47 | const otlpSampleRatio = Number( | ||
| 48 | (import.meta.env.VITE_PUBLIC_OTLP_SAMPLE_RATIO as string | undefined) ?? "1", | ||
| 49 | ); | ||
| 45 | 50 | ||
| 46 | // The tracer must reach the runtime context the atom effects EXECUTE in. | 51 | // The tracer must reach the runtime context the atom effects EXECUTE in. |
| 47 | // Merging it into the `httpClient` option doesn't: AtomHttpApi builds the | 52 | // Merging it into the `httpClient` option doesn't: AtomHttpApi builds the |
| @@ -58,11 +63,15 @@ const otlpTracesUrl = import.meta.env.VITE_PUBLIC_OTLP_TRACES_URL as string | un · 10 unmodified lines | |||
| 58 | // error. URL-scoped, the leak is the desired behavior: any client posting | 63 | // error. URL-scoped, the leak is the desired behavior: any client posting |
| 59 | // to the OTLP endpoint (the exporter) goes untraced, everything else is | 64 | // to the OTLP endpoint (the exporter) goes untraced, everything else is |
| 60 | // traced. | 65 | // traced. |
| 61 | if (otlpTracesUrl) { | 66 | // Browser-only (this module is also evaluated during SSR, where the worker |
| 67 | // has its own tracer and a relative exporter URL would be meaningless). | ||
| 68 | if (otlpTracesUrl && typeof document !== "undefined" && Math.random() < otlpSampleRatio) { | ||
| 62 | Atom.runtime.addGlobalLayer( | 69 | Atom.runtime.addGlobalLayer( |
| 63 | Layer.mergeAll( | 70 | Layer.mergeAll( |
| 64 | OtlpTracer.layer({ | 71 | OtlpTracer.layer({ |
| 65 | url: otlpTracesUrl, | 72 | // Relative paths (the prod shape: "/v1/traces" → the worker's |
| 73 | // forwarding route) resolve against the page's own origin. | ||
| 74 | url: new URL(otlpTracesUrl, window.location.origin).toString(), | ||
| 66 | resource: { serviceName: "executor-web" }, | 75 | resource: { serviceName: "executor-web" }, |
| 67 | // Browser sessions are short; the 5s default loses the tail spans | 76 | // Browser sessions are short; the 5s default loses the tail spans |
| 68 | // when the tab closes. | 77 | // when the tab closes. |