diff --git a/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts b/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts index 0a09771f9b79b4a852a689c09429a5088fe4542c..fefd37a8cf48e31e203813bb9fe3e7202e2c489b 100644 --- a/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts +++ b/apps/desktop/src/backend/DesktopBackendConfiguration.test.ts @@ -996,11 +996,9 @@ describe("DesktopBackendConfiguration", () => { const previousWslEnv = process.env.WSLENV; const previousDisabled = process.env.OTEL_SDK_DISABLED; - const previousTelemetry = process.env.T3CODE_TELEMETRY_ENABLED; try { delete process.env.WSLENV; process.env.OTEL_SDK_DISABLED = "true"; - process.env.T3CODE_TELEMETRY_ENABLED = "false"; yield* Effect.gen(function* () { const configuration = yield* DesktopBackendConfiguration.DesktopBackendConfiguration; @@ -1008,8 +1006,6 @@ describe("DesktopBackendConfiguration", () => { assert.equal(config.env.OTEL_SDK_DISABLED, "true"); assert.include((config.env.WSLENV ?? "").split(":"), "OTEL_SDK_DISABLED"); - assert.equal(config.env.T3CODE_TELEMETRY_ENABLED, "false"); - assert.include((config.env.WSLENV ?? "").split(":"), "T3CODE_TELEMETRY_ENABLED"); }).pipe( Effect.provide( DesktopBackendConfiguration.layer.pipe( @@ -1030,7 +1026,6 @@ describe("DesktopBackendConfiguration", () => { } finally { restoreEnv("WSLENV", previousWslEnv); restoreEnv("OTEL_SDK_DISABLED", previousDisabled); - restoreEnv("T3CODE_TELEMETRY_ENABLED", previousTelemetry); } }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)), ); diff --git a/apps/desktop/src/backend/DesktopBackendConfiguration.ts b/apps/desktop/src/backend/DesktopBackendConfiguration.ts index 9994ee1cda276ed86dfd15e1be0497c8d3688c96..e9962ee5e81448dbaa4bb2f7daabd81ee4444d87 100644 --- a/apps/desktop/src/backend/DesktopBackendConfiguration.ts +++ b/apps/desktop/src/backend/DesktopBackendConfiguration.ts @@ -102,7 +102,6 @@ const DESKTOP_BACKEND_ENV_NAMES = [ const WSL_FORWARDED_ENV_NAMES = [ "OPENAI_API_KEY", "ANTHROPIC_API_KEY", - "T3CODE_TELEMETRY_ENABLED", // Otherwise the WSL server keeps exporting to endpoints from the bootstrap. "T3CODE_OTEL_SDK_DISABLED", "OTEL_SDK_DISABLED", diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.test.ts b/apps/desktop/src/shell/DesktopShellEnvironment.test.ts index 6efe817574a34beba449bdbafbb43bd82ea2fce4..c3de58aaa7b4c12e8aa05b85bf93ffcca39386e2 100644 --- a/apps/desktop/src/shell/DesktopShellEnvironment.test.ts +++ b/apps/desktop/src/shell/DesktopShellEnvironment.test.ts @@ -357,13 +357,11 @@ describe("DesktopShellEnvironment", () => { envOutput({ PATH: "/home/linuxbrew/.linuxbrew/bin:/usr/bin", SSH_AUTH_SOCK: "/tmp/secretive.sock", - T3CODE_TELEMETRY_ENABLED: "false", }), }); assert.equal(env.PATH, "/home/linuxbrew/.linuxbrew/bin:/usr/bin"); assert.equal(env.SSH_AUTH_SOCK, "/tmp/secretive.sock"); - assert.equal(env.T3CODE_TELEMETRY_ENABLED, "false"); }), ); diff --git a/apps/desktop/src/shell/DesktopShellEnvironment.ts b/apps/desktop/src/shell/DesktopShellEnvironment.ts index 2b41b6ea7db94daaee603c9a9d287c242951bfe9..4492b234e4c9b6cc3f288a938e219cabe1ebac98 100644 --- a/apps/desktop/src/shell/DesktopShellEnvironment.ts +++ b/apps/desktop/src/shell/DesktopShellEnvironment.ts @@ -89,7 +89,6 @@ const LOGIN_SHELL_ENV_NAMES = [ "SSL_CERT_FILE", "SSL_CERT_DIR", "NODE_EXTRA_CA_CERTS", - "T3CODE_TELEMETRY_ENABLED", "__MISE_DIFF", ] as const; const WINDOWS_PROFILE_ENV_NAMES = ["PATH", "FNM_DIR", "FNM_MULTISHELL_PATH"] as const; @@ -463,8 +462,6 @@ const installPosixEnvironment = Effect.fn("desktop.shellEnvironment.installPosix "SSL_CERT_FILE", "SSL_CERT_DIR", "NODE_EXTRA_CA_CERTS", - // The telemetry opt-out is documented as a shell variable; GUI launches never see it. - "T3CODE_TELEMETRY_ENABLED", ] as const) { if (!config.env[name] && shellEnvironment[name]) { config.env[name] = shellEnvironment[name]; diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index e03832705daace6b90cbc95aa0ee15d09fa64ee7..353557bb967a5455d8d899e04c647916cc7ae0d3 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -485,11 +485,6 @@ const config: ExpoConfig = { EXPO_PUBLIC_CLERK_GOOGLE_IOS_CLIENT_ID: repoEnv.EXPO_PUBLIC_CLERK_GOOGLE_IOS_CLIENT_ID, EXPO_PUBLIC_CLERK_GOOGLE_ANDROID_CLIENT_ID: repoEnv.EXPO_PUBLIC_CLERK_GOOGLE_ANDROID_CLIENT_ID, EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME: repoEnv.EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME, - observability: { - tracesUrl: repoEnv.EXPO_PUBLIC_OTLP_TRACES_URL ?? "https://api.axiom.co/v1/traces", - tracesDataset: repoEnv.EXPO_PUBLIC_OTLP_TRACES_DATASET ?? null, - tracesToken: repoEnv.EXPO_PUBLIC_OTLP_TRACES_TOKEN ?? null, - }, eas: { projectId: "d763fcb8-d37c-41ea-a773-b54a0ab4a454", }, diff --git a/apps/mobile/src/features/cloud/CloudAuthProvider.tsx b/apps/mobile/src/features/cloud/CloudAuthProvider.tsx index 39e382af866ddb1bdbb81dfc9c4740642b0decee..68b815f538aeab047a384fb5a9685bc13d078747 100644 --- a/apps/mobile/src/features/cloud/CloudAuthProvider.tsx +++ b/apps/mobile/src/features/cloud/CloudAuthProvider.tsx @@ -229,7 +229,7 @@ export function CloudAuthProvider(props: { readonly children: ReactNode }) { } return ( - + {props.children} diff --git a/apps/mobile/src/features/cloud/publicConfig.test.ts b/apps/mobile/src/features/cloud/publicConfig.test.ts index 05bf1a8fbccdc822a6b74a35368947a4c2a35ea6..dbf51d7f1218e3b96dee46c249d9d90ab1ce988a 100644 --- a/apps/mobile/src/features/cloud/publicConfig.test.ts +++ b/apps/mobile/src/features/cloud/publicConfig.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it, vi } from "vite-plus/test"; import { CloudPublicConfigMissingError, - hasTracingPublicConfig, resolveCloudPublicConfig, resolveRelayClerkTokenOptions, } from "./publicConfig"; @@ -31,11 +30,6 @@ describe("resolveCloudPublicConfig", () => { relay: { url: null, }, - observability: { - tracesUrl: null, - tracesDataset: null, - tracesToken: null, - }, }); }); @@ -44,11 +38,6 @@ describe("resolveCloudPublicConfig", () => { resolveCloudPublicConfig({ clerk: { publishableKey: " pk_test_example ", jwtTemplate: " t3-relay " }, relay: { url: " https://relay.example.test/// " }, - observability: { - tracesUrl: " https://api.axiom.co/v1/traces ", - tracesDataset: " mobile-traces ", - tracesToken: " public-ingest-token ", - }, }), ).toEqual({ clerk: { @@ -58,11 +47,6 @@ describe("resolveCloudPublicConfig", () => { relay: { url: "https://relay.example.test", }, - observability: { - tracesUrl: "https://api.axiom.co/v1/traces", - tracesDataset: "mobile-traces", - tracesToken: "public-ingest-token", - }, }); }); @@ -80,52 +64,6 @@ describe("resolveCloudPublicConfig", () => { relay: { url: null, }, - observability: { - tracesUrl: null, - tracesDataset: null, - tracesToken: null, - }, }); }); - - it("rejects an insecure traces URL", () => { - expect( - resolveCloudPublicConfig({ - observability: { - tracesUrl: "http://api.axiom.co/v1/traces", - tracesDataset: "mobile-traces", - tracesToken: "public-ingest-token", - }, - }).observability, - ).toEqual({ - tracesUrl: null, - tracesDataset: "mobile-traces", - tracesToken: "public-ingest-token", - }); - }); - - it("keeps tracing disabled unless every public tracing value is configured", () => { - expect(hasTracingPublicConfig(resolveCloudPublicConfig({}))).toBe(false); - expect( - hasTracingPublicConfig( - resolveCloudPublicConfig({ - observability: { - tracesUrl: "https://api.axiom.co/v1/traces", - tracesDataset: "mobile-traces", - }, - }), - ), - ).toBe(false); - expect( - hasTracingPublicConfig( - resolveCloudPublicConfig({ - observability: { - tracesUrl: "https://api.axiom.co/v1/traces", - tracesDataset: "mobile-traces", - tracesToken: "public-ingest-token", - }, - }), - ), - ).toBe(true); - }); }); diff --git a/apps/mobile/src/features/cloud/publicConfig.ts b/apps/mobile/src/features/cloud/publicConfig.ts index a91a8024d48516008f87e08f3e8905b3abb46930..26886b919a5e4c2d5150533fc7a1b2cd3537cd7a 100644 --- a/apps/mobile/src/features/cloud/publicConfig.ts +++ b/apps/mobile/src/features/cloud/publicConfig.ts @@ -22,11 +22,6 @@ export interface CloudPublicConfig { readonly relay: { readonly url: string | null; }; - readonly observability: { - readonly tracesUrl: string | null; - readonly tracesDataset: string | null; - readonly tracesToken: string | null; - }; } type UntrustedSection = { @@ -43,19 +38,6 @@ function trimNonEmpty(value: unknown): string | null { return typeof value === "string" && value.trim() ? value.trim() : null; } -function normalizeSecureUrl(value: unknown): string | null { - const raw = trimNonEmpty(value); - if (raw === null) { - return null; - } - try { - const url = new URL(raw); - return url.protocol === "https:" ? url.toString() : null; - } catch { - return null; - } -} - export function resolveCloudPublicConfig(extra: ExpoExtra = Constants.expoConfig?.extra) { return { clerk: { @@ -65,11 +47,6 @@ export function resolveCloudPublicConfig(extra: ExpoExtra = Constants.expoConfig relay: { url: normalizeSecureRelayUrl(trimNonEmpty(extra?.relay?.url) ?? ""), }, - observability: { - tracesUrl: normalizeSecureUrl(extra?.observability?.tracesUrl), - tracesDataset: trimNonEmpty(extra?.observability?.tracesDataset), - tracesToken: trimNonEmpty(extra?.observability?.tracesToken), - }, } satisfies CloudPublicConfig; } @@ -78,24 +55,6 @@ export function hasCloudPublicConfig(): boolean { return Boolean(config.clerk.publishableKey && config.clerk.jwtTemplate && config.relay.url); } -type Configured = { - readonly [Key in keyof T]: NonNullable; -}; - -type TracingPublicConfig = Omit & { - readonly observability: Configured; -}; - -export function hasTracingPublicConfig( - config: CloudPublicConfig = resolveCloudPublicConfig(), -): config is TracingPublicConfig { - return Boolean( - config.observability.tracesUrl && - config.observability.tracesDataset && - config.observability.tracesToken, - ); -} - export function resolveRelayClerkTokenOptions() { const { jwtTemplate } = resolveCloudPublicConfig().clerk; if (!jwtTemplate) { diff --git a/apps/mobile/src/features/observability/tracing.test.ts b/apps/mobile/src/features/observability/tracing.test.ts deleted file mode 100644 index ed62f579fea9269962b09f46df61df186cc00e04..0000000000000000000000000000000000000000 --- a/apps/mobile/src/features/observability/tracing.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { expect, it } from "@effect/vitest"; -import * as Cause from "effect/Cause"; -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; -import { vi } from "vite-plus/test"; - -import { layerRemoteHttpClient } from "@t3tools/client-runtime/rpc"; -import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; - -import * as Tracing from "./tracing"; - -vi.mock("expo-constants", () => ({ - default: { - expoConfig: { - extra: {}, - }, - }, -})); - -it.effect("exports spans through the scoped mobile OTLP layer", () => { - const fetchFn = vi.fn(async () => new Response(null, { status: 202 })); - const layerTracing = Tracing.layerFromConfig( - { - tracesUrl: "https://api.axiom.test/v1/traces", - tracesDataset: "mobile-traces", - tracesToken: "public-ingest-token", - }, - { - appVariant: "test", - serviceVersion: "1.2.3", - }, - ).pipe(Layer.provide(layerRemoteHttpClient(fetchFn))); - const layerTracedApplication = Layer.effectDiscard( - Effect.void.pipe(Effect.withSpan("mobile.test.span"), withRelayClientTracing), - ).pipe(Layer.provide(layerTracing)); - - return Effect.gen(function* () { - yield* Layer.build(layerTracedApplication); - - expect(fetchFn).not.toHaveBeenCalled(); - }).pipe( - Effect.scoped, - Effect.andThen( - Effect.sync(() => { - expect(fetchFn).toHaveBeenCalledOnce(); - const [url, init] = fetchFn.mock.calls[0]!; - expect(String(url)).toBe("https://api.axiom.test/v1/traces"); - expect(new Headers(init?.headers).get("authorization")).toBe("Bearer public-ingest-token"); - expect(new Headers(init?.headers).get("x-axiom-dataset")).toBe("mobile-traces"); - expect(new TextDecoder().decode(init?.body as Uint8Array)).toContain("mobile.test.span"); - }), - ), - ); -}); - -it.effect("does not let OTLP serialization failures alter application effects", () => { - const fetchFn = vi.fn(async () => new Response(null, { status: 202 })); - const layerTracing = Tracing.layerFromConfig( - { - tracesUrl: "https://api.axiom.test/v1/traces", - tracesDataset: "mobile-traces", - tracesToken: "public-ingest-token", - }, - { - appVariant: "test", - serviceVersion: "1.2.3", - }, - ).pipe(Layer.provide(layerRemoteHttpClient(fetchFn))); - const failure = { durationNanos: 1n }; - const layerTracedApplication = Layer.effectDiscard( - Effect.fail(failure).pipe( - Effect.withSpan("mobile.test.failed-span"), - withRelayClientTracing, - Effect.exit, - Effect.flatMap((exit) => { - const reason = exit._tag === "Failure" ? exit.cause.reasons[0] : undefined; - return reason && Cause.isFailReason(reason) - ? Effect.sync(() => { - expect(reason.error).toBe(failure); - }) - : Effect.die(new Error("Expected the original typed failure.")); - }), - ), - ).pipe(Layer.provide(layerTracing)); - - return Layer.build(layerTracedApplication).pipe( - Effect.scoped, - Effect.andThen( - Effect.sync(() => { - expect(fetchFn).toHaveBeenCalledOnce(); - expect(new TextDecoder().decode(fetchFn.mock.calls[0]?.[1]?.body as Uint8Array)).toContain( - "mobile.test.failed-span", - ); - }), - ), - ); -}); diff --git a/apps/mobile/src/features/observability/tracing.ts b/apps/mobile/src/features/observability/tracing.ts deleted file mode 100644 index e2b8ee0f1688319d7845d76e6f90dc13758bc0fc..0000000000000000000000000000000000000000 --- a/apps/mobile/src/features/observability/tracing.ts +++ /dev/null @@ -1,41 +0,0 @@ -import Constants from "expo-constants"; -import * as RelayTracing from "@t3tools/shared/relayTracing"; - -import { hasTracingPublicConfig, resolveCloudPublicConfig } from "../cloud/publicConfig"; - -export interface TracingConfig { - readonly tracesUrl: string; - readonly tracesDataset: string; - readonly tracesToken: string; -} - -export interface TracingResource { - readonly serviceVersion?: string; - readonly appVariant: string; -} - -export function resolveTracingConfig(): TracingConfig | null { - const config = resolveCloudPublicConfig(); - if (!hasTracingPublicConfig(config)) { - return null; - } - const { tracesUrl, tracesDataset, tracesToken } = config.observability; - return { tracesUrl, tracesDataset, tracesToken }; -} - -export function layerFromConfig(config: TracingConfig | null, resource: TracingResource) { - return RelayTracing.layer(config, { - serviceName: "t3code-mobile", - serviceVersion: resource.serviceVersion, - runtime: "react-native", - client: `mobile-${resource.appVariant}`, - }); -} - -export const layer = layerFromConfig(resolveTracingConfig(), { - serviceVersion: Constants.expoConfig?.version, - appVariant: - typeof Constants.expoConfig?.extra?.appVariant === "string" - ? Constants.expoConfig.extra.appVariant - : "unknown", -}); diff --git a/apps/mobile/src/lib/authClientMetadata.ts b/apps/mobile/src/lib/authClientMetadata.ts index c8eacd24c9f522e9b5fc8dca27dc9ee167c17f0f..9a6a128b959d440cdc2ae3a670d50cf1b9c3e239 100644 --- a/apps/mobile/src/lib/authClientMetadata.ts +++ b/apps/mobile/src/lib/authClientMetadata.ts @@ -3,9 +3,6 @@ import * as Device from "expo-device"; import { Platform } from "react-native"; export function authClientMetadata(appVersion?: string): AuthClientPresentationMetadata { - const osMajorVersion = Number.parseInt(Device.osVersion?.split(".")[0] ?? "", 10); - const deviceModel = Device.modelName?.trim(); - return { label: "The Knife Mobile", deviceType: @@ -15,8 +12,6 @@ export function authClientMetadata(appVersion?: string): AuthClientPresentationM ? "mobile" : "unknown", ...(Platform.OS === "ios" ? { os: "iOS" } : Platform.OS === "android" ? { os: "Android" } : {}), - ...(Number.isFinite(osMajorVersion) && osMajorVersion > 0 ? { osMajorVersion } : {}), - ...(deviceModel ? { deviceModel } : {}), surface: "mobile", ...(appVersion ? { appVersion } : {}), }; diff --git a/apps/mobile/src/lib/connection.test.ts b/apps/mobile/src/lib/connection.test.ts index 44381534d0f9a7ad0c3b745bf9a261bd036bac50..b8b4e02e547dd0a002868a38f3aa57b3a710773a 100644 --- a/apps/mobile/src/lib/connection.test.ts +++ b/apps/mobile/src/lib/connection.test.ts @@ -14,8 +14,6 @@ const mobileDevice = vi.hoisted(() => ({ DESKTOP: 3, TV: 4, }, - osVersion: "18.4.1", - modelName: "iPhone 15 Pro", })); vi.mock("./runtime", () => ({ @@ -34,8 +32,6 @@ describe("mobile remote connection records", () => { afterEach(() => { mobilePlatform.OS = "ios"; mobileDevice.deviceType = mobileDevice.DeviceType.PHONE; - mobileDevice.osVersion = "18.4.1"; - mobileDevice.modelName = "iPhone 15 Pro"; }); it("identifies mobile token exchanges for authorized-client presentation", () => { @@ -43,32 +39,24 @@ describe("mobile remote connection records", () => { label: "The Knife Mobile", deviceType: "mobile", os: "iOS", - osMajorVersion: 18, - deviceModel: "iPhone 15 Pro", surface: "mobile", }); }); - it("includes only the Android major version and hardware model", () => { + it("identifies Android clients for authorized-client presentation", () => { mobilePlatform.OS = "android"; - mobileDevice.osVersion = "15.2.1"; - mobileDevice.modelName = "Pixel 9"; expect(authClientMetadata()).toMatchObject({ os: "Android", - osMajorVersion: 15, - deviceModel: "Pixel 9", }); }); it("identifies native tablets separately from phones", () => { mobileDevice.deviceType = mobileDevice.DeviceType.TABLET; - mobileDevice.modelName = "iPad Pro 13-inch"; expect(authClientMetadata()).toMatchObject({ deviceType: "tablet", os: "iOS", - deviceModel: "iPad Pro 13-inch", }); }); diff --git a/apps/mobile/src/lib/runtime.ts b/apps/mobile/src/lib/runtime.ts index f46c3709c0e378fc2d35ddefde436edae632d739..6c98055c97d76b6a1bff80ed991f53e3eedb5aab 100644 --- a/apps/mobile/src/lib/runtime.ts +++ b/apps/mobile/src/lib/runtime.ts @@ -7,7 +7,6 @@ import { layerRemoteHttpClient } from "@t3tools/client-runtime/rpc"; import * as Dpop from "../features/cloud/dpop"; import * as ManagedRelayLayer from "../features/cloud/managedRelayLayer"; import { resolveCloudPublicConfig } from "../features/cloud/publicConfig"; -import * as Tracing from "../features/observability/tracing"; import * as Persistence from "../persistence/layer"; import { disposeOnFoundationReplace, type FoundationHotModule } from "./foundation-fast-refresh"; @@ -24,8 +23,7 @@ type RuntimeLayerSource = | typeof Socket.layerWebSocketConstructorGlobal | typeof Dpop.layer | typeof layerHttpClient - | typeof Persistence.layer - | typeof Tracing.layer; + | typeof Persistence.layer; const layerRuntime = Layer.merge( ManagedRelayLayer.layer(configuredRelayUrl()), @@ -33,7 +31,6 @@ const layerRuntime = Layer.merge( ).pipe( Layer.provideMerge(Dpop.layer), Layer.provideMerge(layerHttpClient), - Layer.provideMerge(Tracing.layer.pipe(Layer.provide(layerHttpClient))), Layer.provideMerge(Persistence.layer), ); diff --git a/apps/server/src/auth/http.test.ts b/apps/server/src/auth/http.test.ts index c5a60e5416fdc84a723d0ede23dc3bbb67faea08..afffe7c225919f8a2bd3bc14146e61db18ae1abf 100644 --- a/apps/server/src/auth/http.test.ts +++ b/apps/server/src/auth/http.test.ts @@ -8,13 +8,11 @@ import { EnvironmentAuthenticatedAuth, EnvironmentHttpApi, } from "@t3tools/contracts"; -import { RelayClientTracer } from "@t3tools/shared/relayTracing"; import { expect, it } from "@effect/vitest"; import * as Context from "effect/Context"; import * as Crypto from "effect/Crypto"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; -import * as Option from "effect/Option"; import * as Redacted from "effect/Redacted"; import * as Schema from "effect/Schema"; import * as Tracer from "effect/Tracer"; @@ -165,9 +163,8 @@ it.effect("sets the selected browser session cookies through the HTTP route", () }).pipe(Effect.provide(NodeServices.layer)), ); -it.effect("exports only verified T3 Connect requests", () => +it.effect("keeps authenticated connection requests on the local tracer", () => Effect.gen(function* () { - const productSpans: Array = []; const localSpans: Array = []; const collect = (into: Array) => Tracer.make({ @@ -216,27 +213,18 @@ it.effect("exports only verified T3 Connect requests", () => }), ), ), - Effect.provideService(RelayClientTracer, Option.some(collect(productSpans))), Effect.withTracer(collect(localSpans)), ); yield* handle("DPoP connect"); - expect(productSpans).toEqual([ - "environment.relay.request", - "EnvironmentAuth.authenticateHttpRequest", - "environment.handler", - ]); - expect(localSpans).toEqual(["EnvironmentAuth.authenticateHttpRequest"]); + expect(localSpans).toEqual(["EnvironmentAuth.authenticateHttpRequest", "environment.handler"]); - productSpans.length = 0; localSpans.length = 0; yield* handle("DPoP forged"); - expect(productSpans).toEqual([]); expect(localSpans).toEqual(["EnvironmentAuth.authenticateHttpRequest"]); localSpans.length = 0; yield* handle("Bearer access-token"); - expect(productSpans).toEqual([]); expect(localSpans).toEqual(["EnvironmentAuth.authenticateHttpRequest", "environment.handler"]); }).pipe(Effect.scoped), ); diff --git a/apps/server/src/auth/http.ts b/apps/server/src/auth/http.ts index 1c6814d0af32f9e694b1b7f79d34c69219873bcf..a6ef61f41ad360b618f8730c6eab44ce014d19b0 100644 --- a/apps/server/src/auth/http.ts +++ b/apps/server/src/auth/http.ts @@ -21,11 +21,9 @@ import { import type { AuthEnvironmentScope, DpopFailureReason } from "@t3tools/contracts"; import { parseOAuthScope } from "@t3tools/shared/oauthScope"; import { causeErrorTag } from "@t3tools/shared/observability"; -import * as Clock from "effect/Clock"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; import * as Schema from "effect/Schema"; -import { identity } from "effect/Function"; import * as Layer from "effect/Layer"; import * as Cookies from "effect/http/Cookies"; import * as HttpEffect from "effect/http/HttpEffect"; @@ -34,7 +32,6 @@ import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder"; import * as EnvironmentAuth from "./EnvironmentAuth.ts"; import * as SessionStore from "./SessionStore.ts"; -import { traceAuthenticatedRelayRequest, traceRelayRequest } from "../cloud/traceRelayRequest.ts"; import { deriveAuthClientMetadata } from "./utils.ts"; import { verifyRequestDpopProof } from "./dpop.ts"; @@ -233,7 +230,6 @@ export const layerAuthenticatedAuth = Layer.effect( return (httpEffect) => Effect.gen(function* () { const request = yield* HttpServerRequest.HttpServerRequest; - const startTime = yield* Clock.currentTimeNanos; const session = yield* serverAuth.authenticateHttpRequest(request).pipe( Effect.catchIf(EnvironmentAuth.isServerAuthCredentialError, (error) => failEnvironmentAuthInvalid( @@ -245,16 +241,12 @@ export const layerAuthenticatedAuth = Layer.effect( failEnvironmentInternal("internal_error", error), ), ); - const endTime = yield* Clock.currentTimeNanos; - const handler = httpEffect.pipe( + return yield* httpEffect.pipe( Effect.provideService(EnvironmentAuthenticatedPrincipal, { ...session, scopes: new Set(session.scopes), }), ); - return yield* session.subject === "cloud-connect" - ? traceAuthenticatedRelayRequest(handler, { startTime, endTime }) - : handler; }).pipe(Effect.catchTags({ EnvironmentAuthInvalidError: appendDpopChallengeOnUnauthorized })); }), ); @@ -397,7 +389,6 @@ export const layer = HttpApiBuilder.group( proofKeyThumbprint ? { proofKeyThumbprint } : undefined, ); }, - traceRelayRequest, Effect.catchIf(EnvironmentAuth.isServerAuthCredentialError, (error) => failEnvironmentAuthInvalid( EnvironmentAuth.serverAuthCredentialReason(error), diff --git a/apps/server/src/cli/connect.ts b/apps/server/src/cli/connect.ts index 9f812e615e0088c8942767f0ca3689b2f4727082..4432689f78f94c69395393f3f32fa382b9678ebc 100644 --- a/apps/server/src/cli/connect.ts +++ b/apps/server/src/cli/connect.ts @@ -7,7 +7,6 @@ import { import { RelayOkResponse } from "@t3tools/contracts/relay"; import * as HostProcess from "@t3tools/shared/HostProcess"; import * as RelayClient from "@t3tools/shared/relayClient"; -import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; import * as Cause from "effect/Cause"; import * as Config from "effect/Config"; import * as Console from "effect/Console"; @@ -38,7 +37,6 @@ import { RELAY_URL_SECRET, } from "../cloud/config.ts"; import { relayUrlConfig } from "../cloud/publicConfig.ts"; -import * as RelayTracing from "../cloud/relayTracing.ts"; import * as ServerConfig from "../config.ts"; import * as ServerEnvironment from "../environment/ServerEnvironment.ts"; import * as ExternalLauncher from "../process/externalLauncher.ts"; @@ -346,7 +344,6 @@ const unlinkRelayEnvironment = Effect.fn("cloud.cli.unlink_relay_environment")(f httpClient.execute, Effect.flatMap(filterRelayResponse), Effect.flatMap(HttpClientResponse.schemaBodyJson(RelayOkResponse)), - withRelayClientTracing, ); return response.ok ? ({ status: "revoked" } satisfies RelayUnlinkResult) @@ -448,7 +445,6 @@ const runCloudCommand = Effect.fn("cloud.cli.run_cloud_command")(function* - traceRelayRequest( - toHttpError(cloudLink.mintCredential(payload)).pipe( - Effect.tap(() => appendCloudCredentialResponseHeaders), - ), + toHttpError(cloudLink.mintCredential(payload)).pipe( + Effect.tap(() => appendCloudCredentialResponseHeaders), ), ); }), diff --git a/apps/server/src/cloud/publicConfig.test.ts b/apps/server/src/cloud/publicConfig.test.ts index 02276e87d6de88adef64601e985ba5c6d082502d..1d05a511bfa10105b48d0578656f1ddce53a6441 100644 --- a/apps/server/src/cloud/publicConfig.test.ts +++ b/apps/server/src/cloud/publicConfig.test.ts @@ -3,12 +3,7 @@ import * as ConfigProvider from "effect/ConfigProvider"; import * as Effect from "effect/Effect"; import * as Result from "effect/Result"; -import { - hostedAppUrlConfig, - makeCloudCliOAuthConfig, - makeRelayUrlConfig, - resolveRelayClientTracingConfig, -} from "./publicConfig.ts"; +import { hostedAppUrlConfig, makeCloudCliOAuthConfig, makeRelayUrlConfig } from "./publicConfig.ts"; const provideEnv = (env: Readonly>) => Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env }))); @@ -144,39 +139,3 @@ it.effect("reports malformed Clerk publishable keys as typed configuration failu } }), ); - -it("resolves relay client tracing from runtime config with build-time fallback", () => { - const fallback = { - tracesUrl: "https://embedded.example.test/v1/traces", - tracesDataset: "embedded-dataset", - tracesToken: "embedded-token", - }; - - assert.deepEqual(resolveRelayClientTracingConfig({}, fallback), fallback); - assert.deepEqual( - resolveRelayClientTracingConfig( - { - T3CODE_RELAY_CLIENT_OTLP_TRACES_URL: "https://runtime.example.test/v1/traces", - T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET: "runtime-dataset", - T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN: "runtime-token", - }, - fallback, - ), - { - tracesUrl: "https://runtime.example.test/v1/traces", - tracesDataset: "runtime-dataset", - tracesToken: "runtime-token", - }, - ); - assert.equal( - resolveRelayClientTracingConfig( - { - T3CODE_RELAY_CLIENT_OTLP_TRACES_URL: "http://insecure.example.test/v1/traces", - T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET: "runtime-dataset", - T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN: "runtime-token", - }, - fallback, - ), - null, - ); -}); diff --git a/apps/server/src/cloud/publicConfig.ts b/apps/server/src/cloud/publicConfig.ts index 903f4a88a1e07ee3ef86e04a4e5758faf87ed98b..395a6e1282efe82182672b59cdc0bf466a4c89f3 100644 --- a/apps/server/src/cloud/publicConfig.ts +++ b/apps/server/src/cloud/publicConfig.ts @@ -14,9 +14,6 @@ import * as SchemaIssue from "effect/SchemaIssue"; declare const __T3CODE_BUILD_RELAY_URL__: string | undefined; declare const __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: string | undefined; declare const __T3CODE_BUILD_CLERK_CLI_OAUTH_CLIENT_ID__: string | undefined; -declare const __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_URL__: string | undefined; -declare const __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_DATASET__: string | undefined; -declare const __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_TOKEN__: string | undefined; const CLOUD_CLI_OAUTH_LOOPBACK_PORT = 34338; const CLOUD_CLI_OAUTH_SCOPES = CONNECT_OAUTH_SCOPES; @@ -40,15 +37,6 @@ function readBuildTimeValue(value: string | undefined): string { return typeof value === "undefined" ? "" : value.trim(); } -function normalizeSecureUrl(value: string): string | null { - try { - const url = new URL(value); - return url.protocol === "https:" ? url.toString() : null; - } catch { - return null; - } -} - const buildTimeRelayUrl = typeof __T3CODE_BUILD_RELAY_URL__ === "undefined" ? "" @@ -63,38 +51,6 @@ const buildTimeClerkCliOAuthClientId = readBuildTimeValue( ? undefined : __T3CODE_BUILD_CLERK_CLI_OAUTH_CLIENT_ID__, ); -const buildTimeRelayClientTracing = { - tracesUrl: readBuildTimeValue( - typeof __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_URL__ === "undefined" - ? undefined - : __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_URL__, - ), - tracesDataset: readBuildTimeValue( - typeof __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_DATASET__ === "undefined" - ? undefined - : __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_DATASET__, - ), - tracesToken: readBuildTimeValue( - typeof __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_TOKEN__ === "undefined" - ? undefined - : __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_TOKEN__, - ), -} as const; - -export function resolveRelayClientTracingConfig( - env: Readonly> = process.env, - fallback = buildTimeRelayClientTracing, -) { - const tracesUrl = env.T3CODE_RELAY_CLIENT_OTLP_TRACES_URL?.trim() || fallback.tracesUrl; - const tracesDataset = - env.T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET?.trim() || fallback.tracesDataset; - const tracesToken = env.T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN?.trim() || fallback.tracesToken; - const normalizedTracesUrl = normalizeSecureUrl(tracesUrl); - return normalizedTracesUrl && tracesDataset && tracesToken - ? { tracesUrl: normalizedTracesUrl, tracesDataset, tracesToken } - : null; -} - export function makeRelayUrlConfig(fallback = buildTimeRelayUrl) { const runtimeConfig = Config.NonEmptyString("T3CODE_RELAY_URL"); return (fallback ? runtimeConfig.pipe(Config.withDefault(fallback)) : runtimeConfig).pipe( diff --git a/apps/server/src/cloud/relayTracing.ts b/apps/server/src/cloud/relayTracing.ts deleted file mode 100644 index 1b76f0a228f65f952937ac54cc7cb6c49907e2a5..0000000000000000000000000000000000000000 --- a/apps/server/src/cloud/relayTracing.ts +++ /dev/null @@ -1,18 +0,0 @@ -import * as RelayTracing from "@t3tools/shared/relayTracing"; - -import { resolveRelayClientTracingConfig } from "./publicConfig.ts"; - -const relayClientTracingConfig = resolveRelayClientTracingConfig(); - -export const layerHeadlessRelayClient = RelayTracing.layer(relayClientTracingConfig, { - serviceName: "t3code-server", - runtime: "node", - client: "headless-cli", -}); - -export const layerServerRelayBroker = RelayTracing.layer(relayClientTracingConfig, { - serviceName: "t3code-server", - runtime: "node", - client: "environment-server", - component: "relay-broker", -}); diff --git a/apps/server/src/cloud/traceRelayRequest.test.ts b/apps/server/src/cloud/traceRelayRequest.test.ts deleted file mode 100644 index a65fd571a3bb60c49eb3f187445de95c513e45b1..0000000000000000000000000000000000000000 --- a/apps/server/src/cloud/traceRelayRequest.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { describe, expect, it } from "@effect/vitest"; -import * as Effect from "effect/Effect"; -import * as Option from "effect/Option"; -import * as Tracer from "effect/Tracer"; -import { HttpServerRequest } from "effect/http"; -import { RelayClientTracer } from "@t3tools/shared/relayTracing"; - -import { - traceAuthenticatedRelayRequest, - traceLocalHandlerWork, - traceRelayRequest, -} from "./traceRelayRequest.ts"; - -const AUTHENTICATION = { startTime: 1_000n, endTime: 2_000n }; - -describe("relay request tracing", () => { - it.effect("does not accept an unauthenticated request trace parent", () => - Effect.gen(function* () { - const spans: Array = []; - const productTracer = Tracer.make({ - span: (options) => { - const span = new Tracer.NativeSpan(options); - spans.push(span); - return span; - }, - }); - const request = HttpServerRequest.fromWeb( - new Request("https://environment.example.test/api/t3-cloud/mint-credential", { - headers: { - traceparent: "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01", - }, - }), - ); - - yield* traceRelayRequest(Effect.void.pipe(Effect.withSpan("relay.mint.handler"))).pipe( - Effect.provideService(HttpServerRequest.HttpServerRequest, request), - Effect.provideService(RelayClientTracer, Option.some(productTracer)), - ); - - expect(spans).toHaveLength(1); - const span = spans[0]!; - expect(span.traceId).not.toBe("0123456789abcdef0123456789abcdef"); - expect(Option.isNone(span.parent)).toBe(true); - }), - ); - - it.effect("continues an authenticated relay trace with the product tracer", () => - Effect.gen(function* () { - const spans: Array = []; - const productTracer = Tracer.make({ - span: (options) => { - const span = new Tracer.NativeSpan(options); - spans.push(span); - return span; - }, - }); - const request = HttpServerRequest.fromWeb( - new Request("https://environment.example.test/api/t3-cloud/mint-credential", { - headers: { - traceparent: "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01", - }, - }), - ); - - yield* traceAuthenticatedRelayRequest( - Effect.void.pipe(Effect.withSpan("relay.mint.handler")), - AUTHENTICATION, - ).pipe( - Effect.provideService(HttpServerRequest.HttpServerRequest, request), - Effect.provideService(RelayClientTracer, Option.some(productTracer)), - ); - - expect(spans.map((span) => span.name)).toEqual([ - "environment.relay.request", - "EnvironmentAuth.authenticateHttpRequest", - "relay.mint.handler", - ]); - const [relaySpan, authentication, handler] = spans; - expect(Option.getOrUndefined(authentication!.parent)?.spanId).toBe(relaySpan!.spanId); - expect(relaySpan!.traceId).toBe("0123456789abcdef0123456789abcdef"); - expect(Option.getOrUndefined(relaySpan!.parent)?.spanId).toBe("0123456789abcdef"); - expect(Option.getOrUndefined(handler!.parent)?.spanId).toBe(relaySpan!.spanId); - }), - ); -}); - -describe("relay request tracing boundary", () => { - it.effect("exports a T3 Connect handler span but not its local work", () => - Effect.gen(function* () { - const productSpans: Array = []; - const localSpans: Array = []; - const collect = (into: Array) => - Tracer.make({ - span: (options) => { - into.push(options.name); - return new Tracer.NativeSpan(options); - }, - }); - const request = HttpServerRequest.fromWeb( - new Request("https://environment.example.test/api/orchestration/threads/thread-1"), - ); - - // Shaped like a real handler: an Effect.fn span whose service call runs - // on the local tracer. - const handler = Effect.fn("environment.orchestration.threadSnapshot")(function* () { - yield* Effect.void.pipe( - Effect.withSpan("sql.execute"), - Effect.withSpan("ServerSecretStore.get"), - traceLocalHandlerWork, - ); - }); - - yield* traceAuthenticatedRelayRequest(handler(), AUTHENTICATION).pipe( - Effect.provideService(HttpServerRequest.HttpServerRequest, request), - Effect.provideService(RelayClientTracer, Option.some(collect(productSpans))), - Effect.withTracer(collect(localSpans)), - ); - - expect(productSpans).toEqual([ - "environment.relay.request", - "EnvironmentAuth.authenticateHttpRequest", - "environment.orchestration.threadSnapshot", - ]); - expect(localSpans).toEqual(["ServerSecretStore.get", "sql.execute"]); - }), - ); -}); diff --git a/apps/server/src/cloud/traceRelayRequest.ts b/apps/server/src/cloud/traceRelayRequest.ts deleted file mode 100644 index 8a25ba6e6a4517ea9c38b1ac557553bfae901902..0000000000000000000000000000000000000000 --- a/apps/server/src/cloud/traceRelayRequest.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - RelayClientTracer, - withLocalTracing, - withRelayClientTracing, -} from "@t3tools/shared/relayTracing"; -import * as Clock from "effect/Clock"; -import * as Context from "effect/Context"; -import * as Effect from "effect/Effect"; -import * as Exit from "effect/Exit"; -import * as Option from "effect/Option"; -import * as Tracer from "effect/Tracer"; -import { HttpServerRequest, HttpTraceContext } from "effect/http"; - -/** - * Exports every span of a handler that is itself T3 Connect work (the token - * exchange, the environment descriptor, credential minting), so the whole - * connection path can be measured. - */ -export const traceRelayRequest = ( - effect: Effect.Effect, -): Effect.Effect => effect.pipe(withRelayClientTracing); - -/** - * Traces a request that arrived over T3 Connect, continuing the client's - * trace. The request's span and its authentication timing are exported, so - * their latency and errors are visible; what the handler then does on the - * user's machine (database reads, project indexing, processes) is not, unless - * the handler is connection work and opts back in with {@link traceRelayRequest}. - * - * Call it only after the session is verified as T3 Connect: an unverified - * request must not add spans to a trace it names. Authentication therefore - * runs on the local tracer, and its timing is recorded here afterwards. - */ -export const traceAuthenticatedRelayRequest = ( - effect: Effect.Effect, - authentication: { readonly startTime: bigint; readonly endTime: bigint }, -): Effect.Effect => - Effect.all([RelayClientTracer, HttpServerRequest.HttpServerRequest]).pipe( - Effect.flatMap(([tracer, request]): Effect.Effect => { - if (Option.isNone(tracer)) return effect; - const parent = HttpTraceContext.fromHeaders(request.headers); - const sampled = Option.isNone(parent) || parent.value.sampled; - const startSpan = ( - name: string, - kind: Tracer.SpanKind, - parent: Option.Option, - ) => - tracer.value.span({ - name, - parent, - annotations: Context.empty(), - links: [], - startTime: authentication.startTime, - kind, - root: Option.isNone(parent), - sampled, - }); - const span = startSpan("environment.relay.request", "server", parent); - startSpan("EnvironmentAuth.authenticateHttpRequest", "internal", Option.some(span)).end( - authentication.endTime, - Exit.void, - ); - return effect.pipe( - Effect.withParentSpan(span), - Effect.onExit((exit) => - Clock.currentTimeNanos.pipe(Effect.map((endTime) => span.end(endTime, exit))), - ), - withRelayClientTracing, - ); - }), - ); - -/** - * Keeps the work inside a relay-traced handler on the local tracer. The - * handler's span still records the timing and outcome. - */ -export const traceLocalHandlerWork = withLocalTracing; diff --git a/apps/server/src/config.ts b/apps/server/src/config.ts index f4290a697ea82163c04f76e013e4741542415f2f..c10e851ff9e6fcf3a49f702b175d13cdbe4af820 100644 --- a/apps/server/src/config.ts +++ b/apps/server/src/config.ts @@ -48,7 +48,6 @@ export interface ServerDerivedPaths { readonly providerLogsDir: string; readonly providerEventLogPath: string; readonly terminalLogsDir: string; - readonly anonymousIdPath: string; readonly environmentIdPath: string; readonly serverRuntimeStatePath: string; readonly secretsDir: string; @@ -163,7 +162,6 @@ export const deriveServerPaths = Effect.fn(function* ( providerLogsDir, providerEventLogPath: join(providerLogsDir, "events.log"), terminalLogsDir: join(logsDir, "terminals"), - anonymousIdPath: join(stateDir, "anonymous-id"), environmentIdPath: join(stateDir, "environment-id"), serverRuntimeStatePath: join(stateDir, "server-runtime.json"), secretsDir: join(stateDir, "secrets"), @@ -185,7 +183,6 @@ export const ensureServerDirectories = Effect.fn(function* (derivedPaths: Server fs.makeDirectory(path.dirname(derivedPaths.keybindingsConfigPath), { recursive: true }), fs.makeDirectory(path.dirname(derivedPaths.settingsPath), { recursive: true }), fs.makeDirectory(derivedPaths.providerStatusCacheDir, { recursive: true }), - fs.makeDirectory(path.dirname(derivedPaths.anonymousIdPath), { recursive: true }), fs.makeDirectory(path.dirname(derivedPaths.serverRuntimeStatePath), { recursive: true }), ], { concurrency: "unbounded" }, diff --git a/apps/server/src/http.ts b/apps/server/src/http.ts index 7512de5b500e7ee83391615d7adc1fd157136f14..dbccfed69da9862e06f7faaf70201009f3cfb69a 100644 --- a/apps/server/src/http.ts +++ b/apps/server/src/http.ts @@ -57,7 +57,6 @@ import * as BrowserTraceCollector from "./observability/BrowserTraceCollector.ts import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; import * as SharedAccounts from "./provider/SharedAccounts.ts"; import * as SharedProfiles from "./provider/SharedProfiles.ts"; -import { traceRelayRequest } from "./cloud/traceRelayRequest.ts"; import { annotateEnvironmentRequest, failEnvironmentScopeRequired, @@ -534,7 +533,7 @@ export const layerServerEnvironmentHttpApi = HttpApiBuilder.group( Effect.fn("environment.metadata.descriptor")(function* (args) { yield* annotateEnvironmentRequest(args.endpoint.name); return yield* serverEnvironment.getDescriptor; - }, traceRelayRequest), + }), ); }), ); diff --git a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.testkit.ts b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.testkit.ts index ea949828076da2c0c129b4d8a8f2debd4bd7453f..c53c41143cf77ca919b5955ef1bc35995abdd274 100644 --- a/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.testkit.ts +++ b/apps/server/src/orchestration-v2/Adapters/CodexAdapterV2.testkit.ts @@ -173,7 +173,6 @@ export function makeReplayServerConfig( providerLogsDir, providerEventLogPath: path.join(providerLogsDir, "events.log"), terminalLogsDir, - anonymousIdPath: path.join(stateDir, "anonymous-id"), environmentIdPath: path.join(stateDir, "environment-id"), serverRuntimeStatePath: path.join(stateDir, "server-runtime.json"), secretsDir: path.join(stateDir, "secrets"), diff --git a/apps/server/src/orchestration-v2/ProviderEventIngestor.test.ts b/apps/server/src/orchestration-v2/ProviderEventIngestor.test.ts index f2a8ed7ff0e18c6915d756710af7cae8ba400312..3eabc17e585dbe0e47fea1dd23aa877d26762b89 100644 --- a/apps/server/src/orchestration-v2/ProviderEventIngestor.test.ts +++ b/apps/server/src/orchestration-v2/ProviderEventIngestor.test.ts @@ -143,106 +143,6 @@ function threadCreatedEvent( const layer = it.layer(layerTest); -it.effect("records accepted billed turn usage once without billing the context window", () => { - const recorded: Array>> = []; - const layerAnalytics = Layer.succeed(ProviderEventIngestor.ProviderTurnAnalytics, { - record: (properties: Readonly>) => - Effect.sync(() => { - recorded.push(properties); - }), - }); - return Effect.gen(function* () { - const now = yield* DateTime.now; - const eventSink = yield* EventSink.EventSinkV2; - const ingestor = yield* ProviderEventIngestor.ProviderEventIngestorV2; - const idAllocator = yield* IdAllocator.IdAllocatorV2; - const threadEvent = yield* threadCreatedEvent(now); - yield* eventSink.write({ events: [threadEvent] }); - const providerSessionId = yield* idAllocator.allocate.providerSession({ - providerInstanceId: modelSelection.instanceId, - threadId: threadEvent.threadId, - }); - const providerThreadId = idAllocator.derive.providerThread({ - driver: CODEX_DRIVER, - nativeThreadId: "billing-thread", - }); - const providerTurn = { - id: idAllocator.derive.providerTurn({ driver: CODEX_DRIVER, nativeTurnId: "billing-turn" }), - providerThreadId, - nodeId: NodeId.make("node:billing-turn"), - runAttemptId: null, - nativeTurnRef: null, - ordinal: 1, - status: "completed" as const, - startedAt: now, - completedAt: DateTime.makeUnsafe(DateTime.toEpochMillis(now) + 120), - tokenUsage: { - inputTokens: 4000, - cachedInputTokens: 3000, - outputTokens: 100, - usedTokens: 4100, - updatedAt: DateTime.formatIso(now), - }, - turnTokenUsage: { - usageStatus: "complete" as const, - usageScope: "main_agent" as const, - hasSubagents: false, - inputTokens: 40, - cachedInputTokens: 30, - outputTokens: 10, - }, - }; - const input = { - providerSessionId, - providerInstanceId: modelSelection.instanceId, - threadId: threadEvent.threadId, - analyticsContext: { - modelSelection, - runtimeMode: "full-access" as const, - interactionMode: "default" as const, - }, - event: { type: "provider_turn.updated" as const, driver: CODEX_DRIVER, providerTurn }, - }; - yield* ingestor.ingestNormalized(input); - yield* ingestor.ingestNormalized(input); - const ignored = yield* ingestor.ingestNormalized({ - ...input, - event: { - ...input.event, - providerTurn: { - ...providerTurn, - id: idAllocator.derive.providerTurn({ - driver: CODEX_DRIVER, - nativeTurnId: "stale-billing-turn", - }), - }, - }, - writeIfRunCurrent: { - runId: RunId.make("missing-run"), - activeAttemptId: RunAttemptId.make("stale-attempt"), - expectedStatus: "running", - }, - }); - assert.isEmpty(ignored); - assert.lengthOf(recorded, 1); - assert.deepEqual(recorded[0], { - provider: CODEX_DRIVER, - terminalStatus: "completed", - usageStatus: "complete", - usageScope: "main_agent", - hasSubagents: false, - inputTokens: 40, - cachedInputTokens: 30, - outputTokens: 10, - model: modelSelection.model, - mixedModels: false, - runtimeMode: "full-access", - interactionMode: "default", - durationMs: 120, - }); - }).pipe(Effect.provide(layerTest.pipe(Layer.provide(layerAnalytics)))); -}); - layer("ProviderEventIngestorV2", (it) => { it.effect("normalizes provider events through the real event log and projection store", () => Effect.gen(function* () { diff --git a/apps/server/src/orchestration-v2/ProviderEventIngestor.ts b/apps/server/src/orchestration-v2/ProviderEventIngestor.ts index ddf2efbcff8bf1f2b2c65fd35750790f84e05972..b8fc47aa92233e1998a64d5ca7b2e31629a402b9 100644 --- a/apps/server/src/orchestration-v2/ProviderEventIngestor.ts +++ b/apps/server/src/orchestration-v2/ProviderEventIngestor.ts @@ -5,12 +5,9 @@ import { OrchestrationV2StoredEvent, type OrchestrationV2PlanArtifact, type OrchestrationV2Run, - type OrchestrationV2ProviderTurn, + type ModelSelection, type OrchestrationV2Subagent, type OrchestrationV2ThreadProjection, - type ModelSelection, - type RuntimeMode, - type ProviderInteractionMode, ProviderInstanceId, ProviderSessionId, ProviderThreadId, @@ -27,8 +24,7 @@ import * as Layer from "effect/Layer"; import * as Schema from "effect/Schema"; import { AgentScope } from "@t3tools/shared/AgentScope"; -import { getModelSelectionStringOptionValue, modelSelectionsEqual } from "@t3tools/shared/model"; -import * as AnalyticsService from "../telemetry/AnalyticsService.ts"; +import { modelSelectionsEqual } from "@t3tools/shared/model"; import * as EventSink from "./EventSink.ts"; import * as ProjectionStore from "./ProjectionStore.ts"; import * as IdAllocator from "@t3tools/provider-core/server/IdAllocator"; @@ -71,71 +67,6 @@ export const ProviderEventIngestorV2Error = Schema.Union([ ]); export type ProviderEventIngestorV2Error = typeof ProviderEventIngestorV2Error.Type; -export interface ProviderTurnAnalyticsContext { - readonly modelSelection: ModelSelection; - readonly runtimeMode?: RuntimeMode; - readonly interactionMode?: ProviderInteractionMode; -} - -export class ProviderTurnAnalytics extends Context.Reference<{ - readonly record: (properties: Readonly>) => Effect.Effect; -}>("t3/orchestration-v2/ProviderTurnAnalytics", { - defaultValue: () => ({ record: () => Effect.void }), -}) {} - -export const layerAnalytics = Layer.effect( - ProviderTurnAnalytics, - Effect.gen(function* () { - const analytics = yield* AnalyticsService.AnalyticsService; - return { - record: (properties: Readonly>) => - analytics.record("provider.turn.completed", properties), - }; - }), -); - -function providerTurnAnalyticsProperties(input: { - readonly driver: ProviderAdapter.ProviderAdapterV2Event["driver"]; - readonly providerTurn: OrchestrationV2ProviderTurn; - readonly context?: ProviderTurnAnalyticsContext; -}): Readonly> { - const usage = input.providerTurn.turnTokenUsage; - const modelSelection = input.context?.modelSelection; - const effort = modelSelection - ? (getModelSelectionStringOptionValue(modelSelection, "reasoningEffort") ?? - getModelSelectionStringOptionValue(modelSelection, "effort")) - : undefined; - return { - provider: input.driver, - terminalStatus: input.providerTurn.status, - usageStatus: usage?.usageStatus ?? "unavailable", - usageScope: usage?.usageScope ?? "main_agent", - ...(usage ? { hasSubagents: usage.hasSubagents } : {}), - ...(usage?.inputTokens === undefined ? {} : { inputTokens: usage.inputTokens }), - ...(usage?.cachedInputTokens === undefined - ? {} - : { cachedInputTokens: usage.cachedInputTokens }), - ...(usage?.cacheCreationTokens === undefined - ? {} - : { cacheCreationTokens: usage.cacheCreationTokens }), - ...(usage?.outputTokens === undefined ? {} : { outputTokens: usage.outputTokens }), - ...(usage?.reasoningTokens === undefined ? {} : { reasoningTokens: usage.reasoningTokens }), - ...(modelSelection ? { model: modelSelection.model, mixedModels: false } : {}), - ...(effort ? { effort } : {}), - ...(input.context?.runtimeMode ? { runtimeMode: input.context.runtimeMode } : {}), - ...(input.context?.interactionMode ? { interactionMode: input.context.interactionMode } : {}), - ...(input.providerTurn.startedAt && input.providerTurn.completedAt - ? { - durationMs: Math.max( - 0, - DateTime.toEpochMillis(input.providerTurn.completedAt) - - DateTime.toEpochMillis(input.providerTurn.startedAt), - ), - } - : {}), - }; -} - type TodoListPlan = Extract; function withPlanStepDurations( @@ -260,7 +191,6 @@ export interface ProviderEventIngestInput { readonly nodeId?: NodeId; readonly rawEventId?: RawEventId; readonly event: ProviderAdapter.ProviderAdapterV2Event; - readonly analyticsContext?: ProviderTurnAnalyticsContext; } export interface ProviderEventIngestorV2Shape { @@ -317,9 +247,7 @@ export const layer: Layer.Layer< const projections = yield* ProjectionStore.ProjectionStoreV2; const idAllocator = yield* IdAllocator.IdAllocatorV2; const threadCommands = yield* ThreadCommandExecutor.ThreadCommandExecutor; - const analytics = yield* ProviderTurnAnalytics; const agentScope = yield* AgentScope; - const completedTurnAnalytics = new Set(); const makeDomainEvent = ( input: ProviderEventIngestInput, @@ -795,35 +723,6 @@ export const layer: Layer.Layer< ), ), ), - Effect.tap((storedEvents) => - Effect.gen(function* () { - if (storedEvents.length === 0 || input.event.type !== "provider_turn.updated") return; - const providerTurn = input.event.providerTurn; - if ( - providerTurn.status !== "completed" && - providerTurn.status !== "failed" && - providerTurn.status !== "interrupted" && - providerTurn.status !== "cancelled" - ) - return; - const key = `${input.providerInstanceId}:${providerTurn.id}`; - if (completedTurnAnalytics.has(key)) return; - completedTurnAnalytics.add(key); - if (completedTurnAnalytics.size > 4096) { - const oldest = completedTurnAnalytics.values().next().value; - if (oldest !== undefined) completedTurnAnalytics.delete(oldest); - } - yield* analytics.record( - providerTurnAnalyticsProperties({ - driver: input.event.driver, - providerTurn, - ...(input.analyticsContext === undefined - ? {} - : { context: input.analyticsContext }), - }), - ); - }), - ), (effect) => input.event.type === "app_thread.created" ? threadCommands.withLock(input.event.appThread.id, effect) diff --git a/apps/server/src/orchestration-v2/RunExecutionService.ts b/apps/server/src/orchestration-v2/RunExecutionService.ts index b2fe77f6cd85b90d4f211fbfb450d845d2e8577e..0c4199a65011efb63cbf10444fb3f4d23d4b4108 100644 --- a/apps/server/src/orchestration-v2/RunExecutionService.ts +++ b/apps/server/src/orchestration-v2/RunExecutionService.ts @@ -1246,11 +1246,6 @@ export const layer: Layer.Layer< event.type === "provider_thread.updated" && event.providerThread.id === input.providerThread.id; const storedEvents = yield* providerEventIngestor.ingestNormalized({ - analyticsContext: { - modelSelection: input.modelSelection, - runtimeMode: input.runtimePolicy.runtimeMode, - interactionMode: input.runtimePolicy.interactionMode, - }, providerSessionId: input.providerSessionId, providerInstanceId: input.run.providerInstanceId, threadId: input.run.threadId, diff --git a/apps/server/src/orchestration-v2/http.ts b/apps/server/src/orchestration-v2/http.ts index cccec8e919c147fc658f68bdc5755788bf9808b4..37fc1ae07744bdfbff9c39c03c3585e3ca190bc7 100644 --- a/apps/server/src/orchestration-v2/http.ts +++ b/apps/server/src/orchestration-v2/http.ts @@ -17,7 +17,6 @@ import { requireEnvironmentScope, } from "../auth/http.ts"; import * as ServerEnvironment from "../environment/ServerEnvironment.ts"; -import { traceLocalHandlerWork } from "../cloud/traceRelayRequest.ts"; import * as OrchestrationEventStore from "../persistence/OrchestrationEventStore.ts"; import * as ProjectEnrichmentService from "../project/ProjectEnrichmentService.ts"; import { @@ -162,7 +161,6 @@ export const layer = HttpApiBuilder.group( yield* annotateEnvironmentRequest(args.endpoint.name); yield* requireEnvironmentScope(AuthOrchestrationReadScope); return yield* loadShellSnapshot().pipe( - traceLocalHandlerWork, Effect.catch((cause) => failEnvironmentInternal("orchestration_snapshot_failed", cause), ), @@ -177,7 +175,7 @@ export const layer = HttpApiBuilder.group( const snapshot = yield* loadThreadSnapshot( args.params.threadId, "orchestration_thread_snapshot_failed", - ).pipe(traceLocalHandlerWork); + ); return { snapshotSequence: snapshot.snapshotSequence, projection: snapshot.projection, @@ -191,7 +189,6 @@ export const layer = HttpApiBuilder.group( yield* requireEnvironmentScope(AuthOrchestrationReadScope); return yield* threadManagement.getThreadTranscript(args.params.threadId).pipe( Effect.provideService(ServerEnvironment.ServerEnvironmentIdentity, environment), - traceLocalHandlerWork, Effect.catch( Effect.fnUntraced(function* (error) { if (isThreadTranscriptTooLargeError(error)) { @@ -214,9 +211,7 @@ export const layer = HttpApiBuilder.group( Effect.fn("environment.orchestration.threadBoundedSnapshot")(function* (args) { yield* annotateEnvironmentRequest(args.endpoint.name); yield* requireEnvironmentScope(AuthOrchestrationReadScope); - const snapshot = yield* loadThreadSnapshotWindow(args.params.threadId).pipe( - traceLocalHandlerWork, - ); + const snapshot = yield* loadThreadSnapshotWindow(args.params.threadId); const bounded = buildBoundedThreadProjection({ projection: snapshot.projection, snapshotSequence: snapshot.snapshotSequence, @@ -250,7 +245,6 @@ export const layer = HttpApiBuilder.group( args.query.view === "conversation", ) .pipe( - traceLocalHandlerWork, Effect.catch( Effect.fnUntraced(function* (cause) { if (isThreadNotFound(cause)) diff --git a/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts b/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts index 491a6446a4360dbafc791a3be40d76b6699ba5b1..de43296e80ac633f092703f35be4b5d3fc7a0c9b 100644 --- a/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts +++ b/apps/server/src/orchestration-v2/testkit/ProviderReplayHarness.ts @@ -146,7 +146,6 @@ export function makeReplayServerConfig( providerLogsDir, providerEventLogPath: path.join(providerLogsDir, "events.log"), terminalLogsDir, - anonymousIdPath: path.join(stateDir, "anonymous-id"), environmentIdPath: path.join(stateDir, "environment-id"), serverRuntimeStatePath: path.join(stateDir, "server-runtime.json"), secretsDir: path.join(stateDir, "secrets"), diff --git a/apps/server/src/project/http.ts b/apps/server/src/project/http.ts index e1a04655d8353d1449ee18262dbbf09e890d033f..bec9c916f3d6a2347e8ba165a7e77cc7d28b137e 100644 --- a/apps/server/src/project/http.ts +++ b/apps/server/src/project/http.ts @@ -12,7 +12,6 @@ import { failEnvironmentInvalidRequest, requireEnvironmentScope, } from "../auth/http.ts"; -import { traceLocalHandlerWork } from "../cloud/traceRelayRequest.ts"; import * as ServerRuntimeStartup from "../serverRuntimeStartup.ts"; import * as ProjectService from "./ProjectService.ts"; import { projectMutationOperation } from "./ProjectMutation.ts"; @@ -44,7 +43,6 @@ export const layer = HttpApiBuilder.group( yield* annotateEnvironmentRequest(args.endpoint.name); yield* requireEnvironmentScope(AuthOrchestrationReadScope); return yield* projects.snapshot.pipe( - traceLocalHandlerWork, Effect.catch((cause) => failEnvironmentInternal("project_snapshot_failed", cause)), ); }), @@ -57,9 +55,7 @@ export const layer = HttpApiBuilder.group( yield* requireEnvironmentScope(scope); } const operation = projectMutationOperation(projects, args.payload); - return yield* startup - .enqueueCommand(operation) - .pipe(traceLocalHandlerWork, Effect.catch(failProjectMutation)); + return yield* startup.enqueueCommand(operation).pipe(Effect.catch(failProjectMutation)); }), ); }), diff --git a/apps/server/src/provider/CodexChatGptAuth.test.ts b/apps/server/src/provider/CodexChatGptAuth.test.ts index 96465947b651e7bd46a6e7e810cee502070c9486..a00be2b5864927ab80371b85282a0bc0170a4e60 100644 --- a/apps/server/src/provider/CodexChatGptAuth.test.ts +++ b/apps/server/src/provider/CodexChatGptAuth.test.ts @@ -17,8 +17,6 @@ import { exportJWK, generateKeyPair, SignJWT } from "jose"; import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; import * as ServerEnvironment from "../environment/ServerEnvironment.ts"; import * as ProviderCredentialStore from "./ProviderCredentialStore.ts"; -import * as ServerSettings from "../serverSettings.ts"; -import * as AnalyticsService from "../telemetry/AnalyticsService.ts"; import { makeCodexChatGptAuth } from "./CodexChatGptAuth.ts"; const assertSameCallback = (actual: string | null, expected: string | null) => { @@ -33,7 +31,6 @@ const instanceId = ProviderInstanceId.make("managed-codex-test"); const makeHarnessFor = Effect.fnUntraced(function* ( instanceId: ProviderInstanceId, bytes: Map = new Map(), - failAnalytics = false, ) { const environmentId = environmentIds.get(bytes) ?? EnvironmentId.make(NodeCrypto.randomUUID()); environmentIds.set(bytes, environmentId); @@ -208,25 +205,11 @@ const makeHarnessFor = Effect.fnUntraced(function* ( const address = server.address(); if (!address || typeof address === "string") throw new Error("mock address"); origin = `http://127.0.0.1:${address.port}`; - const analyticsEvents: { - event: string; - properties: Readonly> | undefined; - }[] = []; - const analytics = AnalyticsService.AnalyticsService.of({ - record: (event, properties) => - failAnalytics - ? Effect.die("analytics unavailable") - : Effect.sync(() => { - analyticsEvents.push({ event, properties }); - }), - flush: Effect.void, - }); const auth = yield* makeCodexChatGptAuth({ instanceId, discoveryUrl: `${origin}/discovery`, resource: `${origin}/v1`, }).pipe( - Effect.provideService(AnalyticsService.AnalyticsService, analytics), Effect.provideService(ServerSecretStore.ServerSecretStore, secrets), Effect.provideService(ServerEnvironment.ServerEnvironmentIdentity, environment), ); @@ -278,7 +261,6 @@ const makeHarnessFor = Effect.fnUntraced(function* ( }); return { auth, - analyticsEvents, secrets, environmentId, bytes, @@ -293,7 +275,7 @@ const makeHarnessFor = Effect.fnUntraced(function* ( }, "owner", { discoveryUrl: `${origin}/discovery`, resource: `${origin}/v1` }, - ).pipe(Stream.provideService(AnalyticsService.AnalyticsService, analytics)), + ), finishCallback: (state: { authorizationUrl: string | null }) => Effect.promise(() => fetch(prepareCallback(state))), destination: Effect.gen(function* () { @@ -315,7 +297,6 @@ const makeHarnessFor = Effect.fnUntraced(function* ( discoveryUrl: `${origin}/discovery`, resource: `${origin}/v1`, }).pipe( - Effect.provideService(AnalyticsService.AnalyticsService, analytics), Effect.provideService(ServerSecretStore.ServerSecretStore, destinationStore), Effect.provideService(ServerEnvironment.ServerEnvironmentIdentity, environment), ); @@ -1700,24 +1681,6 @@ it.effect("primary completes OAuth and destination imports and owns the refresh ); assert.isTrue(stopped); assert.strictEqual(imported.phase, "succeeded"); - assert.deepStrictEqual( - h.analyticsEvents.map(({ event }) => event), - [ - "chatgpt.auth.started", - "chatgpt.auth.completed", - "chatgpt.transfer.started", - "chatgpt.transfer.completed", - ], - ); - assert.isTrue( - h.analyticsEvents.every(({ properties }) => properties?.flow === "primary_handoff"), - ); - assert.strictEqual(h.analyticsEvents[1]?.properties?.outcome, "succeeded"); - assert.strictEqual(h.analyticsEvents[3]?.properties?.outcome, "succeeded"); - assert.notProperty(h.analyticsEvents[1]?.properties ?? {}, "connectedAccountCount"); - assert.strictEqual(h.analyticsEvents[1]?.properties?.intent, "different_account"); - assert.strictEqual(h.analyticsEvents[3]?.properties?.connectedAccountCount, 1); - assert.strictEqual(h.analyticsEvents[3]?.properties?.savedConnectionCount, 1); const saved = Option.getOrThrow(yield* destination.auth.read); assert.strictEqual(saved.clientId, result.profile.registration.clientId); assert.strictEqual(saved.refreshToken, "initial-refresh"); @@ -1839,214 +1802,13 @@ it.effect( ), ); -it.effect("records one anonymous auth outcome per success, failure, or cancellation", () => - provision( - Effect.gen(function* () { - const h = yield* makeHarness; - yield* h.signIn; - yield* h.phase("succeeded"); - h.setInvalidNonce(); - yield* h.signIn; - yield* h.phase("failed"); - const { waiting } = yield* h.startRemote(); - yield* h.auth.controller.cancel("owner", waiting.flowId!); - yield* h.phase("cancelled"); - const completed = h.analyticsEvents.filter(({ event }) => event === "chatgpt.auth.completed"); - assert.deepStrictEqual( - completed.map(({ properties }) => properties?.outcome), - ["succeeded", "failed", "cancelled"], - ); - assert.strictEqual( - h.analyticsEvents.filter(({ event }) => event === "chatgpt.auth.started").length, - 3, - ); - assert.strictEqual(completed[1]?.properties?.failureStage, "verify"); - for (const { properties } of completed) { - assert.isNumber(properties?.durationMs); - assert.strictEqual(properties?.flow, "direct"); - assert.deepStrictEqual( - Object.keys(properties!).sort(), - [ - ...(properties?.outcome === "succeeded" - ? [ - "accountCountScope", - "connectedAccountCount", - "connectedConnectionCount", - "savedConnectionCount", - "unidentifiedConnectedConnectionCount", - ] - : []), - "callbackMode", - "durationMs", - ...(properties?.failureStage ? ["failureStage"] : []), - "flow", - "intent", - "outcome", - ].sort(), - ); - } - const serialized = JSON.stringify(h.analyticsEvents); - for (const secret of [ - "hidden@example.test", - "oaiapp_test", - "user-test", - "access_token", - "refresh_token", - ]) { - assert.notInclude(serialized, secret); - } - }), - ), -); - -it.effect("telemetry failures do not fail a verified ChatGPT sign-in", () => - provision( - Effect.gen(function* () { - const h = yield* makeHarnessFor(instanceId, new Map(), true); - yield* h.signIn; - const state = yield* h.phase("succeeded"); - assert.strictEqual(state.phase, "succeeded"); - assert.isTrue(Option.isSome(yield* h.auth.read)); - }), - ), -); - -it.effect("counts accounts separately from saved connections across additions and reconnects", () => - provision( - Effect.gen(function* () { - const h = yield* makeHarness; - yield* h.signIn; - yield* h.phase("succeeded"); - yield* h.signIn; - yield* h.phase("succeeded"); - h.setCallbackClientId("oaiapp_same_account"); - h.setIdentity("another-client-subject", "HIDDEN@example.test"); - yield* h.changeAccount; - yield* h.phase("succeeded"); - h.setCallbackClientId("oaiapp_other_account"); - h.setIdentity("other-user", "other@example.test"); - yield* h.changeAccount; - yield* h.phase("succeeded"); - yield* h.auth.controller.logout(Effect.void); - h.setCallbackClientId("oaiapp_test"); - h.setIdentity("user-test", "hidden@example.test"); - yield* h.reconnectProfile("oaiapp_test"); - yield* h.phase("succeeded"); - const completed = h.analyticsEvents.filter(({ event }) => event === "chatgpt.auth.completed"); - assert.deepStrictEqual( - completed.map(({ properties }) => [ - properties?.connectedAccountCount, - properties?.connectedConnectionCount, - properties?.savedConnectionCount, - ]), - [ - [1, 1, 1], - [1, 1, 1], - [1, 2, 2], - [2, 3, 3], - [1, 2, 3], - ], - ); - assert.isTrue( - completed.every(({ properties }) => properties?.unidentifiedConnectedConnectionCount === 0), - ); - assert.notInclude(JSON.stringify(h.analyticsEvents), "example.test"); - }), - ), -); - -it.effect("includes accounts from other Codex instances in the environment", () => - provision( - Effect.gen(function* () { - const bytes = new Map(); - const personal = yield* makeHarnessFor(instanceId, bytes); - const work = yield* makeHarnessFor(ProviderInstanceId.make("managed-work"), bytes); - yield* personal.signIn; - yield* personal.phase("succeeded"); - work.setCallbackClientId("oaiapp_work"); - work.setIdentity("work-user", "work@example.test"); - yield* work.signIn; - yield* work.phase("succeeded"); - const completed = work.analyticsEvents.find( - ({ event }) => event === "chatgpt.auth.completed", - ); - assert.strictEqual(completed?.properties?.accountCountScope, "environment"); - assert.strictEqual(completed?.properties?.connectedAccountCount, 2); - assert.strictEqual(completed?.properties?.connectedConnectionCount, 2); - assert.strictEqual(completed?.properties?.savedConnectionCount, 2); - }).pipe( - Effect.provide( - ServerSettings.layerTest({ - providerInstances: { - [instanceId]: { driver: "codex", enabled: true }, - [ProviderInstanceId.make("managed-work")]: { driver: "codex", enabled: true }, - }, - }), - ), - ), - ), -); - -it.effect("an unreadable unrelated profile omits counts without failing sign-in", () => - provision( - Effect.gen(function* () { - const h = yield* makeHarness; - const unrelated = yield* ProviderCredentialStore.make("codex-chatgpt", "codex").pipe( - Effect.provideService(ServerSecretStore.ServerSecretStore, h.secrets), - ); - // The harness owns its store; seed the corresponding binding in that store. - h.bytes.set(unrelated.binding.key, new TextEncoder().encode("invalid")); - yield* h.signIn; - yield* h.phase("succeeded"); - const completed = h.analyticsEvents.find(({ event }) => event === "chatgpt.auth.completed"); - assert.strictEqual(completed?.properties?.outcome, "succeeded"); - assert.notProperty(completed?.properties ?? {}, "connectedAccountCount"); - }).pipe(Effect.provide(ServerSettings.layerTest())), - ), -); - -it.effect("reports connections without an email separately from identifiable accounts", () => - provision( - Effect.gen(function* () { - const h = yield* makeHarness; - yield* h.signIn; - yield* h.phase("succeeded"); - const store = yield* ProviderCredentialStore.make("codex-chatgpt", instanceId).pipe( - Effect.provideService(ServerSecretStore.ServerSecretStore, h.secrets), - ); - yield* store.set( - new TextEncoder().encode( - JSON.stringify({ - activeClientId: "oaiapp_test", - sessions: [{ ...Option.getOrThrow(yield* h.auth.read), email: null }], - }), - ), - ); - h.setCallbackClientId("oaiapp_other_account"); - h.setIdentity("other-user", "other@example.test"); - yield* h.changeAccount; - yield* h.phase("succeeded"); - const completed = h.analyticsEvents.findLast( - ({ event }) => event === "chatgpt.auth.completed", - ); - assert.strictEqual(completed?.properties?.connectedAccountCount, 1); - assert.strictEqual(completed?.properties?.connectedConnectionCount, 2); - assert.strictEqual(completed?.properties?.unidentifiedConnectedConnectionCount, 1); - }), - ), -); - -it.effect("records an expired auth outcome when sign-in reaches its deadline", () => +it.effect("expires sign-in when it reaches its deadline", () => provision( Effect.gen(function* () { const h = yield* makeHarness; yield* h.startRemote(); yield* TestClock.adjust(300_001); assert.include((yield* h.phase("failed")).message ?? "", "expired"); - const completed = h.analyticsEvents.filter(({ event }) => event === "chatgpt.auth.completed"); - assert.strictEqual(completed.length, 1); - assert.strictEqual(completed[0]?.properties?.outcome, "expired"); - assert.isAtLeast(completed[0]?.properties?.durationMs as number, 300_000); }), ), ); diff --git a/apps/server/src/provider/CodexChatGptAuth.ts b/apps/server/src/provider/CodexChatGptAuth.ts index 2eda1e31533dedc320fc5c623b17d13d8bab21d5..8ecf3f2b265b8f41c33de94201a5968542252008 100644 --- a/apps/server/src/provider/CodexChatGptAuth.ts +++ b/apps/server/src/provider/CodexChatGptAuth.ts @@ -10,9 +10,6 @@ import { } from "@t3tools/contracts"; import { codexCallbackUrl } from "@t3tools/shared/codexAuthHandoff"; import * as Clock from "effect/Clock"; -import * as Cause from "effect/Cause"; -import * as AnalyticsService from "../telemetry/AnalyticsService.ts"; -import * as Crypto from "effect/Crypto"; import * as Exit from "effect/Exit"; import { codexAuthCallbackPage, codexAuthReturnUrl } from "./CodexAuthCallbackPage.ts"; import * as Effect from "effect/Effect"; @@ -25,10 +22,8 @@ import { HttpClient, HttpClientRequest } from "effect/http"; import * as ProviderCredentialStore from "./ProviderCredentialStore.ts"; import { withChatGptSessionLock } from "./CodexChatGptSessionLock.ts"; import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; -import * as ServerSettings from "../serverSettings.ts"; import * as ServerEnvironment from "../environment/ServerEnvironment.ts"; -const isSetupError = Schema.is(ProviderSetupError); const RESOURCE = "https://api.openai.com/v1"; const REQUIRED_SCOPE = "chatgpt.tokens.use.direct"; const DISCOVERY = "https://auth.openai.com/.well-known/openid-configuration"; @@ -105,53 +100,7 @@ export const makeCodexChatGptAuth = Effect.fn("makeCodexChatGptAuth")(function* readonly resource?: string; readonly defaultReturnUrl?: string; readonly reconnectProfile?: ChatGptReconnectProfile | null; - readonly telemetryFlow?: "direct" | "primary_handoff"; }) { - const analytics = yield* Effect.serviceOption(AnalyticsService.AnalyticsService); - const settings = yield* Effect.serviceOption(ServerSettings.ServerSettingsService); - const track = ( - event: "auth" | "transfer", - properties: Readonly>, - task: Effect.Effect, - expiresAt?: number, - ) => - Effect.gen(function* () { - if (Option.isNone(analytics)) return yield* task; - const record = (name: string, properties: Readonly>) => - analytics.value.record(name, properties).pipe(Effect.ignoreCause); - const startedAt = yield* Clock.currentTimeMillis; - yield* record(`chatgpt.${event}.started`, properties); - return yield* task.pipe( - Effect.onExit((result) => - Effect.gen(function* () { - const endedAt = yield* Clock.currentTimeMillis; - const error = Exit.isFailure(result) - ? Cause.findErrorOption(result.cause) - : Option.none(); - const counts = - Exit.isSuccess(result) && - (event === "transfer" || options.telemetryFlow !== "primary_handoff") - ? yield* accountCounts.pipe(Effect.catchCause(() => Effect.succeed({}))) - : {}; - yield* record(`chatgpt.${event}.completed`, { - ...counts, - ...properties, - outcome: Exit.isSuccess(result) - ? "succeeded" - : Cause.hasInterruptsOnly(result.cause) - ? expiresAt !== undefined && endedAt >= expiresAt - ? "expired" - : "cancelled" - : "failed", - durationMs: Math.max(0, endedAt - startedAt), - ...(Option.isSome(error) && isSetupError(error.value) - ? { failureStage: error.value.operation } - : {}), - }); - }), - ), - ); - }); const http = yield* HttpClient.HttpClient; const store = yield* ProviderCredentialStore.make("codex-chatgpt", options.instanceId); const registrationStore = yield* ProviderCredentialStore.make( @@ -159,7 +108,6 @@ export const makeCodexChatGptAuth = Effect.fn("makeCodexChatGptAuth")(function* options.instanceId, ); const secrets = yield* ServerSecretStore.ServerSecretStore; - const crypto = yield* Crypto.Crypto; const environmentLocks = sessionLocks.get(secrets) ?? new Map(); sessionLocks.set(secrets, environmentLocks); const lock = environmentLocks.get(store.binding.key) ?? (yield* Semaphore.make(1)); @@ -264,54 +212,6 @@ export const makeCodexChatGptAuth = Effect.fn("makeCodexChatGptAuth")(function* ), ), ); - const accountCounts = Effect.gen(function* () { - const instanceIds = new Set([options.instanceId]); - if (Option.isSome(settings)) { - const current = yield* settings.value.getSettings; - // Include the legacy default instance as well as explicitly configured ones. - instanceIds.add("codex"); - for (const [id, instance] of Object.entries(current.providerInstances)) { - if (instance.driver === "codex") instanceIds.add(id); - } - } - const accounts = new Set(); - const connections = new Set(); - const savedConnections = new Set(); - let unidentifiedConnectedConnectionCount = 0; - for (const id of instanceIds) { - const registrations = yield* ProviderCredentialStore.make("codex-chatgpt-registration", id); - const registrationBytes = yield* registrations.get; - if (Option.isSome(registrationBytes)) { - const saved = yield* decodeRegistration(new TextDecoder().decode(registrationBytes.value)); - for (const profile of "profiles" in saved ? saved.profiles : [saved]) { - savedConnections.add(profile.clientId); - } - } - const credentials = yield* ProviderCredentialStore.make("codex-chatgpt", id); - const credentialBytes = yield* credentials.get; - if (Option.isNone(credentialBytes)) continue; - const saved = yield* decodeSessions(new TextDecoder().decode(credentialBytes.value)); - for (const session of "sessions" in saved ? saved.sessions : [saved]) { - if (!session.scopes.includes(REQUIRED_SCOPE) || connections.has(session.clientId)) continue; - connections.add(session.clientId); - // Subjects are client-scoped. Use verified email only for counting locally, - // never export it or merge the underlying profiles. - const email = session.email?.trim().toLowerCase(); - if (email) accounts.add(email); - else unidentifiedConnectedConnectionCount++; - } - } - return { - accountCountScope: Option.isSome(settings) ? "environment" : "provider_instance", - connectedAccountCount: accounts.size, - connectedConnectionCount: connections.size, - savedConnectionCount: savedConnections.size, - unidentifiedConnectedConnectionCount, - }; - }).pipe( - Effect.provideService(ServerSecretStore.ServerSecretStore, secrets), - Effect.provideService(Crypto.Crypto, crypto), - ); const saveRegistration = Effect.fnUntraced(function* (profile: typeof Registration.Type) { const saved = yield* readRegistrations; profile = { @@ -861,28 +761,7 @@ export const makeCodexChatGptAuth = Effect.fn("makeCodexChatGptAuth")(function* })), ]; }), - authenticate: (method, context) => - lock.withPermit( - track( - "auth", - { - flow: options.telemetryFlow ?? "direct", - intent: - options.telemetryFlow === "primary_handoff" - ? options.reconnectProfile - ? "saved_profile" - : "different_account" - : method === "chatgpt-change-account" - ? "different_account" - : method.startsWith("chatgpt-profile:") - ? "saved_profile" - : "default", - callbackMode: context.callbackMode ?? "server", - }, - authenticate(method, context), - context.expiresAt, - ), - ), + authenticate: (method, context) => lock.withPermit(authenticate(method, context)), logout: lock.withPermit(withSessionLock(logout)), }); const reconnectProfile = Effect.fnUntraced(function* (methodId: string) { @@ -971,12 +850,8 @@ export const makeCodexChatGptAuth = Effect.fn("makeCodexChatGptAuth")(function* yield* save(profile.credentials, true); }); return lock.withPermit( - track( - "transfer", - { flow: "primary_handoff" }, - validate.pipe( - Effect.andThen(controller.adoptCredentials!(withSessionLock(saveImported), stopSessions)), - ), + validate.pipe( + Effect.andThen(controller.adoptCredentials!(withSessionLock(saveImported), stopSessions)), ), ); }; diff --git a/apps/server/src/provider/CodexChatGptHandoff.ts b/apps/server/src/provider/CodexChatGptHandoff.ts index d2a9e042836a43ca163410edae3a73f6ffae4873..da9ad2edfb920c889bea7f2aecc2b4c805752151 100644 --- a/apps/server/src/provider/CodexChatGptHandoff.ts +++ b/apps/server/src/provider/CodexChatGptHandoff.ts @@ -35,7 +35,6 @@ export function subscribeChatGptHandoff( ...endpoints, instanceId: input.instanceId, reconnectProfile: input.profile, - telemetryFlow: "primary_handoff", defaultReturnUrl: input.returnUrl, }).pipe( Effect.provideService(ServerSecretStore.ServerSecretStore, store), diff --git a/apps/server/src/pullRequest/http.ts b/apps/server/src/pullRequest/http.ts index 746e29a506a658cabc60c7a2064bb6f5bc40c3ac..8c0c28abd8a4499326250000c2714e6f416f3dce 100644 --- a/apps/server/src/pullRequest/http.ts +++ b/apps/server/src/pullRequest/http.ts @@ -3,7 +3,6 @@ import * as Effect from "effect/Effect"; import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder"; import { annotateEnvironmentRequest, requireEnvironmentScope } from "../auth/http.ts"; -import { traceLocalHandlerWork } from "../cloud/traceRelayRequest.ts"; import * as PullRequestService from "./PullRequestService.ts"; /** The patch is often the largest PR payload and benefits from HTTP compression and flow control. */ @@ -17,7 +16,7 @@ export const layer = HttpApiBuilder.group( Effect.fn("environment.pullRequests.diff")(function* (args) { yield* annotateEnvironmentRequest(args.endpoint.name); yield* requireEnvironmentScope(AuthOrchestrationReadScope); - return yield* pullRequests.diff(args.payload).pipe(traceLocalHandlerWork); + return yield* pullRequests.diff(args.payload); }), ); }), diff --git a/apps/server/src/relay/AgentAwarenessRelay.ts b/apps/server/src/relay/AgentAwarenessRelay.ts index ae9d053ac1fb68dc4b76e9419bd42e3a0ef289ae..e55cd275e35b23d6c54f128853c260fced219e67 100644 --- a/apps/server/src/relay/AgentAwarenessRelay.ts +++ b/apps/server/src/relay/AgentAwarenessRelay.ts @@ -14,7 +14,6 @@ import { import { projectThreadAwarenessV2 } from "@t3tools/shared/agentAwareness"; import { turnItemUpdateCanEndBackgroundWork } from "@t3tools/shared/orchestrationV2PendingBackgroundWork"; import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker"; -import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; import { normalizeRelayIssuer, RELAY_ACTIVITY_PUBLISH_TYP, @@ -521,7 +520,7 @@ export const make = Effect.gen(function* () { ok: response.ok, deliveries: deliveryStats(response.deliveries), }); - }).pipe(Effect.withSpan("relay.agent_activity.publish"), withRelayClientTracing); + }).pipe(Effect.withSpan("relay.agent_activity.publish")); // Per-thread shell read: this publish runs for every activity-relevant // domain event, so materializing the full shell here would make the cost diff --git a/apps/server/src/scheduledTasks/webhookRoute.ts b/apps/server/src/scheduledTasks/webhookRoute.ts index 9fcf8344f50d7a769f0ac0e97dc2d83217b93f87..5aec227015ad2c7e0842bf4ce55acce97b4f1baa 100644 --- a/apps/server/src/scheduledTasks/webhookRoute.ts +++ b/apps/server/src/scheduledTasks/webhookRoute.ts @@ -7,7 +7,6 @@ import * as HttpIncomingMessage from "effect/http/HttpIncomingMessage"; import type * as HttpServerRequest from "effect/http/HttpServerRequest"; import * as HttpServerResponse from "effect/http/HttpServerResponse"; import * as HttpTraceContext from "effect/http/HttpTraceContext"; -import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder"; import * as Metrics from "../observability/Metrics.ts"; @@ -100,8 +99,7 @@ export const layer = HttpApiBuilder.group( }) .pipe( Option.isSome(relayParent) - ? (effect) => - effect.pipe(Effect.withParentSpan(relayParent.value), withRelayClientTracing) + ? (effect) => effect.pipe(Effect.withParentSpan(relayParent.value)) : (effect) => effect, // Defects too, so the sender only ever sees the fixed error body. Effect.catchCause((cause) => diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 47782e1e5a6c0977caadf5f31972de4548626397..ce907c44111caa2f5189d37c44e4ce40414fb073 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -67,8 +67,6 @@ import * as PullRequestFilesViewed from "./persistence/PullRequestFilesViewed.ts import * as PullRequestReviewGuides from "./persistence/PullRequestReviewGuides.ts"; import * as PullRequestReviewGuideService from "./pullRequest/PullRequestReviewGuideService.ts"; import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts"; -import * as AnalyticsService from "./telemetry/AnalyticsService.ts"; -import * as ProviderEventIngestor from "./orchestration-v2/ProviderEventIngestor.ts"; import * as ModelManifest from "./provider/ModelManifest.ts"; import * as ResetCreditCoordinator from "./provider/resetCreditCoordinator.ts"; import * as EventNdjsonLogger from "./provider/EventNdjsonLogger.ts"; @@ -162,7 +160,6 @@ import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; import * as CloudHttp from "./cloud/http.ts"; import * as CloudLink from "./cloud/CloudLink.ts"; import { pendingServiceUpdateExists } from "./cloud/updateHandoff.ts"; -import * as RelayTracing from "./cloud/relayTracing.ts"; import * as CloudManagedEndpointRuntime from "./cloud/ManagedEndpointRuntime.ts"; import { MANAGED_TUNNEL_FIRST_REGISTRATION_JITTER, @@ -510,7 +507,6 @@ const layerScheduledTaskWebhookOrigin = Layer.effect( const layerOrchestrationV2Runtime = RuntimeLayer.layerProduction.pipe( Layer.provide(layerScheduledTaskWebhookOrigin), - Layer.provide(ProviderEventIngestor.layerAnalytics), Layer.provide(layerCheckpointStore), Layer.provide(layerGitWorkflow), Layer.provide(ResourceCleanupService.layer), @@ -769,7 +765,6 @@ const layerRuntimeDependencies = layerRuntimeCoreDependencies.pipe( Layer.provideMerge(layerBackground), Layer.provideMerge(layerResourceDiagnostics), Layer.provideMerge(TraceDiagnostics.layer), - Layer.provideMerge(AnalyticsService.layer), Layer.provideMerge(ExternalLauncher.layer), Layer.provideMerge(RemoteOpenTargets.layer), Layer.provideMerge(DirectEndpoints.layer), @@ -1206,7 +1201,6 @@ const layerMakeServer = Layer.unwrap( ), ), Layer.provide(layerActivation), - Layer.provideMerge(RelayTracing.layerServerRelayBroker), Layer.provideMerge(layerHttpServer), Layer.provide(layerApplicationObservability), Layer.provideMerge(FetchHttpClient.layer), diff --git a/apps/server/src/serverRuntimeStartup.autoPull.test.ts b/apps/server/src/serverRuntimeStartup.autoPull.test.ts index 8313d33dc220dcda7b95a758ab022400c7e7d5bc..27e8a32cca8bb50a2854f4a195e92daa6f7a224c 100644 --- a/apps/server/src/serverRuntimeStartup.autoPull.test.ts +++ b/apps/server/src/serverRuntimeStartup.autoPull.test.ts @@ -30,7 +30,6 @@ import { ServerActivation } from "./serverActivation.ts"; import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts"; import * as ServerRuntimeStartup from "./serverRuntimeStartup.ts"; import * as ServerSettings from "./serverSettings.ts"; -import * as AnalyticsService from "./telemetry/AnalyticsService.ts"; import * as GitVcsDriver from "./vcs/GitVcsDriver.ts"; it.effect("parks automatic pull until activation without delaying command readiness", () => @@ -162,7 +161,6 @@ it.effect("parks automatic pull until activation without delaying command readin Effect.onInterrupt(() => Deferred.succeed(statusInterrupted, undefined)), ), }), - Layer.mock(AnalyticsService.AnalyticsService)({ record: () => Effect.void }), NodeCrypto.layer, Layer.mock(EnvironmentAuth.EnvironmentAuth)({}), Layer.mock(ExternalLauncher.ExternalLauncher)({}), diff --git a/apps/server/src/serverRuntimeStartup.ts b/apps/server/src/serverRuntimeStartup.ts index 84c002e3d60d562e731e1ce45ae48ba3cf93bf3c..93f93a0db5e9fb338714257df8e262b55a61f953 100644 --- a/apps/server/src/serverRuntimeStartup.ts +++ b/apps/server/src/serverRuntimeStartup.ts @@ -44,7 +44,6 @@ import * as AgentAwarenessRelay from "./relay/AgentAwarenessRelay.ts"; import * as ServerLifecycleEvents from "./serverLifecycleEvents.ts"; import * as ServerSettings from "./serverSettings.ts"; import { forkParked, forkParkedFiber } from "./serverActivation.ts"; -import * as AnalyticsService from "./telemetry/AnalyticsService.ts"; import * as ServerEnvironment from "./environment/ServerEnvironment.ts"; import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; import { @@ -143,37 +142,6 @@ export const makeCommandGate = Effect.gen(function* () { } satisfies CommandGate; }); -const recordStartupHeartbeat = Effect.gen(function* () { - const analytics = yield* AnalyticsService.AnalyticsService; - const projects = yield* ProjectService.ProjectService; - const threads = yield* ThreadManagement.ThreadManagementService; - - const { threadCount, projectCount } = yield* Effect.all({ - projects: projects.snapshot, - threads: threads.getShellSnapshot(), - }).pipe( - Effect.map(({ projects: projectSnapshot, threads: shellSnapshot }) => ({ - projectCount: projectSnapshot.projects.length, - threadCount: shellSnapshot.threads.length + shellSnapshot.archivedThreads.length, - })), - Effect.catch((cause) => - Effect.logWarning("failed to gather V2 startup counts for telemetry", { - cause, - }).pipe( - Effect.as({ - threadCount: 0, - projectCount: 0, - }), - ), - ), - ); - - yield* analytics.record("server.boot.heartbeat", { - threadCount, - projectCount, - }); -}); - export const getAutoBootstrapThreadModelSelection = (): ModelSelection => ({ instanceId: ProviderInstanceId.make("codex"), model: DEFAULT_MODEL, @@ -581,13 +549,6 @@ const make = (options?: StartupOptions) => yield* forkParked( Effect.gen(function* () { - yield* Effect.logDebug("startup phase: recording startup heartbeat"); - yield* recordStartupHeartbeat.pipe( - Effect.withSpan("server.startup.heartbeat.record", { - attributes: { "startup.phase": "heartbeat.record" }, - }), - Effect.ignoreCause({ log: true }), - ); if (serverConfig.startupPresentation === "headless") { yield* Effect.logDebug("startup phase: headless access info"); const accessInfo = yield* issueHeadlessServeAccessInfo(); @@ -696,5 +657,3 @@ const make = (options?: StartupOptions) => export const layerWithOptions = (options?: StartupOptions) => Layer.effect(ServerRuntimeStartup, make(options)); - -const layer = layerWithOptions(); diff --git a/apps/server/src/telemetry/AnalyticsService.test.ts b/apps/server/src/telemetry/AnalyticsService.test.ts deleted file mode 100644 index 73f844e4017cb69d61f3674aa11b035e459903ef..0000000000000000000000000000000000000000 --- a/apps/server/src/telemetry/AnalyticsService.test.ts +++ /dev/null @@ -1,277 +0,0 @@ -import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer"; -import * as NodeServices from "@effect/platform-node/NodeServices"; -import { assert, it } from "@effect/vitest"; -import * as ConfigProvider from "effect/ConfigProvider"; -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; -import * as Schema from "effect/Schema"; -import * as TestClock from "effect/testing/TestClock"; -import * as HttpClient from "effect/http/HttpClient"; -import * as HttpClientError from "effect/http/HttpClientError"; -import * as HttpServer from "effect/http/HttpServer"; -import * as HttpServerRequest from "effect/http/HttpServerRequest"; -import * as HttpServerResponse from "effect/http/HttpServerResponse"; -import * as HostProcess from "@t3tools/shared/HostProcess"; - -import * as ServerConfig from "../config.ts"; -import { getTelemetryIdentifier } from "./Identify.ts"; -import * as AnalyticsService from "./AnalyticsService.ts"; - -interface RecordedBatchRequest { - readonly path: string; - readonly body: { - readonly batch?: ReadonlyArray<{ - readonly event?: string; - readonly properties?: { - readonly index?: number; - readonly clientType?: string; - readonly serverOs?: string; - readonly serverArch?: string; - readonly serverAppVersion?: string; - readonly serverMode?: string; - readonly t3CodeVersion?: string; - }; - }>; - } | null; -} - -interface RecordedBatchBody { - readonly batch: ReadonlyArray<{ - readonly event?: string; - readonly properties?: { - readonly index?: number; - readonly clientType?: string; - readonly serverOs?: string; - readonly serverArch?: string; - readonly serverAppVersion?: string; - readonly serverMode?: string; - readonly t3CodeVersion?: string; - }; - }>; -} - -const SentBatch = Schema.fromJsonString( - Schema.Struct({ - batch: Schema.Array(Schema.Struct({ uuid: Schema.String })), - }), -); - -/** - * HTTP client that reads each batch, then fails as if the connection dropped - * before the response arrived. PostHog stores these batches, so the server - * must not send them forever. - */ -const layerAcceptThenFailClient = (batches: Array>) => - Layer.succeed( - HttpClient.HttpClient, - HttpClient.make((request) => - Effect.gen(function* () { - if (request.body._tag === "Uint8Array") { - const body = yield* Schema.decodeEffect(SentBatch)( - new TextDecoder().decode(request.body.body), - ).pipe(Effect.orDie); - batches.push(body.batch); - } - return yield* new HttpClientError.HttpClientError({ - reason: new HttpClientError.TransportError({ request, cause: "connection reset" }), - }); - }), - ), - ); - -it("retryDelayMs doubles from 2s and stays under the 5 minute cap", () => { - assert.equal(AnalyticsService.retryDelayMs(1, 0), 1_000); - assert.equal(AnalyticsService.retryDelayMs(2, 0.999_999), 4_000); - assert.equal(AnalyticsService.retryDelayMs(30, 0), 150_000); - assert.equal(AnalyticsService.retryDelayMs(30, 0.999_999), 300_000); -}); - -it.layer(NodeServices.layer)("AnalyticsService test", (it) => { - it.effect("a batch that keeps failing is retried with backoff, then dropped", () => - Effect.gen(function* () { - const batches: Array> = []; - const layerRuntime = AnalyticsService.layer.pipe( - Layer.provideMerge( - ServerConfig.ServerConfig.layerTest(process.cwd(), { prefix: "t3-telemetry-retry-" }), - ), - Layer.provide( - ConfigProvider.layer( - ConfigProvider.fromUnknown({ - T3CODE_TELEMETRY_ENABLED: true, - T3CODE_POSTHOG_KEY: "phc_test_key", - T3CODE_POSTHOG_HOST: "http://localhost", - }), - ), - ), - Layer.provide( - Layer.mergeAll( - Layer.succeed(HostProcess.Platform, "win32"), - Layer.succeed(HostProcess.Architecture, "x64"), - layerAcceptThenFailClient(batches), - ), - ), - ); - - yield* Effect.gen(function* () { - const analytics = yield* AnalyticsService.AnalyticsService; - for (let index = 0; index < 20; index += 1) { - yield* analytics.record("test.retry", { index }); - } - // Before the fix this loop sent the batch about once a second. - for (let second = 0; second < 600; second += 1) { - yield* TestClock.adjust("1 second"); - } - }).pipe(Effect.provide(layerRuntime)); - - assert.equal(batches.length, 5); - const uuids = batches.map((batch) => batch.map((event) => event.uuid).join(",")); - assert.equal(new Set(uuids).size, 1, "every retry carries the same uuids"); - assert.equal(new Set(batches[0]?.map((event) => event.uuid)).size, 20); - }), - ); - - it.effect("flush drains all buffered events across multiple batches", () => - Effect.gen(function* () { - const capturedRequests: Array = []; - const layerServerConfig = ServerConfig.ServerConfig.layerTest(process.cwd(), { - prefix: "t3-telemetry-base-", - }); - - const layerTelemetry = AnalyticsService.layer.pipe(Layer.provideMerge(layerServerConfig)); - const layerConfig = ConfigProvider.layer( - ConfigProvider.fromUnknown({ - T3CODE_TELEMETRY_ENABLED: true, - T3CODE_POSTHOG_KEY: "phc_test_key", - T3CODE_POSTHOG_HOST: "http://localhost", - T3CODE_TELEMETRY_FLUSH_BATCH_SIZE: 20, - }), - ); - const layerBatchServer = HttpServer.serve( - Effect.gen(function* () { - const request = yield* HttpServerRequest.HttpServerRequest; - if (request.method !== "POST") { - return HttpServerResponse.empty({ status: 404 }); - } - - const payload = yield* request.json.pipe( - Effect.map((body) => body as RecordedBatchRequest["body"]), - Effect.orElseSucceed(() => null), - ); - - capturedRequests.push({ path: request.url, body: payload }); - - return HttpServerResponse.jsonUnsafe({}); - }), - ); - const layerRuntime = layerTelemetry.pipe( - Layer.provide(layerConfig), - Layer.provide( - Layer.mergeAll( - Layer.succeed(HostProcess.Platform, "linux"), - Layer.succeed(HostProcess.Architecture, "arm64"), - ), - ), - Layer.provideMerge(NodeHttpServer.layerTest), - ); - - yield* Effect.gen(function* () { - yield* Layer.launch(layerBatchServer).pipe(Effect.forkScoped); - const telemetryIdentifier = yield* getTelemetryIdentifier; - assert.equal(telemetryIdentifier !== null, true); - const analytics = yield* AnalyticsService.AnalyticsService; - - for (let index = 0; index < 45; index += 1) { - yield* analytics.record("test.flush.drain", { index }); - } - - yield* analytics.flush; - }).pipe(Effect.provide(layerRuntime)); - - const batchRequests = capturedRequests.filter( - (request): request is RecordedBatchRequest & { readonly body: RecordedBatchBody } => - Array.isArray(request.body?.batch), - ); - assert.equal(batchRequests.length, 3); - assert.equal( - batchRequests.every( - (request) => request.path.endsWith("/batch/") || request.path.endsWith("/batch"), - ), - true, - ); - const deliveredIndexes = batchRequests.flatMap((request) => - request.body.batch - .filter((event) => event.event === "test.flush.drain") - .map((event) => event.properties?.index) - .filter((index): index is number => typeof index === "number"), - ); - - const sorted = deliveredIndexes.toSorted((a, b) => a - b); - assert.equal(sorted.length, 45); - assert.deepEqual( - sorted, - Array.from({ length: 45 }, (_, index) => index), - ); - assert.equal( - batchRequests.every((request) => - request.body.batch.every((event) => event.properties?.clientType === "cli-web-client"), - ), - true, - ); - assert.equal( - batchRequests.every((request) => - request.body.batch.every( - (event) => - event.properties?.serverOs === "Linux" && - event.properties.serverArch === "arm64" && - event.properties.serverAppVersion === event.properties.t3CodeVersion && - event.properties.serverMode === "web", - ), - ), - true, - ); - }), - ); - - it.effect("does not send batch requests when telemetry is disabled", () => - Effect.gen(function* () { - const capturedPaths: Array = []; - const layerServerConfig = ServerConfig.ServerConfig.layerTest(process.cwd(), { - prefix: "t3-telemetry-disabled-", - }); - const layerTelemetry = AnalyticsService.layer.pipe(Layer.provideMerge(layerServerConfig)); - const layerConfig = ConfigProvider.layer( - ConfigProvider.fromUnknown({ - T3CODE_TELEMETRY_ENABLED: false, - T3CODE_POSTHOG_KEY: "phc_test_key", - T3CODE_POSTHOG_HOST: "http://localhost", - }), - ); - const layerBatchServer = HttpServer.serve( - Effect.gen(function* () { - const request = yield* HttpServerRequest.HttpServerRequest; - capturedPaths.push(request.url); - return HttpServerResponse.jsonUnsafe({}); - }), - ); - const layerRuntime = layerTelemetry.pipe( - Layer.provide(layerConfig), - Layer.provide( - Layer.mergeAll( - Layer.succeed(HostProcess.Platform, "linux"), - Layer.succeed(HostProcess.Architecture, "arm64"), - ), - ), - Layer.provideMerge(NodeHttpServer.layerTest), - ); - - yield* Effect.gen(function* () { - yield* Layer.launch(layerBatchServer).pipe(Effect.forkScoped); - const analytics = yield* AnalyticsService.AnalyticsService; - yield* analytics.record("test.disabled", { index: 1 }); - yield* analytics.flush; - }).pipe(Effect.provide(layerRuntime)); - - assert.deepEqual(capturedPaths, []); - }), - ); -}); diff --git a/apps/server/src/telemetry/AnalyticsService.ts b/apps/server/src/telemetry/AnalyticsService.ts deleted file mode 100644 index f481385647033bd8ed7a2f638094c7c3fe2c25ca..0000000000000000000000000000000000000000 --- a/apps/server/src/telemetry/AnalyticsService.ts +++ /dev/null @@ -1,288 +0,0 @@ -import { SERVER_VERSION } from "../version.ts"; -/** - * Anonymous PostHog telemetry service. - * - * Persists an installation-scoped anonymous identifier, buffers events in - * memory, and flushes batches over Effect's HTTP client. A failed batch is - * retried with backoff and dropped after a few tries. Each event carries a - * uuid, so PostHog can tell a retried copy from a new event. - * - * @module AnalyticsService - */ -import * as HostProcess from "@t3tools/shared/HostProcess"; -import type { ClientOs } from "@t3tools/contracts"; -import * as Clock from "effect/Clock"; -import * as Config from "effect/Config"; -import * as Context from "effect/Context"; -import * as Crypto from "effect/Crypto"; -import * as DateTime from "effect/DateTime"; -import * as Effect from "effect/Effect"; -import * as Exit from "effect/Exit"; -import * as Layer from "effect/Layer"; -import * as Option from "effect/Option"; -import * as Random from "effect/Random"; -import * as Ref from "effect/Ref"; -import * as Result from "effect/Result"; -import * as Semaphore from "effect/Semaphore"; -import * as HttpClient from "effect/http/HttpClient"; -import * as HttpClientRequest from "effect/http/HttpClientRequest"; -import * as HttpClientResponse from "effect/http/HttpClientResponse"; - -import * as ServerConfig from "../config.ts"; -import { getTelemetryIdentifier } from "./Identify.ts"; - -interface BufferedAnalyticsEvent { - readonly uuid: string; - readonly event: string; - readonly properties?: Readonly>; - readonly capturedAt: string; -} - -interface DeliveryState { - /** Batch that failed last. It is sent again before newer events. */ - readonly failedBatch: ReadonlyArray; - /** Failed sends of `failedBatch`. */ - readonly batchAttempts: number; - /** Failed sends since the last success. Sets the backoff delay. */ - readonly failures: number; - /** The background flush does not send before this time (epoch ms). */ - readonly retryAt: number; -} - -const FLUSH_INTERVAL_MS = 1_000; -// A hung send would hold the flush lock, and with it the shutdown flush. -const SEND_TIMEOUT = "10 seconds"; -const MAX_BATCH_ATTEMPTS = 5; -const RETRY_BASE_DELAY_MS = 2_000; -const RETRY_MAX_DELAY_MS = 300_000; - -/** - * Delay before the next send after `failures` consecutive failed sends. The - * ceiling doubles from 2s up to 5 minutes, and the delay is a random point in - * its upper half. `random` is in [0, 1). - */ -export function retryDelayMs(failures: number, random: number): number { - const ceiling = Math.min(RETRY_MAX_DELAY_MS, RETRY_BASE_DELAY_MS * 2 ** (failures - 1)); - return Math.round(ceiling / 2 + (ceiling / 2) * random); -} - -const TelemetryEnvConfig = Config.all({ - posthogKey: Config.String("T3CODE_POSTHOG_KEY").pipe( - Config.withDefault("phc_XOWci4oZP4VvLiEyrFqkFjP4CZn55mjYYBMREK5Wd6m"), - ), - posthogHost: Config.String("T3CODE_POSTHOG_HOST").pipe( - Config.withDefault("https://us.i.posthog.com"), - ), - enabled: Config.Boolean("T3CODE_TELEMETRY_ENABLED").pipe(Config.withDefault(true)), - flushBatchSize: Config.Number("T3CODE_TELEMETRY_FLUSH_BATCH_SIZE").pipe(Config.withDefault(20)), - maxBufferedEvents: Config.Number("T3CODE_TELEMETRY_MAX_BUFFERED_EVENTS").pipe( - Config.withDefault(1_000), - ), - wslDistroName: Config.String("WSL_DISTRO_NAME").pipe(Config.option), -}); - -export class AnalyticsService extends Context.Service< - AnalyticsService, - { - /** Record an anonymous event for best-effort buffered delivery. */ - readonly record: ( - event: string, - properties?: Readonly>, - ) => Effect.Effect; - - /** Flush all currently queued telemetry events. */ - readonly flush: Effect.Effect; - } ->()("t3/telemetry/AnalyticsService") { - /** No-op layer for callers that intentionally disable telemetry. */ - static readonly layerTest = Layer.succeed( - AnalyticsService, - AnalyticsService.of({ - record: () => Effect.void, - flush: Effect.void, - }), - ); -} - -function serverOsFromNodePlatform(platform: string): ClientOs { - switch (platform) { - case "darwin": - return "macOS"; - case "win32": - return "Windows"; - case "linux": - return "Linux"; - case "android": - return "Android"; - default: - return "other"; - } -} - -/** @public Service construction is part of the canonical Effect module API. */ -export const make = Effect.gen(function* () { - const telemetryConfig = yield* TelemetryEnvConfig; - const httpClient = yield* HttpClient.HttpClient; - const serverConfig = yield* ServerConfig.ServerConfig; - const identifier = yield* getTelemetryIdentifier; - const crypto = yield* Crypto.Crypto; - const bufferRef = yield* Ref.make>([]); - const deliveryRef = yield* Ref.make({ - failedBatch: [], - batchAttempts: 0, - failures: 0, - retryAt: 0, - }); - // The background flush and the shutdown flush must not send the same batch at once. - const flushLock = yield* Semaphore.make(1); - const clientType = serverConfig.mode === "desktop" ? "desktop-app" : "cli-web-client"; - const hostPlatform = yield* HostProcess.Platform; - const hostArchitecture = yield* HostProcess.Architecture; - - const enqueueBufferedEvent = ( - uuid: string, - event: string, - properties?: Readonly>, - ) => - Effect.flatMap(DateTime.now, (now) => - Ref.modify(bufferRef, (current) => { - const appended = [ - ...current, - { - uuid, - event, - ...(properties ? { properties } : {}), - capturedAt: DateTime.formatIso(now), - } satisfies BufferedAnalyticsEvent, - ]; - - const next = - appended.length > telemetryConfig.maxBufferedEvents - ? appended.slice(appended.length - telemetryConfig.maxBufferedEvents) - : appended; - - return [ - { - size: next.length, - dropped: next.length !== appended.length, - } as const, - next, - ] as const; - }), - ); - - const sendBatch = Effect.fn("AnalyticsService.sendBatch")(function* ( - events: ReadonlyArray, - ) { - if (!telemetryConfig.enabled || !identifier) return; - - const payload = { - api_key: telemetryConfig.posthogKey, - batch: events.map((event) => ({ - uuid: event.uuid, - event: event.event, - distinct_id: identifier, - properties: { - ...event.properties, - $process_person_profile: false, - platform: hostPlatform, - wsl: Option.getOrUndefined(telemetryConfig.wslDistroName), - arch: hostArchitecture, - t3CodeVersion: SERVER_VERSION, - clientType, - serverOs: serverOsFromNodePlatform(hostPlatform), - serverArch: hostArchitecture, - serverWslDistro: Option.getOrUndefined(telemetryConfig.wslDistroName), - serverAppVersion: SERVER_VERSION, - serverMode: serverConfig.mode, - }, - timestamp: event.capturedAt, - })), - }; - - yield* HttpClientRequest.post(`${telemetryConfig.posthogHost}/batch/`).pipe( - HttpClientRequest.bodyJson(payload), - Effect.flatMap(httpClient.execute), - Effect.flatMap(HttpClientResponse.filterStatusOk), - Effect.timeout(SEND_TIMEOUT), - ); - }); - - const takeBatch = Ref.modify(bufferRef, (current) => { - const nextBatch = current.slice(0, telemetryConfig.flushBatchSize); - return [nextBatch, current.slice(nextBatch.length)] as const; - }); - - // Sends batches until the buffer is empty or a send fails. A failed batch is - // kept for the next flush, and dropped after MAX_BATCH_ATTEMPTS failed sends. - const flush: AnalyticsService["Service"]["flush"] = Effect.gen(function* () { - while (true) { - const delivery = yield* Ref.get(deliveryRef); - const batch = delivery.failedBatch.length > 0 ? delivery.failedBatch : yield* takeBatch; - if (batch.length === 0) { - return; - } - - const sent = yield* Effect.result(sendBatch(batch)); - if (Result.isSuccess(sent)) { - yield* Ref.set(deliveryRef, { failedBatch: [], batchAttempts: 0, failures: 0, retryAt: 0 }); - continue; - } - - const failures = delivery.failures + 1; - const batchAttempts = delivery.batchAttempts + 1; - const retryAt = (yield* Clock.currentTimeMillis) + retryDelayMs(failures, yield* Random.next); - if (batchAttempts < MAX_BATCH_ATTEMPTS) { - yield* Ref.set(deliveryRef, { failedBatch: batch, batchAttempts, failures, retryAt }); - yield* Effect.logDebug("Failed to send telemetry batch; will retry", { - attempt: batchAttempts, - cause: sent.failure, - }); - return; - } - yield* Ref.set(deliveryRef, { failedBatch: [], batchAttempts: 0, failures, retryAt }); - yield* Effect.logWarning("Dropped telemetry batch after repeated send failures", { - events: batch.length, - attempts: batchAttempts, - cause: sent.failure, - }); - return; - } - }).pipe(flushLock.withPermit); - - const flushWhenDue = Effect.gen(function* () { - const { retryAt } = yield* Ref.get(deliveryRef); - if ((yield* Clock.currentTimeMillis) >= retryAt) { - yield* flush; - } - }); - - const record: AnalyticsService["Service"]["record"] = Effect.fn("AnalyticsService.record")( - function* (event, properties) { - if (!telemetryConfig.enabled || !identifier) return; - - // Telemetry is best effort: an event without a uuid is not sent. The - // Node implementation throws (a defect) rather than failing, so catch both. - const uuid = yield* Effect.exit(crypto.randomUUIDv7()); - if (Exit.isFailure(uuid)) return; - - const enqueueResult = yield* enqueueBufferedEvent(uuid.value, event, properties); - if (enqueueResult.dropped) { - yield* Effect.logDebug("analytics buffer full; dropping oldest event", { - size: enqueueResult.size, - event, - }); - } - }, - ); - - yield* Effect.forever(Effect.sleep(FLUSH_INTERVAL_MS).pipe(Effect.flatMap(() => flushWhenDue)), { - disableYield: true, - }).pipe(Effect.forkScoped); - - yield* Effect.addFinalizer(() => flush); - - return AnalyticsService.of({ record, flush }); -}); - -export const layer = Layer.effect(AnalyticsService, make); diff --git a/apps/server/src/telemetry/Identify.test.ts b/apps/server/src/telemetry/Identify.test.ts deleted file mode 100644 index b556499547aada050f91c77240f39f73bad69824..0000000000000000000000000000000000000000 --- a/apps/server/src/telemetry/Identify.test.ts +++ /dev/null @@ -1,253 +0,0 @@ -import * as NodeServices from "@effect/platform-node/NodeServices"; -import { assert, it } from "@effect/vitest"; -import * as Crypto from "effect/Crypto"; -import * as Effect from "effect/Effect"; -import * as Hex from "effect/encoding/Hex"; -import * as FileSystem from "effect/FileSystem"; -import * as Layer from "effect/Layer"; -import * as Logger from "effect/Logger"; -import * as Path from "effect/Path"; -import * as PlatformError from "effect/PlatformError"; -import * as References from "effect/References"; - -import * as ServerConfig from "../config.ts"; -import * as Identify from "./Identify.ts"; - -interface CapturedLog { - readonly message: unknown; - readonly annotations: Readonly>; -} - -const sha256 = Effect.fn("test.sha256")(function* (value: string) { - const crypto = yield* Crypto.Crypto; - return Hex.encode( - yield* crypto.digest("SHA-256", new TextEncoder().encode(value)).pipe(Effect.orDie), - ); -}); - -const makeCaptureLogger = (logs: CapturedLog[]) => - Logger.make(({ fiber, message }) => { - logs.push({ - message, - annotations: fiber.getRef(References.CurrentLogAnnotations), - }); - }); - -const findIdentityLog = ( - logs: ReadonlyArray, - source: Identify.TelemetryIdentitySource, - errorTag: string, -) => logs.find((log) => log.annotations.source === source && log.annotations.errorTag === errorTag); - -it.layer(NodeServices.layer)("telemetry identity", (it) => { - it.effect("uses the persisted anonymous id when provider identities are absent", () => - Effect.gen(function* () { - const config = yield* ServerConfig.ServerConfig; - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const anonymousId = "persisted-anonymous-id"; - - yield* fileSystem.writeFileString(config.anonymousIdPath, anonymousId); - - const identifier = yield* Identify.getTelemetryIdentifierForHome( - path.join(config.baseDir, "home"), - ); - - assert.equal(identifier, yield* sha256(anonymousId)); - }).pipe( - Effect.provide( - ServerConfig.layerTest(process.cwd(), { - prefix: "t3-telemetry-identify-anonymous-", - }), - ), - ), - ); - - it.effect("leaves no torn anonymous id behind when its write fails midway", () => - Effect.gen(function* () { - const config = yield* ServerConfig.ServerConfig; - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const tornWrites = FileSystem.FileSystem.of({ - ...fileSystem, - writeFileString: (filePath, data, options) => - fileSystem - .writeFileString(filePath, data.slice(0, Math.floor(data.length / 2)), options) - .pipe( - Effect.andThen( - Effect.fail( - PlatformError.systemError({ - _tag: "WriteZero", - module: "FileSystem", - method: "writeFileString", - pathOrDescriptor: filePath, - }), - ), - ), - ), - }); - - const identifier = yield* Identify.getTelemetryIdentifierForHome( - path.join(config.baseDir, "home"), - ).pipe(Effect.provideService(FileSystem.FileSystem, tornWrites)); - - assert.isNull(identifier); - assert.isFalse(yield* fileSystem.exists(config.anonymousIdPath)); - }).pipe( - Effect.provide( - ServerConfig.layerTest(process.cwd(), { - prefix: "t3-telemetry-identify-torn-", - }), - ), - ), - ); - - it.effect.each([ - { - login: "an API key", - secret: "sk-private-openai-api-key", - authJson: (secret: string) => `{"auth_mode":"apikey","OPENAI_API_KEY":"${secret}"}`, - }, - { - login: "an agent identity", - secret: "private-agent-identity-jwt", - authJson: (secret: string) => - `{"auth_mode":"agentIdentity","OPENAI_API_KEY":null,"agent_identity":"${secret}"}`, - }, - { - login: "a personal access token", - secret: "private-personal-access-token", - authJson: (secret: string) => `{"OPENAI_API_KEY":null,"personal_access_token":"${secret}"}`, - }, - ])("falls back quietly when Codex authenticates with $login", ({ secret, authJson }) => { - const logs: CapturedLog[] = []; - const logger = makeCaptureLogger(logs); - - return Effect.gen(function* () { - const config = yield* ServerConfig.ServerConfig; - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const homeDirectory = path.join(config.baseDir, "home"); - const codexAuthPath = path.join(homeDirectory, ".codex", "auth.json"); - const anonymousId = "tokenless-codex-anonymous-id"; - - yield* fileSystem.makeDirectory(path.dirname(codexAuthPath), { recursive: true }); - yield* fileSystem.writeFileString(codexAuthPath, authJson(secret)); - yield* fileSystem.writeFileString(config.anonymousIdPath, anonymousId); - - const identifier = yield* Identify.getTelemetryIdentifierForHome(homeDirectory); - - assert.equal(identifier, yield* sha256(anonymousId)); - assert.isUndefined(findIdentityLog(logs, "codex", "TelemetryIdentityDecodeError")); - assert.isUndefined(findIdentityLog(logs, "codex", "TelemetryIdentityReadError")); - const allLogs = logs - .map((log) => - [String(log.message), ...Object.values(log.annotations).map(String)].join("\n"), - ) - .join("\n"); - assert.notInclude(allLogs, secret); - }).pipe( - Effect.provide( - Layer.merge( - ServerConfig.layerTest(process.cwd(), { - prefix: "t3-telemetry-identify-tokenless-", - }), - Logger.layer([logger], { mergeWithExisting: false }), - ), - ), - ); - }); - - it.effect("logs structured decode context and falls back from malformed Codex auth", () => { - const logs: CapturedLog[] = []; - const logger = makeCaptureLogger(logs); - - return Effect.gen(function* () { - const config = yield* ServerConfig.ServerConfig; - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const homeDirectory = path.join(config.baseDir, "home"); - const codexAuthPath = path.join(homeDirectory, ".codex", "auth.json"); - const anonymousId = "decode-fallback-anonymous-id"; - const privateAccessToken = "private-codex-access-token"; - - yield* fileSystem.makeDirectory(path.dirname(codexAuthPath), { recursive: true }); - yield* fileSystem.writeFileString( - codexAuthPath, - `{"tokens":{"access_token":"${privateAccessToken}"}}`, - ); - yield* fileSystem.writeFileString(config.anonymousIdPath, anonymousId); - - const identifier = yield* Identify.getTelemetryIdentifierForHome(homeDirectory); - - assert.equal(identifier, yield* sha256(anonymousId)); - const decodeLog = findIdentityLog(logs, "codex", "TelemetryIdentityDecodeError"); - assert.isDefined(decodeLog); - assert.equal( - decodeLog?.message, - `Failed to decode codex telemetry identity at '${codexAuthPath}'.`, - ); - - assert.equal(decodeLog?.annotations.filePath, codexAuthPath); - assert.equal(decodeLog?.annotations.causeKind, "schema"); - assert.notProperty(decodeLog?.annotations ?? {}, "cause"); - const errorStack = decodeLog?.annotations.errorStack; - assert.isString(errorStack); - assert.include(errorStack, "Failed to decode codex telemetry identity"); - const annotations = Object.values(decodeLog?.annotations ?? {}) - .map(String) - .join("\n"); - assert.notInclude(annotations, privateAccessToken); - }).pipe( - Effect.provide( - Layer.merge( - ServerConfig.layerTest(process.cwd(), { - prefix: "t3-telemetry-identify-decode-", - }), - Logger.layer([logger], { mergeWithExisting: false }), - ), - ), - ); - }); - - it.effect("does not overwrite the anonymous id path after a non-NotFound read failure", () => { - const logs: CapturedLog[] = []; - const logger = makeCaptureLogger(logs); - - return Effect.gen(function* () { - const config = yield* ServerConfig.ServerConfig; - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - const homeDirectory = path.join(config.baseDir, "home"); - - yield* fileSystem.makeDirectory(config.anonymousIdPath); - - const identifier = yield* Identify.getTelemetryIdentifierForHome(homeDirectory); - - assert.isNull(identifier); - assert.deepEqual(yield* fileSystem.readDirectory(config.anonymousIdPath), []); - - const readLog = findIdentityLog(logs, "anonymous", "TelemetryIdentityReadError"); - assert.isDefined(readLog); - assert.equal(readLog?.annotations.filePath, config.anonymousIdPath); - assert.equal(readLog?.annotations.causeKind, "platform"); - assert.notEqual(readLog?.annotations.platformReason, "NotFound"); - assert.notProperty(readLog?.annotations ?? {}, "cause"); - const errorStack = readLog?.annotations.errorStack; - assert.isString(errorStack); - assert.include(errorStack, "Failed to read anonymous telemetry identity"); - assert.isUndefined( - findIdentityLog(logs, "anonymous", "TelemetryAnonymousIdPersistenceError"), - ); - }).pipe( - Effect.provide( - Layer.merge( - ServerConfig.layerTest(process.cwd(), { - prefix: "t3-telemetry-identify-read-", - }), - Logger.layer([logger], { mergeWithExisting: false }), - ), - ), - ); - }); -}); diff --git a/apps/server/src/telemetry/Identify.ts b/apps/server/src/telemetry/Identify.ts deleted file mode 100644 index 6fd75eb25231644d553c4e5d28bae97cda81c853..0000000000000000000000000000000000000000 --- a/apps/server/src/telemetry/Identify.ts +++ /dev/null @@ -1,317 +0,0 @@ -import * as Crypto from "effect/Crypto"; -import * as Effect from "effect/Effect"; -import * as Hex from "effect/encoding/Hex"; -import * as FileSystem from "effect/FileSystem"; -import * as Option from "effect/Option"; -import * as Path from "effect/Path"; -import * as PlatformError from "effect/PlatformError"; -import * as Schema from "effect/Schema"; - -import { writeFileStringAtomically } from "@t3tools/shared/atomicWrite"; -import * as ServerConfig from "../config.ts"; -import * as HostProcess from "@t3tools/shared/HostProcess"; - -/** - * Codex writes `tokens` only for ChatGPT logins and omits the key for API-key, - * agent-identity, and personal-access-token logins, so its absence is a - * supported install and not a malformed file. - */ -const CodexAuthJsonSchema = Schema.Struct({ - tokens: Schema.optionalKey( - Schema.Struct({ - account_id: Schema.String, - }), - ), -}); - -const ClaudeJsonSchema = Schema.Struct({ - userID: Schema.String, -}); - -export const TelemetryIdentitySource = Schema.Literals(["codex", "claude", "anonymous"]); -export type TelemetryIdentitySource = typeof TelemetryIdentitySource.Type; - -class TelemetryIdentityReadError extends Schema.TaggedError()( - "TelemetryIdentityReadError", - { - source: TelemetryIdentitySource, - filePath: Schema.String, - cause: Schema.Defect(), - }, -) { - override get message(): string { - return `Failed to read ${this.source} telemetry identity at '${this.filePath}'.`; - } -} - -class TelemetryIdentityDecodeError extends Schema.TaggedError()( - "TelemetryIdentityDecodeError", - { - source: Schema.Literals(["codex", "claude"]), - filePath: Schema.String, - cause: Schema.Defect(), - }, -) { - override get message(): string { - return `Failed to decode ${this.source} telemetry identity at '${this.filePath}'.`; - } -} - -export class TelemetryAnonymousIdGenerationError extends Schema.TaggedError()( - "TelemetryAnonymousIdGenerationError", - { - source: Schema.Literal("anonymous"), - filePath: Schema.String, - cause: Schema.Defect(), - }, -) { - override get message(): string { - return `Failed to generate anonymous telemetry identity for '${this.filePath}'.`; - } -} - -export class TelemetryAnonymousIdPersistenceError extends Schema.TaggedError()( - "TelemetryAnonymousIdPersistenceError", - { - source: Schema.Literal("anonymous"), - filePath: Schema.String, - cause: Schema.Defect(), - }, -) { - override get message(): string { - return `Failed to persist anonymous telemetry identity at '${this.filePath}'.`; - } -} - -export class TelemetryIdentityHashError extends Schema.TaggedError()( - "TelemetryIdentityHashError", - { - source: TelemetryIdentitySource, - algorithm: Schema.Literal("SHA-256"), - cause: Schema.Defect(), - }, -) { - override get message(): string { - return `Failed to hash ${this.source} telemetry identity with ${this.algorithm}.`; - } -} - -type TelemetryIdentityError = - | TelemetryIdentityReadError - | TelemetryIdentityDecodeError - | TelemetryAnonymousIdGenerationError - | TelemetryAnonymousIdPersistenceError - | TelemetryIdentityHashError; - -const decodeCodexAuthJson = Schema.decodeEffect(Schema.fromJsonString(CodexAuthJsonSchema)); -const decodeClaudeJson = Schema.decodeEffect(Schema.fromJsonString(ClaudeJsonSchema)); - -function isNotFoundError(error: PlatformError.PlatformError): boolean { - return error.reason._tag === "NotFound"; -} - -const getTelemetryIdentityCauseAnnotations = (cause: unknown) => { - if (cause instanceof PlatformError.PlatformError) { - return { - causeKind: "platform", - platformReason: cause.reason._tag, - }; - } - if (cause instanceof Schema.SchemaError) { - return { causeKind: "schema" }; - } - return { causeKind: "other" }; -}; - -const logTelemetryIdentityError = (error: TelemetryIdentityError) => - Effect.logWarning(error.message).pipe( - Effect.annotateLogs({ - errorTag: error._tag, - source: error.source, - ...("filePath" in error ? { filePath: error.filePath } : {}), - ...getTelemetryIdentityCauseAnnotations(error.cause), - ...(error.stack === undefined ? {} : { errorStack: error.stack }), - }), - ); - -const readIdentityFile = ( - fileSystem: FileSystem.FileSystem, - source: TelemetryIdentitySource, - filePath: string, -) => - fileSystem.readFileString(filePath).pipe( - Effect.asSome, - Effect.catchTags({ - PlatformError: (cause) => - isNotFoundError(cause) - ? Effect.succeed(Option.none()) - : Effect.fail( - new TelemetryIdentityReadError({ - source, - filePath, - cause, - }), - ), - }), - ); - -const hash = (source: TelemetryIdentitySource, value: string) => - Crypto.Crypto.pipe( - Effect.flatMap((crypto) => crypto.digest("SHA-256", new TextEncoder().encode(value))), - Effect.map(Hex.encode), - Effect.mapError( - (cause) => - new TelemetryIdentityHashError({ - source, - algorithm: "SHA-256", - cause, - }), - ), - ); - -const getCodexAccountId = Effect.fn("TelemetryIdentity.getCodexAccountId")(function* ( - homeDirectory: string, -) { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - - const authJsonPath = path.join(homeDirectory, ".codex", "auth.json"); - const encoded = yield* readIdentityFile(fileSystem, "codex", authJsonPath); - if (Option.isNone(encoded)) { - return Option.none(); - } - const authJson = yield* decodeCodexAuthJson(encoded.value).pipe( - Effect.mapError( - (cause) => - new TelemetryIdentityDecodeError({ - source: "codex", - filePath: authJsonPath, - cause, - }), - ), - ); - - return authJson.tokens === undefined - ? Option.none() - : Option.some(authJson.tokens.account_id); -}); - -const getClaudeUserId = Effect.fn("TelemetryIdentity.getClaudeUserId")(function* ( - homeDirectory: string, -) { - const fileSystem = yield* FileSystem.FileSystem; - const path = yield* Path.Path; - - const claudeJsonPath = path.join(homeDirectory, ".claude.json"); - const encoded = yield* readIdentityFile(fileSystem, "claude", claudeJsonPath); - if (Option.isNone(encoded)) { - return Option.none(); - } - const claudeJson = yield* decodeClaudeJson(encoded.value).pipe( - Effect.mapError( - (cause) => - new TelemetryIdentityDecodeError({ - source: "claude", - filePath: claudeJsonPath, - cause, - }), - ), - ); - - return Option.some(claudeJson.userID); -}); - -const upsertAnonymousId = Effect.gen(function* () { - const fileSystem = yield* FileSystem.FileSystem; - const { anonymousIdPath } = yield* ServerConfig.ServerConfig; - - const existing = yield* readIdentityFile(fileSystem, "anonymous", anonymousIdPath); - if (Option.isSome(existing)) { - return existing.value; - } - - const anonymousId = yield* Crypto.Crypto.pipe( - Effect.flatMap((crypto) => crypto.randomUUIDv4()), - Effect.mapError( - (cause) => - new TelemetryAnonymousIdGenerationError({ - source: "anonymous", - filePath: anonymousIdPath, - cause, - }), - ), - ); - yield* writeFileStringAtomically({ filePath: anonymousIdPath, contents: anonymousId }).pipe( - Effect.mapError( - (cause) => - new TelemetryAnonymousIdPersistenceError({ - source: "anonymous", - filePath: anonymousIdPath, - cause, - }), - ), - ); - - return anonymousId; -}); - -/** - * getTelemetryIdentifier - Users are "identified" by finding the first match of the following, then hashing the value. - * 1. ~/.codex/auth.json tokens.account_id - * 2. ~/.claude.json userID - * 3. ~/.t3/telemetry/anonymous-id - * - * A missing file or an API-key-only Codex auth.json falls through quietly. Only - * unreadable or malformed files warn. - */ -export const getTelemetryIdentifierForHome = Effect.fn("getTelemetryIdentifierForHome")( - function* (homeDirectory: string) { - const codexAccountId = yield* getCodexAccountId(homeDirectory).pipe( - Effect.catchTags({ - TelemetryIdentityReadError: (error) => - logTelemetryIdentityError(error).pipe(Effect.as(Option.none())), - TelemetryIdentityDecodeError: (error) => - logTelemetryIdentityError(error).pipe(Effect.as(Option.none())), - }), - ); - if (Option.isSome(codexAccountId)) { - return yield* hash("codex", codexAccountId.value); - } - - const claudeUserId = yield* getClaudeUserId(homeDirectory).pipe( - Effect.catchTags({ - TelemetryIdentityReadError: (error) => - logTelemetryIdentityError(error).pipe(Effect.as(Option.none())), - TelemetryIdentityDecodeError: (error) => - logTelemetryIdentityError(error).pipe(Effect.as(Option.none())), - }), - ); - if (Option.isSome(claudeUserId)) { - return yield* hash("claude", claudeUserId.value); - } - - const anonymousId = yield* upsertAnonymousId.pipe( - Effect.asSome, - Effect.catchTags({ - TelemetryIdentityReadError: (error) => - logTelemetryIdentityError(error).pipe(Effect.as(Option.none())), - TelemetryAnonymousIdGenerationError: (error) => - logTelemetryIdentityError(error).pipe(Effect.as(Option.none())), - TelemetryAnonymousIdPersistenceError: (error) => - logTelemetryIdentityError(error).pipe(Effect.as(Option.none())), - }), - ); - if (Option.isSome(anonymousId)) { - return yield* hash("anonymous", anonymousId.value); - } - - return null; - }, - Effect.tapError(logTelemetryIdentityError), - Effect.orElseSucceed(() => null), -); - -export const getTelemetryIdentifier = Effect.flatMap( - HostProcess.HomeDirectory, - getTelemetryIdentifierForHome, -); diff --git a/apps/server/src/ws.ts b/apps/server/src/ws.ts index 7c26f559a7df9c6bea77f9189f9ac09a9962f46b..e27aa261c1476fa7e81f71cd98e42764d5d22a15 100644 --- a/apps/server/src/ws.ts +++ b/apps/server/src/ws.ts @@ -33,16 +33,11 @@ import { type AuthEnvironmentScope, type ScheduledTaskListResult, AuthSessionId, - ClientConnectionMethod, - ClientDeviceType, - ClientOs, ClientSurface, - ClientWebDeployment, type DiscoveredLocalServerList, type EditorId, type FileManagerRevealKind, type OrchestrationClientOrigin, - type OrchestrationV2Command, type GitActionProgressEvent, type GitManagerServiceError, type MessageId, @@ -224,7 +219,6 @@ import * as ProcessDiagnostics from "./diagnostics/ProcessDiagnostics.ts"; import * as ProcessResourceMonitor from "./diagnostics/ProcessResourceMonitor.ts"; import * as ResourceTelemetry from "./resourceTelemetry/ResourceTelemetry.ts"; import * as HostResources from "./resourceTelemetry/HostResources.ts"; -import * as AnalyticsService from "./telemetry/AnalyticsService.ts"; import * as UsageService from "./usage/UsageService.ts"; import * as TraceDiagnostics from "./diagnostics/TraceDiagnostics.ts"; import * as PullRequestService from "./pullRequest/PullRequestService.ts"; @@ -602,13 +596,7 @@ function toAuthAccessStreamEvent( } const isClientSurface = Schema.is(ClientSurface); -const isClientConnectionMethod = Schema.is(ClientConnectionMethod); -const isClientDeviceType = Schema.is(ClientDeviceType); -const isClientOs = Schema.is(ClientOs); -const isClientWebDeployment = Schema.is(ClientWebDeployment); const MAX_CLIENT_APP_VERSION_LENGTH = 64; -const MAX_CLIENT_BROWSER_LENGTH = 64; -const MAX_CLIENT_DEVICE_MODEL_LENGTH = 80; export function hasCompatibleOrchestrationProtocol(url: URL): boolean { return ( @@ -643,52 +631,6 @@ function readClientConnectionOrigin( }; } -// Client telemetry stays in this socket's RPC layer. It must not become a -// server-global "current client" because several client types can connect at once. -function readClientAnalyticsProps(request: HttpServerRequest.HttpServerRequest) { - const url = HttpServerRequest.toURL(request); - if (Option.isNone(url)) { - return {}; - } - - const surface = url.value.searchParams.get("clientSurface"); - const appVersion = url.value.searchParams.get("clientAppVersion")?.trim() ?? ""; - const deviceType = url.value.searchParams.get("clientDeviceType"); - const os = url.value.searchParams.get("clientOs"); - const webDeployment = url.value.searchParams.get("clientWebDeployment"); - const browser = url.value.searchParams.get("clientBrowser")?.trim() ?? ""; - const connectionMethod = url.value.searchParams.get("connectionMethod"); - const rawOsMajorVersion = url.value.searchParams.get("clientOsMajorVersion") ?? ""; - const osMajorVersion = Number(rawOsMajorVersion); - const deviceModel = url.value.searchParams.get("clientDeviceModel")?.trim() ?? ""; - const isMobile = surface === "mobile"; - const hasOsMajorVersion = - isMobile && rawOsMajorVersion !== "" && Number.isInteger(osMajorVersion) && osMajorVersion > 0; - const hasDeviceModel = - isMobile && deviceModel !== "" && deviceModel.length <= MAX_CLIENT_DEVICE_MODEL_LENGTH; - - return { - ...(isClientSurface(surface) ? { surface } : {}), - ...(appVersion !== "" && appVersion.length <= MAX_CLIENT_APP_VERSION_LENGTH - ? { appVersion, clientAppVersion: appVersion } - : {}), - ...(isClientOs(os) - ? { - clientOs: os, - ...(isMobile && (os === "iOS" || os === "Android") ? { os } : {}), - } - : {}), - ...(isClientDeviceType(deviceType) ? { clientDeviceType: deviceType } : {}), - ...(surface === "web" && isClientWebDeployment(webDeployment) ? { webDeployment } : {}), - ...(surface === "web" && browser !== "" && browser.length <= MAX_CLIENT_BROWSER_LENGTH - ? { clientBrowser: browser } - : {}), - ...(hasOsMajorVersion ? { osMajorVersion, clientOsMajorVersion: osMajorVersion } : {}), - ...(hasDeviceModel ? { deviceModel, clientDeviceModel: deviceModel } : {}), - ...(isClientConnectionMethod(connectionMethod) ? { connectionMethod } : {}), - }; -} - const canReplayPersistedRange = Effect.fnUntraced(function* ( afterSequence: number, headSequence: number, @@ -1200,7 +1142,6 @@ export const subscribeOrchestrationV2Shell = Effect.fn("ws.orchestrationV2.subsc const layerWsRpc = ( currentSession: EnvironmentAuth.AuthenticatedSession, clientOrigin: OrchestrationClientOrigin, - clientAnalyticsProps: Readonly>, previewAutomationBroker: PreviewAutomationBroker.PreviewAutomationBroker["Service"], serverBrowser: ServerBrowser.ServerBrowser["Service"], ) => @@ -1224,19 +1165,6 @@ const layerWsRpc = ( const providerSessionsV2 = yield* ProviderSessionManager.ProviderSessionManagerV2; const mcpAppRequests = yield* McpAppRequests.McpAppRequests; - const analytics = yield* AnalyticsService.AnalyticsService; - // Client-origin attribution (#7774): every thread/turn the connecting - // client starts is credited to its surface + app version. Best-effort: - // attribution must never fail the user's command. - const originProps = clientAnalyticsProps; - const recordClientCommandAnalytics = (command: OrchestrationV2Command) => { - switch (command.type) { - case "message.dispatch": - return analytics.record("client.turn.requested", originProps).pipe(Effect.ignore); - default: - return Effect.void; - } - }; const threadLaunch = yield* ThreadLaunchService.ThreadLaunchService; const providerSessionManager = yield* ProviderSessionManager.ProviderSessionManagerV2; const scheduledTasks = yield* ScheduledTasks.ScheduledTaskService; @@ -1869,7 +1797,6 @@ const layerWsRpc = ( ).pipe(Effect.provide(intakeContext)), ) .pipe( - Effect.tap(() => recordClientCommandAnalytics(command)), Effect.map((result) => ({ sequence: result.sequence })), Effect.mapError((cause) => { const detail = userFacingDispatchErrorMessage(cause); @@ -2009,18 +1936,6 @@ const layerWsRpc = ( }).pipe(Effect.provide(intakeContext)), ) .pipe( - Effect.tap(() => - analytics - .record("client.thread.started", originProps) - .pipe( - Effect.andThen( - input.initialMessage === undefined - ? Effect.void - : analytics.record("client.turn.requested", originProps), - ), - Effect.ignore, - ), - ), Effect.map((result) => ({ ...result, projection: projectThreadProjectionForWire(result.projection), @@ -3178,7 +3093,6 @@ export const layer = Layer.unwrap( } const serverAuth = yield* EnvironmentAuth.EnvironmentAuth; const sessions = yield* SessionStore.SessionStore; - const analytics = yield* AnalyticsService.AnalyticsService; const session = yield* serverAuth.authenticateWebSocketUpgrade(request).pipe( Effect.catchIf(EnvironmentAuth.isServerAuthCredentialError, (error) => failEnvironmentAuthInvalid( @@ -3191,9 +3105,7 @@ export const layer = Layer.unwrap( ), ); const clientOrigin = readClientConnectionOrigin(request); - const clientAnalyticsProps = readClientAnalyticsProps(request); yield* sessions.recordClientConnection(session.sessionId, clientOrigin); - yield* analytics.record("client.connected", clientAnalyticsProps); const rpcWebSocketHttpEffect = yield* Effect.gen(function* () { const { protocol, httpEffect } = yield* RpcServer.makeProtocolWithHttpEffectWebsocket; yield* RpcServer.make(ServerWsRpcGroup, WS_RPC_SERVER_OPTIONS).pipe( @@ -3207,13 +3119,7 @@ export const layer = Layer.unwrap( return httpEffect; }).pipe( Effect.provide( - layerWsRpc( - session, - clientOrigin, - clientAnalyticsProps, - previewAutomationBroker, - serverBrowser, - ).pipe( + layerWsRpc(session, clientOrigin, previewAutomationBroker, serverBrowser).pipe( Layer.provideMerge(RpcSerialization.layerJson), // Request fibers run in the handlers' context, so this reporter sees // their defects, not the rest of the server's. diff --git a/apps/server/vite.config.ts b/apps/server/vite.config.ts index bcb0208d9c8679cbab05ced427af61381ae5cfa4..5308fc03a750f512c83b8658ef18eb4fbf5f93ad 100644 --- a/apps/server/vite.config.ts +++ b/apps/server/vite.config.ts @@ -119,15 +119,6 @@ export default mergeConfig( __T3CODE_BUILD_CLERK_CLI_OAUTH_CLIENT_ID__: JSON.stringify( repoEnv.T3CODE_CLERK_CLI_OAUTH_CLIENT_ID?.trim() ?? "", ), - __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_URL__: JSON.stringify( - repoEnv.T3CODE_RELAY_CLIENT_OTLP_TRACES_URL?.trim() ?? "", - ), - __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_DATASET__: JSON.stringify( - repoEnv.T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET?.trim() ?? "", - ), - __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_TOKEN__: JSON.stringify( - repoEnv.T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN?.trim() ?? "", - ), }, }, test: { diff --git a/apps/web/src/cloud/publicConfig.ts b/apps/web/src/cloud/publicConfig.ts index 8c3b9073e3b4e82dbe12045cbb13b9abe3ffedd5..cbb24b9309d85dfab19f35dd9caa62c3d79d1f46 100644 --- a/apps/web/src/cloud/publicConfig.ts +++ b/apps/web/src/cloud/publicConfig.ts @@ -17,26 +17,12 @@ export interface CloudPublicConfig { readonly clerkPublishableKey: string | null; readonly clerkJwtTemplate: string | null; readonly relayUrl: string | null; - readonly relayTracing: { - readonly tracesUrl: string | null; - readonly tracesDataset: string | null; - readonly tracesToken: string | null; - }; } export function trimNonEmpty(value: string | undefined): string | null { return value?.trim() || null; } -function normalizeSecureUrl(value: string): string | null { - try { - const url = new URL(value); - return url.protocol === "https:" ? url.toString() : null; - } catch { - return null; - } -} - export function resolveCloudPublicConfig(): CloudPublicConfig { return { clerkPublishableKey: trimNonEmpty( @@ -46,29 +32,9 @@ export function resolveCloudPublicConfig(): CloudPublicConfig { relayUrl: normalizeSecureRelayUrl( (import.meta.env.VITE_T3CODE_RELAY_URL as string | undefined) ?? "", ), - relayTracing: { - tracesUrl: normalizeSecureUrl( - (import.meta.env.VITE_RELAY_OTLP_TRACES_URL as string | undefined) ?? "", - ), - tracesDataset: trimNonEmpty( - import.meta.env.VITE_RELAY_OTLP_TRACES_DATASET as string | undefined, - ), - tracesToken: trimNonEmpty(import.meta.env.VITE_RELAY_OTLP_TRACES_TOKEN as string | undefined), - }, }; } -export function resolveRelayTracingConfig() { - const { relayTracing } = resolveCloudPublicConfig(); - return relayTracing.tracesUrl && relayTracing.tracesDataset && relayTracing.tracesToken - ? { - tracesUrl: relayTracing.tracesUrl, - tracesDataset: relayTracing.tracesDataset, - tracesToken: relayTracing.tracesToken, - } - : null; -} - export function hasCloudPublicConfig(): boolean { const config = resolveCloudPublicConfig(); return Boolean(config.clerkPublishableKey && config.clerkJwtTemplate && config.relayUrl); diff --git a/apps/web/src/components/clerk/BrowserManagedAuthShell.tsx b/apps/web/src/components/clerk/BrowserManagedAuthShell.tsx index 6534037072bfb4d4b00fbe6febb07c0aebc6b9af..6a53ebe3d944b5614e91c3ef01bbc506556b78a7 100644 --- a/apps/web/src/components/clerk/BrowserManagedAuthShell.tsx +++ b/apps/web/src/components/clerk/BrowserManagedAuthShell.tsx @@ -11,7 +11,7 @@ export default function BrowserManagedAuthShell({ readonly children: ReactNode; }) { return ( - + {children} ); diff --git a/apps/web/src/components/clerk/ElectronManagedAuthShell.tsx b/apps/web/src/components/clerk/ElectronManagedAuthShell.tsx index 71e69401ea1e8de4be932ecf3eb3f6aeb299b90d..2bf785bdb31af4eb7130bc8cca4eef6a07c08760 100644 --- a/apps/web/src/components/clerk/ElectronManagedAuthShell.tsx +++ b/apps/web/src/components/clerk/ElectronManagedAuthShell.tsx @@ -13,7 +13,12 @@ export default function ElectronManagedAuthShell({ readonly children: ReactNode; }) { return ( - + {children} ); diff --git a/apps/web/src/connection/clientMetadata.test.ts b/apps/web/src/connection/clientMetadata.test.ts index 9555e6e01cef35201dc85e3e5335a97128f54b90..4eca05748185a2bc2b85c1a7fcbd208cfb454b37 100644 --- a/apps/web/src/connection/clientMetadata.test.ts +++ b/apps/web/src/connection/clientMetadata.test.ts @@ -1,11 +1,6 @@ import { describe, expect, it } from "vite-plus/test"; -import { - browserClientOs, - browserDeviceType, - browserFamily, - clientPresentationMetadata, -} from "./clientMetadata"; +import { browserClientOs, browserDeviceType, clientPresentationMetadata } from "./clientMetadata"; const desktopChrome = { userAgent: @@ -14,32 +9,28 @@ const desktopChrome = { maxTouchPoints: 0, }; -describe("client telemetry metadata", () => { - it("distinguishes hosted web from server-served web", () => { +describe("client presentation metadata", () => { + it("identifies web clients and omits the development app version", () => { expect( clientPresentationMetadata({ appVersion: "1.2.3", - hosted: true, identity: desktopChrome, desktopBridge: undefined, }), ).toMatchObject({ surface: "web", - webDeployment: "hosted", deviceType: "desktop", os: "Windows", - browser: "Chrome", appVersion: "1.2.3", }); expect( clientPresentationMetadata({ appVersion: "0.0.0", - hosted: false, identity: desktopChrome, desktopBridge: undefined, }), - ).toMatchObject({ surface: "web", webDeployment: "server" }); + ).not.toHaveProperty("appVersion"); }); it("identifies phone and tablet browsers", () => { @@ -64,7 +55,6 @@ describe("client telemetry metadata", () => { expect(browserDeviceType(iphone)).toBe("mobile"); expect(browserClientOs(iphone)).toBe("iOS"); - expect(browserFamily(iphone.userAgent)).toBe("Safari"); expect(browserDeviceType(androidTablet)).toBe("tablet"); expect(browserClientOs(androidTablet)).toBe("Android"); expect(browserDeviceType(ipadosDesktopUa)).toBe("tablet"); @@ -75,7 +65,6 @@ describe("client telemetry metadata", () => { expect( clientPresentationMetadata({ appVersion: "1.2.3", - hosted: false, identity: desktopChrome, desktopBridge: { getClientPlatform: () => "darwin" }, }), diff --git a/apps/web/src/connection/clientMetadata.ts b/apps/web/src/connection/clientMetadata.ts index d5d2562309d67428926dc8e28a23b82dcfb5f73f..259d2c5acff07582c9a532ab4c4c7d734c0394aa 100644 --- a/apps/web/src/connection/clientMetadata.ts +++ b/apps/web/src/connection/clientMetadata.ts @@ -44,17 +44,6 @@ export function browserClientOs(identity: BrowserIdentity): ClientOs { return "other"; } -export function browserFamily(userAgent: string): string { - if (userAgent.trim() === "") return "unknown"; - if (/edg(?:e|a|ios)?\//i.test(userAgent)) return "Edge"; - if (/opr\/|opios\//i.test(userAgent)) return "Opera"; - if (/samsungbrowser\//i.test(userAgent)) return "Samsung Internet"; - if (/firefox\/|fxios\//i.test(userAgent)) return "Firefox"; - if (/chrome\/|crios\//i.test(userAgent)) return "Chrome"; - if (/safari\//i.test(userAgent)) return "Safari"; - return "other"; -} - export function browserDeviceType(identity: BrowserIdentity): AuthClientMetadataDeviceType { const userAgent = identity.userAgent; if (userAgent.trim() === "") return "unknown"; @@ -71,7 +60,6 @@ export function browserDeviceType(identity: BrowserIdentity): AuthClientMetadata export function clientPresentationMetadata(input: { readonly appVersion: string; - readonly hosted: boolean; readonly identity: BrowserIdentity; readonly desktopBridge: Pick | undefined; }): AuthClientPresentationMetadata { @@ -90,8 +78,6 @@ export function clientPresentationMetadata(input: { deviceType: browserDeviceType(input.identity), os: browserClientOs(input.identity), surface: "web", - webDeployment: input.hosted ? "hosted" : "server", - browser: browserFamily(input.identity.userAgent), ...(input.appVersion === "0.0.0" ? {} : { appVersion: input.appVersion }), }; } diff --git a/apps/web/src/connection/platform.ts b/apps/web/src/connection/platform.ts index d975572d2fbddde2d5bdb8ff166a79eceab75e22..dbdeff377f0127f76d88b6cda4bda6e8ffdd870b 100644 --- a/apps/web/src/connection/platform.ts +++ b/apps/web/src/connection/platform.ts @@ -170,7 +170,6 @@ const layerWakeups = Wakeups.layer({ function clientMetadata() { return clientPresentationMetadata({ appVersion: APP_VERSION, - hosted: isHostedStaticApp(), identity: { userAgent: navigator.userAgent, platform: navigator.platform, diff --git a/apps/web/src/lib/runtime.ts b/apps/web/src/lib/runtime.ts index d983c716e526a9489c920f6a3e07b4d0170be31e..b2bf45c180c8a2c0199a7e9bc344374dec2f049c 100644 --- a/apps/web/src/lib/runtime.ts +++ b/apps/web/src/lib/runtime.ts @@ -4,13 +4,12 @@ import * as Layer from "effect/Layer"; import * as Socket from "effect/socket/Socket"; import { layerRemoteHttpClient } from "@t3tools/client-runtime/rpc"; -import * as RelayTracing from "@t3tools/shared/relayTracing"; import * as PrimaryEnvironmentHttpClient from "../environments/primary/httpClient"; import * as PrimaryEnvironmentHttpLayer from "../environments/primary/httpLayer"; import * as Dpop from "../cloud/dpop"; import * as ManagedRelayLayer from "../cloud/managedRelayLayer"; -import { resolveCloudPublicConfig, resolveRelayTracingConfig } from "../cloud/publicConfig"; +import { resolveCloudPublicConfig } from "../cloud/publicConfig"; import * as ClientTracer from "../observability/clientTracer"; function configuredRelayUrl(): string { @@ -18,18 +17,10 @@ function configuredRelayUrl(): string { } const layerHttpClient = layerRemoteHttpClient((input, init) => globalThis.fetch(input, init)); -const layerRelayTracing = RelayTracing.layer(resolveRelayTracingConfig(), { - serviceName: "t3code-web", - serviceVersion: import.meta.env.APP_VERSION, - runtime: "browser", - client: typeof window !== "undefined" && window.desktopBridge ? "desktop" : "web", -}).pipe(Layer.provide(layerHttpClient)); - type RuntimeLayerSource = | typeof layerHttpClient | typeof Dpop.layer | typeof Socket.layerWebSocketConstructorGlobal - | typeof layerRelayTracing | typeof ClientTracer.layer | ReturnType; @@ -59,7 +50,6 @@ const layerRuntime = Layer.mergeAll( Dpop.layer, Socket.layerWebSocketConstructorGlobal, ClientTracer.layer, - layerRelayTracing, ManagedRelayLayer.layer(configuredRelayUrl()).pipe( Layer.provide(Layer.mergeAll(layerHttpClient, Dpop.layer)), ), diff --git a/apps/web/src/vite-env.d.ts b/apps/web/src/vite-env.d.ts index 31260c8ae666fcb7814d8e078f4dac4a930a0f6d..e77bb9776f6ebe3de6716f5b1b4c26d3d8028c09 100644 --- a/apps/web/src/vite-env.d.ts +++ b/apps/web/src/vite-env.d.ts @@ -10,9 +10,6 @@ interface ImportMetaEnv { readonly VITE_CLERK_PUBLISHABLE_KEY: string; readonly VITE_CLERK_JWT_TEMPLATE: string; readonly VITE_CLERK_CLI_OAUTH_CLIENT_ID: string; - readonly VITE_RELAY_OTLP_TRACES_URL: string; - readonly VITE_RELAY_OTLP_TRACES_DATASET: string; - readonly VITE_RELAY_OTLP_TRACES_TOKEN: string; readonly APP_VERSION: string; } diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 44e07a8e1792844183399a9fdbabda88188ac01c..60de9eeab2f0258ec2ef30f8ced30ffbbc4e50c4 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -39,9 +39,6 @@ const configuredRelayUrl = repoEnv.VITE_T3CODE_RELAY_URL?.trim() || ""; const configuredClerkPublishableKey = repoEnv.VITE_CLERK_PUBLISHABLE_KEY?.trim() || ""; const configuredClerkJwtTemplate = repoEnv.VITE_CLERK_JWT_TEMPLATE?.trim() || ""; const configuredClerkCliOAuthClientId = repoEnv.VITE_CLERK_CLI_OAUTH_CLIENT_ID?.trim() || ""; -const configuredRelayTracingUrl = repoEnv.VITE_RELAY_OTLP_TRACES_URL?.trim() || ""; -const configuredRelayTracingDataset = repoEnv.VITE_RELAY_OTLP_TRACES_DATASET?.trim() || ""; -const configuredRelayTracingToken = repoEnv.VITE_RELAY_OTLP_TRACES_TOKEN?.trim() || ""; const configuredHostedAppChannel = process.env.VITE_HOSTED_APP_CHANNEL?.trim() || ""; const configuredAppVersion = process.env.APP_VERSION?.trim() || pkg.version; const configuredHostedAppUrl = (() => { @@ -211,11 +208,6 @@ export default defineConfig(() => { "import.meta.env.VITE_CLERK_CLI_OAUTH_CLIENT_ID": JSON.stringify( configuredClerkCliOAuthClientId, ), - "import.meta.env.VITE_RELAY_OTLP_TRACES_URL": JSON.stringify(configuredRelayTracingUrl), - "import.meta.env.VITE_RELAY_OTLP_TRACES_DATASET": JSON.stringify( - configuredRelayTracingDataset, - ), - "import.meta.env.VITE_RELAY_OTLP_TRACES_TOKEN": JSON.stringify(configuredRelayTracingToken), "import.meta.env.VITE_HOSTED_APP_URL": JSON.stringify(configuredHostedAppUrl ?? ""), "import.meta.env.VITE_HOSTED_APP_CHANNEL": JSON.stringify(configuredHostedAppChannel), "import.meta.env.APP_VERSION": JSON.stringify(configuredAppVersion), diff --git a/docs/README.md b/docs/README.md index d7628960cb8b654c7372e5be46cb209c1ab441bd..0b1771c85eee7920961b308ffb008c6c3ddc1e0e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,7 +17,7 @@ - [Use T3 Code as your default browser](./user/default-browser.md) - [Devices](./user/devices.md) - [Usage and limits](./user/usage.md) -- [Product usage data](./user/telemetry.md) +- [Diagnostics and data sharing](./user/telemetry.md) - [Remote access](./user/remote-access.md) - [Outside agents (MCP)](./user/outside-agents.md) - [Running in the background](./user/background-service.md) @@ -45,7 +45,6 @@ source alone does not explain. Most code changes do not need an internal documen - [Remote environments](./internals/remote.md) - [Server updates](./internals/server-updates.md) - [Resource telemetry](./internals/resource-telemetry.md) -- [Product analytics](./internals/product-analytics.md) - [Environment auth](./internals/environment-auth.md) - [T3 Connect](./internals/t3-connect.md) - [Assistant citations](./internals/assistant-citations.md) diff --git a/docs/internals/product-analytics.md b/docs/internals/product-analytics.md deleted file mode 100644 index e9123640a541a411038e51c65ed3cbfe8d5c01bb..0000000000000000000000000000000000000000 --- a/docs/internals/product-analytics.md +++ /dev/null @@ -1,56 +0,0 @@ -# Product analytics - -The server owns PostHog delivery, opt-out, and identity for every connected client. -[Identity selection](../../apps/server/src/telemetry/Identify.ts) hashes an available -provider account ID, falling back to an installation-scoped ID. This identity can -span several clients; it does not identify a browser session. Clients do not load -the PostHog browser SDK. Client-use events require an authenticated connection, so -visiting the hosted app without connecting does not count as product use. - -## Attribution boundaries - -Client dimensions belong to the event's WebSocket connection. A server-global -"current client" would misattribute simultaneous web, desktop, and mobile use. -Provider execution has its own events because a turn can outlive the requesting -connection. - -Keep client and server dimensions separate. A desktop host can serve a phone or a -remote browser, and a direct connection can cross a network. Older clients omit -metadata. Missing client values must stay unknown rather than being backfilled -from server properties. The legacy `clientType` property describes how the server -runs; use `surface` for the connected client. - -## Interpreting events - -Use `client.turn.requested` for active-use reports. `client.connected` counts -reconnects, so network behavior can inflate it. One identity can appear in several -client groups during a period; adding those groups double-counts users. - -Provider send and completion counts need not match. Providers can emit synthetic -turns without a send request. Collection is best effort, with no scan or backfill -of provider history. - -Token totals cover the main agent. Child agents and model rerouting prevent a turn -from representing one provider/model combination's full cost. For provider -comparisons, require complete usage, no observed subagents, and no mixed models; -compare matching model, effort, interaction mode, and terminal status. Aggregate -output/input ratios should divide the summed totals. Averaging per-turn ratios -lets small-input turns dominate. - -Unknown counts stay absent. Partial usage contains valid observed counts but -cannot establish a whole-turn total. Keep these distinctions when changing token -normalization or building reports. - -## Delivery - -A send can fail after PostHog has stored the batch, so every retry is a copy. -[Delivery](../../apps/server/src/telemetry/AnalyticsService.ts) gives each event a -uuid when it is recorded, backs off after a failed send, and drops a batch after a -few tries. Without these limits, one stuck batch was sent every second for days. - -## Collection boundary - -Keep analytics payloads to product metadata and normalized measurements. Do not -send prompts, authentication material, raw provider payloads, user-assigned device -names, or conversation identifiers. Client metadata is best effort; invalid values -must not reject a connection. PostHog person profiles remain disabled. diff --git a/docs/operations/observability.md b/docs/operations/observability.md index eedb8732ea2717b9214a822c75896af8a2841935..33cd79cabe00f9543706836c08aa28f76a572bf3 100644 --- a/docs/operations/observability.md +++ b/docs/operations/observability.md @@ -418,8 +418,9 @@ Webhooks have their own families: `ScheduledTaskService.triggerWebhook` spans carry the same outcome per request, and each run started from a delivery is its own `ScheduledTaskService.runWebhookDelivery` trace. For a request -the relay forwarded, the span also goes to the T3 Connect trace export as a child of the relay's -span; requests that reach the environment directly never join a sender's trace. +the relay forwarded, the local span retains the relay's parent span for correlation; The Knife +does not export it to T3 Connect. Requests that reach the environment directly never join a +sender's trace. Use metrics when the question is: diff --git a/docs/operations/relay-observability.md b/docs/operations/relay-observability.md index 848a676acc94ce071824f50e3fbaba3dfe12551f..37077069053279fa7ff7d61ece6472358fc2314f 100644 --- a/docs/operations/relay-observability.md +++ b/docs/operations/relay-observability.md @@ -2,7 +2,9 @@ > For maintainers. Using T3 Code? See [docs/user](../user/). -The relay Alchemy stack owns a shared Axiom trace setup: +The relay Alchemy stack owns a shared Axiom trace setup. The Knife clients and environment +servers do not export to it; local diagnostics use the configured local or user-owned receiver. +The resources below belong to the upstream relay deployment: - `t3-code-relay-traces-prod`, the OpenTelemetry trace dataset shared by the Worker, mobile app, and first-party relay clients @@ -21,8 +23,7 @@ vp run deploy ``` Alchemy resolves account-level Axiom deployment credentials through its provider. At runtime, the -Worker receives only its scoped ingest token. Mobile and relay clients use their own separately -provisioned scoped ingest tokens. +Worker receives only its scoped ingest token. The Knife does not consume the provisioned mobile or relay-client ingest tokens. The Worker emits Effect's built-in HTTP server spans plus endpoint and database child spans. Effect's OpenTelemetry exporter stores semantic HTTP attributes below the `attributes.` prefix. @@ -70,8 +71,8 @@ managed endpoint, and with it the environment. On a forward, `relay.hook.upstrea `x-t3-hook-outcome` response header. A held request's delivery records the same on its `relay.inbox.deliver` span. The relay sends its own `traceparent` with each forward and drops any a sender supplied, and signs each forward (`x-t3-relay-delivery`) so the environment trusts the -relay's delivery id, receive time and trace context only from the relay, so on environments that export to T3 Connect, the environment's -`ScheduledTaskService.triggerWebhook` span lands in the same trace. `relay.hook.rate_limit` says which +relay's delivery id, receive time and trace context only from the relay. The environment records +its `ScheduledTaskService.triggerWebhook` span locally. `relay.hook.rate_limit` says which budget ran out: `endpoint` or `hook`. `relay.hook.rate_limiter_failed_open` is set when the Cloudflare rate limiter was unavailable and the request went through unlimited. diff --git a/docs/user/telemetry.md b/docs/user/telemetry.md index 61925062d3e9b05a51ac6c1144955384f4072a89..31c69b9e74721c6598db8bed825003783f625f77 100644 --- a/docs/user/telemetry.md +++ b/docs/user/telemetry.md @@ -1,17 +1,8 @@ -# Product usage data +# Diagnostics and data sharing -The T3 Code server sends product usage events to PostHog, associated with a hashed account or -installation identifier. Events include the provider, model, reasoning effort, permission mode, -turn result, duration, and main-agent token totals when available. - -Events do not include prompts, responses, file contents, authentication tokens, conversation IDs, -raw provider events, or child-agent output. Child-agent token use is excluded from the totals. - -To disable collection, set `T3CODE_TELEMETRY_ENABLED=false` in the server's environment before -starting it. This stops product events from being recorded or sent. - -The desktop app reads the variable from your shell profile (for example `~/.zshrc`) on macOS and -Linux, so export it there and restart the app. On Windows, set it as a user environment variable. +The Knife does not collect or send product analytics. T3 Connect contacts its account and relay +services only when you use that connection option. Local diagnostics stay on your machine unless +you configure an export endpoint. ## Export diagnostics to your own receiver diff --git a/packages/client-runtime/src/authorization/remote.test.ts b/packages/client-runtime/src/authorization/remote.test.ts index 44dbed210942aa331c49736ffc418615464212df..5bfcdaa507274a65a58419bbd33cfb6c1d2de1d0 100644 --- a/packages/client-runtime/src/authorization/remote.test.ts +++ b/packages/client-runtime/src/authorization/remote.test.ts @@ -304,7 +304,7 @@ describe("remote environment authorization", () => { deviceType: "desktop", os: "unknown", }); - expect(websocketUrl.searchParams.get("clientOs")).toBe("unknown"); + expect(websocketUrl.searchParams.get("clientOs")).toBeNull(); }), ); @@ -532,14 +532,11 @@ describe("remote environment authorization", () => { appVersion: "1.2.3", deviceType: "mobile", os: "Android", - osMajorVersion: 15, - deviceModel: "Pixel 9", }, - connectionMethod: "relay", }).pipe(provideRemoteHttp(fetch.fetchFn)); expect(url).toBe( - "wss://remote.example.com/ws?wsTicket=ws-ticket&clientSurface=mobile&clientAppVersion=1.2.3&clientDeviceType=phone&clientOs=Android&clientOsMajorVersion=15&clientDeviceModel=Pixel+9&connectionMethod=relay", + "wss://remote.example.com/ws?wsTicket=ws-ticket&clientSurface=mobile&clientAppVersion=1.2.3", ); }), ); diff --git a/packages/client-runtime/src/authorization/remote.ts b/packages/client-runtime/src/authorization/remote.ts index 32a3cde3105fe33a42f0112ef392e61d1cc28368..9f19aa4d56bbec7130d10e671be011148e173279 100644 --- a/packages/client-runtime/src/authorization/remote.ts +++ b/packages/client-runtime/src/authorization/remote.ts @@ -3,7 +3,6 @@ import { type AuthClientPresentationMetadata, AuthEnvironmentBootstrapTokenType, AuthTokenExchangeGrantType, - type ClientConnectionMethod, type AuthEnvironmentScope, } from "@t3tools/contracts"; import { encodeOAuthScope } from "@t3tools/shared/oauthScope"; @@ -42,7 +41,6 @@ const clientMetadataTokenExchangeFields = ( export const appendClientConnectionParams = ( url: URL, clientMetadata: AuthClientPresentationMetadata | undefined, - connectionMethod?: ClientConnectionMethod, ): void => { if (clientMetadata?.surface) { url.searchParams.set("clientSurface", clientMetadata.surface); @@ -50,37 +48,6 @@ export const appendClientConnectionParams = ( if (clientMetadata?.appVersion) { url.searchParams.set("clientAppVersion", clientMetadata.appVersion); } - if (clientMetadata?.deviceType) { - const deviceType = - clientMetadata.deviceType === "mobile" - ? "phone" - : clientMetadata.deviceType === "desktop" || clientMetadata.deviceType === "tablet" - ? clientMetadata.deviceType - : "unknown"; - url.searchParams.set("clientDeviceType", deviceType); - } - if (clientMetadata?.os) { - url.searchParams.set("clientOs", clientMetadata.os); - } - if (clientMetadata?.surface === "web") { - if (clientMetadata.webDeployment) { - url.searchParams.set("clientWebDeployment", clientMetadata.webDeployment); - } - if (clientMetadata.browser) { - url.searchParams.set("clientBrowser", clientMetadata.browser); - } - } - if (clientMetadata?.surface === "mobile") { - if (clientMetadata.osMajorVersion !== undefined) { - url.searchParams.set("clientOsMajorVersion", String(clientMetadata.osMajorVersion)); - } - if (clientMetadata.deviceModel) { - url.searchParams.set("clientDeviceModel", clientMetadata.deviceModel); - } - } - if (connectionMethod) { - url.searchParams.set("connectionMethod", connectionMethod); - } }; export const exchangeRemoteDpopAccessToken = Effect.fn( @@ -205,7 +172,6 @@ export const resolveRemoteWebSocketConnectionUrl = Effect.fn( readonly httpBaseUrl: string; readonly bearerToken: string; readonly clientMetadata?: AuthClientPresentationMetadata; - readonly connectionMethod?: ClientConnectionMethod; readonly timeoutMs?: number; }) { const issued = yield* issueRemoteWebSocketTicket({ @@ -216,7 +182,7 @@ export const resolveRemoteWebSocketConnectionUrl = Effect.fn( const url = new URL(environmentWebSocketUrl(input.wsBaseUrl)); url.searchParams.set("wsTicket", issued.ticket); - appendClientConnectionParams(url, input.clientMetadata, input.connectionMethod); + appendClientConnectionParams(url, input.clientMetadata); return url.toString(); }); @@ -228,7 +194,6 @@ export const resolveRemoteDpopWebSocketConnectionUrl = Effect.fn( readonly accessToken: string; readonly dpopProof: string; readonly clientMetadata?: AuthClientPresentationMetadata; - readonly connectionMethod?: ClientConnectionMethod; readonly timeoutMs?: number; }) { const issued = yield* issueRemoteDpopWebSocketTicket({ @@ -239,6 +204,6 @@ export const resolveRemoteDpopWebSocketConnectionUrl = Effect.fn( }); const url = new URL(environmentWebSocketUrl(input.wsBaseUrl)); url.searchParams.set("wsTicket", issued.ticket); - appendClientConnectionParams(url, input.clientMetadata, input.connectionMethod); + appendClientConnectionParams(url, input.clientMetadata); return url.toString(); }); diff --git a/packages/client-runtime/src/authorization/service.ts b/packages/client-runtime/src/authorization/service.ts index 780c3a77fdc0d2f7cb3ad53665559f009d2d6b57..97ebb30426e3231697013071bbbbdbc93f1c3e55 100644 --- a/packages/client-runtime/src/authorization/service.ts +++ b/packages/client-runtime/src/authorization/service.ts @@ -4,7 +4,6 @@ import { type ExecutionEnvironmentDescriptor, } from "@t3tools/contracts"; import { RelayEnvironmentConnectScope } from "@t3tools/contracts/relay"; -import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; import { exchangeRemoteDpopAccessToken, type RemoteEnvironmentAuthError, @@ -197,7 +196,6 @@ export const make = Effect.gen(function* () { httpBaseUrl: input.httpBaseUrl, bearerToken: input.bearerToken, clientMetadata: presentation.metadata, - connectionMethod: input.connectionMethod, }).pipe( Effect.mapError(mapRemoteEnvironmentError), Effect.provideService(HttpClient.HttpClient, httpClient), @@ -242,7 +240,6 @@ export const make = Effect.gen(function* () { accessToken: token.accessToken, dpopProof: ticketProof, clientMetadata: presentation.metadata, - connectionMethod: "relay", ...(timeoutMs === undefined ? {} : { timeoutMs }), }).pipe(Effect.provideService(HttpClient.HttpClient, httpClient)); }, @@ -551,7 +548,6 @@ export const make = Effect.gen(function* () { getDpopToken(input).pipe( Effect.map(({ token }) => httpAuthorization(token)), Effect.withSpan("environment.authorization.http"), - withRelayClientTracing, ), }); }); diff --git a/packages/client-runtime/src/connection/resolver.test.ts b/packages/client-runtime/src/connection/resolver.test.ts index 5b73043bcaa02438d7ef095f9d4b3167bf38d88f..18906da992939ec790fbf1ff329f499d72a2e085 100644 --- a/packages/client-runtime/src/connection/resolver.test.ts +++ b/packages/client-runtime/src/connection/resolver.test.ts @@ -3,7 +3,6 @@ import { ORCHESTRATION_PROTOCOL_VERSION, type DesktopSshEnvironmentTarget, } from "@t3tools/contracts"; -import { RelayClientTracer } from "@t3tools/shared/relayTracing"; import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -269,8 +268,7 @@ describe("ConnectionResolver", () => { environmentId: ENVIRONMENT_ID, label: "Primary", httpBaseUrl: "http://127.0.0.1:3777", - socketUrl: - "ws://127.0.0.1:3777/ws?clientSurface=web&clientDeviceType=desktop&connectionMethod=direct&orchestrationProtocol=2", + socketUrl: "ws://127.0.0.1:3777/ws?clientSurface=web&orchestrationProtocol=2", httpAuthorization: null, target, }); @@ -383,10 +381,9 @@ describe("ConnectionResolver", () => { }), ); - it.effect("exports the complete relay authorization flow through the product tracer", () => + it.effect("records the complete relay authorization flow in local tracing", () => Effect.gen(function* () { const userSpans: Array = []; - const productSpans: Array = []; const target = new RelayConnectionTarget({ environmentId: ENVIRONMENT_ID, label: "Cloud", @@ -409,15 +406,11 @@ describe("ConnectionResolver", () => { yield* broker .prepare(catalogEntry(target)) - .pipe( - Effect.provideService(RelayClientTracer, Option.some(collectingTracer(productSpans))), - Effect.withTracer(collectingTracer(userSpans)), - ); + .pipe(Effect.withTracer(collectingTracer(userSpans))); - expect(productSpans).toContain("clientRuntime.connection.broker.relay"); - expect(productSpans).toContain("test.remote.authorizeDpop"); + expect(userSpans).toContain("clientRuntime.connection.broker.relay"); + expect(userSpans).toContain("test.remote.authorizeDpop"); expect(userSpans).toContain("clientRuntime.connection.broker.prepare"); - expect(userSpans).not.toContain("test.remote.authorizeDpop"); }), ); diff --git a/packages/client-runtime/src/connection/resolver.ts b/packages/client-runtime/src/connection/resolver.ts index 02b4042b4a075331df783aa238b331e5b10ea283..42204d1db28bdfa3e5533e08df0b34be5ec4b674 100644 --- a/packages/client-runtime/src/connection/resolver.ts +++ b/packages/client-runtime/src/connection/resolver.ts @@ -2,7 +2,6 @@ import type { AuthClientPresentationMetadata, ExecutionEnvironmentDescriptor, } from "@t3tools/contracts"; -import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; @@ -77,7 +76,7 @@ function primarySocketUrl( clientMetadata: AuthClientPresentationMetadata | undefined, ): string { const url = new URL(environmentWebSocketUrl(target.wsBaseUrl)); - appendClientConnectionParams(url, clientMetadata, "direct"); + appendClientConnectionParams(url, clientMetadata); return url.toString(); } @@ -224,23 +223,19 @@ const makeBearerBroker = Effect.fn("clientRuntime.connection.broker.makeBearer") const makeRelayBroker = Effect.fn("clientRuntime.connection.broker.makeRelay")(function* () { const remote = yield* RemoteEnvironmentAuthorization.RemoteEnvironmentAuthorization; - return Effect.fnUntraced( - function* (target: RelayConnectionTarget) { - const authorized = yield* remote.authorizeDpop({ - expectedEnvironmentId: target.environmentId, - }); - return { - environmentId: authorized.environmentId, - label: authorized.label, - httpBaseUrl: authorized.httpBaseUrl, - socketUrl: authorized.socketUrl, - httpAuthorization: authorized.httpAuthorization, - target, - } satisfies PreparedConnection; - }, - Effect.withSpan("clientRuntime.connection.broker.relay"), - withRelayClientTracing, - ); + return Effect.fnUntraced(function* (target: RelayConnectionTarget) { + const authorized = yield* remote.authorizeDpop({ + expectedEnvironmentId: target.environmentId, + }); + return { + environmentId: authorized.environmentId, + label: authorized.label, + httpBaseUrl: authorized.httpBaseUrl, + socketUrl: authorized.socketUrl, + httpAuthorization: authorized.httpAuthorization, + target, + } satisfies PreparedConnection; + }, Effect.withSpan("clientRuntime.connection.broker.relay")); }); const makeSshBroker = Effect.fn("clientRuntime.connection.broker.makeSsh")(function* () { diff --git a/packages/client-runtime/src/connection/supervisor.test.ts b/packages/client-runtime/src/connection/supervisor.test.ts index f83598b8095296fa330e6ef62d9a8a5519382b8e..56c8e01b5c79e0e5ba8b53fa793f6a75b1738f83 100644 --- a/packages/client-runtime/src/connection/supervisor.test.ts +++ b/packages/client-runtime/src/connection/supervisor.test.ts @@ -1,5 +1,4 @@ import { AuthStandardClientScopes, EnvironmentId, type ServerConfig } from "@t3tools/contracts"; -import { RelayClientTracer } from "@t3tools/shared/relayTracing"; import { describe, expect, it } from "@effect/vitest"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; @@ -295,7 +294,7 @@ describe("retryDelayMs", () => { }); describe("EnvironmentSupervisor", () => { - it.effect("exports each relay setup as a standalone linked trace that ends at readiness", () => + it.effect("records each relay setup as a local linked trace that ends at readiness", () => Effect.gen(function* () { const spans: Array = []; const tracer = Tracer.make({ @@ -315,10 +314,7 @@ describe("EnvironmentSupervisor", () => { }); const supervisor = yield* EnvironmentSupervisor.make(RELAY_ENTRY, { initiallyDesired: true, - }).pipe( - Effect.provide(harness.dependencies), - Effect.provideService(RelayClientTracer, Option.some(tracer)), - ); + }).pipe(Effect.provide(harness.dependencies), Effect.withTracer(tracer)); yield* awaitState( supervisor.state, diff --git a/packages/client-runtime/src/connection/supervisor.ts b/packages/client-runtime/src/connection/supervisor.ts index 88a53216541da5c330e11c1ba47e99ba80987e34..eb4a83f121508849be6dcefa9aa364bd40276486 100644 --- a/packages/client-runtime/src/connection/supervisor.ts +++ b/packages/client-runtime/src/connection/supervisor.ts @@ -1,4 +1,3 @@ -import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; import * as Cause from "effect/Cause"; import * as Clock from "effect/Clock"; import * as Context from "effect/Context"; @@ -484,7 +483,7 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* ( "connection.retry.reason": retry.reason, }), ), - }).pipe(withRelayClientTracing); + }); }; const establishTracedConnection = Effect.fnUntraced(function* ( diff --git a/packages/client-runtime/src/relay/managedRelay.ts b/packages/client-runtime/src/relay/managedRelay.ts index 339d28f421e848f8d1f1bfd270b5f5924aa9df4f..d487999259c76083cf1e75ab59a79f8ce250f6ad 100644 --- a/packages/client-runtime/src/relay/managedRelay.ts +++ b/packages/client-runtime/src/relay/managedRelay.ts @@ -31,7 +31,6 @@ import { } from "@t3tools/contracts/relay"; import { encodeOAuthScope, oauthScopeSetEquals } from "@t3tools/shared/oauthScope"; import { decodeRelayJwt } from "@t3tools/shared/relayJwt"; -import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; import { normalizeSecureRelayUrl } from "@t3tools/shared/relayUrl"; import * as Clock from "effect/Clock"; import * as Context from "effect/Context"; @@ -725,228 +724,183 @@ export const make = Effect.fn("ManagedRelayClient.make")(function* ( return ManagedRelayClient.of({ relayUrl, - listEnvironments: Effect.fnUntraced( - function* (input) { - return yield* client.client - .listEnvironments({ headers: bearerHeaders(input.clerkToken) }) - .pipe( - Effect.map((response) => response.environments), - Effect.mapError(relayRequestError("list relay-managed environments")), - timeoutRelayRequest("Relay environment listing"), - ); - }, - Effect.withSpan("clientRuntime.managedRelay.listEnvironments"), - withRelayClientTracing, - ), - listDevices: Effect.fnUntraced( - function* (input) { - return yield* client.client - .listDevicesV2({ - headers: bearerHeaders(input.clerkToken), - }) - .pipe( - Effect.map((response) => response.devices), - Effect.mapError(relayRequestError("list relay client devices")), - timeoutRelayRequest("Relay client device listing"), - ); - }, - Effect.withSpan("clientRuntime.managedRelay.listDevices"), - withRelayClientTracing, - ), - createEnvironmentLinkChallenge: Effect.fnUntraced( - function* (input) { - return yield* client.client - .createEnvironmentLinkChallenge({ - headers: bearerHeaders(input.clerkToken), - payload: input.payload, - }) - .pipe( - Effect.mapError(relayRequestError("create relay environment link challenge")), - timeoutRelayRequest("Relay environment link challenge"), - ); - }, - Effect.withSpan("clientRuntime.managedRelay.createEnvironmentLinkChallenge"), - withRelayClientTracing, - ), - linkEnvironment: Effect.fnUntraced( - function* (input) { - return yield* client.client - .linkEnvironment({ - headers: bearerHeaders(input.clerkToken), - payload: input.payload, - }) - .pipe( - Effect.mapError(relayRequestError("link relay environment")), - timeoutRelayRequest("Relay environment linking"), - ); - }, - Effect.withSpan("clientRuntime.managedRelay.linkEnvironment"), - withRelayClientTracing, - ), - unlinkEnvironment: Effect.fnUntraced( - function* (input) { - return yield* client.client - .unlinkEnvironment({ - headers: bearerHeaders(input.clerkToken), - params: { environmentId: input.environmentId }, - }) - .pipe( - Effect.mapError(relayRequestError("unlink relay environment")), - timeoutRelayRequest("Relay environment unlinking"), - ); - }, - Effect.withSpan("clientRuntime.managedRelay.unlinkEnvironment"), - withRelayClientTracing, - ), - getEnvironmentStatus: Effect.fnUntraced( - function* (input) { - yield* Effect.annotateCurrentSpan({ - "environment.id": input.environmentId, - }); - return yield* runDpopRequest( - { - clerkToken: input.clerkToken, - scopes: input.scopes, - target: dpopProofTargets.getEnvironmentStatus(input.environmentId), - }, - (authorization) => - client.dpopClient - .getEnvironmentStatus({ - headers: dpopHeaders(authorization), - params: { environmentId: input.environmentId }, - }) - .pipe( - Effect.mapError(relayRequestError("get relay environment status")), - timeoutRelayRequest("Relay environment status request"), - ), + listEnvironments: Effect.fnUntraced(function* (input) { + return yield* client.client + .listEnvironments({ headers: bearerHeaders(input.clerkToken) }) + .pipe( + Effect.map((response) => response.environments), + Effect.mapError(relayRequestError("list relay-managed environments")), + timeoutRelayRequest("Relay environment listing"), ); - }, - Effect.withSpan("clientRuntime.managedRelay.getEnvironmentStatus"), - withRelayClientTracing, - ), - connectEnvironment: Effect.fnUntraced( - function* (input) { - yield* Effect.annotateCurrentSpan({ - "environment.id": input.environmentId, - }); - return yield* runDpopRequest( - { - clerkToken: input.clerkToken, - scopes: input.scopes, - target: dpopProofTargets.connectEnvironment(input.environmentId), - }, - (authorization) => { - const payload: RelayEnvironmentConnectRequest = { - ...(input.deviceId ? { deviceId: input.deviceId } : {}), - clientKeyThumbprint: authorization.thumbprint, - }; - return client.dpopClient - .connectEnvironment({ - headers: dpopHeaders(authorization), - params: { environmentId: input.environmentId }, - payload, - }) - .pipe( - Effect.mapError(relayRequestError("connect relay environment")), - timeoutRelayRequest("Relay environment connection"), - ); - }, + }, Effect.withSpan("clientRuntime.managedRelay.listEnvironments")), + listDevices: Effect.fnUntraced(function* (input) { + return yield* client.client + .listDevicesV2({ + headers: bearerHeaders(input.clerkToken), + }) + .pipe( + Effect.map((response) => response.devices), + Effect.mapError(relayRequestError("list relay client devices")), + timeoutRelayRequest("Relay client device listing"), ); - }, - Effect.withSpan("clientRuntime.managedRelay.connectEnvironment"), - withRelayClientTracing, - ), - registerDevice: Effect.fnUntraced( - function* (input) { - return yield* mobileRegistrationRequest( - { - clerkToken: input.clerkToken, - target: dpopProofTargets.registerDevice(), - }, - (authorization) => - client.mobile - .registerDevice({ - headers: dpopHeaders(authorization), - payload: input.payload, - }) - .pipe( - Effect.mapError(relayRequestError("register relay mobile device")), - timeoutRelayRequest("Relay mobile device registration"), - ), + }, Effect.withSpan("clientRuntime.managedRelay.listDevices")), + createEnvironmentLinkChallenge: Effect.fnUntraced(function* (input) { + return yield* client.client + .createEnvironmentLinkChallenge({ + headers: bearerHeaders(input.clerkToken), + payload: input.payload, + }) + .pipe( + Effect.mapError(relayRequestError("create relay environment link challenge")), + timeoutRelayRequest("Relay environment link challenge"), ); - }, - Effect.withSpan("clientRuntime.managedRelay.registerDevice"), - withRelayClientTracing, - ), - unregisterDevice: Effect.fnUntraced( - function* (input) { - return yield* mobileRegistrationRequest( - { - clerkToken: input.clerkToken, - target: dpopProofTargets.unregisterDevice(input.deviceId), - }, - (authorization) => - client.mobile - .unregisterDevice({ - headers: dpopHeaders(authorization), - params: { deviceId: input.deviceId }, - }) - .pipe( - Effect.mapError(relayRequestError("unregister relay mobile device")), - timeoutRelayRequest("Relay mobile device unregistration"), - ), + }, Effect.withSpan("clientRuntime.managedRelay.createEnvironmentLinkChallenge")), + linkEnvironment: Effect.fnUntraced(function* (input) { + return yield* client.client + .linkEnvironment({ + headers: bearerHeaders(input.clerkToken), + payload: input.payload, + }) + .pipe( + Effect.mapError(relayRequestError("link relay environment")), + timeoutRelayRequest("Relay environment linking"), ); - }, - Effect.withSpan("clientRuntime.managedRelay.unregisterDevice"), - withRelayClientTracing, - ), - getAgentActivitySnapshot: Effect.fnUntraced( - function* (input) { - return yield* mobileRegistrationRequest( - { - clerkToken: input.clerkToken, - target: dpopProofTargets.getAgentActivitySnapshot(), - }, - (authorization) => - client.mobile - .getAgentActivitySnapshot({ - headers: dpopHeaders(authorization), - }) - .pipe( - Effect.mapError(relayRequestError("read relay agent activity snapshot")), - timeoutRelayRequest("Relay agent activity snapshot"), - ), + }, Effect.withSpan("clientRuntime.managedRelay.linkEnvironment")), + unlinkEnvironment: Effect.fnUntraced(function* (input) { + return yield* client.client + .unlinkEnvironment({ + headers: bearerHeaders(input.clerkToken), + params: { environmentId: input.environmentId }, + }) + .pipe( + Effect.mapError(relayRequestError("unlink relay environment")), + timeoutRelayRequest("Relay environment unlinking"), ); - }, - Effect.withSpan("clientRuntime.managedRelay.getAgentActivitySnapshot"), - withRelayClientTracing, - ), - registerLiveActivity: Effect.fnUntraced( - function* (input) { - return yield* mobileRegistrationRequest( - { - clerkToken: input.clerkToken, - target: dpopProofTargets.registerLiveActivity(), - }, - (authorization) => - client.mobile - .registerLiveActivity({ - headers: dpopHeaders(authorization), - payload: input.payload, - }) - .pipe( - Effect.mapError(relayRequestError("register relay live activity")), - timeoutRelayRequest("Relay Live Activity registration"), - ), - ); - }, - Effect.withSpan("clientRuntime.managedRelay.registerLiveActivity"), - withRelayClientTracing, - ), + }, Effect.withSpan("clientRuntime.managedRelay.unlinkEnvironment")), + getEnvironmentStatus: Effect.fnUntraced(function* (input) { + yield* Effect.annotateCurrentSpan({ + "environment.id": input.environmentId, + }); + return yield* runDpopRequest( + { + clerkToken: input.clerkToken, + scopes: input.scopes, + target: dpopProofTargets.getEnvironmentStatus(input.environmentId), + }, + (authorization) => + client.dpopClient + .getEnvironmentStatus({ + headers: dpopHeaders(authorization), + params: { environmentId: input.environmentId }, + }) + .pipe( + Effect.mapError(relayRequestError("get relay environment status")), + timeoutRelayRequest("Relay environment status request"), + ), + ); + }, Effect.withSpan("clientRuntime.managedRelay.getEnvironmentStatus")), + connectEnvironment: Effect.fnUntraced(function* (input) { + yield* Effect.annotateCurrentSpan({ + "environment.id": input.environmentId, + }); + return yield* runDpopRequest( + { + clerkToken: input.clerkToken, + scopes: input.scopes, + target: dpopProofTargets.connectEnvironment(input.environmentId), + }, + (authorization) => { + const payload: RelayEnvironmentConnectRequest = { + ...(input.deviceId ? { deviceId: input.deviceId } : {}), + clientKeyThumbprint: authorization.thumbprint, + }; + return client.dpopClient + .connectEnvironment({ + headers: dpopHeaders(authorization), + params: { environmentId: input.environmentId }, + payload, + }) + .pipe( + Effect.mapError(relayRequestError("connect relay environment")), + timeoutRelayRequest("Relay environment connection"), + ); + }, + ); + }, Effect.withSpan("clientRuntime.managedRelay.connectEnvironment")), + registerDevice: Effect.fnUntraced(function* (input) { + return yield* mobileRegistrationRequest( + { + clerkToken: input.clerkToken, + target: dpopProofTargets.registerDevice(), + }, + (authorization) => + client.mobile + .registerDevice({ + headers: dpopHeaders(authorization), + payload: input.payload, + }) + .pipe( + Effect.mapError(relayRequestError("register relay mobile device")), + timeoutRelayRequest("Relay mobile device registration"), + ), + ); + }, Effect.withSpan("clientRuntime.managedRelay.registerDevice")), + unregisterDevice: Effect.fnUntraced(function* (input) { + return yield* mobileRegistrationRequest( + { + clerkToken: input.clerkToken, + target: dpopProofTargets.unregisterDevice(input.deviceId), + }, + (authorization) => + client.mobile + .unregisterDevice({ + headers: dpopHeaders(authorization), + params: { deviceId: input.deviceId }, + }) + .pipe( + Effect.mapError(relayRequestError("unregister relay mobile device")), + timeoutRelayRequest("Relay mobile device unregistration"), + ), + ); + }, Effect.withSpan("clientRuntime.managedRelay.unregisterDevice")), + getAgentActivitySnapshot: Effect.fnUntraced(function* (input) { + return yield* mobileRegistrationRequest( + { + clerkToken: input.clerkToken, + target: dpopProofTargets.getAgentActivitySnapshot(), + }, + (authorization) => + client.mobile + .getAgentActivitySnapshot({ + headers: dpopHeaders(authorization), + }) + .pipe( + Effect.mapError(relayRequestError("read relay agent activity snapshot")), + timeoutRelayRequest("Relay agent activity snapshot"), + ), + ); + }, Effect.withSpan("clientRuntime.managedRelay.getAgentActivitySnapshot")), + registerLiveActivity: Effect.fnUntraced(function* (input) { + return yield* mobileRegistrationRequest( + { + clerkToken: input.clerkToken, + target: dpopProofTargets.registerLiveActivity(), + }, + (authorization) => + client.mobile + .registerLiveActivity({ + headers: dpopHeaders(authorization), + payload: input.payload, + }) + .pipe( + Effect.mapError(relayRequestError("register relay live activity")), + timeoutRelayRequest("Relay Live Activity registration"), + ), + ); + }, Effect.withSpan("clientRuntime.managedRelay.registerLiveActivity")), resetTokenCache: SynchronizedRef.set(cachedTokens, []).pipe( Effect.andThen(options.accessTokenStore ? options.accessTokenStore.clear : Effect.void), Effect.withSpan("clientRuntime.managedRelay.resetTokenCache"), - withRelayClientTracing, ), }); }); diff --git a/packages/client-runtime/src/state/environmentHttpAuth.test.ts b/packages/client-runtime/src/state/environmentHttpAuth.test.ts index 2e1f6d22aa2bddc3d65453a1786109e8124974df..2265fc3bc118c69804c45dc7ab4c55d09f1d1995 100644 --- a/packages/client-runtime/src/state/environmentHttpAuth.test.ts +++ b/packages/client-runtime/src/state/environmentHttpAuth.test.ts @@ -12,14 +12,12 @@ import { OrchestrationV2ThreadTranscript, type OrchestrationV2ThreadHistoryPage, } from "@t3tools/contracts"; -import { RelayClientTracer } from "@t3tools/shared/relayTracing"; import * as Deferred from "effect/Deferred"; import * as Effect from "effect/Effect"; import * as Fiber from "effect/Fiber"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; -import * as Tracer from "effect/Tracer"; import { TestClock } from "effect/testing"; import type { HttpClient } from "effect/http"; @@ -764,36 +762,6 @@ describe("authenticated environment HTTP requests", () => { ); }); -describe("relay request tracing", () => { - it.effect("starts an exported trace for a T3 Connect request", () => - Effect.gen(function* () { - const productSpans: Array<{ readonly name: string; readonly root: boolean }> = []; - const productTracer = Tracer.make({ - span: (options) => { - productSpans.push({ name: options.name, root: Option.isNone(options.parent) }); - return new Tracer.NativeSpan(options); - }, - }); - const harness = makeHarness(() => Response.json(DIFF_RESULT)); - - yield* PullRequestDiffLoader.fetchEnvironmentPullRequestDiff({ - ...harness.input, - diff: DIFF, - }).pipe( - Effect.withSpan("mobile.screen.local"), - Effect.provide(harness.httpLayer), - Effect.provideService(RelayClientTracer, Option.some(productTracer)), - ); - - expect(productSpans[0]).toEqual({ - name: "clientRuntime.state.executeAuthenticatedEnvironmentHttpRequest", - root: true, - }); - expect(productSpans.map((span) => span.name)).not.toContain("mobile.screen.local"); - }), - ); -}); - describe("orchestration HTTP compatibility header", () => { it("announces the current protocol without replacing bearer or DPoP authentication", () => { expect( diff --git a/packages/client-runtime/src/state/environmentHttpAuth.ts b/packages/client-runtime/src/state/environmentHttpAuth.ts index 6e0b389156f37082de1ceca48ee37b1d82942c04..38a2775fdb1e82e379a1f0339a5888e36422d421 100644 --- a/packages/client-runtime/src/state/environmentHttpAuth.ts +++ b/packages/client-runtime/src/state/environmentHttpAuth.ts @@ -5,7 +5,6 @@ import { ORCHESTRATION_PROTOCOL_VERSION_TEXT, } from "@t3tools/contracts"; import * as Result from "effect/Result"; -import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; import { FetchHttpClient, type HttpMethod } from "effect/http"; import { HostedAuthAccess } from "../platform/capabilities.ts"; @@ -106,10 +105,6 @@ const buildEnvironmentAuthHeaders = ( * Resolve relay credentials at request time without replacing the live socket. * A rejected credential gets one refresh and retry, with a new request-bound * proof. Cookie and bearer requests keep their existing authentication behavior. - * - * A DPoP request is T3 Connect work, so its span starts an exported trace that - * the environment continues; its local caller's span would leave that trace - * without a root. */ export const executeAuthenticatedEnvironmentHttpRequest = < Group extends Parameters[1], @@ -118,13 +113,7 @@ export const executeAuthenticatedEnvironmentHttpRequest = < R, >( input: Parameters>[0], -) => - input.prepared.httpAuthorization?._tag === "Dpop" - ? executeEnvironmentRequest(input).pipe( - Effect.withSpan(ENVIRONMENT_REQUEST_SPAN, { root: true }), - withRelayClientTracing, - ) - : executeEnvironmentRequest(input).pipe(Effect.withSpan(ENVIRONMENT_REQUEST_SPAN)); +) => executeEnvironmentRequest(input).pipe(Effect.withSpan(ENVIRONMENT_REQUEST_SPAN)); const ENVIRONMENT_REQUEST_SPAN = "clientRuntime.state.executeAuthenticatedEnvironmentHttpRequest"; diff --git a/packages/contracts/src/auth.ts b/packages/contracts/src/auth.ts index ca7581d6b865804c5de32569e0a0132ad2a6c3f4..74c26cca60e8be93cf3dfa1638780576e07ab806 100644 --- a/packages/contracts/src/auth.ts +++ b/packages/contracts/src/auth.ts @@ -5,7 +5,6 @@ import { AuthSessionId, ForwardCompatibleArray, ClientSurface, - ClientWebDeployment, TrimmedNonEmptyString, } from "./baseSchemas.ts"; @@ -297,11 +296,7 @@ export const AuthClientPresentationMetadata = Schema.Struct({ label: Schema.optionalKey(TrimmedNonEmptyString), deviceType: Schema.optionalKey(AuthClientMetadataDeviceType), os: Schema.optionalKey(TrimmedNonEmptyString), - osMajorVersion: Schema.optionalKey(Schema.Int), - deviceModel: Schema.optionalKey(TrimmedNonEmptyString), surface: Schema.optionalKey(ClientSurface), - webDeployment: Schema.optionalKey(ClientWebDeployment), - browser: Schema.optionalKey(TrimmedNonEmptyString), appVersion: Schema.optionalKey(TrimmedNonEmptyString), }); export type AuthClientPresentationMetadata = typeof AuthClientPresentationMetadata.Type; diff --git a/packages/contracts/src/baseSchemas.ts b/packages/contracts/src/baseSchemas.ts index 3ca88df66b19d20956eb386a4c8a4041d9efc7b3..a2bbe22a10b2300f2486d65b32721ced0c5a20ff 100644 --- a/packages/contracts/src/baseSchemas.ts +++ b/packages/contracts/src/baseSchemas.ts @@ -357,12 +357,6 @@ export const ClientOs = Schema.Literals([ ]); export type ClientOs = typeof ClientOs.Type; -export const ClientDeviceType = Schema.Literals(["desktop", "phone", "tablet", "unknown"]); -export type ClientDeviceType = typeof ClientDeviceType.Type; - -export const ClientWebDeployment = Schema.Literals(["hosted", "server"]); -export type ClientWebDeployment = typeof ClientWebDeployment.Type; - export const ClientConnectionMethod = Schema.Literals(["direct", "ssh", "relay", "unknown"]); export type ClientConnectionMethod = typeof ClientConnectionMethod.Type; diff --git a/packages/shared/package.json b/packages/shared/package.json index f3a07e22b032c4f704d1d380d658648a7d0bf46b..6454d52e72dbb0c4a4ebc09cb0f965f08d8a826a 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -295,10 +295,6 @@ "types": "./src/relayClient.ts", "import": "./src/relayClient.ts" }, - "./relayTracing": { - "types": "./src/relayTracing.ts", - "import": "./src/relayTracing.ts" - }, "./preview": { "types": "./src/preview.ts", "import": "./src/preview.ts" diff --git a/packages/shared/src/relayTracing.test.ts b/packages/shared/src/relayTracing.test.ts deleted file mode 100644 index 980c003328832510c5da4009d0e83ff36d60dfc6..0000000000000000000000000000000000000000 --- a/packages/shared/src/relayTracing.test.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { describe, expect, it } from "@effect/vitest"; -import * as Effect from "effect/Effect"; -import * as Layer from "effect/Layer"; -import * as Option from "effect/Option"; -import * as Tracer from "effect/Tracer"; -import { FetchHttpClient } from "effect/http"; -import { vi } from "vite-plus/test"; - -import { RelayClientTracer, withLocalTracing, withRelayClientTracing } from "./relayTracing.ts"; -import * as RelayTracing from "./relayTracing.ts"; - -function collectingTracer(spans: Array): Tracer.Tracer { - return Tracer.make({ - span: (options) => { - const span = new Tracer.NativeSpan(options); - const end = span.end.bind(span); - span.end = (endTime, exit) => { - end(endTime, exit); - spans.push(span.name); - }; - return span; - }, - }); -} - -describe("withRelayClientTracing", () => { - it.effect("uses the product tracer only for relay operations", () => - Effect.gen(function* () { - const userSpans: Array = []; - const productSpans: Array = []; - const userTracer = collectingTracer(userSpans); - const productTracer = collectingTracer(productSpans); - - yield* Effect.void.pipe(Effect.withSpan("user.operation"), Effect.withTracer(userTracer)); - yield* Effect.void.pipe( - Effect.withSpan("relay.operation"), - withRelayClientTracing, - Effect.provideService(RelayClientTracer, Option.some(productTracer)), - Effect.withTracer(userTracer), - ); - - expect(userSpans).toEqual(["user.operation"]); - expect(productSpans).toEqual(["relay.operation"]); - }), - ); - - it.effect("preserves the active tracer when product tracing is disabled", () => - Effect.gen(function* () { - const userSpans: Array = []; - const userTracer = collectingTracer(userSpans); - - yield* Effect.void.pipe( - Effect.withSpan("relay.operation"), - withRelayClientTracing, - Effect.withTracer(userTracer), - ); - - expect(userSpans).toEqual(["relay.operation"]); - }), - ); - - it.effect("preserves nested error causes in exported relay spans", () => { - const fetchFn = vi.fn(async () => new Response(null, { status: 202 })); - const layerHttpClient = FetchHttpClient.layer.pipe( - Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetchFn)), - ); - const layerTracing = RelayTracing.layer( - { - tracesUrl: "https://api.axiom.test/v1/traces", - tracesDataset: "relay-traces", - tracesToken: "public-ingest-token", - }, - { - serviceName: "relay-test", - runtime: "test", - client: "test", - }, - ).pipe(Layer.provide(layerHttpClient)); - const rootCause = new Error("relay socket closed"); - const failure = new Error("relay request failed", { cause: rootCause }); - const layerTracedApplication = Layer.effectDiscard( - Effect.fail(failure).pipe( - Effect.withSpan("relay.failed-operation"), - withRelayClientTracing, - Effect.exit, - ), - ).pipe(Layer.provide(layerTracing)); - - return Layer.build(layerTracedApplication).pipe( - Effect.scoped, - Effect.andThen( - Effect.sync(() => { - expect(fetchFn).toHaveBeenCalledOnce(); - const payload = new TextDecoder().decode(fetchFn.mock.calls[0]?.[1]?.body as Uint8Array); - expect(payload).toContain("relay request failed"); - expect(payload).toContain("relay socket closed"); - expect(payload).toContain('"key":"service.name","value":{"stringValue":"relay-test"}'); - expect(payload).toContain('"key":"service.namespace","value":{"stringValue":"t3code"}'); - }), - ), - ); - }); -}); - -describe("withLocalTracing", () => { - it.effect("keeps local work inside a relay span off the product tracer", () => - Effect.gen(function* () { - const localSpans: Array = []; - const productSpans: Array = []; - const localTracer = collectingTracer(localSpans); - const productTracer = collectingTracer(productSpans); - - yield* Effect.void.pipe( - Effect.withSpan("relay.connection.nested"), - Effect.andThen( - Effect.void.pipe( - Effect.withSpan("sql.execute"), - Effect.withSpan("ServerSecretStore.get"), - withLocalTracing, - ), - ), - Effect.withSpan("environment.orchestration.threadSnapshot"), - withRelayClientTracing, - Effect.provideService(RelayClientTracer, Option.some(productTracer)), - Effect.withTracer(localTracer), - ); - - expect(productSpans).toEqual([ - "relay.connection.nested", - "environment.orchestration.threadSnapshot", - ]); - expect(localSpans).toEqual(["sql.execute", "ServerSecretStore.get"]); - }), - ); - - it.effect("leaves the current tracer alone outside relay tracing", () => - Effect.gen(function* () { - const localSpans: Array = []; - - yield* Effect.void.pipe( - Effect.withSpan("sql.execute"), - withLocalTracing, - Effect.withTracer(collectingTracer(localSpans)), - ); - - expect(localSpans).toEqual(["sql.execute"]); - }), - ); -}); diff --git a/packages/shared/src/relayTracing.ts b/packages/shared/src/relayTracing.ts deleted file mode 100644 index 8f7aa3bc9c244f8804e8e289476709bd9b06fbce..0000000000000000000000000000000000000000 --- a/packages/shared/src/relayTracing.ts +++ /dev/null @@ -1,193 +0,0 @@ -import * as Cause from "effect/Cause"; -import * as Context from "effect/Context"; -import * as Effect from "effect/Effect"; -import * as Exit from "effect/Exit"; -import * as Layer from "effect/Layer"; -import * as Option from "effect/Option"; -import * as Tracer from "effect/Tracer"; -import type { HttpClient } from "effect/http"; -import { OtlpSerialization, OtlpTracer } from "effect/observability"; - -export interface RelayClientTracingConfig { - readonly tracesUrl: string; - readonly tracesDataset: string; - readonly tracesToken: string; -} - -export interface RelayClientTracingResource { - readonly serviceName: string; - readonly serviceVersion?: string; - readonly runtime: string; - readonly client: string; - readonly component?: string; -} - -export class RelayClientTracer extends Context.Reference( - "@t3tools/shared/relayTracing/RelayClientTracer", - { - defaultValue: () => Option.none(), - }, -) {} - -/** - * The tracer that was active before relay tracing took over, so work nested - * inside a relay span can return to it. - */ -class LocalTracer extends Context.Reference("@t3tools/shared/relayTracing/LocalTracer", { - defaultValue: () => Option.none(), -}) {} - -/** - * Exports every span `effect` creates through the product tracer. Use it only - * around T3 Connect work; wrap local work inside it with - * {@link withLocalTracing} so it stays off the product tracer. - */ -export const withRelayClientTracing = ( - effect: Effect.Effect, -): Effect.Effect => - RelayClientTracer.pipe( - Effect.flatMap( - Option.match({ - onNone: () => effect, - onSome: (tracer) => - Tracer.Tracer.pipe( - Effect.flatMap((local) => - effect.pipe( - Effect.provideService(Tracer.Tracer, tracer), - Effect.provideService(LocalTracer, Option.some(local)), - ), - ), - ), - }), - ), - ); - -/** - * Runs `effect` on the tracer that was active outside relay tracing. Its spans - * stay in the local trace and are never exported, so a relay span can time the - * work without shipping what the user's machine did to answer it. - */ -export const withLocalTracing = (effect: Effect.Effect): Effect.Effect => - LocalTracer.pipe( - Effect.flatMap( - Option.match({ - onNone: () => effect, - onSome: (local) => effect.pipe(Effect.provideService(Tracer.Tracer, local)), - }), - ), - ); - -function cleanTraceStack(error: Error): string { - const stack = error.stack ?? `${error.name}: ${error.message}`; - const lines = stack.split("\n"); - const effectFrameIndex = lines.findIndex( - (line, index) => index > 0 && /(?:Generator\.next|~effect\/Effect)/.test(line), - ); - return effectFrameIndex < 0 ? stack : lines.slice(0, effectFrameIndex).join("\n"); -} - -function traceSafeError(value: unknown, seen = new WeakSet()): Error { - const message = - value instanceof Error - ? value.message - : typeof value === "object" && - value !== null && - "message" in value && - typeof value.message === "string" - ? value.message - : String(value); - - let cause: Error | undefined; - if (typeof value === "object" && value !== null && !seen.has(value)) { - seen.add(value); - if ("cause" in value && value.cause !== undefined) { - cause = traceSafeError(value.cause, seen); - } - } - - const error = new Error(message, cause ? { cause } : undefined); - if (value instanceof Error) { - error.name = value.name; - error.stack = cleanTraceStack(value); - } else if ( - typeof value === "object" && - value !== null && - "name" in value && - typeof value.name === "string" - ) { - error.name = value.name; - } - if (cause) { - error.stack = `${error.stack ?? `${error.name}: ${error.message}`}\nCaused by: ${cause.stack ?? `${cause.name}: ${cause.message}`}`; - } - return error; -} - -function traceSafeExit(exit: Exit.Exit): Exit.Exit { - if (Exit.isSuccess(exit)) { - return exit; - } - return Exit.failCause( - Cause.fromReasons( - exit.cause.reasons.map((reason) => { - if (Cause.isFailReason(reason)) { - return Cause.makeFailReason(traceSafeError(reason.error)); - } - if (Cause.isDieReason(reason)) { - return Cause.makeDieReason(traceSafeError(reason.defect)); - } - return reason; - }), - ), - ); -} - -function nonInterferingTracer(delegate: Tracer.Tracer): Tracer.Tracer { - return Tracer.make({ - span(options) { - const span = delegate.span(options); - const end = span.end.bind(span); - span.end = (endTime, exit) => { - try { - end(endTime, traceSafeExit(exit)); - } catch { - // Telemetry is best-effort and must never change application behavior. - } - }; - return span; - }, - ...(delegate.context ? { context: delegate.context } : {}), - }); -} - -export function layer( - config: RelayClientTracingConfig | null, - resource: RelayClientTracingResource, -): Layer.Layer { - if (config === null) { - return Layer.succeed(RelayClientTracer, Option.none()); - } - - const layerTracer = OtlpTracer.layer({ - url: config.tracesUrl, - headers: { - Authorization: `Bearer ${config.tracesToken}`, - "X-Axiom-Dataset": config.tracesDataset, - }, - resource: { - serviceName: resource.serviceName, - serviceVersion: resource.serviceVersion, - attributes: { - "service.namespace": "t3code", - "service.runtime": resource.runtime, - "service.component": resource.component ?? "relay-client", - "t3.client.surface": resource.client, - }, - }, - }).pipe(Layer.provide(OtlpSerialization.layerJson)); - - return Layer.effect( - RelayClientTracer, - Tracer.Tracer.pipe(Effect.map(nonInterferingTracer), Effect.asSome), - ).pipe(Layer.provide(layerTracer)); -} diff --git a/scripts/lib/public-config.test.ts b/scripts/lib/public-config.test.ts index 62d383484cf480e8d2fbef8ac54d8e2c612fd1d5..bfb53697db03f3531eebe0ce26d933c53db88808 100644 --- a/scripts/lib/public-config.test.ts +++ b/scripts/lib/public-config.test.ts @@ -27,18 +27,6 @@ describe("loadRepoEnv", () => { expect(env.EXPO_PUBLIC_CLERK_JWT_TEMPLATE).toBeUndefined(); expect(env.T3CODE_RELAY_URL).toBeUndefined(); expect(env.VITE_T3CODE_RELAY_URL).toBeUndefined(); - expect(env.T3CODE_MOBILE_OTLP_TRACES_URL).toBeUndefined(); - expect(env.T3CODE_MOBILE_OTLP_TRACES_DATASET).toBeUndefined(); - expect(env.T3CODE_MOBILE_OTLP_TRACES_TOKEN).toBeUndefined(); - expect(env.EXPO_PUBLIC_OTLP_TRACES_URL).toBeUndefined(); - expect(env.EXPO_PUBLIC_OTLP_TRACES_DATASET).toBeUndefined(); - expect(env.EXPO_PUBLIC_OTLP_TRACES_TOKEN).toBeUndefined(); - expect(env.T3CODE_RELAY_CLIENT_OTLP_TRACES_URL).toBeUndefined(); - expect(env.T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET).toBeUndefined(); - expect(env.T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN).toBeUndefined(); - expect(env.VITE_RELAY_OTLP_TRACES_URL).toBeUndefined(); - expect(env.VITE_RELAY_OTLP_TRACES_DATASET).toBeUndefined(); - expect(env.VITE_RELAY_OTLP_TRACES_TOKEN).toBeUndefined(); }); it("applies process, root local, and root precedence in that order", () => { @@ -85,64 +73,12 @@ describe("loadRepoEnv", () => { VITE_CLERK_JWT_TEMPLATE: "template_legacy", T3CODE_CLERK_CLI_OAUTH_CLIENT_ID: "oauth_canonical", VITE_T3CODE_RELAY_URL: "https://legacy.example.test", - EXPO_PUBLIC_OTLP_TRACES_URL: "https://api.axiom.co/v1/traces", - EXPO_PUBLIC_OTLP_TRACES_DATASET: "mobile-traces", - EXPO_PUBLIC_OTLP_TRACES_TOKEN: "mobile-token", }), ).toEqual({ clerkPublishableKey: "pk_legacy", clerkJwtTemplate: "template_legacy", clerkCliOAuthClientId: "oauth_canonical", relayUrl: "https://legacy.example.test", - mobileOtlpTracesUrl: "https://api.axiom.co/v1/traces", - mobileOtlpTracesDataset: "mobile-traces", - mobileOtlpTracesToken: "mobile-token", - relayClientOtlpTracesUrl: undefined, - relayClientOtlpTracesDataset: undefined, - relayClientOtlpTracesToken: undefined, - }); - }); - - it("projects canonical relay client tracing values to web build aliases", () => { - expect( - loadRepoEnv({ - baseEnv: { - T3CODE_RELAY_CLIENT_OTLP_TRACES_URL: "https://api.axiom.co/v1/traces", - T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET: "relay-client-traces", - T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN: "relay-client-token", - }, - repoRoot: makeTemporaryDirectory(), - }), - ).toEqual({ - T3CODE_RELAY_CLIENT_OTLP_TRACES_URL: "https://api.axiom.co/v1/traces", - T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET: "relay-client-traces", - T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN: "relay-client-token", - VITE_RELAY_OTLP_TRACES_URL: "https://api.axiom.co/v1/traces", - VITE_RELAY_OTLP_TRACES_DATASET: "relay-client-traces", - VITE_RELAY_OTLP_TRACES_TOKEN: "relay-client-token", - }); - }); - - it("projects canonical mobile tracing values to Expo public aliases", () => { - expect( - loadRepoEnv({ - baseEnv: { - T3CODE_RELAY_URL: "https://relay.example.test", - T3CODE_MOBILE_OTLP_TRACES_URL: "https://api.axiom.co/v1/traces", - T3CODE_MOBILE_OTLP_TRACES_DATASET: "mobile-traces", - T3CODE_MOBILE_OTLP_TRACES_TOKEN: "mobile-token", - }, - repoRoot: makeTemporaryDirectory(), - }), - ).toEqual({ - T3CODE_RELAY_URL: "https://relay.example.test", - VITE_T3CODE_RELAY_URL: "https://relay.example.test", - T3CODE_MOBILE_OTLP_TRACES_URL: "https://api.axiom.co/v1/traces", - T3CODE_MOBILE_OTLP_TRACES_DATASET: "mobile-traces", - T3CODE_MOBILE_OTLP_TRACES_TOKEN: "mobile-token", - EXPO_PUBLIC_OTLP_TRACES_URL: "https://api.axiom.co/v1/traces", - EXPO_PUBLIC_OTLP_TRACES_DATASET: "mobile-traces", - EXPO_PUBLIC_OTLP_TRACES_TOKEN: "mobile-token", }); }); }); diff --git a/scripts/lib/public-config.ts b/scripts/lib/public-config.ts index 36ca5497f80338ce8db05903ccb6d52b0983431c..75c45e9c821f300e925de17ca720deba7ce6ffee 100644 --- a/scripts/lib/public-config.ts +++ b/scripts/lib/public-config.ts @@ -9,12 +9,6 @@ export interface T3CodePublicConfig { readonly clerkJwtTemplate: string | undefined; readonly clerkCliOAuthClientId: string | undefined; readonly relayUrl: string | undefined; - readonly mobileOtlpTracesUrl: string | undefined; - readonly mobileOtlpTracesDataset: string | undefined; - readonly mobileOtlpTracesToken: string | undefined; - readonly relayClientOtlpTracesUrl: string | undefined; - readonly relayClientOtlpTracesDataset: string | undefined; - readonly relayClientOtlpTracesToken: string | undefined; } type Environment = Readonly>; @@ -64,42 +58,6 @@ export function loadRepoEnv({ VITE_T3CODE_RELAY_URL: config.relayUrl, } : {}), - ...(config.mobileOtlpTracesUrl - ? { - T3CODE_MOBILE_OTLP_TRACES_URL: config.mobileOtlpTracesUrl, - EXPO_PUBLIC_OTLP_TRACES_URL: config.mobileOtlpTracesUrl, - } - : {}), - ...(config.mobileOtlpTracesDataset - ? { - T3CODE_MOBILE_OTLP_TRACES_DATASET: config.mobileOtlpTracesDataset, - EXPO_PUBLIC_OTLP_TRACES_DATASET: config.mobileOtlpTracesDataset, - } - : {}), - ...(config.mobileOtlpTracesToken - ? { - T3CODE_MOBILE_OTLP_TRACES_TOKEN: config.mobileOtlpTracesToken, - EXPO_PUBLIC_OTLP_TRACES_TOKEN: config.mobileOtlpTracesToken, - } - : {}), - ...(config.relayClientOtlpTracesUrl - ? { - T3CODE_RELAY_CLIENT_OTLP_TRACES_URL: config.relayClientOtlpTracesUrl, - VITE_RELAY_OTLP_TRACES_URL: config.relayClientOtlpTracesUrl, - } - : {}), - ...(config.relayClientOtlpTracesDataset - ? { - T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET: config.relayClientOtlpTracesDataset, - VITE_RELAY_OTLP_TRACES_DATASET: config.relayClientOtlpTracesDataset, - } - : {}), - ...(config.relayClientOtlpTracesToken - ? { - T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN: config.relayClientOtlpTracesToken, - VITE_RELAY_OTLP_TRACES_TOKEN: config.relayClientOtlpTracesToken, - } - : {}), }; } @@ -123,36 +81,6 @@ export function resolvePublicConfig(...sources: readonly Environment[]): T3CodeP "VITE_CLERK_CLI_OAUTH_CLIENT_ID", ), relayUrl: firstNonEmpty(sources, "T3CODE_RELAY_URL", "VITE_T3CODE_RELAY_URL"), - mobileOtlpTracesUrl: firstNonEmpty( - sources, - "T3CODE_MOBILE_OTLP_TRACES_URL", - "EXPO_PUBLIC_OTLP_TRACES_URL", - ), - mobileOtlpTracesDataset: firstNonEmpty( - sources, - "T3CODE_MOBILE_OTLP_TRACES_DATASET", - "EXPO_PUBLIC_OTLP_TRACES_DATASET", - ), - mobileOtlpTracesToken: firstNonEmpty( - sources, - "T3CODE_MOBILE_OTLP_TRACES_TOKEN", - "EXPO_PUBLIC_OTLP_TRACES_TOKEN", - ), - relayClientOtlpTracesUrl: firstNonEmpty( - sources, - "T3CODE_RELAY_CLIENT_OTLP_TRACES_URL", - "VITE_RELAY_OTLP_TRACES_URL", - ), - relayClientOtlpTracesDataset: firstNonEmpty( - sources, - "T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET", - "VITE_RELAY_OTLP_TRACES_DATASET", - ), - relayClientOtlpTracesToken: firstNonEmpty( - sources, - "T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN", - "VITE_RELAY_OTLP_TRACES_TOKEN", - ), }; }