1import { afterEach, describe, expect, it, vi } from "@effect/vitest";
2import { withRelayClientTracing } from "@t3tools/shared/relayTracing";
3import * as Effect from "effect/Effect";
4import * as Tracer from "effect/Tracer";
5
6afterEach(() => {
7 vi.unstubAllEnvs();
8 vi.unstubAllGlobals();
9});
10
11describe("web runtime relay telemetry policy", () => {
12 it.each([
13 {
14 surface: "desktop opt-out",
15 desktopBridge: { getRelayTelemetryEnabled: () => false },
16 exports: false,
17 },
18 {
19 surface: "desktop enabled",
20 desktopBridge: { getRelayTelemetryEnabled: () => true },
21 exports: true,
22 },
23 {
24 surface: "ordinary browser ignores host opt-out",
25 desktopBridge: undefined,
26 exports: true,
27 },
28 ])("applies $surface before the first relay span", async ({ desktopBridge, exports }) => {
29 vi.resetModules();
30 vi.stubEnv("T3CODE_TELEMETRY_ENABLED", "false");
31 vi.stubEnv("T3CODE_OTEL_SDK_DISABLED", "true");
32 vi.stubEnv("OTEL_SDK_DISABLED", "true");
33 vi.stubEnv("VITE_RELAY_OTLP_TRACES_URL", "https://telemetry.example.test/v1/traces");
34 vi.stubEnv("VITE_RELAY_OTLP_TRACES_DATASET", "relay-test");
35 vi.stubEnv("VITE_RELAY_OTLP_TRACES_TOKEN", "test-token");
36 vi.stubGlobal("window", { desktopBridge });
37 const fetchFn = vi.fn<typeof fetch>(async () => new Response(null, { status: 202 }));
38 vi.stubGlobal("fetch", fetchFn);
39 // Re-import after setting runtime policy: static initialization creates the exporter layer.
40 const { runtime: freshRuntime } = await import("./runtime");
41 const { setDelegate } = await import("../observability/clientTracer");
42 const userSpans: Array<string> = [];
43
44 try {
45 expect(
46 await freshRuntime.runPromise(
47 Effect.succeed("relay operation completed").pipe(
48 Effect.withSpan("relay.first-operation"),
49 withRelayClientTracing,
50 ),
51 ),
52 ).toBe("relay operation completed");
53
54 setDelegate(
55 Tracer.make({
56 span(options) {
57 userSpans.push(options.name);
58 return new Tracer.NativeSpan(options);
59 },
60 }),
61 );
62 await freshRuntime.runPromise(Effect.void.pipe(Effect.withSpan("user.work")));
63 expect(userSpans).toEqual(["user.work"]);
64 } finally {
65 setDelegate(null);
66 // Closing the runtime flushes the exporter without waiting for its batch timer.
67 await freshRuntime.dispose();
68 }
69
70 if (exports) {
71 expect(fetchFn).toHaveBeenCalledOnce();
72 expect(String(fetchFn.mock.calls[0]?.[0])).toBe("https://telemetry.example.test/v1/traces");
73 const payload = new TextDecoder().decode(fetchFn.mock.calls[0]?.[1]?.body as Uint8Array);
74 expect(payload).toContain('"name":"relay.first-operation"');
75 } else {
76 expect(fetchFn).not.toHaveBeenCalled();
77 }
78 });
79});