History

Browser span export works in production: same-origin OTLP → Axiom (#981)

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).

Rhys Sullivan committed 5 months ago

Commit 2d865811 parent bf23a238 files · +181 −6Browse files

Unified · Split

8 files
  1. apps/cloud/src/env-augment.d.ts +2 −0
  2. apps/cloud/src/observability/browser-traces.test.ts +85 −0
  3. apps/cloud/src/observability/browser-traces.ts +52 −0
  4. apps/cloud/src/server.ts +6 −0
  5. apps/cloud/vite.config.ts +9 −1
  6. apps/cloud/wrangler.jsonc +7 −0
  7. bun.lock +9 −3
  8. packages/react/src/api/client.tsx +11 −2
apps/cloud/src/env-augment.d.ts+2−0
@@ -48,6 +48,8 @@ declare global { · 47 unmodified lines
48 48
49 // Shared with frontend49 // 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}
apps/cloud/src/observability/browser-traces.test.ts+85−0
@@ -0,0 +1,85 @@
1import { describe, expect, it } from "@effect/vitest";
2
3import { browserTracesResponse } from "./browser-traces";
4
5const makeRequest = (init?: RequestInit & { path?: string }) =>
6 new Request(`https://executor.sh${init?.path ?? "/v1/traces"}`, {
7 method: "POST",
8 body: "{}",
9 ...init,
10 });
11
12const baseEnv = {
13 AXIOM_TOKEN: "axiom-secret",
14 AXIOM_DATASET: "executor-cloud",
15} as Env;
16
17describe("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});
apps/cloud/src/observability/browser-traces.ts+52−0
@@ -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
12const MAX_BODY_BYTES = 2_000_000;
13
14export const BROWSER_TRACES_PATH = "/v1/traces";
15
16export 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};
apps/cloud/src/server.ts+6−0
@@ -10,6 +10,7 @@ import * as Sentry from "@sentry/cloudflare"; · 9 unmodified lines
10import handler from "@tanstack/react-start/server-entry";10import handler from "@tanstack/react-start/server-entry";
11 11
12import { McpSessionDO as McpSessionDOBase } from "./mcp/session-durable-object";12import { McpSessionDO as McpSessionDOBase } from "./mcp/session-durable-object";
13import { browserTracesResponse } from "./observability/browser-traces";
13import { flushTracerProvider, installTracerProvider } from "./observability/telemetry";14import { 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
69const cloudflareHandler: ExportedHandler<Env> = {70const 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 }
apps/cloud/vite.config.ts+9−1
@@ -49,13 +49,21 @@ const loadWranglerPublicVars = () => { · 48 unmodified lines
49// proxy isn't routed anyway.49// proxy isn't routed anyway.
50const ANALYTICS_PATH = process.env.VITE_PUBLIC_ANALYTICS_PATH ?? "a";50const ANALYTICS_PATH = process.env.VITE_PUBLIC_ANALYTICS_PATH ?? "a";
51 51
52export default defineConfig(({ mode }) => {52export 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(
apps/cloud/wrangler.jsonc+7−0
@@ -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}
bun.lock+9−3
@@ -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=="],
packages/react/src/api/client.tsx+11−2
@@ -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 would42// `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.
44const otlpTracesUrl = import.meta.env.VITE_PUBLIC_OTLP_TRACES_URL as string | undefined;44const 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.
47const 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 the52// 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 posting63// error. URL-scoped, the leak is the desired behavior: any client posting
59// to the OTLP endpoint (the exporter) goes untraced, everything else is64// to the OTLP endpoint (the exporter) goes untraced, everything else is
60// traced.65// traced.
61if (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).
68if (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 spans76 // Browser sessions are short; the 5s default loses the tail spans
68 // when the tab closes.77 // when the tab closes.