import { Exit, Layer, ManagedRuntime, Scope, Tracer } from "effect"; import { FetchHttpClient, HttpClient } from "effect/unstable/http"; import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability"; import { resolvePrimaryEnvironmentHttpUrl } from "../environments/primary"; import { isElectron } from "../env"; import { APP_VERSION } from "~/branding"; const DEFAULT_EXPORT_INTERVAL_MS = 1_000; const CLIENT_TRACING_RESOURCE = { serviceName: "t3-web", attributes: { "service.runtime": "t3-web", "service.mode": isElectron ? "electron" : "browser", "service.version": APP_VERSION, }, } as const; const delegateRuntimeLayer = Layer.mergeAll( FetchHttpClient.layer, OtlpSerialization.layerJson, Layer.succeed(HttpClient.TracerDisabledWhen, () => true), ); let activeDelegate: Tracer.Tracer | null = null; let activeRuntime: ManagedRuntime.ManagedRuntime | null = null; let activeScope: Scope.Closeable | null = null; let activeConfigKey: string | null = null; let configurationGeneration = 0; let pendingConfiguration = Promise.resolve(); export interface ClientTracingConfig { readonly exportIntervalMs?: number; } export const ClientTracingLive = Layer.succeed( Tracer.Tracer, Tracer.make({ span(options) { return activeDelegate?.span(options) ?? new Tracer.NativeSpan(options); }, }), ); export function configureClientTracing(config: ClientTracingConfig = {}): Promise { if (config.exportIntervalMs === undefined && activeConfigKey !== null) { return pendingConfiguration; } pendingConfiguration = pendingConfiguration.finally(() => applyClientTracingConfig(config)); return pendingConfiguration; } async function applyClientTracingConfig(config: ClientTracingConfig): Promise { const otlpTracesUrl = resolvePrimaryEnvironmentHttpUrl("/api/observability/v1/traces"); const exportIntervalMs = Math.max(10, config.exportIntervalMs ?? DEFAULT_EXPORT_INTERVAL_MS); const nextConfigKey = `${otlpTracesUrl}|${exportIntervalMs}`; if (activeConfigKey === nextConfigKey && activeDelegate !== null) { return; } activeConfigKey = nextConfigKey; const generation = ++configurationGeneration; const previousRuntime = activeRuntime; const previousScope = activeScope; activeDelegate = null; activeRuntime = null; activeScope = null; await disposeTracerRuntime(previousRuntime, previousScope); const runtime = ManagedRuntime.make(delegateRuntimeLayer); const scope = runtime.runSync(Scope.make()); try { const delegate = await runtime.runPromise( Scope.provide(scope)( OtlpTracer.make({ url: otlpTracesUrl, exportInterval: `${exportIntervalMs} millis`, resource: CLIENT_TRACING_RESOURCE, }), ), ); if (generation !== configurationGeneration) { await disposeTracerRuntime(runtime, scope); return; } activeDelegate = delegate; activeRuntime = runtime; activeScope = scope; } catch (error) { await disposeTracerRuntime(runtime, scope); if (generation === configurationGeneration) { console.warn("Failed to configure client tracing exporter", { error: formatError(error), otlpTracesUrl, }); } } } async function disposeTracerRuntime( runtime: ManagedRuntime.ManagedRuntime | null, scope: Scope.Closeable | null, ): Promise { if (runtime === null || scope === null) { return; } await runtime .runPromise(Scope.close(scope, Exit.void)) .catch(() => undefined) .finally(() => { runtime.dispose(); }); } function formatError(error: unknown): string { if (error instanceof Error && error.message.trim().length > 0) { return error.message; } return String(error); } export async function __resetClientTracingForTests() { configurationGeneration++; activeConfigKey = null; activeDelegate = null; pendingConfiguration = Promise.resolve(); const runtime = activeRuntime; const scope = activeScope; activeRuntime = null; activeScope = null; await disposeTracerRuntime(runtime, scope); }