Skip to content

Commit 0e4a435

Browse files
[codex] Extract infrastructure, telemetry, and test tooling (#2994)
Co-authored-by: codex <codex@users.noreply.github.com>
1 parent 3ea6adf commit 0e4a435

34 files changed

Lines changed: 810 additions & 113 deletions

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,8 @@
1010

1111
# Get this from your relay deployment. `infra/relay` deploys update it automatically.
1212
# T3CODE_RELAY_URL=https://relay.example.com
13+
14+
# Public, ingest-only mobile OpenTelemetry configuration.
15+
# T3CODE_MOBILE_OTLP_TRACES_URL=https://api.axiom.co/v1/traces
16+
# T3CODE_MOBILE_OTLP_TRACES_DATASET=t3-code-mobile-traces-dev
17+
# T3CODE_MOBILE_OTLP_TRACES_TOKEN=xaat-...

apps/mobile/app.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@ const config: ExpoConfig = {
161161
publishableKey: repoEnv.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY ?? null,
162162
jwtTemplate: repoEnv.EXPO_PUBLIC_CLERK_JWT_TEMPLATE ?? null,
163163
},
164+
observability: {
165+
tracesUrl: repoEnv.EXPO_PUBLIC_OTLP_TRACES_URL ?? "https://api.axiom.co/v1/traces",
166+
tracesDataset: repoEnv.EXPO_PUBLIC_OTLP_TRACES_DATASET ?? null,
167+
tracesToken: repoEnv.EXPO_PUBLIC_OTLP_TRACES_TOKEN ?? null,
168+
},
164169
eas: {
165170
projectId: "d763fcb8-d37c-41ea-a773-b54a0ab4a454",
166171
},

apps/mobile/src/features/agent-awareness/remoteRegistration.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export function normalizeAgentAwarenessRelayBaseUrl(
4141
}
4242

4343
function readRelayConfig(): { readonly url: string } | null {
44-
const relayUrl = resolveCloudPublicConfig().relayUrl;
44+
const relayUrl = resolveCloudPublicConfig().relay.url;
4545
if (!relayUrl) {
4646
logRegistrationDebug("relay registration skipped; relay config missing");
4747
return null;

apps/mobile/src/features/cloud/CloudAuthProvider.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ function CloudAuthBridge(props: { readonly children: ReactNode }) {
6666
}
6767

6868
export function CloudAuthProvider(props: { readonly children: ReactNode }) {
69-
const { clerkPublishableKey: publishableKey, relayUrl } = resolveCloudPublicConfig();
69+
const config = resolveCloudPublicConfig();
70+
const publishableKey = config.clerk.publishableKey;
71+
const relayUrl = config.relay.url;
7072

7173
useEffect(() => {
7274
if (!publishableKey || !relayUrl) {

apps/mobile/src/features/cloud/linkEnvironment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export function normalizeRelayBaseUrl(value: string | null | undefined): string
5050
}
5151

5252
function readRelayUrl(): string | null {
53-
return resolveCloudPublicConfig().relayUrl;
53+
return resolveCloudPublicConfig().relay.url;
5454
}
5555

5656
export class CloudEnvironmentLinkError extends Data.TaggedError("CloudEnvironmentLinkError")<{

apps/mobile/src/features/cloud/managedRelayState.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,20 @@
11
import { useAtomValue } from "@effect/atom-react";
22
import {
33
createManagedRelayQueryManager,
4-
ManagedRelayClient,
54
managedRelaySessionAtom,
65
readManagedRelaySnapshotState,
76
} from "@t3tools/client-runtime";
87
import type {
98
RelayClientEnvironmentRecord,
109
RelayEnvironmentStatusResponse,
1110
} from "@t3tools/contracts/relay";
12-
import * as Context from "effect/Context";
13-
import * as Effect from "effect/Effect";
14-
import * as Layer from "effect/Layer";
1511
import { AsyncResult, Atom } from "effect/unstable/reactivity";
1612
import { useCallback } from "react";
1713

18-
import { mobileRuntime } from "../../lib/runtime";
14+
import { mobileRuntimeContextLayer } from "../../lib/runtime";
1915
import { appAtomRegistry } from "../../state/atom-registry";
2016

21-
const managedRelayAtomRuntime = Atom.runtime(
22-
Layer.effect(
23-
ManagedRelayClient,
24-
mobileRuntime.contextEffect.pipe(
25-
Effect.map((context) => Context.get(context, ManagedRelayClient)),
26-
),
27-
),
28-
);
17+
const managedRelayAtomRuntime = Atom.runtime(mobileRuntimeContextLayer);
2918

3019
export const managedRelayQueryManager = createManagedRelayQueryManager(managedRelayAtomRuntime);
3120

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it, vi } from "vite-plus/test";
22

3-
import { resolveCloudPublicConfig } from "./publicConfig";
3+
import { hasMobileTracingPublicConfig, resolveCloudPublicConfig } from "./publicConfig";
44

55
vi.mock("expo-constants", () => ({
66
default: {
@@ -13,9 +13,18 @@ vi.mock("expo-constants", () => ({
1313
describe("resolveCloudPublicConfig", () => {
1414
it("returns no cloud configuration for an unconfigured build", () => {
1515
expect(resolveCloudPublicConfig({})).toEqual({
16-
clerkPublishableKey: null,
17-
clerkJwtTemplate: null,
18-
relayUrl: null,
16+
clerk: {
17+
publishableKey: null,
18+
jwtTemplate: null,
19+
},
20+
relay: {
21+
url: null,
22+
},
23+
observability: {
24+
tracesUrl: null,
25+
tracesDataset: null,
26+
tracesToken: null,
27+
},
1928
});
2029
});
2130

@@ -24,11 +33,25 @@ describe("resolveCloudPublicConfig", () => {
2433
resolveCloudPublicConfig({
2534
clerk: { publishableKey: " pk_test_example ", jwtTemplate: " t3-relay " },
2635
relay: { url: " https://relay.example.test/// " },
36+
observability: {
37+
tracesUrl: " https://api.axiom.co/v1/traces ",
38+
tracesDataset: " mobile-traces ",
39+
tracesToken: " public-ingest-token ",
40+
},
2741
}),
2842
).toEqual({
29-
clerkPublishableKey: "pk_test_example",
30-
clerkJwtTemplate: "t3-relay",
31-
relayUrl: "https://relay.example.test",
43+
clerk: {
44+
publishableKey: "pk_test_example",
45+
jwtTemplate: "t3-relay",
46+
},
47+
relay: {
48+
url: "https://relay.example.test",
49+
},
50+
observability: {
51+
tracesUrl: "https://api.axiom.co/v1/traces",
52+
tracesDataset: "mobile-traces",
53+
tracesToken: "public-ingest-token",
54+
},
3255
});
3356
});
3457

@@ -39,9 +62,59 @@ describe("resolveCloudPublicConfig", () => {
3962
relay: { url: "http://relay.example.test" },
4063
}),
4164
).toEqual({
42-
clerkPublishableKey: "pk_test_example",
43-
clerkJwtTemplate: "t3-relay",
44-
relayUrl: null,
65+
clerk: {
66+
publishableKey: "pk_test_example",
67+
jwtTemplate: "t3-relay",
68+
},
69+
relay: {
70+
url: null,
71+
},
72+
observability: {
73+
tracesUrl: null,
74+
tracesDataset: null,
75+
tracesToken: null,
76+
},
4577
});
4678
});
79+
80+
it("rejects an insecure traces URL", () => {
81+
expect(
82+
resolveCloudPublicConfig({
83+
observability: {
84+
tracesUrl: "http://api.axiom.co/v1/traces",
85+
tracesDataset: "mobile-traces",
86+
tracesToken: "public-ingest-token",
87+
},
88+
}).observability,
89+
).toEqual({
90+
tracesUrl: null,
91+
tracesDataset: "mobile-traces",
92+
tracesToken: "public-ingest-token",
93+
});
94+
});
95+
96+
it("keeps tracing disabled unless every public tracing value is configured", () => {
97+
expect(hasMobileTracingPublicConfig(resolveCloudPublicConfig({}))).toBe(false);
98+
expect(
99+
hasMobileTracingPublicConfig(
100+
resolveCloudPublicConfig({
101+
observability: {
102+
tracesUrl: "https://api.axiom.co/v1/traces",
103+
tracesDataset: "mobile-traces",
104+
},
105+
}),
106+
),
107+
).toBe(false);
108+
expect(
109+
hasMobileTracingPublicConfig(
110+
resolveCloudPublicConfig({
111+
observability: {
112+
tracesUrl: "https://api.axiom.co/v1/traces",
113+
tracesDataset: "mobile-traces",
114+
tracesToken: "public-ingest-token",
115+
},
116+
}),
117+
),
118+
).toBe(true);
119+
});
47120
});

apps/mobile/src/features/cloud/publicConfig.ts

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,92 @@ import Constants from "expo-constants";
22
import { relayClerkTokenOptions } from "@t3tools/shared/relayAuth";
33
import { normalizeSecureRelayUrl } from "@t3tools/shared/relayUrl";
44

5-
type ExpoExtra = Readonly<Record<string, unknown>> | undefined;
6-
75
export interface CloudPublicConfig {
8-
readonly clerkPublishableKey: string | null;
9-
readonly clerkJwtTemplate: string | null;
10-
readonly relayUrl: string | null;
6+
readonly clerk: {
7+
readonly publishableKey: string | null;
8+
readonly jwtTemplate: string | null;
9+
};
10+
readonly relay: {
11+
readonly url: string | null;
12+
};
13+
readonly observability: {
14+
readonly tracesUrl: string | null;
15+
readonly tracesDataset: string | null;
16+
readonly tracesToken: string | null;
17+
};
1118
}
1219

20+
type UntrustedSection<T> = {
21+
readonly [Key in keyof T]?: unknown;
22+
};
23+
24+
type ExpoExtra =
25+
| {
26+
readonly [Section in keyof CloudPublicConfig]?: UntrustedSection<CloudPublicConfig[Section]>;
27+
}
28+
| undefined;
29+
1330
function trimNonEmpty(value: unknown): string | null {
1431
return typeof value === "string" && value.trim() ? value.trim() : null;
1532
}
1633

17-
export function resolveCloudPublicConfig(extra: ExpoExtra = Constants.expoConfig?.extra) {
18-
const clerk = extra?.clerk as
19-
| { readonly publishableKey?: unknown; readonly jwtTemplate?: unknown }
20-
| undefined;
21-
const relay = extra?.relay as { readonly url?: unknown } | undefined;
34+
function normalizeSecureUrl(value: unknown): string | null {
35+
const raw = trimNonEmpty(value);
36+
if (raw === null) {
37+
return null;
38+
}
39+
try {
40+
const url = new URL(raw);
41+
return url.protocol === "https:" ? url.toString() : null;
42+
} catch {
43+
return null;
44+
}
45+
}
2246

47+
export function resolveCloudPublicConfig(extra: ExpoExtra = Constants.expoConfig?.extra) {
2348
return {
24-
clerkPublishableKey: trimNonEmpty(clerk?.publishableKey),
25-
clerkJwtTemplate: trimNonEmpty(clerk?.jwtTemplate),
26-
relayUrl: normalizeSecureRelayUrl(trimNonEmpty(relay?.url) ?? ""),
49+
clerk: {
50+
publishableKey: trimNonEmpty(extra?.clerk?.publishableKey),
51+
jwtTemplate: trimNonEmpty(extra?.clerk?.jwtTemplate),
52+
},
53+
relay: {
54+
url: normalizeSecureRelayUrl(trimNonEmpty(extra?.relay?.url) ?? ""),
55+
},
56+
observability: {
57+
tracesUrl: normalizeSecureUrl(extra?.observability?.tracesUrl),
58+
tracesDataset: trimNonEmpty(extra?.observability?.tracesDataset),
59+
tracesToken: trimNonEmpty(extra?.observability?.tracesToken),
60+
},
2761
} satisfies CloudPublicConfig;
2862
}
2963

3064
export function hasCloudPublicConfig(): boolean {
3165
const config = resolveCloudPublicConfig();
32-
return Boolean(config.clerkPublishableKey && config.clerkJwtTemplate && config.relayUrl);
66+
return Boolean(config.clerk.publishableKey && config.clerk.jwtTemplate && config.relay.url);
67+
}
68+
69+
type Configured<T> = {
70+
readonly [Key in keyof T]: NonNullable<T[Key]>;
71+
};
72+
73+
type MobileTracingPublicConfig = Omit<CloudPublicConfig, "observability"> & {
74+
readonly observability: Configured<CloudPublicConfig["observability"]>;
75+
};
76+
77+
export function hasMobileTracingPublicConfig(
78+
config: CloudPublicConfig = resolveCloudPublicConfig(),
79+
): config is MobileTracingPublicConfig {
80+
return Boolean(
81+
config.observability.tracesUrl &&
82+
config.observability.tracesDataset &&
83+
config.observability.tracesToken,
84+
);
3385
}
3486

3587
export function resolveRelayClerkTokenOptions() {
36-
const { clerkJwtTemplate } = resolveCloudPublicConfig();
37-
if (!clerkJwtTemplate) {
88+
const { jwtTemplate } = resolveCloudPublicConfig().clerk;
89+
if (!jwtTemplate) {
3890
throw new Error("T3CODE_CLERK_JWT_TEMPLATE is not configured.");
3991
}
40-
return relayClerkTokenOptions(clerkJwtTemplate);
92+
return relayClerkTokenOptions(jwtTemplate);
4193
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { expect, it } from "@effect/vitest";
2+
import * as Effect from "effect/Effect";
3+
import * as Layer from "effect/Layer";
4+
import { vi } from "vite-plus/test";
5+
6+
import { remoteHttpClientLayer } from "@t3tools/client-runtime";
7+
8+
import { makeMobileTracingLayer } from "./mobileTracing";
9+
10+
vi.mock("expo-constants", () => ({
11+
default: {
12+
expoConfig: {
13+
extra: {},
14+
},
15+
},
16+
}));
17+
18+
it.effect("exports spans through the scoped mobile OTLP layer", () => {
19+
const fetchFn = vi.fn<typeof fetch>(async () => new Response(null, { status: 202 }));
20+
const tracingLayer = makeMobileTracingLayer(
21+
{
22+
tracesUrl: "https://api.axiom.test/v1/traces",
23+
tracesDataset: "mobile-traces",
24+
tracesToken: "public-ingest-token",
25+
},
26+
{
27+
appVariant: "test",
28+
serviceVersion: "1.2.3",
29+
},
30+
).pipe(Layer.provide(remoteHttpClientLayer(fetchFn)));
31+
const tracedApplication = Layer.effectDiscard(
32+
Effect.void.pipe(Effect.withSpan("mobile.test.span")),
33+
).pipe(Layer.provide(tracingLayer));
34+
35+
return Effect.gen(function* () {
36+
yield* Layer.build(tracedApplication);
37+
38+
expect(fetchFn).not.toHaveBeenCalled();
39+
}).pipe(
40+
Effect.scoped,
41+
Effect.andThen(
42+
Effect.sync(() => {
43+
expect(fetchFn).toHaveBeenCalledOnce();
44+
const [url, init] = fetchFn.mock.calls[0]!;
45+
expect(String(url)).toBe("https://api.axiom.test/v1/traces");
46+
expect(new Headers(init?.headers).get("authorization")).toBe("Bearer public-ingest-token");
47+
expect(new Headers(init?.headers).get("x-axiom-dataset")).toBe("mobile-traces");
48+
expect(new TextDecoder().decode(init?.body as Uint8Array)).toContain("mobile.test.span");
49+
}),
50+
),
51+
);
52+
});

0 commit comments

Comments
 (0)