authorgravatar for yordis.prieto@gmail.comYordis Prieto <yordis.prieto@gmail.com> 2026-09-18 13:52:38-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-18 10:52:38-07:00
logbd5dc58c65b87bfd2222e4697c4742b48e639ea4
treea2d246d140c8809c65e5dc6f43a62afdf17f92eb
parentccad9f69aae7465c4ad4ea169d273f61ffac1eb5
signature Signed by PGP key B5690EEEBB952194

fix(web): client spans reach the trace proxy again (#12332)

Signed-off-by: Yordis Prieto <yordis.prieto@gmail.com>

4 files changed, 64 insertions(+), 5 deletions(-)

apps/web/src/lib/runtime.test.ts created+28
...@@ -0,0 +1,28 @@
1import { describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as Tracer from "effect/Tracer";
4
5import * as ClientTracer from "../observability/clientTracer";
6import { runtime } from "./runtime";
7
8describe("web runtime", () => {
9 it("routes client spans to the exporter client tracing configured", async () => {
10 const exported: Array<string> = [];
11 ClientTracer.setDelegate(
12 Tracer.make({
13 span(options) {
14 exported.push(options.name);
15 return new Tracer.NativeSpan(options);
16 },
17 }),
18 );
19
20 try {
21 await runtime.runPromise(Effect.void.pipe(Effect.withSpan("client.work")));
22 } finally {
23 ClientTracer.setDelegate(null);
24 }
25
26 expect(exported).toEqual(["client.work"]);
27 });
28});
apps/web/src/lib/runtime.ts+3
...@@ -11,6 +11,7 @@ import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer";...@@ -11,6 +11,7 @@ import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer";
11import { browserCryptoLayer } from "../cloud/dpop";11import { browserCryptoLayer } from "../cloud/dpop";
12import { managedRelayClientLayer } from "../cloud/managedRelayLayer";12import { managedRelayClientLayer } from "../cloud/managedRelayLayer";
13import { resolveCloudPublicConfig, resolveRelayTracingConfig } from "../cloud/publicConfig";13import { resolveCloudPublicConfig, resolveRelayTracingConfig } from "../cloud/publicConfig";
14import * as ClientTracer from "../observability/clientTracer";
1415
15function configuredRelayUrl(): string {16function configuredRelayUrl(): string {
16 return resolveCloudPublicConfig().relayUrl ?? "http://relay.invalid";17 return resolveCloudPublicConfig().relayUrl ?? "http://relay.invalid";
...@@ -29,6 +30,7 @@ type RuntimeLayerSource =...@@ -29,6 +30,7 @@ type RuntimeLayerSource =
29 | typeof browserCryptoLayer30 | typeof browserCryptoLayer
30 | typeof Socket.layerWebSocketConstructorGlobal31 | typeof Socket.layerWebSocketConstructorGlobal
31 | typeof relayTracingLayer32 | typeof relayTracingLayer
33 | typeof ClientTracer.layer
32 | ReturnType<typeof managedRelayClientLayer>;34 | ReturnType<typeof managedRelayClientLayer>;
3335
34const primaryHttpRuntime = ManagedRuntime.make(36const primaryHttpRuntime = ManagedRuntime.make(
...@@ -56,6 +58,7 @@ const runtimeLayer = Layer.mergeAll(...@@ -56,6 +58,7 @@ const runtimeLayer = Layer.mergeAll(
56 httpClientLayer,58 httpClientLayer,
57 browserCryptoLayer,59 browserCryptoLayer,
58 Socket.layerWebSocketConstructorGlobal,60 Socket.layerWebSocketConstructorGlobal,
61 ClientTracer.layer,
59 relayTracingLayer,62 relayTracingLayer,
60 managedRelayClientLayer(configuredRelayUrl()).pipe(63 managedRelayClientLayer(configuredRelayUrl()).pipe(
61 Layer.provide(Layer.mergeAll(httpClientLayer, browserCryptoLayer)),64 Layer.provide(Layer.mergeAll(httpClientLayer, browserCryptoLayer)),
apps/web/src/observability/clientTracer.ts created+29
...@@ -0,0 +1,29 @@
1import * as Layer from "effect/Layer";
2import * as Tracer from "effect/Tracer";
3
4let delegate: Tracer.Tracer | null = null;
5
6/**
7 * Points the installed tracer at the exporter `configureClientTracing` just built,
8 * or at nothing while no exporter is configured.
9 */
10export function setDelegate(next: Tracer.Tracer | null): void {
11 delegate = next;
12}
13
14export function hasDelegate(): boolean {
15 return delegate !== null;
16}
17
18/**
19 * Installed once when the client runtime is built, before any exporter exists, so
20 * client spans keep flowing to whatever exporter is configured later on.
21 */
22export const layer = Layer.succeed(
23 Tracer.Tracer,
24 Tracer.make({
25 span(options) {
26 return delegate?.span(options) ?? new Tracer.NativeSpan(options);
27 },
28 }),
29);
apps/web/src/observability/clientTracing.ts+4-5
...@@ -2,13 +2,13 @@ import * as Exit from "effect/Exit";...@@ -2,13 +2,13 @@ import * as Exit from "effect/Exit";
2import * as Layer from "effect/Layer";2import * as Layer from "effect/Layer";
3import * as ManagedRuntime from "effect/ManagedRuntime";3import * as ManagedRuntime from "effect/ManagedRuntime";
4import * as Scope from "effect/Scope";4import * as Scope from "effect/Scope";
5import * as Tracer from "effect/Tracer";
6import { HttpClient } from "effect/unstable/http";5import { HttpClient } from "effect/unstable/http";
7import { OtlpExporter, OtlpSerialization, OtlpTracer } from "effect/unstable/observability";6import { OtlpExporter, OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
87
9import { settleAsyncResult, squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";8import { settleAsyncResult, squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
10import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors";9import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors";
11import { resolvePrimaryEnvironmentHttpUrl } from "../environments/primary";10import { resolvePrimaryEnvironmentHttpUrl } from "../environments/primary";
11import * as ClientTracer from "./clientTracer";
12import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer";12import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer";
13import { isElectron } from "../env";13import { isElectron } from "../env";
14import { APP_VERSION } from "~/branding";14import { APP_VERSION } from "~/branding";
...@@ -30,7 +30,6 @@ const delegateRuntimeLayer = Layer.mergeAll(...@@ -30,7 +30,6 @@ const delegateRuntimeLayer = Layer.mergeAll(
30 Layer.succeed(HttpClient.TracerDisabledWhen, () => true),30 Layer.succeed(HttpClient.TracerDisabledWhen, () => true),
31);31);
3232
33let activeDelegate: Tracer.Tracer | null = null;
34let activeRuntime: ManagedRuntime.ManagedRuntime<never, never> | null = null;33let activeRuntime: ManagedRuntime.ManagedRuntime<never, never> | null = null;
35let activeScope: Scope.Closeable | null = null;34let activeScope: Scope.Closeable | null = null;
36let activeConfigKey: string | null = null;35let activeConfigKey: string | null = null;
...@@ -54,7 +53,7 @@ async function applyClientTracingConfig(config: ClientTracingConfig): Promise<vo...@@ -54,7 +53,7 @@ async function applyClientTracingConfig(config: ClientTracingConfig): Promise<vo
54 const exportIntervalMs = Math.max(10, config.exportIntervalMs ?? DEFAULT_EXPORT_INTERVAL_MS);53 const exportIntervalMs = Math.max(10, config.exportIntervalMs ?? DEFAULT_EXPORT_INTERVAL_MS);
55 const nextConfigKey = `${otlpTracesUrl}|${exportIntervalMs}`;54 const nextConfigKey = `${otlpTracesUrl}|${exportIntervalMs}`;
5655
57 if (activeConfigKey === nextConfigKey && activeDelegate !== null) {56 if (activeConfigKey === nextConfigKey && ClientTracer.hasDelegate()) {
58 return;57 return;
59 }58 }
6059
...@@ -64,7 +63,7 @@ async function applyClientTracingConfig(config: ClientTracingConfig): Promise<vo...@@ -64,7 +63,7 @@ async function applyClientTracingConfig(config: ClientTracingConfig): Promise<vo
64 const previousRuntime = activeRuntime;63 const previousRuntime = activeRuntime;
65 const previousScope = activeScope;64 const previousScope = activeScope;
6665
67 activeDelegate = null;66 ClientTracer.setDelegate(null);
68 activeRuntime = null;67 activeRuntime = null;
69 activeScope = null;68 activeScope = null;
7069
...@@ -106,7 +105,7 @@ async function applyClientTracingConfig(config: ClientTracingConfig): Promise<vo...@@ -106,7 +105,7 @@ async function applyClientTracingConfig(config: ClientTracingConfig): Promise<vo
106 return;105 return;
107 }106 }
108107
109 activeDelegate = delegateResult.value;108 ClientTracer.setDelegate(delegateResult.value);
110 activeRuntime = runtime;109 activeRuntime = runtime;
111 activeScope = scope;110 activeScope = scope;
112}111}