| author | |
| committer | |
| log | 708d5383a9c7415c3795890ca4f664c7b00f9a47 |
| tree | 5c6a42f522eb1fcc23858558ea998a72826c118b |
| parent | 5cd27441007aee0218f8145f8033de7b8a495f3d |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: codex <codex@users.noreply.github.com>31 files changed, 900 insertions(+), 78 deletions(-)
.github/workflows/release.yml+79| ... | @@ -180,12 +180,46 @@ jobs: | ... | @@ -180,12 +180,46 @@ jobs: |
| 180 | clerk_cli_oauth_client_id: ${{ steps.public_config.outputs.clerk_cli_oauth_client_id }} | 180 | clerk_cli_oauth_client_id: ${{ steps.public_config.outputs.clerk_cli_oauth_client_id }} |
| 181 | relay_url: ${{ steps.public_config.outputs.relay_url }} | 181 | relay_url: ${{ steps.public_config.outputs.relay_url }} |
| 182 | env: | 182 | env: |
| 183 | CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }} | ||
| 184 | CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| 183 | RELAY_DOMAIN: ${{ vars.RELAY_DOMAIN }} | 185 | RELAY_DOMAIN: ${{ vars.RELAY_DOMAIN }} |
| 184 | RELAY_API_ZONE_NAME: ${{ vars.RELAY_API_ZONE_NAME }} | 186 | RELAY_API_ZONE_NAME: ${{ vars.RELAY_API_ZONE_NAME }} |
| 185 | CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }} | 187 | CLERK_PUBLISHABLE_KEY: ${{ vars.CLERK_PUBLISHABLE_KEY }} |
| 186 | CLERK_JWT_TEMPLATE: ${{ vars.CLERK_JWT_TEMPLATE }} | 188 | CLERK_JWT_TEMPLATE: ${{ vars.CLERK_JWT_TEMPLATE }} |
| 187 | CLERK_CLI_OAUTH_CLIENT_ID: ${{ vars.CLERK_CLI_OAUTH_CLIENT_ID }} | 189 | CLERK_CLI_OAUTH_CLIENT_ID: ${{ vars.CLERK_CLI_OAUTH_CLIENT_ID }} |
| 188 | steps: | 190 | steps: |
| 191 | - name: Checkout | ||
| 192 | uses: actions/checkout@v6 | ||
| 193 | with: | ||
| 194 | ref: ${{ needs.preflight.outputs.ref }} | ||
| 195 | |||
| 196 | - name: Setup Vite+ | ||
| 197 | uses: voidzero-dev/setup-vp@v1 | ||
| 198 | with: | ||
| 199 | node-version-file: package.json | ||
| 200 | cache: true | ||
| 201 | run-install: | | ||
| 202 | args: | ||
| 203 | - --filter=t3code-relay... | ||
| 204 | |||
| 205 | - id: relay_state | ||
| 206 | name: Read production relay tracing config | ||
| 207 | shell: bash | ||
| 208 | run: | | ||
| 209 | vp run --filter t3code-relay deploy \ | ||
| 210 | --stage prod \ | ||
| 211 | --read-state \ | ||
| 212 | --github-output \ | ||
| 213 | --github-env-file "$RUNNER_TEMP/relay-client-tracing.env" | ||
| 214 | |||
| 215 | - name: Upload relay client tracing config | ||
| 216 | uses: actions/upload-artifact@v7 | ||
| 217 | with: | ||
| 218 | name: relay-client-tracing-config | ||
| 219 | path: ${{ runner.temp }}/relay-client-tracing.env | ||
| 220 | if-no-files-found: error | ||
| 221 | retention-days: 1 | ||
| 222 | |||
| 189 | - id: public_config | 223 | - id: public_config |
| 190 | name: Resolve production relay public config | 224 | name: Resolve production relay public config |
| 191 | shell: bash | 225 | shell: bash |
| ... | @@ -272,6 +306,20 @@ jobs: | ... | @@ -272,6 +306,20 @@ jobs: |
| 272 | cache: true | 306 | cache: true |
| 273 | run-install: true | 307 | run-install: true |
| 274 | 308 | ||
| 309 | - name: Download relay client tracing config | ||
| 310 | uses: actions/download-artifact@v8 | ||
| 311 | with: | ||
| 312 | name: relay-client-tracing-config | ||
| 313 | path: ${{ runner.temp }}/relay-client-tracing | ||
| 314 | |||
| 315 | - name: Load relay client tracing config | ||
| 316 | shell: bash | ||
| 317 | run: | | ||
| 318 | config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env" | ||
| 319 | tracing_token="$(sed -n 's/^T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=//p' "$config_path")" | ||
| 320 | echo "::add-mask::$tracing_token" | ||
| 321 | cat "$config_path" >> "$GITHUB_ENV" | ||
| 322 | |||
| 275 | - name: Align package versions to release version | 323 | - name: Align package versions to release version |
| 276 | run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" | 324 | run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" |
| 277 | 325 | ||
| ... | @@ -508,6 +556,20 @@ jobs: | ... | @@ -508,6 +556,20 @@ jobs: |
| 508 | - --filter=@t3tools/web... | 556 | - --filter=@t3tools/web... |
| 509 | - --filter=@t3tools/scripts... | 557 | - --filter=@t3tools/scripts... |
| 510 | 558 | ||
| 559 | - name: Download relay client tracing config | ||
| 560 | uses: actions/download-artifact@v8 | ||
| 561 | with: | ||
| 562 | name: relay-client-tracing-config | ||
| 563 | path: ${{ runner.temp }}/relay-client-tracing | ||
| 564 | |||
| 565 | - name: Load relay client tracing config | ||
| 566 | shell: bash | ||
| 567 | run: | | ||
| 568 | config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env" | ||
| 569 | tracing_token="$(sed -n 's/^T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=//p' "$config_path")" | ||
| 570 | echo "::add-mask::$tracing_token" | ||
| 571 | cat "$config_path" >> "$GITHUB_ENV" | ||
| 572 | |||
| 511 | - name: Align package versions to release version | 573 | - name: Align package versions to release version |
| 512 | run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" | 574 | run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" |
| 513 | 575 | ||
| ... | @@ -670,6 +732,20 @@ jobs: | ... | @@ -670,6 +732,20 @@ jobs: |
| 670 | - --filter=@t3tools/scripts... | 732 | - --filter=@t3tools/scripts... |
| 671 | - --filter=@t3tools/web... | 733 | - --filter=@t3tools/web... |
| 672 | 734 | ||
| 735 | - name: Download relay client tracing config | ||
| 736 | uses: actions/download-artifact@v8 | ||
| 737 | with: | ||
| 738 | name: relay-client-tracing-config | ||
| 739 | path: ${{ runner.temp }}/relay-client-tracing | ||
| 740 | |||
| 741 | - name: Load relay client tracing config | ||
| 742 | shell: bash | ||
| 743 | run: | | ||
| 744 | config_path="$RUNNER_TEMP/relay-client-tracing/relay-client-tracing.env" | ||
| 745 | tracing_token="$(sed -n 's/^T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=//p' "$config_path")" | ||
| 746 | echo "::add-mask::$tracing_token" | ||
| 747 | cat "$config_path" >> "$GITHUB_ENV" | ||
| 748 | |||
| 673 | - name: Align package versions to release version | 749 | - name: Align package versions to release version |
| 674 | run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" | 750 | run: node scripts/update-release-package-versions.ts "${{ needs.preflight.outputs.version }}" |
| 675 | 751 | ||
| ... | @@ -716,6 +792,9 @@ jobs: | ... | @@ -716,6 +792,9 @@ jobs: |
| 716 | --build-env "T3CODE_CLERK_PUBLISHABLE_KEY=${T3CODE_CLERK_PUBLISHABLE_KEY:-}" \ | 792 | --build-env "T3CODE_CLERK_PUBLISHABLE_KEY=${T3CODE_CLERK_PUBLISHABLE_KEY:-}" \ |
| 717 | --build-env "T3CODE_CLERK_JWT_TEMPLATE=${T3CODE_CLERK_JWT_TEMPLATE:-}" \ | 793 | --build-env "T3CODE_CLERK_JWT_TEMPLATE=${T3CODE_CLERK_JWT_TEMPLATE:-}" \ |
| 718 | --build-env "T3CODE_RELAY_URL=${T3CODE_RELAY_URL:-}" \ | 794 | --build-env "T3CODE_RELAY_URL=${T3CODE_RELAY_URL:-}" \ |
| 795 | --build-env "T3CODE_RELAY_CLIENT_OTLP_TRACES_URL=${T3CODE_RELAY_CLIENT_OTLP_TRACES_URL:-}" \ | ||
| 796 | --build-env "T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET=${T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET:-}" \ | ||
| 797 | --build-env "T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=${T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN:-}" \ | ||
| 719 | --build-env "VITE_HOSTED_APP_URL=$router_url" \ | 798 | --build-env "VITE_HOSTED_APP_URL=$router_url" \ |
| 720 | --build-env "VITE_HOSTED_APP_CHANNEL=$channel_name" | 799 | --build-env "VITE_HOSTED_APP_CHANNEL=$channel_name" |
| 721 | )" | 800 | )" |
apps/mobile/src/features/observability/mobileTracing.test.ts+2-1| ... | @@ -4,6 +4,7 @@ import * as Layer from "effect/Layer"; | ... | @@ -4,6 +4,7 @@ import * as Layer from "effect/Layer"; |
| 4 | import { vi } from "vite-plus/test"; | 4 | import { vi } from "vite-plus/test"; |
| 5 | 5 | ||
| 6 | import { remoteHttpClientLayer } from "@t3tools/client-runtime"; | 6 | import { remoteHttpClientLayer } from "@t3tools/client-runtime"; |
| 7 | import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; | ||
| 7 | 8 | ||
| 8 | import { makeMobileTracingLayer } from "./mobileTracing"; | 9 | import { makeMobileTracingLayer } from "./mobileTracing"; |
| 9 | 10 | ||
| ... | @@ -29,7 +30,7 @@ it.effect("exports spans through the scoped mobile OTLP layer", () => { | ... | @@ -29,7 +30,7 @@ it.effect("exports spans through the scoped mobile OTLP layer", () => { |
| 29 | }, | 30 | }, |
| 30 | ).pipe(Layer.provide(remoteHttpClientLayer(fetchFn))); | 31 | ).pipe(Layer.provide(remoteHttpClientLayer(fetchFn))); |
| 31 | const tracedApplication = Layer.effectDiscard( | 32 | const tracedApplication = Layer.effectDiscard( |
| 32 | Effect.void.pipe(Effect.withSpan("mobile.test.span")), | 33 | Effect.void.pipe(Effect.withSpan("mobile.test.span"), withRelayClientTracing), |
| 33 | ).pipe(Layer.provide(tracingLayer)); | 34 | ).pipe(Layer.provide(tracingLayer)); |
| 34 | 35 | ||
| 35 | return Effect.gen(function* () { | 36 | return Effect.gen(function* () { |
apps/mobile/src/features/observability/mobileTracing.ts+8-24| ... | @@ -1,7 +1,5 @@ | ... | @@ -1,7 +1,5 @@ |
| 1 | import Constants from "expo-constants"; | 1 | import Constants from "expo-constants"; |
| 2 | import * as Layer from "effect/Layer"; | 2 | import { makeRelayClientTracingLayer } from "@t3tools/shared/relayTracing"; |
| 3 | import type { HttpClient } from "effect/unstable/http"; | ||
| 4 | import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability"; | ||
| 5 | 3 | ||
| 6 | import { hasMobileTracingPublicConfig, resolveCloudPublicConfig } from "../cloud/publicConfig"; | 4 | import { hasMobileTracingPublicConfig, resolveCloudPublicConfig } from "../cloud/publicConfig"; |
| 7 | 5 | ||
| ... | @@ -28,27 +26,13 @@ export function resolveMobileTracingConfig(): MobileTracingConfig | null { | ... | @@ -28,27 +26,13 @@ export function resolveMobileTracingConfig(): MobileTracingConfig | null { |
| 28 | export function makeMobileTracingLayer( | 26 | export function makeMobileTracingLayer( |
| 29 | config: MobileTracingConfig | null, | 27 | config: MobileTracingConfig | null, |
| 30 | resource: MobileTracingResource, | 28 | resource: MobileTracingResource, |
| 31 | ): Layer.Layer<never, never, HttpClient.HttpClient> { | 29 | ) { |
| 32 | if (config === null) { | 30 | return makeRelayClientTracingLayer(config, { |
| 33 | return Layer.empty; | 31 | serviceName: "t3-mobile-relay-client", |
| 34 | } | 32 | serviceVersion: resource.serviceVersion, |
| 35 | 33 | runtime: "react-native", | |
| 36 | return OtlpTracer.layer({ | 34 | client: `mobile-${resource.appVariant}`, |
| 37 | url: config.tracesUrl, | 35 | }); |
| 38 | headers: { | ||
| 39 | Authorization: `Bearer ${config.tracesToken}`, | ||
| 40 | "X-Axiom-Dataset": config.tracesDataset, | ||
| 41 | }, | ||
| 42 | resource: { | ||
| 43 | serviceName: "t3-mobile", | ||
| 44 | serviceVersion: resource.serviceVersion, | ||
| 45 | attributes: { | ||
| 46 | "service.runtime": "react-native", | ||
| 47 | "service.component": "mobile", | ||
| 48 | "deployment.environment.name": resource.appVariant, | ||
| 49 | }, | ||
| 50 | }, | ||
| 51 | }).pipe(Layer.provide(OtlpSerialization.layerJson)); | ||
| 52 | } | 36 | } |
| 53 | 37 | ||
| 54 | export const mobileTracingLayer = makeMobileTracingLayer(resolveMobileTracingConfig(), { | 38 | export const mobileTracingLayer = makeMobileTracingLayer(resolveMobileTracingConfig(), { |
apps/server/src/cli/connect.ts+4| ... | @@ -6,6 +6,7 @@ import { | ... | @@ -6,6 +6,7 @@ import { |
| 6 | } from "@t3tools/contracts"; | 6 | } from "@t3tools/contracts"; |
| 7 | import { RelayOkResponse } from "@t3tools/contracts/relay"; | 7 | import { RelayOkResponse } from "@t3tools/contracts/relay"; |
| 8 | import * as RelayClient from "@t3tools/shared/relayClient"; | 8 | import * as RelayClient from "@t3tools/shared/relayClient"; |
| 9 | import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; | ||
| 9 | import * as Console from "effect/Console"; | 10 | import * as Console from "effect/Console"; |
| 10 | import * as Duration from "effect/Duration"; | 11 | import * as Duration from "effect/Duration"; |
| 11 | import * as Effect from "effect/Effect"; | 12 | import * as Effect from "effect/Effect"; |
| ... | @@ -29,6 +30,7 @@ import * as CliState from "../cloud/CliState.ts"; | ... | @@ -29,6 +30,7 @@ import * as CliState from "../cloud/CliState.ts"; |
| 29 | import * as CliTokenManager from "../cloud/CliTokenManager.ts"; | 30 | import * as CliTokenManager from "../cloud/CliTokenManager.ts"; |
| 30 | import { CLOUD_LINKED_USER_ID, RELAY_URL_SECRET } from "../cloud/config.ts"; | 31 | import { CLOUD_LINKED_USER_ID, RELAY_URL_SECRET } from "../cloud/config.ts"; |
| 31 | import { relayUrlConfig } from "../cloud/publicConfig.ts"; | 32 | import { relayUrlConfig } from "../cloud/publicConfig.ts"; |
| 33 | import { headlessRelayClientTracingLayer } from "../cloud/relayTracing.ts"; | ||
| 32 | import { ServerConfig } from "../config.ts"; | 34 | import { ServerConfig } from "../config.ts"; |
| 33 | import { ServerEnvironmentLive } from "../environment/Layers/ServerEnvironment.ts"; | 35 | import { ServerEnvironmentLive } from "../environment/Layers/ServerEnvironment.ts"; |
| 34 | import { ServerEnvironment } from "../environment/Services/ServerEnvironment.ts"; | 36 | import { ServerEnvironment } from "../environment/Services/ServerEnvironment.ts"; |
| ... | @@ -228,6 +230,7 @@ const unlinkRelayEnvironment = Effect.fn("cloud.cli.unlink_relay_environment")(f | ... | @@ -228,6 +230,7 @@ const unlinkRelayEnvironment = Effect.fn("cloud.cli.unlink_relay_environment")(f |
| 228 | httpClient.execute, | 230 | httpClient.execute, |
| 229 | Effect.flatMap(HttpClientResponse.filterStatusOk), | 231 | Effect.flatMap(HttpClientResponse.filterStatusOk), |
| 230 | Effect.flatMap(HttpClientResponse.schemaBodyJson(RelayOkResponse)), | 232 | Effect.flatMap(HttpClientResponse.schemaBodyJson(RelayOkResponse)), |
| 233 | withRelayClientTracing, | ||
| 231 | ); | 234 | ); |
| 232 | return response.ok | 235 | return response.ok |
| 233 | ? ({ status: "revoked" } satisfies RelayUnlinkResult) | 236 | ? ({ status: "revoked" } satisfies RelayUnlinkResult) |
| ... | @@ -299,6 +302,7 @@ const runCloudCommand = <A, E>( | ... | @@ -299,6 +302,7 @@ const runCloudCommand = <A, E>( |
| 299 | RelayClient.layerCloudflared({ baseDir: config.baseDir }), | 302 | RelayClient.layerCloudflared({ baseDir: config.baseDir }), |
| 300 | EnvironmentAuth.runtimeLayer, | 303 | EnvironmentAuth.runtimeLayer, |
| 301 | ServerEnvironmentLive, | 304 | ServerEnvironmentLive, |
| 305 | headlessRelayClientTracingLayer, | ||
| 302 | ).pipe( | 306 | ).pipe( |
| 303 | Layer.provideMerge(FetchHttpClient.layer), | 307 | Layer.provideMerge(FetchHttpClient.layer), |
| 304 | Layer.provideMerge(Layer.succeed(ServerConfig, config)), | 308 | Layer.provideMerge(Layer.succeed(ServerConfig, config)), |
apps/server/src/cloud/http.test.ts+40-2| ... | @@ -3,13 +3,19 @@ import { describe, expect, it } from "@effect/vitest"; | ... | @@ -3,13 +3,19 @@ import { describe, expect, it } from "@effect/vitest"; |
| 3 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 4 | import * as Option from "effect/Option"; | 4 | import * as Option from "effect/Option"; |
| 5 | import * as PlatformError from "effect/PlatformError"; | 5 | import * as PlatformError from "effect/PlatformError"; |
| 6 | import { HttpClient } from "effect/unstable/http"; | 6 | import * as Tracer from "effect/Tracer"; |
| 7 | import { HttpClient, HttpServerRequest } from "effect/unstable/http"; | ||
| 7 | 8 | ||
| 9 | import { RelayClientTracer } from "@t3tools/shared/relayTracing"; | ||
| 8 | import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts"; | 10 | import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts"; |
| 9 | import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; | 11 | import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; |
| 10 | import { ServerEnvironment } from "../environment/Services/ServerEnvironment.ts"; | 12 | import { ServerEnvironment } from "../environment/Services/ServerEnvironment.ts"; |
| 11 | import * as CliTokenManager from "./CliTokenManager.ts"; | 13 | import * as CliTokenManager from "./CliTokenManager.ts"; |
| 12 | import { consumeCloudReplayGuards, reconcileDesiredCloudLink } from "./http.ts"; | 14 | import { |
| 15 | consumeCloudReplayGuards, | ||
| 16 | reconcileDesiredCloudLink, | ||
| 17 | traceRelayBrokerHandler, | ||
| 18 | } from "./http.ts"; | ||
| 13 | import { | 19 | import { |
| 14 | CloudManagedEndpointRuntime, | 20 | CloudManagedEndpointRuntime, |
| 15 | type CloudManagedEndpointRuntimeShape, | 21 | type CloudManagedEndpointRuntimeShape, |
| ... | @@ -69,6 +75,38 @@ describe("consumeCloudReplayGuards", () => { | ... | @@ -69,6 +75,38 @@ describe("consumeCloudReplayGuards", () => { |
| 69 | ); | 75 | ); |
| 70 | }); | 76 | }); |
| 71 | 77 | ||
| 78 | describe("traceRelayBrokerHandler", () => { | ||
| 79 | it.effect("continues the incoming relay trace with the product tracer", () => | ||
| 80 | Effect.gen(function* () { | ||
| 81 | const spans: Array<Tracer.Span> = []; | ||
| 82 | const productTracer = Tracer.make({ | ||
| 83 | span: (options) => { | ||
| 84 | const span = new Tracer.NativeSpan(options); | ||
| 85 | spans.push(span); | ||
| 86 | return span; | ||
| 87 | }, | ||
| 88 | }); | ||
| 89 | const request = HttpServerRequest.fromWeb( | ||
| 90 | new Request("https://environment.example.test/api/t3-cloud/mint-credential", { | ||
| 91 | headers: { | ||
| 92 | traceparent: "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01", | ||
| 93 | }, | ||
| 94 | }), | ||
| 95 | ); | ||
| 96 | |||
| 97 | yield* traceRelayBrokerHandler(Effect.void.pipe(Effect.withSpan("relay.mint.handler"))).pipe( | ||
| 98 | Effect.provideService(HttpServerRequest.HttpServerRequest, request), | ||
| 99 | Effect.provideService(RelayClientTracer, Option.some(productTracer)), | ||
| 100 | ); | ||
| 101 | |||
| 102 | expect(spans).toHaveLength(1); | ||
| 103 | const span = spans[0]!; | ||
| 104 | expect(span.traceId).toBe("0123456789abcdef0123456789abcdef"); | ||
| 105 | expect(Option.getOrUndefined(span.parent)?.spanId).toBe("0123456789abcdef"); | ||
| 106 | }), | ||
| 107 | ); | ||
| 108 | }); | ||
| 109 | |||
| 72 | describe("reconcileDesiredCloudLink", () => { | 110 | describe("reconcileDesiredCloudLink", () => { |
| 73 | it.effect("requires stored CLI authorization without exposing an HTTP endpoint", () => | 111 | it.effect("requires stored CLI authorization without exposing an HTTP endpoint", () => |
| 74 | Effect.gen(function* () { | 112 | Effect.gen(function* () { |
apps/server/src/cloud/http.ts+17-2| ... | @@ -29,6 +29,7 @@ import { | ... | @@ -29,6 +29,7 @@ import { |
| 29 | RelayLinkProofRequest, | 29 | RelayLinkProofRequest, |
| 30 | RelayManagedEndpointOrigin, | 30 | RelayManagedEndpointOrigin, |
| 31 | } from "@t3tools/contracts/relay"; | 31 | } from "@t3tools/contracts/relay"; |
| 32 | import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; | ||
| 32 | import { | 33 | import { |
| 33 | normalizeRelayIssuer, | 34 | normalizeRelayIssuer, |
| 34 | RELAY_HEALTH_REQUEST_TYP, | 35 | RELAY_HEALTH_REQUEST_TYP, |
| ... | @@ -47,7 +48,7 @@ import * as Effect from "effect/Effect"; | ... | @@ -47,7 +48,7 @@ import * as Effect from "effect/Effect"; |
| 47 | import * as Option from "effect/Option"; | 48 | import * as Option from "effect/Option"; |
| 48 | import * as Schema from "effect/Schema"; | 49 | import * as Schema from "effect/Schema"; |
| 49 | import * as HttpEffect from "effect/unstable/http/HttpEffect"; | 50 | import * as HttpEffect from "effect/unstable/http/HttpEffect"; |
| 50 | import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http"; | 51 | import { HttpServerRequest, HttpServerResponse, HttpTraceContext } from "effect/unstable/http"; |
| 51 | import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; | 52 | import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http"; |
| 52 | import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder"; | 53 | import * as HttpApiBuilder from "effect/unstable/httpapi/HttpApiBuilder"; |
| 53 | 54 | ||
| ... | @@ -110,6 +111,19 @@ const requireRelayUrl = relayUrlConfig.pipe( | ... | @@ -110,6 +111,19 @@ const requireRelayUrl = relayUrlConfig.pipe( |
| 110 | ), | 111 | ), |
| 111 | ); | 112 | ); |
| 112 | 113 | ||
| 114 | export const traceRelayBrokerHandler = <A, E, R>( | ||
| 115 | effect: Effect.Effect<A, E, R>, | ||
| 116 | ): Effect.Effect<A, E, R | HttpServerRequest.HttpServerRequest> => | ||
| 117 | HttpServerRequest.HttpServerRequest.pipe( | ||
| 118 | Effect.flatMap((request) => | ||
| 119 | Option.match(HttpTraceContext.fromHeaders(request.headers), { | ||
| 120 | onNone: () => effect, | ||
| 121 | onSome: (parent) => effect.pipe(Effect.withParentSpan(parent)), | ||
| 122 | }), | ||
| 123 | ), | ||
| 124 | withRelayClientTracing, | ||
| 125 | ); | ||
| 126 | |||
| 113 | function bytesToString(bytes: Uint8Array): string { | 127 | function bytesToString(bytes: Uint8Array): string { |
| 114 | return new TextDecoder().decode(bytes); | 128 | return new TextDecoder().decode(bytes); |
| 115 | } | 129 | } |
| ... | @@ -512,6 +526,7 @@ const relayClientRequest = <A>( | ... | @@ -512,6 +526,7 @@ const relayClientRequest = <A>( |
| 512 | message: `T3 Connect relay request failed: ${String(cause)}`, | 526 | message: `T3 Connect relay request failed: ${String(cause)}`, |
| 513 | }), | 527 | }), |
| 514 | ), | 528 | ), |
| 529 | withRelayClientTracing, | ||
| 515 | ); | 530 | ); |
| 516 | 531 | ||
| 517 | const reconcileDesiredCloudLinkWith = Effect.fn("environment.cloud.reconcileDesiredLinkWith")( | 532 | const reconcileDesiredCloudLinkWith = Effect.fn("environment.cloud.reconcileDesiredLinkWith")( |
| ... | @@ -938,7 +953,7 @@ export const connectHttpApiLayer = HttpApiBuilder.group( | ... | @@ -938,7 +953,7 @@ export const connectHttpApiLayer = HttpApiBuilder.group( |
| 938 | .handle("health", ({ payload }) => cloudEnvironmentHealthHandler(dependencies, payload)) | 953 | .handle("health", ({ payload }) => cloudEnvironmentHealthHandler(dependencies, payload)) |
| 939 | .handle("mintCredential", ({ payload }) => cloudMintCredentialHandler(dependencies, payload)) | 954 | .handle("mintCredential", ({ payload }) => cloudMintCredentialHandler(dependencies, payload)) |
| 940 | .handle("t3MintCredential", ({ payload }) => | 955 | .handle("t3MintCredential", ({ payload }) => |
| 941 | cloudMintCredentialHandler(dependencies, payload), | 956 | traceRelayBrokerHandler(cloudMintCredentialHandler(dependencies, payload)), |
| 942 | ); | 957 | ); |
| 943 | }), | 958 | }), |
| 944 | ); | 959 | ); |
apps/server/src/cloud/publicConfig.test.ts+41-1| ... | @@ -2,7 +2,11 @@ import { assert, it } from "@effect/vitest"; | ... | @@ -2,7 +2,11 @@ import { assert, it } from "@effect/vitest"; |
| 2 | import * as ConfigProvider from "effect/ConfigProvider"; | 2 | import * as ConfigProvider from "effect/ConfigProvider"; |
| 3 | import * as Effect from "effect/Effect"; | 3 | import * as Effect from "effect/Effect"; |
| 4 | 4 | ||
| 5 | import { makeCloudCliOAuthConfig, makeRelayUrlConfig } from "./publicConfig.ts"; | 5 | import { |
| 6 | makeCloudCliOAuthConfig, | ||
| 7 | makeRelayUrlConfig, | ||
| 8 | resolveRelayClientTracingConfig, | ||
| 9 | } from "./publicConfig.ts"; | ||
| 6 | 10 | ||
| 7 | const provideEnv = (env: Readonly<Record<string, string>>) => | 11 | const provideEnv = (env: Readonly<Record<string, string>>) => |
| 8 | Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env }))); | 12 | Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env }))); |
| ... | @@ -83,3 +87,39 @@ it.effect("requires Clerk OAuth config when the server bundle has no injected va | ... | @@ -83,3 +87,39 @@ it.effect("requires Clerk OAuth config when the server bundle has no injected va |
| 83 | clerkCliOAuthClientIdFallback: "", | 87 | clerkCliOAuthClientIdFallback: "", |
| 84 | }).pipe(provideEnv({}), Effect.flip), | 88 | }).pipe(provideEnv({}), Effect.flip), |
| 85 | ); | 89 | ); |
| 90 | |||
| 91 | it("resolves relay client tracing from runtime config with build-time fallback", () => { | ||
| 92 | const fallback = { | ||
| 93 | tracesUrl: "https://embedded.example.test/v1/traces", | ||
| 94 | tracesDataset: "embedded-dataset", | ||
| 95 | tracesToken: "embedded-token", | ||
| 96 | }; | ||
| 97 | |||
| 98 | assert.deepEqual(resolveRelayClientTracingConfig({}, fallback), fallback); | ||
| 99 | assert.deepEqual( | ||
| 100 | resolveRelayClientTracingConfig( | ||
| 101 | { | ||
| 102 | T3CODE_RELAY_CLIENT_OTLP_TRACES_URL: "https://runtime.example.test/v1/traces", | ||
| 103 | T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET: "runtime-dataset", | ||
| 104 | T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN: "runtime-token", | ||
| 105 | }, | ||
| 106 | fallback, | ||
| 107 | ), | ||
| 108 | { | ||
| 109 | tracesUrl: "https://runtime.example.test/v1/traces", | ||
| 110 | tracesDataset: "runtime-dataset", | ||
| 111 | tracesToken: "runtime-token", | ||
| 112 | }, | ||
| 113 | ); | ||
| 114 | assert.equal( | ||
| 115 | resolveRelayClientTracingConfig( | ||
| 116 | { | ||
| 117 | T3CODE_RELAY_CLIENT_OTLP_TRACES_URL: "http://insecure.example.test/v1/traces", | ||
| 118 | T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET: "runtime-dataset", | ||
| 119 | T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN: "runtime-token", | ||
| 120 | }, | ||
| 121 | fallback, | ||
| 122 | ), | ||
| 123 | null, | ||
| 124 | ); | ||
| 125 | }); |
apps/server/src/cloud/publicConfig.ts+43| ... | @@ -9,6 +9,9 @@ import * as SchemaIssue from "effect/SchemaIssue"; | ... | @@ -9,6 +9,9 @@ import * as SchemaIssue from "effect/SchemaIssue"; |
| 9 | declare const __T3CODE_BUILD_RELAY_URL__: string | undefined; | 9 | declare const __T3CODE_BUILD_RELAY_URL__: string | undefined; |
| 10 | declare const __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: string | undefined; | 10 | declare const __T3CODE_BUILD_CLERK_PUBLISHABLE_KEY__: string | undefined; |
| 11 | declare const __T3CODE_BUILD_CLERK_CLI_OAUTH_CLIENT_ID__: string | undefined; | 11 | declare const __T3CODE_BUILD_CLERK_CLI_OAUTH_CLIENT_ID__: string | undefined; |
| 12 | declare const __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_URL__: string | undefined; | ||
| 13 | declare const __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_DATASET__: string | undefined; | ||
| 14 | declare const __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_TOKEN__: string | undefined; | ||
| 12 | 15 | ||
| 13 | const CLOUD_CLI_OAUTH_REDIRECT_URI = "http://127.0.0.1:34338/callback"; | 16 | const CLOUD_CLI_OAUTH_REDIRECT_URI = "http://127.0.0.1:34338/callback"; |
| 14 | const CLOUD_CLI_OAUTH_SCOPES = ["openid", "profile", "email"] as const; | 17 | const CLOUD_CLI_OAUTH_SCOPES = ["openid", "profile", "email"] as const; |
| ... | @@ -32,6 +35,15 @@ function readBuildTimeValue(value: string | undefined): string { | ... | @@ -32,6 +35,15 @@ function readBuildTimeValue(value: string | undefined): string { |
| 32 | return typeof value === "undefined" ? "" : value.trim(); | 35 | return typeof value === "undefined" ? "" : value.trim(); |
| 33 | } | 36 | } |
| 34 | 37 | ||
| 38 | function normalizeSecureUrl(value: string): string | null { | ||
| 39 | try { | ||
| 40 | const url = new URL(value); | ||
| 41 | return url.protocol === "https:" ? url.toString() : null; | ||
| 42 | } catch { | ||
| 43 | return null; | ||
| 44 | } | ||
| 45 | } | ||
| 46 | |||
| 35 | export const buildTimeRelayUrl = | 47 | export const buildTimeRelayUrl = |
| 36 | typeof __T3CODE_BUILD_RELAY_URL__ === "undefined" | 48 | typeof __T3CODE_BUILD_RELAY_URL__ === "undefined" |
| 37 | ? "" | 49 | ? "" |
| ... | @@ -46,6 +58,37 @@ export const buildTimeClerkCliOAuthClientId = readBuildTimeValue( | ... | @@ -46,6 +58,37 @@ export const buildTimeClerkCliOAuthClientId = readBuildTimeValue( |
| 46 | ? undefined | 58 | ? undefined |
| 47 | : __T3CODE_BUILD_CLERK_CLI_OAUTH_CLIENT_ID__, | 59 | : __T3CODE_BUILD_CLERK_CLI_OAUTH_CLIENT_ID__, |
| 48 | ); | 60 | ); |
| 61 | export const buildTimeRelayClientTracing = { | ||
| 62 | tracesUrl: readBuildTimeValue( | ||
| 63 | typeof __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_URL__ === "undefined" | ||
| 64 | ? undefined | ||
| 65 | : __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_URL__, | ||
| 66 | ), | ||
| 67 | tracesDataset: readBuildTimeValue( | ||
| 68 | typeof __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_DATASET__ === "undefined" | ||
| 69 | ? undefined | ||
| 70 | : __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_DATASET__, | ||
| 71 | ), | ||
| 72 | tracesToken: readBuildTimeValue( | ||
| 73 | typeof __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_TOKEN__ === "undefined" | ||
| 74 | ? undefined | ||
| 75 | : __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_TOKEN__, | ||
| 76 | ), | ||
| 77 | } as const; | ||
| 78 | |||
| 79 | export function resolveRelayClientTracingConfig( | ||
| 80 | env: Readonly<Record<string, string | undefined>> = process.env, | ||
| 81 | fallback = buildTimeRelayClientTracing, | ||
| 82 | ) { | ||
| 83 | const tracesUrl = env.T3CODE_RELAY_CLIENT_OTLP_TRACES_URL?.trim() || fallback.tracesUrl; | ||
| 84 | const tracesDataset = | ||
| 85 | env.T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET?.trim() || fallback.tracesDataset; | ||
| 86 | const tracesToken = env.T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN?.trim() || fallback.tracesToken; | ||
| 87 | const normalizedTracesUrl = normalizeSecureUrl(tracesUrl); | ||
| 88 | return normalizedTracesUrl && tracesDataset && tracesToken | ||
| 89 | ? { tracesUrl: normalizedTracesUrl, tracesDataset, tracesToken } | ||
| 90 | : null; | ||
| 91 | } | ||
| 49 | 92 | ||
| 50 | export function makeRelayUrlConfig(fallback = buildTimeRelayUrl) { | 93 | export function makeRelayUrlConfig(fallback = buildTimeRelayUrl) { |
| 51 | const runtimeConfig = Config.nonEmptyString("T3CODE_RELAY_URL"); | 94 | const runtimeConfig = Config.nonEmptyString("T3CODE_RELAY_URL"); |
apps/server/src/cloud/relayTracing.ts created+21| ... | @@ -0,0 +1,21 @@ | ||
| 1 | import { makeRelayClientTracingLayer } from "@t3tools/shared/relayTracing"; | ||
| 2 | |||
| 3 | import { resolveRelayClientTracingConfig } from "./publicConfig.ts"; | ||
| 4 | |||
| 5 | const relayClientTracingConfig = resolveRelayClientTracingConfig(); | ||
| 6 | |||
| 7 | export const headlessRelayClientTracingLayer = makeRelayClientTracingLayer( | ||
| 8 | relayClientTracingConfig, | ||
| 9 | { | ||
| 10 | serviceName: "t3-headless-relay-client", | ||
| 11 | runtime: "node", | ||
| 12 | client: "headless-cli", | ||
| 13 | }, | ||
| 14 | ); | ||
| 15 | |||
| 16 | export const serverRelayBrokerTracingLayer = makeRelayClientTracingLayer(relayClientTracingConfig, { | ||
| 17 | serviceName: "t3-server", | ||
| 18 | runtime: "node", | ||
| 19 | client: "environment-server", | ||
| 20 | component: "relay-broker", | ||
| 21 | }); | ||
apps/server/src/relay/AgentAwarenessRelay.test.ts+20| ... | @@ -17,6 +17,7 @@ import type { | ... | @@ -17,6 +17,7 @@ import type { |
| 17 | RelayAgentActivityState, | 17 | RelayAgentActivityState, |
| 18 | } from "@t3tools/contracts/relay"; | 18 | } from "@t3tools/contracts/relay"; |
| 19 | import { CommandId, ProviderInstanceId } from "@t3tools/contracts"; | 19 | import { CommandId, ProviderInstanceId } from "@t3tools/contracts"; |
| 20 | import { RelayClientTracer } from "@t3tools/shared/relayTracing"; | ||
| 20 | import { RELAY_ACTIVITY_PUBLISH_TYP, verifyRelayJwt } from "@t3tools/shared/relayJwt"; | 21 | import { RELAY_ACTIVITY_PUBLISH_TYP, verifyRelayJwt } from "@t3tools/shared/relayJwt"; |
| 21 | import { describe, expect, it } from "@effect/vitest"; | 22 | import { describe, expect, it } from "@effect/vitest"; |
| 22 | import * as Deferred from "effect/Deferred"; | 23 | import * as Deferred from "effect/Deferred"; |
| ... | @@ -25,6 +26,7 @@ import * as Layer from "effect/Layer"; | ... | @@ -25,6 +26,7 @@ import * as Layer from "effect/Layer"; |
| 25 | import * as Option from "effect/Option"; | 26 | import * as Option from "effect/Option"; |
| 26 | import * as Queue from "effect/Queue"; | 27 | import * as Queue from "effect/Queue"; |
| 27 | import * as Stream from "effect/Stream"; | 28 | import * as Stream from "effect/Stream"; |
| 29 | import * as Tracer from "effect/Tracer"; | ||
| 28 | 30 | ||
| 29 | import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; | 31 | import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; |
| 30 | import { ServerEnvironment } from "../environment/Services/ServerEnvironment.ts"; | 32 | import { ServerEnvironment } from "../environment/Services/ServerEnvironment.ts"; |
| ... | @@ -522,6 +524,20 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { | ... | @@ -522,6 +524,20 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { |
| 522 | const runFork = Effect.runForkWith(context); | 524 | const runFork = Effect.runForkWith(context); |
| 523 | const events = yield* Queue.unbounded<OrchestrationEvent>(); | 525 | const events = yield* Queue.unbounded<OrchestrationEvent>(); |
| 524 | const fetchSeen = yield* Deferred.make<URL>(); | 526 | const fetchSeen = yield* Deferred.make<URL>(); |
| 527 | const userSpans: Array<string> = []; | ||
| 528 | const productSpans: Array<string> = []; | ||
| 529 | const collectingTracer = (spans: Array<string>) => | ||
| 530 | Tracer.make({ | ||
| 531 | span: (options) => { | ||
| 532 | const span = new Tracer.NativeSpan(options); | ||
| 533 | const end = span.end.bind(span); | ||
| 534 | span.end = (endTime, exit) => { | ||
| 535 | end(endTime, exit); | ||
| 536 | spans.push(span.name); | ||
| 537 | }; | ||
| 538 | return span; | ||
| 539 | }, | ||
| 540 | }); | ||
| 525 | const secrets = makeMemorySecretStore(); | 541 | const secrets = makeMemorySecretStore(); |
| 526 | const now = "2026-05-25T00:00:00.000Z"; | 542 | const now = "2026-05-25T00:00:00.000Z"; |
| 527 | const projectId = "project-1" as ProjectId; | 543 | const projectId = "project-1" as ProjectId; |
| ... | @@ -648,6 +664,8 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { | ... | @@ -648,6 +664,8 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { |
| 648 | 664 | ||
| 649 | const url = yield* Deferred.await(fetchSeen).pipe(Effect.timeout("2 seconds")); | 665 | const url = yield* Deferred.await(fetchSeen).pipe(Effect.timeout("2 seconds")); |
| 650 | expect(url.origin).toBe("https://transport.example.test"); | 666 | expect(url.origin).toBe("https://transport.example.test"); |
| 667 | expect(productSpans).toContain("makePublishProof"); | ||
| 668 | expect(userSpans).not.toContain("makePublishProof"); | ||
| 651 | }).pipe( | 669 | }).pipe( |
| 652 | Effect.provide( | 670 | Effect.provide( |
| 653 | AgentAwarenessRelay.layer.pipe( | 671 | AgentAwarenessRelay.layer.pipe( |
| ... | @@ -655,6 +673,8 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { | ... | @@ -655,6 +673,8 @@ describe.sequential("signRelayAgentActivityPublishProof", () => { |
| 655 | Layer.provideMerge(NodeServices.layer), | 673 | Layer.provideMerge(NodeServices.layer), |
| 656 | ), | 674 | ), |
| 657 | ), | 675 | ), |
| 676 | Effect.provideService(RelayClientTracer, Option.some(collectingTracer(productSpans))), | ||
| 677 | Effect.withTracer(collectingTracer(userSpans)), | ||
| 658 | ); | 678 | ); |
| 659 | }), | 679 | }), |
| 660 | ), | 680 | ), |
apps/server/src/relay/AgentAwarenessRelay.ts+2| ... | @@ -12,6 +12,7 @@ import type { | ... | @@ -12,6 +12,7 @@ import type { |
| 12 | } from "@t3tools/contracts"; | 12 | } from "@t3tools/contracts"; |
| 13 | import { projectThreadAwareness } from "@t3tools/shared/agentAwareness"; | 13 | import { projectThreadAwareness } from "@t3tools/shared/agentAwareness"; |
| 14 | import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker"; | 14 | import { makeDrainableWorker } from "@t3tools/shared/DrainableWorker"; |
| 15 | import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; | ||
| 15 | import { | 16 | import { |
| 16 | RELAY_ACTIVITY_PUBLISH_TYP, | 17 | RELAY_ACTIVITY_PUBLISH_TYP, |
| 17 | signRelayJwt, | 18 | signRelayJwt, |
| ... | @@ -409,6 +410,7 @@ const make = Effect.gen(function* () { | ... | @@ -409,6 +410,7 @@ const make = Effect.gen(function* () { |
| 409 | }); | 410 | }); |
| 410 | }), | 411 | }), |
| 411 | Effect.withSpan("AgentAwarenessRelay.publishThread"), | 412 | Effect.withSpan("AgentAwarenessRelay.publishThread"), |
| 413 | withRelayClientTracing, | ||
| 412 | ); | 414 | ); |
| 413 | 415 | ||
| 414 | const publishActiveThreadsUnsafe = Effect.gen(function* () { | 416 | const publishActiveThreadsUnsafe = Effect.gen(function* () { |
apps/server/src/server.ts+2| ... | @@ -74,6 +74,7 @@ import { authHttpApiLayer, environmentAuthenticatedAuthLayer } from "./auth/http | ... | @@ -74,6 +74,7 @@ import { authHttpApiLayer, environmentAuthenticatedAuthLayer } from "./auth/http |
| 74 | import * as ServerSecretStore from "./auth/ServerSecretStore.ts"; | 74 | import * as ServerSecretStore from "./auth/ServerSecretStore.ts"; |
| 75 | import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; | 75 | import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts"; |
| 76 | import { connectHttpApiLayer, reconcileDesiredCloudLink } from "./cloud/http.ts"; | 76 | import { connectHttpApiLayer, reconcileDesiredCloudLink } from "./cloud/http.ts"; |
| 77 | import { serverRelayBrokerTracingLayer } from "./cloud/relayTracing.ts"; | ||
| 77 | import * as CloudManagedEndpointRuntime from "./cloud/ManagedEndpointRuntime.ts"; | 78 | import * as CloudManagedEndpointRuntime from "./cloud/ManagedEndpointRuntime.ts"; |
| 78 | import * as CloudCliTokenManager from "./cloud/CliTokenManager.ts"; | 79 | import * as CloudCliTokenManager from "./cloud/CliTokenManager.ts"; |
| 79 | import * as CloudCliState from "./cloud/CliState.ts"; | 80 | import * as CloudCliState from "./cloud/CliState.ts"; |
| ... | @@ -477,6 +478,7 @@ export const makeServerLayer = Layer.unwrap( | ... | @@ -477,6 +478,7 @@ export const makeServerLayer = Layer.unwrap( |
| 477 | 478 | ||
| 478 | return serverApplicationLayer.pipe( | 479 | return serverApplicationLayer.pipe( |
| 479 | Layer.provideMerge(RuntimeServicesLive), | 480 | Layer.provideMerge(RuntimeServicesLive), |
| 481 | Layer.provideMerge(serverRelayBrokerTracingLayer), | ||
| 480 | Layer.provideMerge(HttpServerLive), | 482 | Layer.provideMerge(HttpServerLive), |
| 481 | Layer.provide(ObservabilityLive), | 483 | Layer.provide(ObservabilityLive), |
| 482 | Layer.provideMerge(FetchHttpClient.layer), | 484 | Layer.provideMerge(FetchHttpClient.layer), |
apps/server/vite.config.ts+9| ... | @@ -49,6 +49,15 @@ export default mergeConfig( | ... | @@ -49,6 +49,15 @@ export default mergeConfig( |
| 49 | __T3CODE_BUILD_CLERK_CLI_OAUTH_CLIENT_ID__: JSON.stringify( | 49 | __T3CODE_BUILD_CLERK_CLI_OAUTH_CLIENT_ID__: JSON.stringify( |
| 50 | repoEnv.T3CODE_CLERK_CLI_OAUTH_CLIENT_ID?.trim() ?? "", | 50 | repoEnv.T3CODE_CLERK_CLI_OAUTH_CLIENT_ID?.trim() ?? "", |
| 51 | ), | 51 | ), |
| 52 | __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_URL__: JSON.stringify( | ||
| 53 | repoEnv.T3CODE_RELAY_CLIENT_OTLP_TRACES_URL?.trim() ?? "", | ||
| 54 | ), | ||
| 55 | __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_DATASET__: JSON.stringify( | ||
| 56 | repoEnv.T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET?.trim() ?? "", | ||
| 57 | ), | ||
| 58 | __T3CODE_BUILD_RELAY_CLIENT_OTLP_TRACES_TOKEN__: JSON.stringify( | ||
| 59 | repoEnv.T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN?.trim() ?? "", | ||
| 60 | ), | ||
| 52 | }, | 61 | }, |
| 53 | }, | 62 | }, |
| 54 | test: { | 63 | test: { |
apps/web/src/cloud/linkEnvironment.test.ts+8| ... | @@ -318,6 +318,14 @@ describe("web cloud link environment client", () => { | ... | @@ -318,6 +318,14 @@ describe("web cloud link environment client", () => { |
| 318 | method: "POST", | 318 | method: "POST", |
| 319 | url: "https://managed.example.test/oauth/token", | 319 | url: "https://managed.example.test/oauth/token", |
| 320 | }); | 320 | }); |
| 321 | const traceparents = fetchMock.mock.calls.map( | ||
| 322 | (call) => call[1]?.headers.traceparent as string | undefined, | ||
| 323 | ); | ||
| 324 | expect(traceparents.every((traceparent) => typeof traceparent === "string")).toBe(true); | ||
| 325 | expect(new Set(traceparents.map((traceparent) => traceparent?.split("-")[1])).size).toBe(1); | ||
| 326 | expect(connection.relayTraceHeaders.traceparent?.split("-")[1]).toBe( | ||
| 327 | traceparents[0]?.split("-")[1], | ||
| 328 | ); | ||
| 321 | }), | 329 | }), |
| 322 | ); | 330 | ); |
| 323 | 331 |
apps/web/src/cloud/linkEnvironment.ts+11-2| ... | @@ -1,7 +1,7 @@ | ... | @@ -1,7 +1,7 @@ |
| 1 | import * as Data from "effect/Data"; | 1 | import * as Data from "effect/Data"; |
| 2 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Schema from "effect/Schema"; | 3 | import * as Schema from "effect/Schema"; |
| 4 | import { HttpClient } from "effect/unstable/http"; | 4 | import { HttpClient, HttpTraceContext, type Headers } from "effect/unstable/http"; |
| 5 | import { | 5 | import { |
| 6 | EnvironmentCloudEndpointUnavailableError, | 6 | EnvironmentCloudEndpointUnavailableError, |
| 7 | type EnvironmentCloudLinkStateResult, | 7 | type EnvironmentCloudLinkStateResult, |
| ... | @@ -29,6 +29,7 @@ import { | ... | @@ -29,6 +29,7 @@ import { |
| 29 | ManagedRelayDpopSigner, | 29 | ManagedRelayDpopSigner, |
| 30 | type WsRpcClient, | 30 | type WsRpcClient, |
| 31 | } from "@t3tools/client-runtime"; | 31 | } from "@t3tools/client-runtime"; |
| 32 | import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; | ||
| 32 | 33 | ||
| 33 | import { ensureLocalApi } from "../localApi"; | 34 | import { ensureLocalApi } from "../localApi"; |
| 34 | import { | 35 | import { |
| ... | @@ -245,6 +246,7 @@ export interface CloudManagedConnection { | ... | @@ -245,6 +246,7 @@ export interface CloudManagedConnection { |
| 245 | readonly wsBaseUrl: string; | 246 | readonly wsBaseUrl: string; |
| 246 | readonly relayUrl: string; | 247 | readonly relayUrl: string; |
| 247 | readonly accessToken: string; | 248 | readonly accessToken: string; |
| 249 | readonly relayTraceHeaders: Headers.Headers; | ||
| 248 | } | 250 | } |
| 249 | 251 | ||
| 250 | export function collectCloudLinkTargets(input: { | 252 | export function collectCloudLinkTargets(input: { |
| ... | @@ -437,8 +439,15 @@ export function connectManagedCloudEnvironment(input: { | ... | @@ -437,8 +439,15 @@ export function connectManagedCloudEnvironment(input: { |
| 437 | wsBaseUrl: connected.endpoint.wsBaseUrl, | 439 | wsBaseUrl: connected.endpoint.wsBaseUrl, |
| 438 | relayUrl: configuredRelayUrl, | 440 | relayUrl: configuredRelayUrl, |
| 439 | accessToken: session.access_token, | 441 | accessToken: session.access_token, |
| 442 | relayTraceHeaders: HttpTraceContext.toHeaders(yield* Effect.currentSpan.pipe(Effect.orDie)), | ||
| 440 | }; | 443 | }; |
| 441 | }); | 444 | }).pipe( |
| 445 | Effect.withSpan("relay.environment.connect", { | ||
| 446 | root: true, | ||
| 447 | attributes: { "relay.environment_id": input.environment.environmentId }, | ||
| 448 | }), | ||
| 449 | withRelayClientTracing, | ||
| 450 | ); | ||
| 442 | } | 451 | } |
| 443 | 452 | ||
| 444 | export function readPrimaryCloudLinkState(): Effect.Effect< | 453 | export function readPrimaryCloudLinkState(): Effect.Effect< |
apps/web/src/cloud/publicConfig.ts+34| ... | @@ -5,12 +5,26 @@ export interface CloudPublicConfig { | ... | @@ -5,12 +5,26 @@ export interface CloudPublicConfig { |
| 5 | readonly clerkPublishableKey: string | null; | 5 | readonly clerkPublishableKey: string | null; |
| 6 | readonly clerkJwtTemplate: string | null; | 6 | readonly clerkJwtTemplate: string | null; |
| 7 | readonly relayUrl: string | null; | 7 | readonly relayUrl: string | null; |
| 8 | readonly relayTracing: { | ||
| 9 | readonly tracesUrl: string | null; | ||
| 10 | readonly tracesDataset: string | null; | ||
| 11 | readonly tracesToken: string | null; | ||
| 12 | }; | ||
| 8 | } | 13 | } |
| 9 | 14 | ||
| 10 | function trimNonEmpty(value: string | undefined): string | null { | 15 | function trimNonEmpty(value: string | undefined): string | null { |
| 11 | return value?.trim() || null; | 16 | return value?.trim() || null; |
| 12 | } | 17 | } |
| 13 | 18 | ||
| 19 | function normalizeSecureUrl(value: string): string | null { | ||
| 20 | try { | ||
| 21 | const url = new URL(value); | ||
| 22 | return url.protocol === "https:" ? url.toString() : null; | ||
| 23 | } catch { | ||
| 24 | return null; | ||
| 25 | } | ||
| 26 | } | ||
| 27 | |||
| 14 | export function resolveCloudPublicConfig(): CloudPublicConfig { | 28 | export function resolveCloudPublicConfig(): CloudPublicConfig { |
| 15 | return { | 29 | return { |
| 16 | clerkPublishableKey: trimNonEmpty( | 30 | clerkPublishableKey: trimNonEmpty( |
| ... | @@ -20,9 +34,29 @@ export function resolveCloudPublicConfig(): CloudPublicConfig { | ... | @@ -20,9 +34,29 @@ export function resolveCloudPublicConfig(): CloudPublicConfig { |
| 20 | relayUrl: normalizeSecureRelayUrl( | 34 | relayUrl: normalizeSecureRelayUrl( |
| 21 | (import.meta.env.VITE_T3CODE_RELAY_URL as string | undefined) ?? "", | 35 | (import.meta.env.VITE_T3CODE_RELAY_URL as string | undefined) ?? "", |
| 22 | ), | 36 | ), |
| 37 | relayTracing: { | ||
| 38 | tracesUrl: normalizeSecureUrl( | ||
| 39 | (import.meta.env.VITE_RELAY_OTLP_TRACES_URL as string | undefined) ?? "", | ||
| 40 | ), | ||
| 41 | tracesDataset: trimNonEmpty( | ||
| 42 | import.meta.env.VITE_RELAY_OTLP_TRACES_DATASET as string | undefined, | ||
| 43 | ), | ||
| 44 | tracesToken: trimNonEmpty(import.meta.env.VITE_RELAY_OTLP_TRACES_TOKEN as string | undefined), | ||
| 45 | }, | ||
| 23 | }; | 46 | }; |
| 24 | } | 47 | } |
| 25 | 48 | ||
| 49 | export function resolveRelayTracingConfig() { | ||
| 50 | const { relayTracing } = resolveCloudPublicConfig(); | ||
| 51 | return relayTracing.tracesUrl && relayTracing.tracesDataset && relayTracing.tracesToken | ||
| 52 | ? { | ||
| 53 | tracesUrl: relayTracing.tracesUrl, | ||
| 54 | tracesDataset: relayTracing.tracesDataset, | ||
| 55 | tracesToken: relayTracing.tracesToken, | ||
| 56 | } | ||
| 57 | : null; | ||
| 58 | } | ||
| 59 | |||
| 26 | export function hasCloudPublicConfig(): boolean { | 60 | export function hasCloudPublicConfig(): boolean { |
| 27 | const config = resolveCloudPublicConfig(); | 61 | const config = resolveCloudPublicConfig(); |
| 28 | return Boolean(config.clerkPublishableKey && config.clerkJwtTemplate && config.relayUrl); | 62 | return Boolean(config.clerkPublishableKey && config.clerkJwtTemplate && config.relayUrl); |
apps/web/src/environments/runtime/service.addSavedEnvironment.test.ts+44-1| ... | @@ -1,8 +1,13 @@ | ... | @@ -1,8 +1,13 @@ |
| 1 | import { EnvironmentAuthInvalidError, EnvironmentId } from "@t3tools/contracts"; | 1 | import { EnvironmentAuthInvalidError, EnvironmentId } from "@t3tools/contracts"; |
| 2 | import * as Effect from "effect/Effect"; | 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Option from "effect/Option"; | ||
| 3 | import * as Schema from "effect/Schema"; | 4 | import * as Schema from "effect/Schema"; |
| 5 | import * as Tracer from "effect/Tracer"; | ||
| 6 | import { Headers } from "effect/unstable/http"; | ||
| 4 | import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; | 7 | import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; |
| 5 | 8 | ||
| 9 | import { RelayClientTracer } from "@t3tools/shared/relayTracing"; | ||
| 10 | |||
| 6 | const decodeEnvironmentAuthInvalidError = Schema.decodeUnknownSync(EnvironmentAuthInvalidError); | 11 | const decodeEnvironmentAuthInvalidError = Schema.decodeUnknownSync(EnvironmentAuthInvalidError); |
| 7 | 12 | ||
| 8 | let mockSavedRecords: Array<Record<string, unknown>> = []; | 13 | let mockSavedRecords: Array<Record<string, unknown>> = []; |
| ... | @@ -12,8 +17,11 @@ const mockFetchRemoteEnvironmentDescriptor = vi.fn(); | ... | @@ -12,8 +17,11 @@ const mockFetchRemoteEnvironmentDescriptor = vi.fn(); |
| 12 | const mockBootstrapRemoteBearerSession = vi.fn(); | 17 | const mockBootstrapRemoteBearerSession = vi.fn(); |
| 13 | const mockFetchRemoteSessionState = vi.fn(); | 18 | const mockFetchRemoteSessionState = vi.fn(); |
| 14 | const mockFetchRemoteDpopSessionState = vi.fn(); | 19 | const mockFetchRemoteDpopSessionState = vi.fn(); |
| 20 | const mockResolveRemoteDpopWebSocketConnectionUrl = vi.fn(); | ||
| 15 | const mockResolveRemoteWebSocketConnectionUrl = vi.fn(); | 21 | const mockResolveRemoteWebSocketConnectionUrl = vi.fn(); |
| 22 | const mockWsTransportConnectors: Array<() => Promise<string>> = []; | ||
| 16 | let managedRelayDpopSigner: typeof import("@t3tools/client-runtime").ManagedRelayDpopSigner; | 23 | let managedRelayDpopSigner: typeof import("@t3tools/client-runtime").ManagedRelayDpopSigner; |
| 24 | let mockRelayClientTracer = Option.none<Tracer.Tracer>(); | ||
| 17 | const mockRemoteHttpRunPromise = vi.fn(<A, E>(effect: Effect.Effect<A, E>) => | 25 | const mockRemoteHttpRunPromise = vi.fn(<A, E>(effect: Effect.Effect<A, E>) => |
| 18 | Effect.runPromise( | 26 | Effect.runPromise( |
| 19 | effect.pipe( | 27 | effect.pipe( |
| ... | @@ -24,6 +32,7 @@ const mockRemoteHttpRunPromise = vi.fn(<A, E>(effect: Effect.Effect<A, E>) => | ... | @@ -24,6 +32,7 @@ const mockRemoteHttpRunPromise = vi.fn(<A, E>(effect: Effect.Effect<A, E>) => |
| 24 | createProof: () => Effect.succeed("dpop-proof"), | 32 | createProof: () => Effect.succeed("dpop-proof"), |
| 25 | }), | 33 | }), |
| 26 | ), | 34 | ), |
| 35 | Effect.provideService(RelayClientTracer, mockRelayClientTracer), | ||
| 27 | ), | 36 | ), |
| 28 | ), | 37 | ), |
| 29 | ); | 38 | ); |
| ... | @@ -164,12 +173,16 @@ vi.mock("@t3tools/client-runtime", async (importOriginal) => { | ... | @@ -164,12 +173,16 @@ vi.mock("@t3tools/client-runtime", async (importOriginal) => { |
| 164 | fetchRemoteEnvironmentDescriptor: mockFetchRemoteEnvironmentDescriptor, | 173 | fetchRemoteEnvironmentDescriptor: mockFetchRemoteEnvironmentDescriptor, |
| 165 | fetchRemoteSessionState: mockFetchRemoteSessionState, | 174 | fetchRemoteSessionState: mockFetchRemoteSessionState, |
| 166 | fetchRemoteDpopSessionState: mockFetchRemoteDpopSessionState, | 175 | fetchRemoteDpopSessionState: mockFetchRemoteDpopSessionState, |
| 176 | resolveRemoteDpopWebSocketConnectionUrl: mockResolveRemoteDpopWebSocketConnectionUrl, | ||
| 167 | resolveRemoteWebSocketConnectionUrl: mockResolveRemoteWebSocketConnectionUrl, | 177 | resolveRemoteWebSocketConnectionUrl: mockResolveRemoteWebSocketConnectionUrl, |
| 168 | }; | 178 | }; |
| 169 | }); | 179 | }); |
| 170 | 180 | ||
| 171 | vi.mock("../../rpc/wsTransport", () => ({ | 181 | vi.mock("../../rpc/wsTransport", () => ({ |
| 172 | WsTransport: vi.fn(), | 182 | WsTransport: vi.fn(function WsTransport(connect: () => Promise<string>) { |
| 183 | mockWsTransportConnectors.push(connect); | ||
| 184 | return {}; | ||
| 185 | }), | ||
| 173 | })); | 186 | })); |
| 174 | 187 | ||
| 175 | describe("addSavedEnvironment", () => { | 188 | describe("addSavedEnvironment", () => { |
| ... | @@ -181,6 +194,8 @@ describe("addSavedEnvironment", () => { | ... | @@ -181,6 +194,8 @@ describe("addSavedEnvironment", () => { |
| 181 | vi.resetModules(); | 194 | vi.resetModules(); |
| 182 | vi.clearAllMocks(); | 195 | vi.clearAllMocks(); |
| 183 | mockSavedRecords = []; | 196 | mockSavedRecords = []; |
| 197 | mockRelayClientTracer = Option.none(); | ||
| 198 | mockWsTransportConnectors.length = 0; | ||
| 184 | vi.stubGlobal("window", { | 199 | vi.stubGlobal("window", { |
| 185 | desktopBridge: { | 200 | desktopBridge: { |
| 186 | ensureSshEnvironment: mockEnsureSshEnvironment, | 201 | ensureSshEnvironment: mockEnsureSshEnvironment, |
| ... | @@ -237,6 +252,9 @@ describe("addSavedEnvironment", () => { | ... | @@ -237,6 +252,9 @@ describe("addSavedEnvironment", () => { |
| 237 | mockResolveRemoteWebSocketConnectionUrl.mockReturnValue( | 252 | mockResolveRemoteWebSocketConnectionUrl.mockReturnValue( |
| 238 | Effect.succeed("wss://remote.example.com/?wsTicket=remote-token"), | 253 | Effect.succeed("wss://remote.example.com/?wsTicket=remote-token"), |
| 239 | ); | 254 | ); |
| 255 | mockResolveRemoteDpopWebSocketConnectionUrl.mockReturnValue( | ||
| 256 | Effect.succeed("wss://remote.example.com/?wsTicket=remote-dpop-token"), | ||
| 257 | ); | ||
| 240 | mockFetchSshEnvironmentDescriptor.mockResolvedValue({ | 258 | mockFetchSshEnvironmentDescriptor.mockResolvedValue({ |
| 241 | environmentId: EnvironmentId.make("environment-1"), | 259 | environmentId: EnvironmentId.make("environment-1"), |
| 242 | label: "Remote environment", | 260 | label: "Remote environment", |
| ... | @@ -386,6 +404,7 @@ describe("addSavedEnvironment", () => { | ... | @@ -386,6 +404,7 @@ describe("addSavedEnvironment", () => { |
| 386 | wsBaseUrl: "wss://managed.example.com/", | 404 | wsBaseUrl: "wss://managed.example.com/", |
| 387 | relayUrl: "https://relay.example.com", | 405 | relayUrl: "https://relay.example.com", |
| 388 | accessToken: "managed-access-token", | 406 | accessToken: "managed-access-token", |
| 407 | relayTraceHeaders: Headers.empty, | ||
| 389 | }); | 408 | }); |
| 390 | 409 | ||
| 391 | expect(mockWriteSavedEnvironmentCredential).toHaveBeenCalledWith( | 410 | expect(mockWriteSavedEnvironmentCredential).toHaveBeenCalledWith( |
| ... | @@ -406,6 +425,19 @@ describe("addSavedEnvironment", () => { | ... | @@ -406,6 +425,19 @@ describe("addSavedEnvironment", () => { |
| 406 | 425 | ||
| 407 | it("renews expired managed DPoP credentials through the relay", async () => { | 426 | it("renews expired managed DPoP credentials through the relay", async () => { |
| 408 | const environmentId = EnvironmentId.make("environment-1"); | 427 | const environmentId = EnvironmentId.make("environment-1"); |
| 428 | const productSpans: Array<Tracer.Span> = []; | ||
| 429 | mockRelayClientTracer = Option.some( | ||
| 430 | Tracer.make({ | ||
| 431 | span: (options) => { | ||
| 432 | const span = new Tracer.NativeSpan(options); | ||
| 433 | productSpans.push(span); | ||
| 434 | return span; | ||
| 435 | }, | ||
| 436 | }), | ||
| 437 | ); | ||
| 438 | const relayTraceHeaders = Headers.fromInput({ | ||
| 439 | traceparent: "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01", | ||
| 440 | }); | ||
| 409 | mockSavedRecords = [ | 441 | mockSavedRecords = [ |
| 410 | { | 442 | { |
| 411 | environmentId, | 443 | environmentId, |
| ... | @@ -443,6 +475,7 @@ describe("addSavedEnvironment", () => { | ... | @@ -443,6 +475,7 @@ describe("addSavedEnvironment", () => { |
| 443 | wsBaseUrl: "wss://managed.example.com/", | 475 | wsBaseUrl: "wss://managed.example.com/", |
| 444 | relayUrl: "https://relay.example.com", | 476 | relayUrl: "https://relay.example.com", |
| 445 | accessToken: "renewed-access-token", | 477 | accessToken: "renewed-access-token", |
| 478 | relayTraceHeaders, | ||
| 446 | }), | 479 | }), |
| 447 | ); | 480 | ); |
| 448 | 481 | ||
| ... | @@ -460,6 +493,16 @@ describe("addSavedEnvironment", () => { | ... | @@ -460,6 +493,16 @@ describe("addSavedEnvironment", () => { |
| 460 | method: "dpop", | 493 | method: "dpop", |
| 461 | accessToken: "renewed-access-token", | 494 | accessToken: "renewed-access-token", |
| 462 | }); | 495 | }); |
| 496 | const renewedTransportConnector = mockWsTransportConnectors.at(-1); | ||
| 497 | expect(renewedTransportConnector).toBeDefined(); | ||
| 498 | await renewedTransportConnector!(); | ||
| 499 | expect(mockResolveRemoteDpopWebSocketConnectionUrl).toHaveBeenCalledWith({ | ||
| 500 | wsBaseUrl: "wss://managed.example.com/", | ||
| 501 | httpBaseUrl: "https://managed.example.com/", | ||
| 502 | accessToken: "renewed-access-token", | ||
| 503 | dpopProof: "dpop-proof", | ||
| 504 | }); | ||
| 505 | expect(productSpans.some((span) => span.name === "relay.environment.reconnect")).toBe(false); | ||
| 463 | await resetEnvironmentServiceForTests(); | 506 | await resetEnvironmentServiceForTests(); |
| 464 | }); | 507 | }); |
| 465 | 508 |
apps/web/src/environments/runtime/service.ts+60-30| ... | @@ -26,7 +26,10 @@ import { | ... | @@ -26,7 +26,10 @@ import { |
| 26 | import { type QueryClient } from "@tanstack/react-query"; | 26 | import { type QueryClient } from "@tanstack/react-query"; |
| 27 | import { Throttler } from "@tanstack/react-pacer"; | 27 | import { Throttler } from "@tanstack/react-pacer"; |
| 28 | import * as Effect from "effect/Effect"; | 28 | import * as Effect from "effect/Effect"; |
| 29 | import * as Option from "effect/Option"; | ||
| 29 | import * as Schema from "effect/Schema"; | 30 | import * as Schema from "effect/Schema"; |
| 31 | import { Headers, HttpTraceContext } from "effect/unstable/http"; | ||
| 32 | import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; | ||
| 30 | import { | 33 | import { |
| 31 | createKnownEnvironment, | 34 | createKnownEnvironment, |
| 32 | getKnownEnvironmentWsBaseUrl, | 35 | getKnownEnvironmentWsBaseUrl, |
| ... | @@ -1173,6 +1176,7 @@ function createPrimaryEnvironmentClient( | ... | @@ -1173,6 +1176,7 @@ function createPrimaryEnvironmentClient( |
| 1173 | function createSavedEnvironmentClient( | 1176 | function createSavedEnvironmentClient( |
| 1174 | environmentId: EnvironmentId, | 1177 | environmentId: EnvironmentId, |
| 1175 | credentialRef: { current: SavedEnvironmentCredential }, | 1178 | credentialRef: { current: SavedEnvironmentCredential }, |
| 1179 | relayTraceHeadersRef: { current: Headers.Headers | null }, | ||
| 1176 | ): WsRpcClient { | 1180 | ): WsRpcClient { |
| 1177 | useSavedEnvironmentRuntimeStore.getState().ensure(environmentId); | 1181 | useSavedEnvironmentRuntimeStore.getState().ensure(environmentId); |
| 1178 | 1182 | ||
| ... | @@ -1196,21 +1200,10 @@ function createSavedEnvironmentClient( | ... | @@ -1196,21 +1200,10 @@ function createSavedEnvironmentClient( |
| 1196 | } | 1200 | } |
| 1197 | if (credential.method === "dpop") { | 1201 | if (credential.method === "dpop") { |
| 1198 | try { | 1202 | try { |
| 1203 | const relayTraceHeaders = relayTraceHeadersRef.current; | ||
| 1204 | relayTraceHeadersRef.current = null; | ||
| 1199 | return await webRuntime.runPromise( | 1205 | return await webRuntime.runPromise( |
| 1200 | createManagedRelayDpopProof({ | 1206 | resolveManagedRelayWebSocketUrl(record, credential, relayTraceHeaders), |
| 1201 | method: "POST", | ||
| 1202 | url: new URL("/api/auth/websocket-ticket", record.httpBaseUrl).toString(), | ||
| 1203 | accessToken: credential.accessToken, | ||
| 1204 | }).pipe( | ||
| 1205 | Effect.flatMap((proof) => | ||
| 1206 | resolveRemoteDpopWebSocketConnectionUrl({ | ||
| 1207 | wsBaseUrl: record.wsBaseUrl, | ||
| 1208 | httpBaseUrl: record.httpBaseUrl, | ||
| 1209 | accessToken: credential.accessToken, | ||
| 1210 | dpopProof: proof, | ||
| 1211 | }), | ||
| 1212 | ), | ||
| 1213 | ), | ||
| 1214 | ); | 1207 | ); |
| 1215 | } catch (error) { | 1208 | } catch (error) { |
| 1216 | if (!isEnvironmentAuthInvalidError(error)) { | 1209 | if (!isEnvironmentAuthInvalidError(error)) { |
| ... | @@ -1223,19 +1216,10 @@ function createSavedEnvironmentClient( | ... | @@ -1223,19 +1216,10 @@ function createSavedEnvironmentClient( |
| 1223 | const renewedCredential = renewed.credential; | 1216 | const renewedCredential = renewed.credential; |
| 1224 | credentialRef.current = renewedCredential; | 1217 | credentialRef.current = renewedCredential; |
| 1225 | return await webRuntime.runPromise( | 1218 | return await webRuntime.runPromise( |
| 1226 | createManagedRelayDpopProof({ | 1219 | resolveManagedRelayWebSocketUrl( |
| 1227 | method: "POST", | 1220 | renewed.record, |
| 1228 | url: new URL("/api/auth/websocket-ticket", renewed.record.httpBaseUrl).toString(), | 1221 | renewedCredential, |
| 1229 | accessToken: renewedCredential.accessToken, | 1222 | renewed.relayTraceHeaders, |
| 1230 | }).pipe( | ||
| 1231 | Effect.flatMap((proof) => | ||
| 1232 | resolveRemoteDpopWebSocketConnectionUrl({ | ||
| 1233 | wsBaseUrl: renewed.record.wsBaseUrl, | ||
| 1234 | httpBaseUrl: renewed.record.httpBaseUrl, | ||
| 1235 | accessToken: renewedCredential.accessToken, | ||
| 1236 | dpopProof: proof, | ||
| 1237 | }), | ||
| 1238 | ), | ||
| 1239 | ), | 1223 | ), |
| 1240 | ); | 1224 | ); |
| 1241 | } | 1225 | } |
| ... | @@ -1339,9 +1323,44 @@ async function refreshSavedEnvironmentMetadata( | ... | @@ -1339,9 +1323,44 @@ async function refreshSavedEnvironmentMetadata( |
| 1339 | .rename(record.environmentId, serverConfig.environment.label); | 1323 | .rename(record.environmentId, serverConfig.environment.label); |
| 1340 | } | 1324 | } |
| 1341 | 1325 | ||
| 1326 | const resolveManagedRelayWebSocketUrl = Effect.fn( | ||
| 1327 | "web.environment.resolveManagedRelayWebSocketUrl", | ||
| 1328 | )(function* ( | ||
| 1329 | record: SavedEnvironmentRecord, | ||
| 1330 | credential: Extract<SavedEnvironmentCredential, { readonly method: "dpop" }>, | ||
| 1331 | traceHeaders: Headers.Headers | null, | ||
| 1332 | ) { | ||
| 1333 | const request = createManagedRelayDpopProof({ | ||
| 1334 | method: "POST", | ||
| 1335 | url: new URL("/api/auth/websocket-ticket", record.httpBaseUrl).toString(), | ||
| 1336 | accessToken: credential.accessToken, | ||
| 1337 | }).pipe( | ||
| 1338 | Effect.flatMap((proof) => | ||
| 1339 | resolveRemoteDpopWebSocketConnectionUrl({ | ||
| 1340 | wsBaseUrl: record.wsBaseUrl, | ||
| 1341 | httpBaseUrl: record.httpBaseUrl, | ||
| 1342 | accessToken: credential.accessToken, | ||
| 1343 | dpopProof: proof, | ||
| 1344 | }), | ||
| 1345 | ), | ||
| 1346 | ); | ||
| 1347 | const parent = traceHeaders ? HttpTraceContext.fromHeaders(traceHeaders) : Option.none(); | ||
| 1348 | return yield* ( | ||
| 1349 | Option.isSome(parent) | ||
| 1350 | ? request.pipe(Effect.withParentSpan(parent.value)) | ||
| 1351 | : request.pipe( | ||
| 1352 | Effect.withSpan("relay.environment.reconnect", { | ||
| 1353 | root: true, | ||
| 1354 | attributes: { "relay.environment_id": record.environmentId }, | ||
| 1355 | }), | ||
| 1356 | ) | ||
| 1357 | ).pipe(withRelayClientTracing); | ||
| 1358 | }); | ||
| 1359 | |||
| 1342 | async function renewManagedRelayCredential(record: SavedEnvironmentRecord): Promise<{ | 1360 | async function renewManagedRelayCredential(record: SavedEnvironmentRecord): Promise<{ |
| 1343 | readonly record: SavedEnvironmentRecord; | 1361 | readonly record: SavedEnvironmentRecord; |
| 1344 | readonly credential: SavedEnvironmentCredential; | 1362 | readonly credential: SavedEnvironmentCredential; |
| 1363 | readonly relayTraceHeaders: Headers.Headers; | ||
| 1345 | } | null> { | 1364 | } | null> { |
| 1346 | if (!record.relayManaged) { | 1365 | if (!record.relayManaged) { |
| 1347 | return null; | 1366 | return null; |
| ... | @@ -1382,7 +1401,7 @@ async function renewManagedRelayCredential(record: SavedEnvironmentRecord): Prom | ... | @@ -1382,7 +1401,7 @@ async function renewManagedRelayCredential(record: SavedEnvironmentRecord): Prom |
| 1382 | throw new Error("Unable to persist refreshed managed environment credentials."); | 1401 | throw new Error("Unable to persist refreshed managed environment credentials."); |
| 1383 | } | 1402 | } |
| 1384 | useSavedEnvironmentRegistryStore.getState().upsert(nextRecord); | 1403 | useSavedEnvironmentRegistryStore.getState().upsert(nextRecord); |
| 1385 | return { record: nextRecord, credential }; | 1404 | return { record: nextRecord, credential, relayTraceHeaders: connected.relayTraceHeaders }; |
| 1386 | } | 1405 | } |
| 1387 | 1406 | ||
| 1388 | function registerConnection(connection: EnvironmentConnection): EnvironmentConnection { | 1407 | function registerConnection(connection: EnvironmentConnection): EnvironmentConnection { |
| ... | @@ -1466,6 +1485,7 @@ async function ensureSavedEnvironmentConnection( | ... | @@ -1466,6 +1485,7 @@ async function ensureSavedEnvironmentConnection( |
| 1466 | readonly scopes?: ReadonlyArray<AuthEnvironmentScope> | null; | 1485 | readonly scopes?: ReadonlyArray<AuthEnvironmentScope> | null; |
| 1467 | readonly serverConfig?: ServerConfig | null; | 1486 | readonly serverConfig?: ServerConfig | null; |
| 1468 | readonly allowManagedRenewal?: boolean; | 1487 | readonly allowManagedRenewal?: boolean; |
| 1488 | readonly relayTraceHeaders?: Headers.Headers; | ||
| 1469 | }, | 1489 | }, |
| 1470 | ): Promise<EnvironmentConnection> { | 1490 | ): Promise<EnvironmentConnection> { |
| 1471 | const existing = environmentConnections.get(record.environmentId); | 1491 | const existing = environmentConnections.get(record.environmentId); |
| ... | @@ -1511,9 +1531,14 @@ async function ensureSavedEnvironmentConnection( | ... | @@ -1511,9 +1531,14 @@ async function ensureSavedEnvironmentConnection( |
| 1511 | } | 1531 | } |
| 1512 | 1532 | ||
| 1513 | const activeCredential = { current: credential }; | 1533 | const activeCredential = { current: credential }; |
| 1534 | const relayTraceHeaders = { current: options?.relayTraceHeaders ?? null }; | ||
| 1514 | const client = | 1535 | const client = |
| 1515 | options?.client ?? | 1536 | options?.client ?? |
| 1516 | createSavedEnvironmentClient(activeRecord.environmentId, activeCredential); | 1537 | createSavedEnvironmentClient( |
| 1538 | activeRecord.environmentId, | ||
| 1539 | activeCredential, | ||
| 1540 | relayTraceHeaders, | ||
| 1541 | ); | ||
| 1517 | const initialConfigSnapshot = createDeferredPromise<ServerConfig>(); | 1542 | const initialConfigSnapshot = createDeferredPromise<ServerConfig>(); |
| 1518 | const knownEnvironment = createKnownEnvironment({ | 1543 | const knownEnvironment = createKnownEnvironment({ |
| 1519 | id: activeRecord.environmentId, | 1544 | id: activeRecord.environmentId, |
| ... | @@ -1589,6 +1614,7 @@ async function ensureSavedEnvironmentConnection( | ... | @@ -1589,6 +1614,7 @@ async function ensureSavedEnvironmentConnection( |
| 1589 | scopes: scopeHint, | 1614 | scopes: scopeHint, |
| 1590 | serverConfig: options?.serverConfig ?? null, | 1615 | serverConfig: options?.serverConfig ?? null, |
| 1591 | allowManagedRenewal: false, | 1616 | allowManagedRenewal: false, |
| 1617 | relayTraceHeaders: renewed.relayTraceHeaders, | ||
| 1592 | }); | 1618 | }); |
| 1593 | } | 1619 | } |
| 1594 | } | 1620 | } |
| ... | @@ -1916,6 +1942,7 @@ export async function addManagedRelayEnvironment(input: { | ... | @@ -1916,6 +1942,7 @@ export async function addManagedRelayEnvironment(input: { |
| 1916 | readonly wsBaseUrl: string; | 1942 | readonly wsBaseUrl: string; |
| 1917 | readonly relayUrl: string; | 1943 | readonly relayUrl: string; |
| 1918 | readonly accessToken: string; | 1944 | readonly accessToken: string; |
| 1945 | readonly relayTraceHeaders: Headers.Headers; | ||
| 1919 | }): Promise<SavedEnvironmentRecord> { | 1946 | }): Promise<SavedEnvironmentRecord> { |
| 1920 | const existingRecord = getSavedEnvironmentRecord(input.environmentId); | 1947 | const existingRecord = getSavedEnvironmentRecord(input.environmentId); |
| 1921 | const record: SavedEnvironmentRecord = { | 1948 | const record: SavedEnvironmentRecord = { |
| ... | @@ -1939,7 +1966,10 @@ export async function addManagedRelayEnvironment(input: { | ... | @@ -1939,7 +1966,10 @@ export async function addManagedRelayEnvironment(input: { |
| 1939 | } | 1966 | } |
| 1940 | useSavedEnvironmentRegistryStore.getState().upsert(record); | 1967 | useSavedEnvironmentRegistryStore.getState().upsert(record); |
| 1941 | await removeConnection(record.environmentId).catch(() => false); | 1968 | await removeConnection(record.environmentId).catch(() => false); |
| 1942 | await ensureSavedEnvironmentConnection(record, { credential }); | 1969 | await ensureSavedEnvironmentConnection(record, { |
| 1970 | credential, | ||
| 1971 | relayTraceHeaders: input.relayTraceHeaders, | ||
| 1972 | }); | ||
| 1943 | return record; | 1973 | return record; |
| 1944 | } | 1974 | } |
| 1945 | 1975 |
apps/web/src/lib/runtime.ts+9-1| ... | @@ -5,6 +5,7 @@ import { FetchHttpClient } from "effect/unstable/http"; | ... | @@ -5,6 +5,7 @@ import { FetchHttpClient } from "effect/unstable/http"; |
| 5 | 5 | ||
| 6 | import { remoteHttpClientLayer } from "@t3tools/client-runtime"; | 6 | import { remoteHttpClientLayer } from "@t3tools/client-runtime"; |
| 7 | import { httpHeaderRedactionLayer } from "@t3tools/shared/httpObservability"; | 7 | import { httpHeaderRedactionLayer } from "@t3tools/shared/httpObservability"; |
| 8 | import { makeRelayClientTracingLayer } from "@t3tools/shared/relayTracing"; | ||
| 8 | import { | 9 | import { |
| 9 | PrimaryEnvironmentHttpClient, | 10 | PrimaryEnvironmentHttpClient, |
| 10 | primaryEnvironmentHttpClientLive, | 11 | primaryEnvironmentHttpClientLive, |
| ... | @@ -13,13 +14,19 @@ import { primaryEnvironmentRequestInit } from "../environments/primary/requestIn | ... | @@ -13,13 +14,19 @@ import { primaryEnvironmentRequestInit } from "../environments/primary/requestIn |
| 13 | 14 | ||
| 14 | import { browserCryptoLayer } from "../cloud/dpop"; | 15 | import { browserCryptoLayer } from "../cloud/dpop"; |
| 15 | import { webManagedRelayClientLayer } from "../cloud/managedRelayLayer"; | 16 | import { webManagedRelayClientLayer } from "../cloud/managedRelayLayer"; |
| 16 | import { resolveCloudPublicConfig } from "../cloud/publicConfig"; | 17 | import { resolveCloudPublicConfig, resolveRelayTracingConfig } from "../cloud/publicConfig"; |
| 17 | 18 | ||
| 18 | function configuredRelayUrl(): string { | 19 | function configuredRelayUrl(): string { |
| 19 | return resolveCloudPublicConfig().relayUrl ?? "http://relay.invalid"; | 20 | return resolveCloudPublicConfig().relayUrl ?? "http://relay.invalid"; |
| 20 | } | 21 | } |
| 21 | 22 | ||
| 22 | const webHttpClientLayer = remoteHttpClientLayer(globalThis.fetch); | 23 | const webHttpClientLayer = remoteHttpClientLayer(globalThis.fetch); |
| 24 | const webRelayTracingLayer = makeRelayClientTracingLayer(resolveRelayTracingConfig(), { | ||
| 25 | serviceName: "t3-web-relay-client", | ||
| 26 | serviceVersion: import.meta.env.APP_VERSION, | ||
| 27 | runtime: "browser", | ||
| 28 | client: typeof window !== "undefined" && window.desktopBridge ? "desktop" : "web", | ||
| 29 | }).pipe(Layer.provide(webHttpClientLayer)); | ||
| 23 | 30 | ||
| 24 | export const remoteHttpRuntime = ManagedRuntime.make(webHttpClientLayer); | 31 | export const remoteHttpRuntime = ManagedRuntime.make(webHttpClientLayer); |
| 25 | 32 | ||
| ... | @@ -57,6 +64,7 @@ export const webRuntime = ManagedRuntime.make( | ... | @@ -57,6 +64,7 @@ export const webRuntime = ManagedRuntime.make( |
| 57 | browserCryptoLayer, | 64 | browserCryptoLayer, |
| 58 | webManagedRelayClientLayer(configuredRelayUrl()).pipe( | 65 | webManagedRelayClientLayer(configuredRelayUrl()).pipe( |
| 59 | Layer.provide(Layer.mergeAll(webHttpClientLayer, browserCryptoLayer)), | 66 | Layer.provide(Layer.mergeAll(webHttpClientLayer, browserCryptoLayer)), |
| 67 | Layer.provideMerge(webRelayTracingLayer), | ||
| 60 | ), | 68 | ), |
| 61 | ), | 69 | ), |
| 62 | ); | 70 | ); |
apps/web/src/vite-env.d.ts+3| ... | @@ -9,6 +9,9 @@ interface ImportMetaEnv { | ... | @@ -9,6 +9,9 @@ interface ImportMetaEnv { |
| 9 | readonly VITE_HOSTED_APP_CHANNEL: string; | 9 | readonly VITE_HOSTED_APP_CHANNEL: string; |
| 10 | readonly VITE_CLERK_PUBLISHABLE_KEY: string; | 10 | readonly VITE_CLERK_PUBLISHABLE_KEY: string; |
| 11 | readonly VITE_CLERK_JWT_TEMPLATE: string; | 11 | readonly VITE_CLERK_JWT_TEMPLATE: string; |
| 12 | readonly VITE_RELAY_OTLP_TRACES_URL: string; | ||
| 13 | readonly VITE_RELAY_OTLP_TRACES_DATASET: string; | ||
| 14 | readonly VITE_RELAY_OTLP_TRACES_TOKEN: string; | ||
| 12 | readonly APP_VERSION: string; | 15 | readonly APP_VERSION: string; |
| 13 | } | 16 | } |
| 14 | 17 |
apps/web/vite.config.ts+8| ... | @@ -19,6 +19,9 @@ const configuredWsUrl = process.env.VITE_WS_URL?.trim(); | ... | @@ -19,6 +19,9 @@ const configuredWsUrl = process.env.VITE_WS_URL?.trim(); |
| 19 | const configuredRelayUrl = repoEnv.VITE_T3CODE_RELAY_URL?.trim() || ""; | 19 | const configuredRelayUrl = repoEnv.VITE_T3CODE_RELAY_URL?.trim() || ""; |
| 20 | const configuredClerkPublishableKey = repoEnv.VITE_CLERK_PUBLISHABLE_KEY?.trim() || ""; | 20 | const configuredClerkPublishableKey = repoEnv.VITE_CLERK_PUBLISHABLE_KEY?.trim() || ""; |
| 21 | const configuredClerkJwtTemplate = repoEnv.VITE_CLERK_JWT_TEMPLATE?.trim() || ""; | 21 | const configuredClerkJwtTemplate = repoEnv.VITE_CLERK_JWT_TEMPLATE?.trim() || ""; |
| 22 | const configuredRelayTracingUrl = repoEnv.VITE_RELAY_OTLP_TRACES_URL?.trim() || ""; | ||
| 23 | const configuredRelayTracingDataset = repoEnv.VITE_RELAY_OTLP_TRACES_DATASET?.trim() || ""; | ||
| 24 | const configuredRelayTracingToken = repoEnv.VITE_RELAY_OTLP_TRACES_TOKEN?.trim() || ""; | ||
| 22 | const configuredHostedAppChannel = process.env.VITE_HOSTED_APP_CHANNEL?.trim() || ""; | 25 | const configuredHostedAppChannel = process.env.VITE_HOSTED_APP_CHANNEL?.trim() || ""; |
| 23 | const configuredAppVersion = process.env.APP_VERSION?.trim() || pkg.version; | 26 | const configuredAppVersion = process.env.APP_VERSION?.trim() || pkg.version; |
| 24 | const configuredHostedAppUrl = (() => { | 27 | const configuredHostedAppUrl = (() => { |
| ... | @@ -135,6 +138,11 @@ export default defineConfig(() => { | ... | @@ -135,6 +138,11 @@ export default defineConfig(() => { |
| 135 | "import.meta.env.VITE_T3CODE_RELAY_URL": JSON.stringify(configuredRelayUrl), | 138 | "import.meta.env.VITE_T3CODE_RELAY_URL": JSON.stringify(configuredRelayUrl), |
| 136 | "import.meta.env.VITE_CLERK_PUBLISHABLE_KEY": JSON.stringify(configuredClerkPublishableKey), | 139 | "import.meta.env.VITE_CLERK_PUBLISHABLE_KEY": JSON.stringify(configuredClerkPublishableKey), |
| 137 | "import.meta.env.VITE_CLERK_JWT_TEMPLATE": JSON.stringify(configuredClerkJwtTemplate), | 140 | "import.meta.env.VITE_CLERK_JWT_TEMPLATE": JSON.stringify(configuredClerkJwtTemplate), |
| 141 | "import.meta.env.VITE_RELAY_OTLP_TRACES_URL": JSON.stringify(configuredRelayTracingUrl), | ||
| 142 | "import.meta.env.VITE_RELAY_OTLP_TRACES_DATASET": JSON.stringify( | ||
| 143 | configuredRelayTracingDataset, | ||
| 144 | ), | ||
| 145 | "import.meta.env.VITE_RELAY_OTLP_TRACES_TOKEN": JSON.stringify(configuredRelayTracingToken), | ||
| 138 | "import.meta.env.VITE_HOSTED_APP_URL": JSON.stringify(configuredHostedAppUrl ?? ""), | 146 | "import.meta.env.VITE_HOSTED_APP_URL": JSON.stringify(configuredHostedAppUrl ?? ""), |
| 139 | "import.meta.env.VITE_HOSTED_APP_CHANNEL": JSON.stringify(configuredHostedAppChannel), | 147 | "import.meta.env.VITE_HOSTED_APP_CHANNEL": JSON.stringify(configuredHostedAppChannel), |
| 140 | "import.meta.env.APP_VERSION": JSON.stringify(configuredAppVersion), | 148 | "import.meta.env.APP_VERSION": JSON.stringify(configuredAppVersion), |
infra/relay/alchemy.run.ts+3| ... | @@ -42,6 +42,9 @@ export default Alchemy.Stack( | ... | @@ -42,6 +42,9 @@ export default Alchemy.Stack( |
| 42 | mobileTracingUrl: observability.traces.otelTracesEndpoint, | 42 | mobileTracingUrl: observability.traces.otelTracesEndpoint, |
| 43 | mobileTracingDataset: observability.traces.name, | 43 | mobileTracingDataset: observability.traces.name, |
| 44 | mobileTracingToken: observability.mobileIngestToken.token, | 44 | mobileTracingToken: observability.mobileIngestToken.token, |
| 45 | clientTracingUrl: observability.traces.otelTracesEndpoint, | ||
| 46 | clientTracingDataset: observability.traces.name, | ||
| 47 | clientTracingToken: observability.clientIngestToken.token, | ||
| 45 | }; | 48 | }; |
| 46 | }), | 49 | }), |
| 47 | ); | 50 | ); |
infra/relay/scripts/deploy.test.ts+89-1| ... | @@ -1,10 +1,16 @@ | ... | @@ -1,10 +1,16 @@ |
| 1 | import { describe, expect, it } from "vite-plus/test"; | 1 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 2 | import { describe, expect, it } from "@effect/vitest"; | ||
| 3 | import * as Effect from "effect/Effect"; | ||
| 4 | import * as FileSystem from "effect/FileSystem"; | ||
| 5 | import * as Path from "effect/Path"; | ||
| 2 | 6 | ||
| 3 | import { | 7 | import { |
| 4 | hasDeployChanges, | 8 | hasDeployChanges, |
| 9 | publicConfigFromOutput, | ||
| 5 | reconcileRootEnvPublicConfig, | 10 | reconcileRootEnvPublicConfig, |
| 6 | reconcileRootEnvRelayUrl, | 11 | reconcileRootEnvRelayUrl, |
| 7 | serializeGithubOutput, | 12 | serializeGithubOutput, |
| 13 | serializeRelayClientTracingEnvironment, | ||
| 8 | } from "./deploy.ts"; | 14 | } from "./deploy.ts"; |
| 9 | 15 | ||
| 10 | describe("hasDeployChanges", () => { | 16 | describe("hasDeployChanges", () => { |
| ... | @@ -62,6 +68,9 @@ describe("reconcileRootEnvPublicConfig", () => { | ... | @@ -62,6 +68,9 @@ describe("reconcileRootEnvPublicConfig", () => { |
| 62 | mobileTracingUrl: "https://api.axiom.co/v1/traces", | 68 | mobileTracingUrl: "https://api.axiom.co/v1/traces", |
| 63 | mobileTracingDataset: "t3-code-mobile-traces-dev", | 69 | mobileTracingDataset: "t3-code-mobile-traces-dev", |
| 64 | mobileTracingToken: "xaat-public-ingest", | 70 | mobileTracingToken: "xaat-public-ingest", |
| 71 | clientTracingUrl: "https://api.axiom.co/v1/traces", | ||
| 72 | clientTracingDataset: "t3-code-relay-client-traces-dev", | ||
| 73 | clientTracingToken: "xaat-relay-client-ingest", | ||
| 65 | } as const; | 74 | } as const; |
| 66 | 75 | ||
| 67 | it("adds the complete local client config", () => { | 76 | it("adds the complete local client config", () => { |
| ... | @@ -71,6 +80,9 @@ describe("reconcileRootEnvPublicConfig", () => { | ... | @@ -71,6 +80,9 @@ describe("reconcileRootEnvPublicConfig", () => { |
| 71 | "T3CODE_MOBILE_OTLP_TRACES_URL=https://api.axiom.co/v1/traces", | 80 | "T3CODE_MOBILE_OTLP_TRACES_URL=https://api.axiom.co/v1/traces", |
| 72 | "T3CODE_MOBILE_OTLP_TRACES_DATASET=t3-code-mobile-traces-dev", | 81 | "T3CODE_MOBILE_OTLP_TRACES_DATASET=t3-code-mobile-traces-dev", |
| 73 | "T3CODE_MOBILE_OTLP_TRACES_TOKEN=xaat-public-ingest", | 82 | "T3CODE_MOBILE_OTLP_TRACES_TOKEN=xaat-public-ingest", |
| 83 | "T3CODE_RELAY_CLIENT_OTLP_TRACES_URL=https://api.axiom.co/v1/traces", | ||
| 84 | "T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET=t3-code-relay-client-traces-dev", | ||
| 85 | "T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=xaat-relay-client-ingest", | ||
| 74 | "", | 86 | "", |
| 75 | ].join("\n"), | 87 | ].join("\n"), |
| 76 | ); | 88 | ); |
| ... | @@ -85,6 +97,9 @@ describe("reconcileRootEnvPublicConfig", () => { | ... | @@ -85,6 +97,9 @@ describe("reconcileRootEnvPublicConfig", () => { |
| 85 | "T3CODE_MOBILE_OTLP_TRACES_URL=https://old.example.test/v1/traces", | 97 | "T3CODE_MOBILE_OTLP_TRACES_URL=https://old.example.test/v1/traces", |
| 86 | "T3CODE_MOBILE_OTLP_TRACES_DATASET=old-dataset", | 98 | "T3CODE_MOBILE_OTLP_TRACES_DATASET=old-dataset", |
| 87 | "T3CODE_MOBILE_OTLP_TRACES_TOKEN=old-token", | 99 | "T3CODE_MOBILE_OTLP_TRACES_TOKEN=old-token", |
| 100 | "T3CODE_RELAY_CLIENT_OTLP_TRACES_URL=https://old.example.test/v1/traces", | ||
| 101 | "T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET=old-client-dataset", | ||
| 102 | "T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=old-client-token", | ||
| 88 | "", | 103 | "", |
| 89 | ].join("\n"), | 104 | ].join("\n"), |
| 90 | config, | 105 | config, |
| ... | @@ -96,6 +111,9 @@ describe("reconcileRootEnvPublicConfig", () => { | ... | @@ -96,6 +111,9 @@ describe("reconcileRootEnvPublicConfig", () => { |
| 96 | "T3CODE_MOBILE_OTLP_TRACES_URL=https://api.axiom.co/v1/traces", | 111 | "T3CODE_MOBILE_OTLP_TRACES_URL=https://api.axiom.co/v1/traces", |
| 97 | "T3CODE_MOBILE_OTLP_TRACES_DATASET=t3-code-mobile-traces-dev", | 112 | "T3CODE_MOBILE_OTLP_TRACES_DATASET=t3-code-mobile-traces-dev", |
| 98 | "T3CODE_MOBILE_OTLP_TRACES_TOKEN=xaat-public-ingest", | 113 | "T3CODE_MOBILE_OTLP_TRACES_TOKEN=xaat-public-ingest", |
| 114 | "T3CODE_RELAY_CLIENT_OTLP_TRACES_URL=https://api.axiom.co/v1/traces", | ||
| 115 | "T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET=t3-code-relay-client-traces-dev", | ||
| 116 | "T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=xaat-relay-client-ingest", | ||
| 99 | "", | 117 | "", |
| 100 | ].join("\n"), | 118 | ].join("\n"), |
| 101 | ); | 119 | ); |
| ... | @@ -113,3 +131,73 @@ describe("serializeGithubOutput", () => { | ... | @@ -113,3 +131,73 @@ describe("serializeGithubOutput", () => { |
| 113 | ).toBe("changed=false\nresult=noop\nrelay_url=https://relay.example.test\n"); | 131 | ).toBe("changed=false\nresult=noop\nrelay_url=https://relay.example.test\n"); |
| 114 | }); | 132 | }); |
| 115 | }); | 133 | }); |
| 134 | |||
| 135 | describe("serializeRelayClientTracingEnvironment", () => { | ||
| 136 | it("serializes tracing config for downstream GITHUB_ENV loading", () => { | ||
| 137 | expect( | ||
| 138 | serializeRelayClientTracingEnvironment({ | ||
| 139 | relayUrl: "https://relay.example.test", | ||
| 140 | mobileTracingUrl: "https://api.axiom.co/v1/traces", | ||
| 141 | mobileTracingDataset: "mobile", | ||
| 142 | mobileTracingToken: "mobile-token", | ||
| 143 | clientTracingUrl: "https://api.axiom.co/v1/traces", | ||
| 144 | clientTracingDataset: "relay", | ||
| 145 | clientTracingToken: "client-token", | ||
| 146 | }), | ||
| 147 | ).toBe( | ||
| 148 | [ | ||
| 149 | "T3CODE_RELAY_CLIENT_OTLP_TRACES_URL=https://api.axiom.co/v1/traces", | ||
| 150 | "T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET=relay", | ||
| 151 | "T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN=client-token", | ||
| 152 | "", | ||
| 153 | ].join("\n"), | ||
| 154 | ); | ||
| 155 | }); | ||
| 156 | }); | ||
| 157 | |||
| 158 | describe("release workflow tracing config propagation", () => { | ||
| 159 | it.effect("uses an artifact instead of a masked cross-job token output", () => | ||
| 160 | Effect.gen(function* () { | ||
| 161 | const fileSystem = yield* FileSystem.FileSystem; | ||
| 162 | const path = yield* Path.Path; | ||
| 163 | const workflowPath = yield* path.fromFileUrl( | ||
| 164 | new URL("../../../.github/workflows/release.yml", import.meta.url), | ||
| 165 | ); | ||
| 166 | const workflow = yield* fileSystem.readFileString(workflowPath); | ||
| 167 | |||
| 168 | expect(workflow).not.toContain("client_tracing_token:"); | ||
| 169 | expect(workflow).not.toContain("needs.relay_public_config.outputs.client_tracing_token"); | ||
| 170 | expect(workflow).toContain('--github-env-file "$RUNNER_TEMP/relay-client-tracing.env"'); | ||
| 171 | expect(workflow).toContain("name: relay-client-tracing-config"); | ||
| 172 | expect(workflow).toContain('cat "$config_path" >> "$GITHUB_ENV"'); | ||
| 173 | }).pipe(Effect.provide(NodeServices.layer)), | ||
| 174 | ); | ||
| 175 | }); | ||
| 176 | |||
| 177 | describe("publicConfigFromOutput", () => { | ||
| 178 | it("reads the complete public tracing config from persisted Alchemy output", () => { | ||
| 179 | expect( | ||
| 180 | publicConfigFromOutput({ | ||
| 181 | url: "https://relay.example.test", | ||
| 182 | mobileTracingUrl: "https://api.axiom.co/v1/traces", | ||
| 183 | mobileTracingDataset: "mobile", | ||
| 184 | mobileTracingToken: "mobile-token", | ||
| 185 | clientTracingUrl: "https://api.axiom.co/v1/traces", | ||
| 186 | clientTracingDataset: "relay", | ||
| 187 | clientTracingToken: "client-token", | ||
| 188 | }), | ||
| 189 | ).toEqual({ | ||
| 190 | relayUrl: "https://relay.example.test", | ||
| 191 | mobileTracingUrl: "https://api.axiom.co/v1/traces", | ||
| 192 | mobileTracingDataset: "mobile", | ||
| 193 | mobileTracingToken: "mobile-token", | ||
| 194 | clientTracingUrl: "https://api.axiom.co/v1/traces", | ||
| 195 | clientTracingDataset: "relay", | ||
| 196 | clientTracingToken: "client-token", | ||
| 197 | }); | ||
| 198 | }); | ||
| 199 | |||
| 200 | it("rejects incomplete stack output", () => { | ||
| 201 | expect(publicConfigFromOutput({ url: "https://relay.example.test" })).toBeNull(); | ||
| 202 | }); | ||
| 203 | }); |
infra/relay/scripts/deploy.ts+127-7| ... | @@ -8,10 +8,12 @@ import { provideFreshArtifactStore } from "alchemy/Artifacts"; | ... | @@ -8,10 +8,12 @@ import { provideFreshArtifactStore } from "alchemy/Artifacts"; |
| 8 | import { AuthProviders } from "alchemy/Auth/AuthProvider"; | 8 | import { AuthProviders } from "alchemy/Auth/AuthProvider"; |
| 9 | import { CredentialsStoreLive } from "alchemy/Auth/Credentials"; | 9 | import { CredentialsStoreLive } from "alchemy/Auth/Credentials"; |
| 10 | import { ProfileLive } from "alchemy/Auth/Profile"; | 10 | import { ProfileLive } from "alchemy/Auth/Profile"; |
| 11 | import * as Cloudflare from "alchemy/Cloudflare"; | ||
| 11 | import { Cli } from "alchemy/Cli/Cli"; | 12 | import { Cli } from "alchemy/Cli/Cli"; |
| 12 | import { LoggingCli } from "alchemy/Cli/LoggingCli"; | 13 | import { LoggingCli } from "alchemy/Cli/LoggingCli"; |
| 13 | import * as Plan from "alchemy/Plan"; | 14 | import * as Plan from "alchemy/Plan"; |
| 14 | import * as Stage from "alchemy/Stage"; | 15 | import * as Stage from "alchemy/Stage"; |
| 16 | import * as State from "alchemy/State/State"; | ||
| 15 | import { TelemetryLive } from "alchemy/Telemetry/Layer"; | 17 | import { TelemetryLive } from "alchemy/Telemetry/Layer"; |
| 16 | import { PlatformServices } from "alchemy/Util/PlatformServices"; | 18 | import { PlatformServices } from "alchemy/Util/PlatformServices"; |
| 17 | import * as Config from "effect/Config"; | 19 | import * as Config from "effect/Config"; |
| ... | @@ -41,6 +43,8 @@ export interface RelayDeployOptions { | ... | @@ -41,6 +43,8 @@ export interface RelayDeployOptions { |
| 41 | readonly yes: boolean; | 43 | readonly yes: boolean; |
| 42 | readonly adopt: boolean; | 44 | readonly adopt: boolean; |
| 43 | readonly githubOutput: boolean; | 45 | readonly githubOutput: boolean; |
| 46 | readonly githubEnvFile: Option.Option<string>; | ||
| 47 | readonly readState: boolean; | ||
| 44 | } | 48 | } |
| 45 | 49 | ||
| 46 | export interface RelayPublicConfig { | 50 | export interface RelayPublicConfig { |
| ... | @@ -48,6 +52,9 @@ export interface RelayPublicConfig { | ... | @@ -48,6 +52,9 @@ export interface RelayPublicConfig { |
| 48 | readonly mobileTracingUrl: string; | 52 | readonly mobileTracingUrl: string; |
| 49 | readonly mobileTracingDataset: string; | 53 | readonly mobileTracingDataset: string; |
| 50 | readonly mobileTracingToken: string; | 54 | readonly mobileTracingToken: string; |
| 55 | readonly clientTracingUrl: string; | ||
| 56 | readonly clientTracingDataset: string; | ||
| 57 | readonly clientTracingToken: string; | ||
| 51 | } | 58 | } |
| 52 | 59 | ||
| 53 | const publicConfigEnvEntries = (config: RelayPublicConfig) => | 60 | const publicConfigEnvEntries = (config: RelayPublicConfig) => |
| ... | @@ -56,6 +63,9 @@ const publicConfigEnvEntries = (config: RelayPublicConfig) => | ... | @@ -56,6 +63,9 @@ const publicConfigEnvEntries = (config: RelayPublicConfig) => |
| 56 | T3CODE_MOBILE_OTLP_TRACES_URL: config.mobileTracingUrl, | 63 | T3CODE_MOBILE_OTLP_TRACES_URL: config.mobileTracingUrl, |
| 57 | T3CODE_MOBILE_OTLP_TRACES_DATASET: config.mobileTracingDataset, | 64 | T3CODE_MOBILE_OTLP_TRACES_DATASET: config.mobileTracingDataset, |
| 58 | T3CODE_MOBILE_OTLP_TRACES_TOKEN: config.mobileTracingToken, | 65 | T3CODE_MOBILE_OTLP_TRACES_TOKEN: config.mobileTracingToken, |
| 66 | T3CODE_RELAY_CLIENT_OTLP_TRACES_URL: config.clientTracingUrl, | ||
| 67 | T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET: config.clientTracingDataset, | ||
| 68 | T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN: config.clientTracingToken, | ||
| 59 | }) as const; | 69 | }) as const; |
| 60 | 70 | ||
| 61 | export function reconcileRootEnvPublicConfig(contents: string, config: RelayPublicConfig): string { | 71 | export function reconcileRootEnvPublicConfig(contents: string, config: RelayPublicConfig): string { |
| ... | @@ -82,9 +92,13 @@ export function reconcileRootEnvRelayUrl(contents: string, relayUrl: string): st | ... | @@ -82,9 +92,13 @@ export function reconcileRootEnvRelayUrl(contents: string, relayUrl: string): st |
| 82 | mobileTracingUrl: "", | 92 | mobileTracingUrl: "", |
| 83 | mobileTracingDataset: "", | 93 | mobileTracingDataset: "", |
| 84 | mobileTracingToken: "", | 94 | mobileTracingToken: "", |
| 95 | clientTracingUrl: "", | ||
| 96 | clientTracingDataset: "", | ||
| 97 | clientTracingToken: "", | ||
| 85 | }) | 98 | }) |
| 86 | .split("\n") | 99 | .split("\n") |
| 87 | .filter((line) => !line.startsWith("T3CODE_MOBILE_OTLP_TRACES_")) | 100 | .filter((line) => !line.startsWith("T3CODE_MOBILE_OTLP_TRACES_")) |
| 101 | .filter((line) => !line.startsWith("T3CODE_RELAY_CLIENT_OTLP_TRACES_")) | ||
| 88 | .join("\n"); | 102 | .join("\n"); |
| 89 | } | 103 | } |
| 90 | 104 | ||
| ... | @@ -98,7 +112,7 @@ export function hasDeployChanges(plan: Plan.Plan): boolean { | ... | @@ -98,7 +112,7 @@ export function hasDeployChanges(plan: Plan.Plan): boolean { |
| 98 | ); | 112 | ); |
| 99 | } | 113 | } |
| 100 | 114 | ||
| 101 | export type RelayDeployResult = "applied" | "noop" | "dry-run" | "cancelled"; | 115 | export type RelayDeployResult = "applied" | "noop" | "dry-run" | "cancelled" | "state"; |
| 102 | 116 | ||
| 103 | export interface RelayDeployOutcome { | 117 | export interface RelayDeployOutcome { |
| 104 | readonly result: RelayDeployResult; | 118 | readonly result: RelayDeployResult; |
| ... | @@ -112,6 +126,14 @@ export function serializeGithubOutput(entries: Readonly<Record<string, string | | ... | @@ -112,6 +126,14 @@ export function serializeGithubOutput(entries: Readonly<Record<string, string | |
| 112 | .join(""); | 126 | .join(""); |
| 113 | } | 127 | } |
| 114 | 128 | ||
| 129 | export function serializeRelayClientTracingEnvironment(config: RelayPublicConfig): string { | ||
| 130 | return serializeGithubOutput({ | ||
| 131 | T3CODE_RELAY_CLIENT_OTLP_TRACES_URL: config.clientTracingUrl, | ||
| 132 | T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET: config.clientTracingDataset, | ||
| 133 | T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN: config.clientTracingToken, | ||
| 134 | }); | ||
| 135 | } | ||
| 136 | |||
| 115 | const relayRoot = Effect.service(Path.Path).pipe( | 137 | const relayRoot = Effect.service(Path.Path).pipe( |
| 116 | Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))), | 138 | Effect.flatMap((path) => path.fromFileUrl(new URL("..", import.meta.url))), |
| 117 | ); | 139 | ); |
| ... | @@ -151,7 +173,7 @@ const reconcileRootEnv = Effect.fn("relay.deploy.reconcileRootEnv")(function* ( | ... | @@ -151,7 +173,7 @@ const reconcileRootEnv = Effect.fn("relay.deploy.reconcileRootEnv")(function* ( |
| 151 | const contents = (yield* fs.exists(rootEnvPath)) ? yield* fs.readFileString(rootEnvPath) : ""; | 173 | const contents = (yield* fs.exists(rootEnvPath)) ? yield* fs.readFileString(rootEnvPath) : ""; |
| 152 | 174 | ||
| 153 | yield* fs.writeFileString(rootEnvPath, reconcileRootEnvPublicConfig(contents, config)); | 175 | yield* fs.writeFileString(rootEnvPath, reconcileRootEnvPublicConfig(contents, config)); |
| 154 | yield* Console.log(`Updated ${rootEnvPath} with relay URL and mobile tracing configuration`); | 176 | yield* Console.log(`Updated ${rootEnvPath} with relay public client configuration`); |
| 155 | }); | 177 | }); |
| 156 | 178 | ||
| 157 | const writeGithubOutput = Effect.fn("relay.deploy.writeGithubOutput")(function* ( | 179 | const writeGithubOutput = Effect.fn("relay.deploy.writeGithubOutput")(function* ( |
| ... | @@ -165,14 +187,34 @@ const writeGithubOutput = Effect.fn("relay.deploy.writeGithubOutput")(function* | ... | @@ -165,14 +187,34 @@ const writeGithubOutput = Effect.fn("relay.deploy.writeGithubOutput")(function* |
| 165 | changed: outcome.changed, | 187 | changed: outcome.changed, |
| 166 | result: outcome.result, | 188 | result: outcome.result, |
| 167 | ...(Option.isSome(outcome.publicConfig) | 189 | ...(Option.isSome(outcome.publicConfig) |
| 168 | ? { relay_url: outcome.publicConfig.value.relayUrl } | 190 | ? { |
| 191 | relay_url: outcome.publicConfig.value.relayUrl, | ||
| 192 | } | ||
| 169 | : {}), | 193 | : {}), |
| 170 | }), | 194 | }), |
| 171 | { flag: "a" }, | 195 | { flag: "a" }, |
| 172 | ); | 196 | ); |
| 173 | }); | 197 | }); |
| 174 | 198 | ||
| 175 | const deployServices = Layer.mergeAll( | 199 | const writeGithubEnvFile = Effect.fn("relay.deploy.writeGithubEnvFile")(function* ( |
| 200 | outcome: RelayDeployOutcome, | ||
| 201 | outputPath: string, | ||
| 202 | ) { | ||
| 203 | if (Option.isNone(outcome.publicConfig)) { | ||
| 204 | return yield* new RelayDeployError({ | ||
| 205 | message: "Relay public client config is unavailable for the GitHub environment file", | ||
| 206 | }); | ||
| 207 | } | ||
| 208 | const fs = yield* FileSystem.FileSystem; | ||
| 209 | yield* Console.log(`::add-mask::${outcome.publicConfig.value.clientTracingToken}`); | ||
| 210 | yield* fs.writeFileString( | ||
| 211 | outputPath, | ||
| 212 | serializeRelayClientTracingEnvironment(outcome.publicConfig.value), | ||
| 213 | ); | ||
| 214 | }); | ||
| 215 | |||
| 216 | const deployBaseServices = Layer.mergeAll( | ||
| 217 | Layer.succeed(AuthProviders, {}), | ||
| 176 | Layer.provideMerge(AlchemyContextLive, PlatformServices), | 218 | Layer.provideMerge(AlchemyContextLive, PlatformServices), |
| 177 | Layer.provide(ProfileLive, PlatformServices), | 219 | Layer.provide(ProfileLive, PlatformServices), |
| 178 | Layer.provide(CredentialsStoreLive, PlatformServices), | 220 | Layer.provide(CredentialsStoreLive, PlatformServices), |
| ... | @@ -180,6 +222,64 @@ const deployServices = Layer.mergeAll( | ... | @@ -180,6 +222,64 @@ const deployServices = Layer.mergeAll( |
| 180 | TelemetryLive, | 222 | TelemetryLive, |
| 181 | LoggingCli, | 223 | LoggingCli, |
| 182 | ); | 224 | ); |
| 225 | const deployServices = deployBaseServices; | ||
| 226 | |||
| 227 | export function publicConfigFromOutput(output: unknown): RelayPublicConfig | null { | ||
| 228 | if (typeof output !== "object" || output === null) { | ||
| 229 | return null; | ||
| 230 | } | ||
| 231 | const value = output as Record<string, unknown>; | ||
| 232 | const text = (name: string) => (typeof value[name] === "string" ? value[name] : undefined); | ||
| 233 | const secret = (name: string): string | undefined => { | ||
| 234 | const candidate = value[name]; | ||
| 235 | if (!Redacted.isRedacted(candidate)) { | ||
| 236 | return text(name); | ||
| 237 | } | ||
| 238 | const redacted = Redacted.value(candidate); | ||
| 239 | return typeof redacted === "string" ? redacted : undefined; | ||
| 240 | }; | ||
| 241 | const relayUrl = text("url"); | ||
| 242 | const mobileTracingUrl = text("mobileTracingUrl"); | ||
| 243 | const mobileTracingDataset = text("mobileTracingDataset"); | ||
| 244 | const mobileTracingToken = secret("mobileTracingToken"); | ||
| 245 | const clientTracingUrl = text("clientTracingUrl"); | ||
| 246 | const clientTracingDataset = text("clientTracingDataset"); | ||
| 247 | const clientTracingToken = secret("clientTracingToken"); | ||
| 248 | return relayUrl && | ||
| 249 | mobileTracingUrl && | ||
| 250 | mobileTracingDataset && | ||
| 251 | mobileTracingToken && | ||
| 252 | clientTracingUrl && | ||
| 253 | clientTracingDataset && | ||
| 254 | clientTracingToken | ||
| 255 | ? { | ||
| 256 | relayUrl, | ||
| 257 | mobileTracingUrl, | ||
| 258 | mobileTracingDataset, | ||
| 259 | mobileTracingToken, | ||
| 260 | clientTracingUrl, | ||
| 261 | clientTracingDataset, | ||
| 262 | clientTracingToken, | ||
| 263 | } | ||
| 264 | : null; | ||
| 265 | } | ||
| 266 | |||
| 267 | const readRelayPublicConfig = Effect.fn("relay.deploy.readState")(function* (stage: string) { | ||
| 268 | const state = yield* State.State; | ||
| 269 | const service = yield* state; | ||
| 270 | const output = yield* service.getOutput({ stack: "T3CodeRelay", stage }); | ||
| 271 | const publicConfig = publicConfigFromOutput(output); | ||
| 272 | if (publicConfig === null) { | ||
| 273 | return yield* new RelayDeployError({ | ||
| 274 | message: `Alchemy relay state for stage ${stage} did not include complete public client config`, | ||
| 275 | }); | ||
| 276 | } | ||
| 277 | return { | ||
| 278 | result: "state", | ||
| 279 | changed: false, | ||
| 280 | publicConfig: Option.some(publicConfig), | ||
| 281 | } satisfies RelayDeployOutcome; | ||
| 282 | }); | ||
| 183 | 283 | ||
| 184 | const runRelayDeploy = Effect.fn("relay.deploy.run")( | 284 | const runRelayDeploy = Effect.fn("relay.deploy.run")( |
| 185 | function* ( | 285 | function* ( |
| ... | @@ -188,7 +288,6 @@ const runRelayDeploy = Effect.fn("relay.deploy.run")( | ... | @@ -188,7 +288,6 @@ const runRelayDeploy = Effect.fn("relay.deploy.run")( |
| 188 | _stage: string, | 288 | _stage: string, |
| 189 | ) { | 289 | ) { |
| 190 | const stack = yield* RelayStack; | 290 | const stack = yield* RelayStack; |
| 191 | |||
| 192 | const cli = yield* Cli; | 291 | const cli = yield* Cli; |
| 193 | const plan = yield* Plan.make(stack, { force: options.force }).pipe( | 292 | const plan = yield* Plan.make(stack, { force: options.force }).pipe( |
| 194 | Effect.provide(stack.services), | 293 | Effect.provide(stack.services), |
| ... | @@ -223,7 +322,10 @@ const runRelayDeploy = Effect.fn("relay.deploy.run")( | ... | @@ -223,7 +322,10 @@ const runRelayDeploy = Effect.fn("relay.deploy.run")( |
| 223 | output.url === undefined || | 322 | output.url === undefined || |
| 224 | output.mobileTracingUrl === undefined || | 323 | output.mobileTracingUrl === undefined || |
| 225 | output.mobileTracingDataset === undefined || | 324 | output.mobileTracingDataset === undefined || |
| 226 | output.mobileTracingToken === undefined | 325 | output.mobileTracingToken === undefined || |
| 326 | output.clientTracingUrl === undefined || | ||
| 327 | output.clientTracingDataset === undefined || | ||
| 328 | output.clientTracingToken === undefined | ||
| 227 | ) { | 329 | ) { |
| 228 | return yield* new RelayDeployError({ | 330 | return yield* new RelayDeployError({ |
| 229 | message: "Alchemy relay deploy output did not include complete public client config", | 331 | message: "Alchemy relay deploy output did not include complete public client config", |
| ... | @@ -237,6 +339,9 @@ const runRelayDeploy = Effect.fn("relay.deploy.run")( | ... | @@ -237,6 +339,9 @@ const runRelayDeploy = Effect.fn("relay.deploy.run")( |
| 237 | mobileTracingUrl: output.mobileTracingUrl, | 339 | mobileTracingUrl: output.mobileTracingUrl, |
| 238 | mobileTracingDataset: output.mobileTracingDataset, | 340 | mobileTracingDataset: output.mobileTracingDataset, |
| 239 | mobileTracingToken: Redacted.value(output.mobileTracingToken), | 341 | mobileTracingToken: Redacted.value(output.mobileTracingToken), |
| 342 | clientTracingUrl: output.clientTracingUrl, | ||
| 343 | clientTracingDataset: output.clientTracingDataset, | ||
| 344 | clientTracingToken: Redacted.value(output.clientTracingToken), | ||
| 240 | }), | 345 | }), |
| 241 | } satisfies RelayDeployOutcome; | 346 | } satisfies RelayDeployOutcome; |
| 242 | }, | 347 | }, |
| ... | @@ -264,13 +369,18 @@ export const deploy = Effect.fn("relay.deploy")(function* (options: RelayDeployO | ... | @@ -264,13 +369,18 @@ export const deploy = Effect.fn("relay.deploy")(function* (options: RelayDeployO |
| 264 | Effect.provide(ConfigProvider.layer(configProvider)), | 369 | Effect.provide(ConfigProvider.layer(configProvider)), |
| 265 | ); | 370 | ); |
| 266 | const stage = Option.getOrElse(options.stage, () => configuredStage); | 371 | const stage = Option.getOrElse(options.stage, () => configuredStage); |
| 267 | const outcome = yield* runRelayDeploy(options, configProvider, stage); | 372 | const outcome = options.readState |
| 373 | ? yield* readRelayPublicConfig(stage).pipe(Effect.provide(Cloudflare.state())) | ||
| 374 | : yield* runRelayDeploy(options, configProvider, stage); | ||
| 268 | if (Option.isSome(outcome.publicConfig)) { | 375 | if (Option.isSome(outcome.publicConfig)) { |
| 269 | yield* reconcileRootEnv(outcome.publicConfig.value); | 376 | yield* reconcileRootEnv(outcome.publicConfig.value); |
| 270 | } | 377 | } |
| 271 | if (options.githubOutput) { | 378 | if (options.githubOutput) { |
| 272 | yield* writeGithubOutput(outcome); | 379 | yield* writeGithubOutput(outcome); |
| 273 | } | 380 | } |
| 381 | if (Option.isSome(options.githubEnvFile)) { | ||
| 382 | yield* writeGithubEnvFile(outcome, options.githubEnvFile.value); | ||
| 383 | } | ||
| 274 | }); | 384 | }); |
| 275 | 385 | ||
| 276 | export const relayDeployCommand = Command.make( | 386 | export const relayDeployCommand = Command.make( |
| ... | @@ -306,6 +416,16 @@ export const relayDeployCommand = Command.make( | ... | @@ -306,6 +416,16 @@ export const relayDeployCommand = Command.make( |
| 306 | Flag.withDescription("Append relay deployment metadata to GITHUB_OUTPUT."), | 416 | Flag.withDescription("Append relay deployment metadata to GITHUB_OUTPUT."), |
| 307 | Flag.withDefault(false), | 417 | Flag.withDefault(false), |
| 308 | ), | 418 | ), |
| 419 | githubEnvFile: Flag.string("github-env-file").pipe( | ||
| 420 | Flag.withDescription( | ||
| 421 | "Write relay client tracing variables to a file suitable for GITHUB_ENV.", | ||
| 422 | ), | ||
| 423 | Flag.optional, | ||
| 424 | ), | ||
| 425 | readState: Flag.boolean("read-state").pipe( | ||
| 426 | Flag.withDescription("Read the deployed stack output without planning or applying changes."), | ||
| 427 | Flag.withDefault(false), | ||
| 428 | ), | ||
| 309 | }, | 429 | }, |
| 310 | deploy, | 430 | deploy, |
| 311 | ).pipe(Command.withDescription("Deploy the T3 Code relay through Alchemy.")); | 431 | ).pipe(Command.withDescription("Deploy the T3 Code relay through Alchemy.")); |
infra/relay/src/observability.ts+9-1| ... | @@ -50,6 +50,14 @@ export const RelayObservability = Effect.gen(function* () { | ... | @@ -50,6 +50,14 @@ export const RelayObservability = Effect.gen(function* () { |
| 50 | })), | 50 | })), |
| 51 | }); | 51 | }); |
| 52 | 52 | ||
| 53 | const clientIngestToken = yield* Axiom.ApiToken("RelayClientAxiomIngestToken", { | ||
| 54 | name: relayResourceNameForStage("t3-code-relay-client-otel-ingest", stage), | ||
| 55 | description: "Owned by Alchemy. Scoped OTLP ingest token for first-party relay client spans.", | ||
| 56 | datasetCapabilities: Output.map(traces.name, (dataset) => ({ | ||
| 57 | [dataset]: { ingest: ["create" as const] }, | ||
| 58 | })), | ||
| 59 | }); | ||
| 60 | |||
| 53 | yield* Axiom.View("RelayRecentSpansView", { | 61 | yield* Axiom.View("RelayRecentSpansView", { |
| 54 | name: relayResourceNameForStage("t3-code-relay-recent-spans", stage), | 62 | name: relayResourceNameForStage("t3-code-relay-recent-spans", stage), |
| 55 | description: "Recent relay HTTP request spans.", | 63 | description: "Recent relay HTTP request spans.", |
| ... | @@ -57,7 +65,7 @@ export const RelayObservability = Effect.gen(function* () { | ... | @@ -57,7 +65,7 @@ export const RelayObservability = Effect.gen(function* () { |
| 57 | aplQuery: Output.map(traces.name, relayRecentSpansQuery), | 65 | aplQuery: Output.map(traces.name, relayRecentSpansQuery), |
| 58 | }); | 66 | }); |
| 59 | 67 | ||
| 60 | return { traces, workerIngestToken, mobileIngestToken } as const; | 68 | return { traces, workerIngestToken, mobileIngestToken, clientIngestToken } as const; |
| 61 | }); | 69 | }); |
| 62 | 70 | ||
| 63 | export const withSpanAttributes = | 71 | export const withSpanAttributes = |
packages/client-runtime/src/managedRelay.ts+11-5| ... | @@ -26,6 +26,7 @@ import { | ... | @@ -26,6 +26,7 @@ import { |
| 26 | RelayUnregisterDeviceEndpoint, | 26 | RelayUnregisterDeviceEndpoint, |
| 27 | } from "@t3tools/contracts/relay"; | 27 | } from "@t3tools/contracts/relay"; |
| 28 | import { encodeOAuthScope, oauthScopeSetEquals } from "@t3tools/shared/oauthScope"; | 28 | import { encodeOAuthScope, oauthScopeSetEquals } from "@t3tools/shared/oauthScope"; |
| 29 | import { withRelayClientTracing } from "@t3tools/shared/relayTracing"; | ||
| 29 | import { normalizeSecureRelayUrl } from "@t3tools/shared/relayUrl"; | 30 | import { normalizeSecureRelayUrl } from "@t3tools/shared/relayUrl"; |
| 30 | import * as Clock from "effect/Clock"; | 31 | import * as Clock from "effect/Clock"; |
| 31 | import * as Context from "effect/Context"; | 32 | import * as Context from "effect/Context"; |
| ... | @@ -357,6 +358,7 @@ export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) | ... | @@ -357,6 +358,7 @@ export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) |
| 357 | relayClientError("Could not list relay-managed environments.", cause), | 358 | relayClientError("Could not list relay-managed environments.", cause), |
| 358 | ), | 359 | ), |
| 359 | timeoutRelayRequest("Relay environment listing timed out."), | 360 | timeoutRelayRequest("Relay environment listing timed out."), |
| 361 | withRelayClientTracing, | ||
| 360 | ), | 362 | ), |
| 361 | listDevices: (input) => | 363 | listDevices: (input) => |
| 362 | client.client | 364 | client.client |
| ... | @@ -369,6 +371,7 @@ export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) | ... | @@ -369,6 +371,7 @@ export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) |
| 369 | relayClientError("Could not list relay client devices.", cause), | 371 | relayClientError("Could not list relay client devices.", cause), |
| 370 | ), | 372 | ), |
| 371 | timeoutRelayRequest("Relay client device listing timed out."), | 373 | timeoutRelayRequest("Relay client device listing timed out."), |
| 374 | withRelayClientTracing, | ||
| 372 | ), | 375 | ), |
| 373 | createEnvironmentLinkChallenge: (input) => | 376 | createEnvironmentLinkChallenge: (input) => |
| 374 | client.client | 377 | client.client |
| ... | @@ -381,6 +384,7 @@ export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) | ... | @@ -381,6 +384,7 @@ export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) |
| 381 | relayClientError("Could not create relay environment link challenge.", cause), | 384 | relayClientError("Could not create relay environment link challenge.", cause), |
| 382 | ), | 385 | ), |
| 383 | timeoutRelayRequest("Relay environment link challenge timed out."), | 386 | timeoutRelayRequest("Relay environment link challenge timed out."), |
| 387 | withRelayClientTracing, | ||
| 384 | ), | 388 | ), |
| 385 | linkEnvironment: (input) => | 389 | linkEnvironment: (input) => |
| 386 | client.client | 390 | client.client |
| ... | @@ -393,6 +397,7 @@ export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) | ... | @@ -393,6 +397,7 @@ export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) |
| 393 | relayClientError("Could not link relay environment.", cause), | 397 | relayClientError("Could not link relay environment.", cause), |
| 394 | ), | 398 | ), |
| 395 | timeoutRelayRequest("Relay environment linking timed out."), | 399 | timeoutRelayRequest("Relay environment linking timed out."), |
| 400 | withRelayClientTracing, | ||
| 396 | ), | 401 | ), |
| 397 | unlinkEnvironment: (input) => | 402 | unlinkEnvironment: (input) => |
| 398 | client.client | 403 | client.client |
| ... | @@ -405,6 +410,7 @@ export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) | ... | @@ -405,6 +410,7 @@ export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) |
| 405 | relayClientError("Could not unlink relay environment.", cause), | 410 | relayClientError("Could not unlink relay environment.", cause), |
| 406 | ), | 411 | ), |
| 407 | timeoutRelayRequest("Relay environment unlinking timed out."), | 412 | timeoutRelayRequest("Relay environment unlinking timed out."), |
| 413 | withRelayClientTracing, | ||
| 408 | ), | 414 | ), |
| 409 | getEnvironmentStatus: (input) => | 415 | getEnvironmentStatus: (input) => |
| 410 | Effect.gen(function* () { | 416 | Effect.gen(function* () { |
| ... | @@ -424,7 +430,7 @@ export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) | ... | @@ -424,7 +430,7 @@ export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) |
| 424 | ), | 430 | ), |
| 425 | timeoutRelayRequest("Relay environment status request timed out."), | 431 | timeoutRelayRequest("Relay environment status request timed out."), |
| 426 | ); | 432 | ); |
| 427 | }), | 433 | }).pipe(withRelayClientTracing), |
| 428 | connectEnvironment: (input) => | 434 | connectEnvironment: (input) => |
| 429 | Effect.gen(function* () { | 435 | Effect.gen(function* () { |
| 430 | const authorization = yield* authorize({ | 436 | const authorization = yield* authorize({ |
| ... | @@ -448,7 +454,7 @@ export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) | ... | @@ -448,7 +454,7 @@ export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) |
| 448 | ), | 454 | ), |
| 449 | timeoutRelayRequest("Relay environment connection timed out."), | 455 | timeoutRelayRequest("Relay environment connection timed out."), |
| 450 | ); | 456 | ); |
| 451 | }), | 457 | }).pipe(withRelayClientTracing), |
| 452 | registerDevice: (input) => | 458 | registerDevice: (input) => |
| 453 | Effect.gen(function* () { | 459 | Effect.gen(function* () { |
| 454 | const authorization = yield* authorizeMobileRegistration({ | 460 | const authorization = yield* authorizeMobileRegistration({ |
| ... | @@ -466,7 +472,7 @@ export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) | ... | @@ -466,7 +472,7 @@ export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) |
| 466 | ), | 472 | ), |
| 467 | timeoutRelayRequest("Relay mobile device registration timed out."), | 473 | timeoutRelayRequest("Relay mobile device registration timed out."), |
| 468 | ); | 474 | ); |
| 469 | }), | 475 | }).pipe(withRelayClientTracing), |
| 470 | unregisterDevice: (input) => | 476 | unregisterDevice: (input) => |
| 471 | Effect.gen(function* () { | 477 | Effect.gen(function* () { |
| 472 | const authorization = yield* authorizeMobileRegistration({ | 478 | const authorization = yield* authorizeMobileRegistration({ |
| ... | @@ -484,7 +490,7 @@ export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) | ... | @@ -484,7 +490,7 @@ export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) |
| 484 | ), | 490 | ), |
| 485 | timeoutRelayRequest("Relay mobile device unregistration timed out."), | 491 | timeoutRelayRequest("Relay mobile device unregistration timed out."), |
| 486 | ); | 492 | ); |
| 487 | }), | 493 | }).pipe(withRelayClientTracing), |
| 488 | registerLiveActivity: (input) => | 494 | registerLiveActivity: (input) => |
| 489 | Effect.gen(function* () { | 495 | Effect.gen(function* () { |
| 490 | const authorization = yield* authorizeMobileRegistration({ | 496 | const authorization = yield* authorizeMobileRegistration({ |
| ... | @@ -502,7 +508,7 @@ export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) | ... | @@ -502,7 +508,7 @@ export function managedRelayClientLayer(options: ManagedRelayClientLayerOptions) |
| 502 | ), | 508 | ), |
| 503 | timeoutRelayRequest("Relay Live Activity registration timed out."), | 509 | timeoutRelayRequest("Relay Live Activity registration timed out."), |
| 504 | ); | 510 | ); |
| 505 | }), | 511 | }).pipe(withRelayClientTracing), |
| 506 | resetTokenCache: SynchronizedRef.set(cachedTokens, []), | 512 | resetTokenCache: SynchronizedRef.set(cachedTokens, []), |
| 507 | }); | 513 | }); |
| 508 | }), | 514 | }), |
packages/shared/package.json+4| ... | @@ -158,6 +158,10 @@ | ... | @@ -158,6 +158,10 @@ |
| 158 | "./hostProcess": { | 158 | "./hostProcess": { |
| 159 | "types": "./src/hostProcess.ts", | 159 | "types": "./src/hostProcess.ts", |
| 160 | "import": "./src/hostProcess.ts" | 160 | "import": "./src/hostProcess.ts" |
| 161 | }, | ||
| 162 | "./relayTracing": { | ||
| 163 | "types": "./src/relayTracing.ts", | ||
| 164 | "import": "./src/relayTracing.ts" | ||
| 161 | } | 165 | } |
| 162 | }, | 166 | }, |
| 163 | "scripts": { | 167 | "scripts": { |
packages/shared/src/relayTracing.test.ts created+57| ... | @@ -0,0 +1,57 @@ | ||
| 1 | import { describe, expect, it } from "@effect/vitest"; | ||
| 2 | import * as Effect from "effect/Effect"; | ||
| 3 | import * as Option from "effect/Option"; | ||
| 4 | import * as Tracer from "effect/Tracer"; | ||
| 5 | |||
| 6 | import { RelayClientTracer, withRelayClientTracing } from "./relayTracing.ts"; | ||
| 7 | |||
| 8 | function collectingTracer(spans: Array<string>): Tracer.Tracer { | ||
| 9 | return Tracer.make({ | ||
| 10 | span: (options) => { | ||
| 11 | const span = new Tracer.NativeSpan(options); | ||
| 12 | const end = span.end.bind(span); | ||
| 13 | span.end = (endTime, exit) => { | ||
| 14 | end(endTime, exit); | ||
| 15 | spans.push(span.name); | ||
| 16 | }; | ||
| 17 | return span; | ||
| 18 | }, | ||
| 19 | }); | ||
| 20 | } | ||
| 21 | |||
| 22 | describe("withRelayClientTracing", () => { | ||
| 23 | it.effect("uses the product tracer only for relay operations", () => | ||
| 24 | Effect.gen(function* () { | ||
| 25 | const userSpans: Array<string> = []; | ||
| 26 | const productSpans: Array<string> = []; | ||
| 27 | const userTracer = collectingTracer(userSpans); | ||
| 28 | const productTracer = collectingTracer(productSpans); | ||
| 29 | |||
| 30 | yield* Effect.void.pipe(Effect.withSpan("user.operation"), Effect.withTracer(userTracer)); | ||
| 31 | yield* Effect.void.pipe( | ||
| 32 | Effect.withSpan("relay.operation"), | ||
| 33 | withRelayClientTracing, | ||
| 34 | Effect.provideService(RelayClientTracer, Option.some(productTracer)), | ||
| 35 | Effect.withTracer(userTracer), | ||
| 36 | ); | ||
| 37 | |||
| 38 | expect(userSpans).toEqual(["user.operation"]); | ||
| 39 | expect(productSpans).toEqual(["relay.operation"]); | ||
| 40 | }), | ||
| 41 | ); | ||
| 42 | |||
| 43 | it.effect("preserves the active tracer when product tracing is disabled", () => | ||
| 44 | Effect.gen(function* () { | ||
| 45 | const userSpans: Array<string> = []; | ||
| 46 | const userTracer = collectingTracer(userSpans); | ||
| 47 | |||
| 48 | yield* Effect.void.pipe( | ||
| 49 | Effect.withSpan("relay.operation"), | ||
| 50 | withRelayClientTracing, | ||
| 51 | Effect.withTracer(userTracer), | ||
| 52 | ); | ||
| 53 | |||
| 54 | expect(userSpans).toEqual(["relay.operation"]); | ||
| 55 | }), | ||
| 56 | ); | ||
| 57 | }); | ||
packages/shared/src/relayTracing.ts created+70| ... | @@ -0,0 +1,70 @@ | ||
| 1 | import * as Context from "effect/Context"; | ||
| 2 | import * as Effect from "effect/Effect"; | ||
| 3 | import * as Layer from "effect/Layer"; | ||
| 4 | import * as Option from "effect/Option"; | ||
| 5 | import * as Tracer from "effect/Tracer"; | ||
| 6 | import type { HttpClient } from "effect/unstable/http"; | ||
| 7 | import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability"; | ||
| 8 | |||
| 9 | export interface RelayClientTracingConfig { | ||
| 10 | readonly tracesUrl: string; | ||
| 11 | readonly tracesDataset: string; | ||
| 12 | readonly tracesToken: string; | ||
| 13 | } | ||
| 14 | |||
| 15 | export interface RelayClientTracingResource { | ||
| 16 | readonly serviceName: string; | ||
| 17 | readonly serviceVersion?: string; | ||
| 18 | readonly runtime: string; | ||
| 19 | readonly client: string; | ||
| 20 | readonly component?: string; | ||
| 21 | } | ||
| 22 | |||
| 23 | export class RelayClientTracer extends Context.Reference( | ||
| 24 | "@t3tools/shared/relayTracing/RelayClientTracer", | ||
| 25 | { | ||
| 26 | defaultValue: () => Option.none<Tracer.Tracer>(), | ||
| 27 | }, | ||
| 28 | ) {} | ||
| 29 | |||
| 30 | export const withRelayClientTracing = <A, E, R>( | ||
| 31 | effect: Effect.Effect<A, E, R>, | ||
| 32 | ): Effect.Effect<A, E, R> => | ||
| 33 | RelayClientTracer.pipe( | ||
| 34 | Effect.flatMap( | ||
| 35 | Option.match({ | ||
| 36 | onNone: () => effect, | ||
| 37 | onSome: (tracer) => effect.pipe(Effect.provideService(Tracer.Tracer, tracer)), | ||
| 38 | }), | ||
| 39 | ), | ||
| 40 | ); | ||
| 41 | |||
| 42 | export function makeRelayClientTracingLayer( | ||
| 43 | config: RelayClientTracingConfig | null, | ||
| 44 | resource: RelayClientTracingResource, | ||
| 45 | ): Layer.Layer<never, never, HttpClient.HttpClient> { | ||
| 46 | if (config === null) { | ||
| 47 | return Layer.succeed(RelayClientTracer, Option.none()); | ||
| 48 | } | ||
| 49 | |||
| 50 | const tracerLayer = OtlpTracer.layer({ | ||
| 51 | url: config.tracesUrl, | ||
| 52 | headers: { | ||
| 53 | Authorization: `Bearer ${config.tracesToken}`, | ||
| 54 | "X-Axiom-Dataset": config.tracesDataset, | ||
| 55 | }, | ||
| 56 | resource: { | ||
| 57 | serviceName: resource.serviceName, | ||
| 58 | serviceVersion: resource.serviceVersion, | ||
| 59 | attributes: { | ||
| 60 | "service.runtime": resource.runtime, | ||
| 61 | "service.component": resource.component ?? "relay-client", | ||
| 62 | "t3.client.surface": resource.client, | ||
| 63 | }, | ||
| 64 | }, | ||
| 65 | }).pipe(Layer.provide(OtlpSerialization.layerJson)); | ||
| 66 | |||
| 67 | return Layer.effect(RelayClientTracer, Tracer.Tracer.pipe(Effect.map(Option.some))).pipe( | ||
| 68 | Layer.provide(tracerLayer), | ||
| 69 | ); | ||
| 70 | } | ||
scripts/lib/public-config.test.ts+29| ... | @@ -33,6 +33,12 @@ describe("loadRepoEnv", () => { | ... | @@ -33,6 +33,12 @@ describe("loadRepoEnv", () => { |
| 33 | expect(env.EXPO_PUBLIC_OTLP_TRACES_URL).toBeUndefined(); | 33 | expect(env.EXPO_PUBLIC_OTLP_TRACES_URL).toBeUndefined(); |
| 34 | expect(env.EXPO_PUBLIC_OTLP_TRACES_DATASET).toBeUndefined(); | 34 | expect(env.EXPO_PUBLIC_OTLP_TRACES_DATASET).toBeUndefined(); |
| 35 | expect(env.EXPO_PUBLIC_OTLP_TRACES_TOKEN).toBeUndefined(); | 35 | expect(env.EXPO_PUBLIC_OTLP_TRACES_TOKEN).toBeUndefined(); |
| 36 | expect(env.T3CODE_RELAY_CLIENT_OTLP_TRACES_URL).toBeUndefined(); | ||
| 37 | expect(env.T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET).toBeUndefined(); | ||
| 38 | expect(env.T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN).toBeUndefined(); | ||
| 39 | expect(env.VITE_RELAY_OTLP_TRACES_URL).toBeUndefined(); | ||
| 40 | expect(env.VITE_RELAY_OTLP_TRACES_DATASET).toBeUndefined(); | ||
| 41 | expect(env.VITE_RELAY_OTLP_TRACES_TOKEN).toBeUndefined(); | ||
| 36 | }); | 42 | }); |
| 37 | 43 | ||
| 38 | it("applies process, root local, and root precedence in that order", () => { | 44 | it("applies process, root local, and root precedence in that order", () => { |
| ... | @@ -91,6 +97,29 @@ describe("loadRepoEnv", () => { | ... | @@ -91,6 +97,29 @@ describe("loadRepoEnv", () => { |
| 91 | mobileOtlpTracesUrl: "https://api.axiom.co/v1/traces", | 97 | mobileOtlpTracesUrl: "https://api.axiom.co/v1/traces", |
| 92 | mobileOtlpTracesDataset: "mobile-traces", | 98 | mobileOtlpTracesDataset: "mobile-traces", |
| 93 | mobileOtlpTracesToken: "mobile-token", | 99 | mobileOtlpTracesToken: "mobile-token", |
| 100 | relayClientOtlpTracesUrl: undefined, | ||
| 101 | relayClientOtlpTracesDataset: undefined, | ||
| 102 | relayClientOtlpTracesToken: undefined, | ||
| 103 | }); | ||
| 104 | }); | ||
| 105 | |||
| 106 | it("projects canonical relay client tracing values to web build aliases", () => { | ||
| 107 | expect( | ||
| 108 | loadRepoEnv({ | ||
| 109 | baseEnv: { | ||
| 110 | T3CODE_RELAY_CLIENT_OTLP_TRACES_URL: "https://api.axiom.co/v1/traces", | ||
| 111 | T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET: "relay-client-traces", | ||
| 112 | T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN: "relay-client-token", | ||
| 113 | }, | ||
| 114 | repoRoot: makeTemporaryDirectory(), | ||
| 115 | }), | ||
| 116 | ).toEqual({ | ||
| 117 | T3CODE_RELAY_CLIENT_OTLP_TRACES_URL: "https://api.axiom.co/v1/traces", | ||
| 118 | T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET: "relay-client-traces", | ||
| 119 | T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN: "relay-client-token", | ||
| 120 | VITE_RELAY_OTLP_TRACES_URL: "https://api.axiom.co/v1/traces", | ||
| 121 | VITE_RELAY_OTLP_TRACES_DATASET: "relay-client-traces", | ||
| 122 | VITE_RELAY_OTLP_TRACES_TOKEN: "relay-client-token", | ||
| 94 | }); | 123 | }); |
| 95 | }); | 124 | }); |
| 96 | 125 |
scripts/lib/public-config.ts+36| ... | @@ -12,6 +12,9 @@ export interface T3CodePublicConfig { | ... | @@ -12,6 +12,9 @@ export interface T3CodePublicConfig { |
| 12 | readonly mobileOtlpTracesUrl: string | undefined; | 12 | readonly mobileOtlpTracesUrl: string | undefined; |
| 13 | readonly mobileOtlpTracesDataset: string | undefined; | 13 | readonly mobileOtlpTracesDataset: string | undefined; |
| 14 | readonly mobileOtlpTracesToken: string | undefined; | 14 | readonly mobileOtlpTracesToken: string | undefined; |
| 15 | readonly relayClientOtlpTracesUrl: string | undefined; | ||
| 16 | readonly relayClientOtlpTracesDataset: string | undefined; | ||
| 17 | readonly relayClientOtlpTracesToken: string | undefined; | ||
| 15 | } | 18 | } |
| 16 | 19 | ||
| 17 | type Environment = Readonly<Record<string, string | undefined>>; | 20 | type Environment = Readonly<Record<string, string | undefined>>; |
| ... | @@ -78,6 +81,24 @@ export function loadRepoEnv({ | ... | @@ -78,6 +81,24 @@ export function loadRepoEnv({ |
| 78 | EXPO_PUBLIC_OTLP_TRACES_TOKEN: config.mobileOtlpTracesToken, | 81 | EXPO_PUBLIC_OTLP_TRACES_TOKEN: config.mobileOtlpTracesToken, |
| 79 | } | 82 | } |
| 80 | : {}), | 83 | : {}), |
| 84 | ...(config.relayClientOtlpTracesUrl | ||
| 85 | ? { | ||
| 86 | T3CODE_RELAY_CLIENT_OTLP_TRACES_URL: config.relayClientOtlpTracesUrl, | ||
| 87 | VITE_RELAY_OTLP_TRACES_URL: config.relayClientOtlpTracesUrl, | ||
| 88 | } | ||
| 89 | : {}), | ||
| 90 | ...(config.relayClientOtlpTracesDataset | ||
| 91 | ? { | ||
| 92 | T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET: config.relayClientOtlpTracesDataset, | ||
| 93 | VITE_RELAY_OTLP_TRACES_DATASET: config.relayClientOtlpTracesDataset, | ||
| 94 | } | ||
| 95 | : {}), | ||
| 96 | ...(config.relayClientOtlpTracesToken | ||
| 97 | ? { | ||
| 98 | T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN: config.relayClientOtlpTracesToken, | ||
| 99 | VITE_RELAY_OTLP_TRACES_TOKEN: config.relayClientOtlpTracesToken, | ||
| 100 | } | ||
| 101 | : {}), | ||
| 81 | }; | 102 | }; |
| 82 | } | 103 | } |
| 83 | 104 | ||
| ... | @@ -112,6 +133,21 @@ export function resolvePublicConfig(...sources: readonly Environment[]): T3CodeP | ... | @@ -112,6 +133,21 @@ export function resolvePublicConfig(...sources: readonly Environment[]): T3CodeP |
| 112 | "T3CODE_MOBILE_OTLP_TRACES_TOKEN", | 133 | "T3CODE_MOBILE_OTLP_TRACES_TOKEN", |
| 113 | "EXPO_PUBLIC_OTLP_TRACES_TOKEN", | 134 | "EXPO_PUBLIC_OTLP_TRACES_TOKEN", |
| 114 | ), | 135 | ), |
| 136 | relayClientOtlpTracesUrl: firstNonEmpty( | ||
| 137 | sources, | ||
| 138 | "T3CODE_RELAY_CLIENT_OTLP_TRACES_URL", | ||
| 139 | "VITE_RELAY_OTLP_TRACES_URL", | ||
| 140 | ), | ||
| 141 | relayClientOtlpTracesDataset: firstNonEmpty( | ||
| 142 | sources, | ||
| 143 | "T3CODE_RELAY_CLIENT_OTLP_TRACES_DATASET", | ||
| 144 | "VITE_RELAY_OTLP_TRACES_DATASET", | ||
| 145 | ), | ||
| 146 | relayClientOtlpTracesToken: firstNonEmpty( | ||
| 147 | sources, | ||
| 148 | "T3CODE_RELAY_CLIENT_OTLP_TRACES_TOKEN", | ||
| 149 | "VITE_RELAY_OTLP_TRACES_TOKEN", | ||
| 150 | ), | ||
| 115 | }; | 151 | }; |
| 116 | } | 152 | } |
| 117 | 153 |