11import * as Atom from "effect/unstable/reactivity/Atom" ;
22import * as AtomHttpApi from "effect/unstable/reactivity/AtomHttpApi" ;
3- import { FetchHttpClient , HttpClient , HttpClientRequest } from "effect/unstable/http" ;
3+ import {
4+ FetchHttpClient ,
5+ HttpClient ,
6+ HttpClientRequest ,
7+ } from "effect/unstable/http" ;
48import * as HttpClientError from "effect/unstable/http/HttpClientError" ;
59import { OtlpSerialization , OtlpTracer } from "effect/unstable/observability" ;
610import { ExecutorApi } from "@executor-js/api/client" ;
@@ -11,15 +15,21 @@ import * as Option from "effect/Option";
1115import * as Schema from "effect/Schema" ;
1216
1317import { reportHandledFrontendError } from "./error-reporting" ;
14- import { getExecutorApiBaseUrl , getExecutorServerAuthorizationHeader } from "./server-connection" ;
18+ import {
19+ getExecutorApiBaseUrl ,
20+ getExecutorServerAuthorizationHeader ,
21+ } from "./server-connection" ;
1522
1623const isApiClientInfrastructureCause = ( cause : Cause . Cause < unknown > ) : boolean =>
1724 Option . match ( Cause . findErrorOption ( cause ) , {
1825 onNone : ( ) => false ,
19- onSome : ( error ) => Schema . isSchemaError ( error ) || HttpClientError . isHttpClientError ( error ) ,
26+ onSome : ( error ) =>
27+ Schema . isSchemaError ( error ) || HttpClientError . isHttpClientError ( error ) ,
2028 } ) ;
2129
22- export const reportApiClientInfrastructureCause = ( cause : Cause . Cause < unknown > ) =>
30+ export const reportApiClientInfrastructureCause = (
31+ cause : Cause . Cause < unknown > ,
32+ ) =>
2333 Effect . sync ( ( ) => {
2434 if ( ! isApiClientInfrastructureCause ( cause ) ) return ;
2535 reportHandledFrontendError ( cause , {
@@ -41,7 +51,14 @@ export const reportApiClientInfrastructureCause = (cause: Cause.Cause<unknown>)
4151// Plain member access — vite `define` rewrites the exact expression
4252// `import.meta.env.VITE_PUBLIC_OTLP_TRACES_URL`; optional chaining would
4353// dodge the replacement and always read undefined.
44- const otlpTracesUrl = import . meta. env . VITE_PUBLIC_OTLP_TRACES_URL as string | undefined ;
54+ const otlpTracesUrl = import . meta. env . VITE_PUBLIC_OTLP_TRACES_URL as
55+ | string
56+ | undefined ;
57+ // Per-SESSION sampling for production: a page either traces everything it
58+ // does or nothing (per-span sampling would shred the waterfalls). Unset = 1.
59+ const otlpSampleRatio = Number (
60+ ( import . meta. env . VITE_PUBLIC_OTLP_SAMPLE_RATIO as string | undefined ) ?? "1" ,
61+ ) ;
4562
4663// The tracer must reach the runtime context the atom effects EXECUTE in.
4764// Merging it into the `httpClient` option doesn't: AtomHttpApi builds the
@@ -58,17 +75,30 @@ const otlpTracesUrl = import.meta.env.VITE_PUBLIC_OTLP_TRACES_URL as string | un
5875// error. URL-scoped, the leak is the desired behavior: any client posting
5976// to the OTLP endpoint (the exporter) goes untraced, everything else is
6077// traced.
61- if ( otlpTracesUrl ) {
78+ // Browser-only (this module is also evaluated during SSR, where the worker
79+ // has its own tracer and a relative exporter URL would be meaningless).
80+ if (
81+ otlpTracesUrl &&
82+ typeof document !== "undefined" &&
83+ Math . random ( ) < otlpSampleRatio
84+ ) {
6285 Atom . runtime . addGlobalLayer (
6386 Layer . mergeAll (
6487 OtlpTracer . layer ( {
65- url : otlpTracesUrl ,
88+ // Relative paths (the prod shape: "/v1/traces" → the worker's
89+ // forwarding route) resolve against the page's own origin.
90+ url : new URL ( otlpTracesUrl , window . location . origin ) . toString ( ) ,
6691 resource : { serviceName : "executor-web" } ,
6792 // Browser sessions are short; the 5s default loses the tail spans
6893 // when the tab closes.
6994 exportInterval : "1 second" ,
70- } ) . pipe ( Layer . provide ( OtlpSerialization . layerJson ) , Layer . provide ( FetchHttpClient . layer ) ) ,
71- Layer . succeed ( HttpClient . TracerDisabledWhen , ( request ) => request . url . includes ( "/v1/traces" ) ) ,
95+ } ) . pipe (
96+ Layer . provide ( OtlpSerialization . layerJson ) ,
97+ Layer . provide ( FetchHttpClient . layer ) ,
98+ ) ,
99+ Layer . succeed ( HttpClient . TracerDisabledWhen , ( request ) =>
100+ request . url . includes ( "/v1/traces" ) ,
101+ ) ,
72102 ) ,
73103 ) ;
74104}
@@ -77,18 +107,26 @@ if (otlpTracesUrl) {
77107// Core API client — tools + secrets
78108// ---------------------------------------------------------------------------
79109
80- const ExecutorApiClient = AtomHttpApi . Service < "ExecutorApiClient" > ( ) ( "ExecutorApiClient" , {
81- api : ExecutorApi ,
82- httpClient : FetchHttpClient . layer ,
83- transformClient : HttpClient . mapRequest ( ( request ) => {
84- let next = HttpClientRequest . prependUrl ( request , getExecutorApiBaseUrl ( ) ) ;
85- const authorization = getExecutorServerAuthorizationHeader ( ) ;
86- if ( authorization ) {
87- next = HttpClientRequest . setHeader ( next , "authorization" , authorization ) ;
88- }
89- return next ;
90- } ) ,
91- transformResponse : ( effect ) => Effect . tapCause ( effect , reportApiClientInfrastructureCause ) ,
92- } ) ;
110+ const ExecutorApiClient = AtomHttpApi . Service < "ExecutorApiClient" > ( ) (
111+ "ExecutorApiClient" ,
112+ {
113+ api : ExecutorApi ,
114+ httpClient : FetchHttpClient . layer ,
115+ transformClient : HttpClient . mapRequest ( ( request ) => {
116+ let next = HttpClientRequest . prependUrl ( request , getExecutorApiBaseUrl ( ) ) ;
117+ const authorization = getExecutorServerAuthorizationHeader ( ) ;
118+ if ( authorization ) {
119+ next = HttpClientRequest . setHeader (
120+ next ,
121+ "authorization" ,
122+ authorization ,
123+ ) ;
124+ }
125+ return next ;
126+ } ) ,
127+ transformResponse : ( effect ) =>
128+ Effect . tapCause ( effect , reportApiClientInfrastructureCause ) ,
129+ } ,
130+ ) ;
93131
94132export { ExecutorApiClient } ;
0 commit comments