authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-06-20 11:50:46-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-20 11:50:46-07:00
log0cd0ed1a3dc3280cf0be6128b377abc095ba806f
tree659d3656059781ce8a89ab90c0b59298d5fa530e
parentd53237cbd14cbdc7fe6b1f1ec2f8aa224a3f7aff
signature Signed by PGP key B5690EEEBB952194

[codex] Sanitize client error log diagnostics (#3405)

Co-authored-by: codex <codex@users.noreply.github.com>

14 files changed, 260 insertions(+), 40 deletions(-)

apps/mobile/src/state/use-thread-composer-state.ts+7-1
...@@ -2,6 +2,7 @@ import { useAtomValue } from "@effect/atom-react";...@@ -2,6 +2,7 @@ import { useAtomValue } from "@effect/atom-react";
2import { useCallback, useEffect, useMemo } from "react";2import { useCallback, useEffect, useMemo } from "react";
33
4import { CommandId, MessageId, type EnvironmentId, type ThreadId } from "@t3tools/contracts";4import { CommandId, MessageId, type EnvironmentId, type ThreadId } from "@t3tools/contracts";
5import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors";
5import { deriveActiveWorkStartedAt } from "@t3tools/shared/orchestrationTiming";6import { deriveActiveWorkStartedAt } from "@t3tools/shared/orchestrationTiming";
67
7import { makeQueuedMessageMetadata } from "../lib/commandMetadata";8import { makeQueuedMessageMetadata } from "../lib/commandMetadata";
...@@ -231,7 +232,12 @@ export function useThreadComposerState() {...@@ -231,7 +232,12 @@ export function useThreadComposerState() {
231 appendComposerDraftAttachments(threadKey, images);232 appendComposerDraftAttachments(threadKey, images);
232 }233 }
233 } catch (error) {234 } catch (error) {
234 console.error("[native paste] error converting images", error);235 console.error("[native paste] error converting images", {
236 environmentId: selectedThreadShell.environmentId,
237 threadId: selectedThreadShell.id,
238 uriCount: uris.length,
239 ...safeErrorLogAttributes(error),
240 });
235 }241 }
236 },242 },
237 [composerDrafts, selectedThreadShell],243 [composerDrafts, selectedThreadShell],
apps/web/src/components/DiffPanel.tsx+11-1
...@@ -4,6 +4,7 @@ import {...@@ -4,6 +4,7 @@ import {
4 isAtomCommandInterrupted,4 isAtomCommandInterrupted,
5 squashAtomCommandFailure,5 squashAtomCommandFailure,
6} from "@t3tools/client-runtime/state/runtime";6} from "@t3tools/client-runtime/state/runtime";
7import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors";
7import type { ScopedThreadRef, TurnId } from "@t3tools/contracts";8import type { ScopedThreadRef, TurnId } from "@t3tools/contracts";
8import {9import {
9 ArrowRightIcon,10 ArrowRightIcon,
...@@ -452,7 +453,16 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff...@@ -452,7 +453,16 @@ export default function DiffPanel({ mode = "inline", composerDraftTarget }: Diff
452 void (async () => {453 void (async () => {
453 const result = await openInPreferredEditor(targetPath);454 const result = await openInPreferredEditor(targetPath);
454 if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {455 if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {
455 console.warn("Failed to open diff file in editor.", squashAtomCommandFailure(result));456 console.warn("Failed to open diff file in editor.", {
457 operation: "open-diff-file",
458 ...(routeThreadRef
459 ? {
460 environmentId: routeThreadRef.environmentId,
461 threadId: routeThreadRef.threadId,
462 }
463 : {}),
464 ...safeErrorLogAttributes(squashAtomCommandFailure(result)),
465 });
456 }466 }
457 })();467 })();
458 },468 },
apps/web/src/components/Sidebar.tsx+3-2
...@@ -55,6 +55,7 @@ import {...@@ -55,6 +55,7 @@ import {
55 scopeProjectRef,55 scopeProjectRef,
56 scopeThreadRef,56 scopeThreadRef,
57} from "@t3tools/client-runtime/environment";57} from "@t3tools/client-runtime/environment";
58import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors";
58import {59import {
59 isAtomCommandInterrupted,60 isAtomCommandInterrupted,
60 settlePromise,61 settlePromise,
...@@ -1523,7 +1524,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec...@@ -1523,7 +1524,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
1523 console.error("Failed to remove project", {1524 console.error("Failed to remove project", {
1524 projectId: member.id,1525 projectId: member.id,
1525 environmentId: member.environmentId,1526 environmentId: member.environmentId,
1526 error,1527 ...safeErrorLogAttributes(error),
1527 });1528 });
1528 toastManager.add(1529 toastManager.add(
1529 stackedThreadToast({1530 stackedThreadToast({
...@@ -1558,7 +1559,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec...@@ -1558,7 +1559,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
1558 console.error("Failed to remove project", {1559 console.error("Failed to remove project", {
1559 projectId: member.id,1560 projectId: member.id,
1560 environmentId: member.environmentId,1561 environmentId: member.environmentId,
1561 error,1562 ...safeErrorLogAttributes(error),
1562 });1563 });
1563 toastManager.add(1564 toastManager.add(
1564 stackedThreadToast({1565 stackedThreadToast({
apps/web/src/components/settings/SettingsPanels.tsx+6-1
...@@ -12,6 +12,7 @@ import {...@@ -12,6 +12,7 @@ import {
12 type ScopedThreadRef,12 type ScopedThreadRef,
13} from "@t3tools/contracts";13} from "@t3tools/contracts";
14import { scopeThreadRef } from "@t3tools/client-runtime/environment";14import { scopeThreadRef } from "@t3tools/client-runtime/environment";
15import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors";
15import {16import {
16 isAtomCommandInterrupted,17 isAtomCommandInterrupted,
17 settlePromise,18 settlePromise,
...@@ -1038,7 +1039,11 @@ export function ProviderSettingsPanel() {...@@ -1038,7 +1039,11 @@ export function ProviderSettingsPanel() {
1038 refreshingRef.current = false;1039 refreshingRef.current = false;
1039 setIsRefreshingProviders(false);1040 setIsRefreshingProviders(false);
1040 if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {1041 if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {
1041 console.warn("Failed to refresh providers", squashAtomCommandFailure(result));1042 console.warn("Failed to refresh providers", {
1043 operation: "refresh-providers",
1044 environmentId: primaryEnvironment.environmentId,
1045 ...safeErrorLogAttributes(squashAtomCommandFailure(result)),
1046 });
1042 }1047 }
1043 })();1048 })();
1044 }, [primaryEnvironment, refreshServerProviders]);1049 }, [primaryEnvironment, refreshServerProviders]);
apps/web/src/hooks/useSettings.ts+9-2
...@@ -23,6 +23,7 @@ import {...@@ -23,6 +23,7 @@ import {
23 DEFAULT_CLIENT_SETTINGS,23 DEFAULT_CLIENT_SETTINGS,
24 type UnifiedSettings,24 type UnifiedSettings,
25} from "@t3tools/contracts/settings";25} from "@t3tools/contracts/settings";
26import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors";
26import { ensureLocalApi } from "~/localApi";27import { ensureLocalApi } from "~/localApi";
27import * as Struct from "effect/Struct";28import * as Struct from "effect/Struct";
28import { primaryServerSettingsAtom, serverEnvironment } from "~/state/server";29import { primaryServerSettingsAtom, serverEnvironment } from "~/state/server";
...@@ -106,7 +107,10 @@ async function hydrateClientSettings(): Promise<void> {...@@ -106,7 +107,10 @@ async function hydrateClientSettings(): Promise<void> {
106 replaceClientSettingsSnapshot({ ...DEFAULT_CLIENT_SETTINGS, ...persistedSettings });107 replaceClientSettingsSnapshot({ ...DEFAULT_CLIENT_SETTINGS, ...persistedSettings });
107 }108 }
108 } catch (error) {109 } catch (error) {
109 console.error(`${CLIENT_SETTINGS_PERSISTENCE_ERROR_SCOPE} hydrate failed`, error);110 console.error(`${CLIENT_SETTINGS_PERSISTENCE_ERROR_SCOPE} hydrate failed`, {
111 operation: "hydrate",
112 ...safeErrorLogAttributes(error),
113 });
110 } finally {114 } finally {
111 if (hydrationGeneration === clientSettingsHydrationGeneration) {115 if (hydrationGeneration === clientSettingsHydrationGeneration) {
112 setClientSettingsHydrated(true);116 setClientSettingsHydrated(true);
...@@ -129,7 +133,10 @@ function persistClientSettings(settings: ClientSettings): void {...@@ -129,7 +133,10 @@ function persistClientSettings(settings: ClientSettings): void {
129 void ensureLocalApi()133 void ensureLocalApi()
130 .persistence.setClientSettings(settings)134 .persistence.setClientSettings(settings)
131 .catch((error) => {135 .catch((error) => {
132 console.error(`${CLIENT_SETTINGS_PERSISTENCE_ERROR_SCOPE} persist failed`, error);136 console.error(`${CLIENT_SETTINGS_PERSISTENCE_ERROR_SCOPE} persist failed`, {
137 operation: "persist",
138 ...safeErrorLogAttributes(error),
139 });
133 });140 });
134}141}
135142
apps/web/src/observability/clientTracing.ts+8-10
...@@ -7,6 +7,7 @@ import { HttpClient } from "effect/unstable/http";...@@ -7,6 +7,7 @@ import { HttpClient } from "effect/unstable/http";
7import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability";7import { OtlpSerialization, OtlpTracer } from "effect/unstable/observability";
88
9import { settleAsyncResult, squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";9import { settleAsyncResult, squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime";
10import { safeErrorLogAttributes } from "@t3tools/client-runtime/errors";
10import { resolvePrimaryEnvironmentHttpUrl } from "../environments/primary";11import { resolvePrimaryEnvironmentHttpUrl } from "../environments/primary";
11import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer";12import { primaryEnvironmentHttpLayer } from "../environments/primary/httpLayer";
12import { isElectron } from "../env";13import { isElectron } from "../env";
...@@ -95,9 +96,14 @@ async function applyClientTracingConfig(config: ClientTracingConfig): Promise<vo...@@ -95,9 +96,14 @@ async function applyClientTracingConfig(config: ClientTracingConfig): Promise<vo
95 await disposeTracerRuntime(runtime, scope);96 await disposeTracerRuntime(runtime, scope);
9697
97 if (generation === configurationGeneration) {98 if (generation === configurationGeneration) {
99 const error = squashAtomCommandFailure(delegateResult);
100 const tracesUrl = new URL(otlpTracesUrl);
98 console.warn("Failed to configure client tracing exporter", {101 console.warn("Failed to configure client tracing exporter", {
99 error: formatError(squashAtomCommandFailure(delegateResult)),102 scheme: tracesUrl.protocol.replace(/:$/, ""),
100 otlpTracesUrl,103 host: tracesUrl.hostname,
104 port: tracesUrl.port || undefined,
105 exportIntervalMs,
106 ...safeErrorLogAttributes(error),
101 });107 });
102 }108 }
103 return;109 return;
...@@ -125,14 +131,6 @@ async function disposeTracerRuntime(...@@ -125,14 +131,6 @@ async function disposeTracerRuntime(
125 runtime.dispose();131 runtime.dispose();
126}132}
127133
128function formatError(error: unknown): string {
129 if (error instanceof Error && error.message.trim().length > 0) {
130 return error.message;
131 }
132
133 return String(error);
134}
135
136export async function __resetClientTracingForTests() {134export async function __resetClientTracingForTests() {
137 configurationGeneration++;135 configurationGeneration++;
138 activeConfigKey = null;136 activeConfigKey = null;
packages/client-runtime/src/connection/supervisor.ts+4-1
...@@ -26,6 +26,7 @@ import {...@@ -26,6 +26,7 @@ import {
26 type SupervisorConnectionState,26 type SupervisorConnectionState,
27} from "./model.ts";27} from "./model.ts";
28import * as RpcSession from "../rpc/session.ts";28import * as RpcSession from "../rpc/session.ts";
29import { safeErrorLogAttributes } from "../errors/safeLog.ts";
29import * as ConnectionWakeups from "./wakeups.ts";30import * as ConnectionWakeups from "./wakeups.ts";
3031
31const RETRY_DELAYS_MS = [1_000, 2_000, 4_000, 8_000, 16_000] as const;32const RETRY_DELAYS_MS = [1_000, 2_000, 4_000, 8_000, 16_000] as const;
...@@ -503,11 +504,13 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* (...@@ -503,11 +504,13 @@ export const make = Effect.fn("EnvironmentSupervisor.make")(function* (
503 !establishment.exit.cause.reasons.some(Cause.isFailReason);504 !establishment.exit.cause.reasons.some(Cause.isFailReason);
504 const outcome = failureFromExit(target, establishment.exit, false, false);505 const outcome = failureFromExit(target, establishment.exit, false, false);
505 if (isUnexpectedDefect) {506 if (isUnexpectedDefect) {
507 const defect = establishment.exit.cause.reasons.find(Cause.isDieReason)?.defect;
506 yield* Effect.logError("Connection attempt failed with an unexpected defect.").pipe(508 yield* Effect.logError("Connection attempt failed with an unexpected defect.").pipe(
507 Effect.annotateLogs({509 Effect.annotateLogs({
508 "environment.id": target.environmentId,510 "environment.id": target.environmentId,
509 "environment.label": target.label,511 "environment.label": target.label,
510 cause: Cause.pretty(establishment.exit.cause),512 "cause.reason_count": establishment.exit.cause.reasons.length,
513 ...safeErrorLogAttributes(defect),
511 }),514 }),
512 );515 );
513 }516 }
packages/client-runtime/src/errors/index.ts+1
...@@ -1,2 +1,3 @@...@@ -1,2 +1,3 @@
1export * from "./errorTrace.ts";1export * from "./errorTrace.ts";
2export * from "./safeLog.ts";
2export * from "./transport.ts";3export * from "./transport.ts";
packages/client-runtime/src/errors/safeLog.test.ts created+55
...@@ -0,0 +1,55 @@
1import { describe, expect, it } from "vite-plus/test";
2
3import { safeErrorLogAttributes } from "./safeLog.ts";
4
5describe("safeErrorLogAttributes", () => {
6 it("keeps correlation and stack frames without serializing messages or nested causes", () => {
7 const cause = Object.assign(new Error("nested-cause-secret-sentinel"), {
8 traceId: "trace-safe-123",
9 });
10 const error = Object.assign(new Error("outer-error-secret-sentinel", { cause }), {
11 _tag: "ProjectRemovalError",
12 });
13 error.stack = [
14 "ProjectRemovalError: outer-error-secret-sentinel",
15 " at removeProject (https://user:password@example.com/project.ts?token=secret#fragment)",
16 ].join("\n");
17
18 const attributes = safeErrorLogAttributes(error);
19
20 expect(attributes).toMatchObject({
21 errorType: "error",
22 errorName: "Error",
23 errorTag: "ProjectRemovalError",
24 traceId: "trace-safe-123",
25 stack: " at removeProject (https://example.com/project.ts)",
26 });
27 const diagnosticText = Object.values(attributes).map(String).join("\n");
28 expect(diagnosticText).not.toContain("outer-error-secret-sentinel");
29 expect(diagnosticText).not.toContain("nested-cause-secret-sentinel");
30 expect(diagnosticText).not.toContain("user:password");
31 expect(diagnosticText).not.toContain("token=secret");
32 });
33
34 it("does not trust arbitrary object messages or tags", () => {
35 const attributes = safeErrorLogAttributes({
36 _tag: "payload-secret-sentinel",
37 message: "message-secret-sentinel",
38 cause: { traceId: "trace id with unsafe whitespace" },
39 });
40
41 expect(attributes).toEqual({ errorType: "object" });
42 });
43
44 it("skips an unsafe outer trace id when a nested safe trace id is available", () => {
45 const attributes = safeErrorLogAttributes({
46 traceId: "unsafe trace id",
47 cause: { traceId: "trace-safe-inner" },
48 });
49
50 expect(attributes).toEqual({
51 errorType: "object",
52 traceId: "trace-safe-inner",
53 });
54 });
55});
packages/client-runtime/src/errors/safeLog.ts created+107
...@@ -0,0 +1,107 @@
1const SAFE_ERROR_LABEL =
2 /^(?:Error|EvalError|RangeError|ReferenceError|SyntaxError|TypeError|URIError|AggregateError|DOMException|[A-Za-z][A-Za-z0-9]*(?:Error|Failure))$/;
3const SAFE_TRACE_ID = /^[A-Za-z0-9._:-]{1,128}$/;
4const STACK_FRAME_LIMIT = 32;
5
6export interface SafeErrorLogAttributes {
7 readonly errorType: "error" | "array" | "null" | "object" | "primitive";
8 readonly errorName?: string;
9 readonly errorTag?: string;
10 readonly traceId?: string;
11 readonly stack?: string;
12}
13
14function readSafeLabel(value: unknown): string | undefined {
15 return typeof value === "string" && SAFE_ERROR_LABEL.test(value) ? value : undefined;
16}
17
18function sanitizeStackUrl(value: string): string {
19 try {
20 const url = new URL(value);
21 url.username = "";
22 url.password = "";
23 url.search = "";
24 url.hash = "";
25 return url.toString();
26 } catch {
27 return value;
28 }
29}
30
31function sanitizeStackFrame(frame: string): string {
32 return frame.replace(/(?:https?|file):\/\/[^\s)]+/g, sanitizeStackUrl);
33}
34
35function readSafeStack(error: Error): string | undefined {
36 try {
37 const frames = error.stack
38 ?.split(/\r?\n/)
39 .filter((line) => /^\s*at\s+/.test(line) || /^[^@\s]+@(?:https?|file):\/\//.test(line))
40 .slice(0, STACK_FRAME_LIMIT)
41 .map(sanitizeStackFrame);
42 return frames && frames.length > 0 ? frames.join("\n") : undefined;
43 } catch {
44 return undefined;
45 }
46}
47
48function readErrorTag(error: unknown): string | undefined {
49 try {
50 if (typeof error !== "object" || error === null) {
51 return undefined;
52 }
53 return readSafeLabel((error as { readonly _tag?: unknown })._tag);
54 } catch {
55 return undefined;
56 }
57}
58
59function readTraceId(error: unknown): string | undefined {
60 try {
61 const seen = new Set<object>();
62 let current: unknown = error;
63
64 while (typeof current === "object" && current !== null && !seen.has(current)) {
65 seen.add(current);
66 const record = current as { readonly cause?: unknown; readonly traceId?: unknown };
67 if (typeof record.traceId === "string" && SAFE_TRACE_ID.test(record.traceId)) {
68 return record.traceId;
69 }
70 current = record.cause;
71 }
72
73 return undefined;
74 } catch {
75 return undefined;
76 }
77}
78
79export function safeErrorLogAttributes(error: unknown): SafeErrorLogAttributes {
80 const errorTag = readErrorTag(error);
81 const traceId = readTraceId(error);
82
83 if (error instanceof Error) {
84 const errorName = readSafeLabel(error.name);
85 const stack = readSafeStack(error);
86 return {
87 errorType: "error",
88 ...(errorName !== undefined ? { errorName } : {}),
89 ...(errorTag !== undefined ? { errorTag } : {}),
90 ...(traceId !== undefined ? { traceId } : {}),
91 ...(stack !== undefined ? { stack } : {}),
92 };
93 }
94
95 return {
96 errorType:
97 error === null
98 ? "null"
99 : Array.isArray(error)
100 ? "array"
101 : typeof error === "object"
102 ? "object"
103 : "primitive",
104 ...(errorTag !== undefined ? { errorTag } : {}),
105 ...(traceId !== undefined ? { traceId } : {}),
106 };
107}
packages/client-runtime/src/state/archivedThreads.test.ts+26-1
...@@ -1,7 +1,10 @@...@@ -1,7 +1,10 @@
1import { EnvironmentId } from "@t3tools/contracts";1import { EnvironmentId, type OrchestrationShellSnapshot } from "@t3tools/contracts";
2import * as Cause from "effect/Cause";
3import { AsyncResult, Atom, AtomRegistry } from "effect/unstable/reactivity";
2import { expect, it } from "vite-plus/test";4import { expect, it } from "vite-plus/test";
35
4import {6import {
7 createArchivedThreadSnapshotsAtomFamily,
5 makeArchivedThreadsEnvironmentKey,8 makeArchivedThreadsEnvironmentKey,
6 parseArchivedThreadsEnvironmentKey,9 parseArchivedThreadsEnvironmentKey,
7} from "./archivedThreads.ts";10} from "./archivedThreads.ts";
...@@ -13,3 +16,25 @@ it("round-trips environment keys in sorted order", () => {...@@ -13,3 +16,25 @@ it("round-trips environment keys in sorted order", () => {
1316
14 expect(parseArchivedThreadsEnvironmentKey(key)).toEqual([envA, envB]);17 expect(parseArchivedThreadsEnvironmentKey(key)).toEqual([envA, envB]);
15});18});
19
20it("does not expose an archived snapshot failure message", () => {
21 const environmentId = EnvironmentId.make("env-sensitive");
22 const snapshotsAtom = createArchivedThreadSnapshotsAtomFamily<Error>({
23 getSnapshotAtom: () =>
24 Atom.make(
25 AsyncResult.failure<OrchestrationShellSnapshot, Error>(
26 Cause.fail(new Error("credential=secret-value")),
27 ),
28 ),
29 labelPrefix: "test:archived-thread-snapshots",
30 });
31 const registry = AtomRegistry.make();
32
33 expect(registry.get(snapshotsAtom(makeArchivedThreadsEnvironmentKey([environmentId])))).toEqual({
34 snapshots: [],
35 error: "Failed to load archived threads.",
36 isLoading: false,
37 });
38
39 registry.dispose();
40});
packages/client-runtime/src/state/archivedThreads.ts+1-6
...@@ -1,6 +1,5 @@...@@ -1,6 +1,5 @@
1import { EnvironmentId, type OrchestrationShellSnapshot } from "@t3tools/contracts";1import { EnvironmentId, type OrchestrationShellSnapshot } from "@t3tools/contracts";
2import * as Arr from "effect/Array";2import * as Arr from "effect/Array";
3import * as Cause from "effect/Cause";
4import { pipe } from "effect/Function";3import { pipe } from "effect/Function";
5import * as Option from "effect/Option";4import * as Option from "effect/Option";
6import * as Order from "effect/Order";5import * as Order from "effect/Order";
...@@ -60,11 +59,7 @@ export function createArchivedThreadSnapshotsAtomFamily<E>(options: {...@@ -60,11 +59,7 @@ export function createArchivedThreadSnapshotsAtomFamily<E>(options: {
60 }59 }
6160
62 if (error === null && result._tag === "Failure") {61 if (error === null && result._tag === "Failure") {
63 const cause = Cause.squash(result.cause);62 error = "Failed to load archived threads.";
64 error =
65 cause instanceof Error && cause.message.trim().length > 0
66 ? cause.message
67 : "Failed to load archived threads.";
68 }63 }
69 }64 }
7065
packages/client-runtime/src/state/session.ts+5-3
...@@ -8,6 +8,7 @@ import { AsyncResult, Atom } from "effect/unstable/reactivity";...@@ -8,6 +8,7 @@ import { AsyncResult, Atom } from "effect/unstable/reactivity";
8import { EnvironmentRegistry } from "../connection/registry.ts";8import { EnvironmentRegistry } from "../connection/registry.ts";
9import type { PreparedConnection } from "../connection/model.ts";9import type { PreparedConnection } from "../connection/model.ts";
10import { EnvironmentSupervisor } from "../connection/supervisor.ts";10import { EnvironmentSupervisor } from "../connection/supervisor.ts";
11import { safeErrorLogAttributes } from "../errors/safeLog.ts";
11import { followStreamInEnvironment } from "./runtime.ts";12import { followStreamInEnvironment } from "./runtime.ts";
1213
13export function initialConfigOption<E>(14export function initialConfigOption<E>(
...@@ -16,9 +17,10 @@ export function initialConfigOption<E>(...@@ -16,9 +17,10 @@ export function initialConfigOption<E>(
16 return initialConfig.pipe(17 return initialConfig.pipe(
17 Effect.map(Option.some),18 Effect.map(Option.some),
18 Effect.catch((error) =>19 Effect.catch((error) =>
19 Effect.logWarning("Could not load the initial environment configuration.", {20 Effect.logWarning("Could not load the initial environment configuration.").pipe(
20 error,21 Effect.annotateLogs({ ...safeErrorLogAttributes(error) }),
21 }).pipe(Effect.as(Option.none<ServerConfig>())),22 Effect.as(Option.none<ServerConfig>()),
23 ),
22 ),24 ),
23 );25 );
24}26}
packages/client-runtime/src/state/shell.ts+17-12
...@@ -16,6 +16,7 @@ import { AsyncResult, Atom } from "effect/unstable/reactivity";...@@ -16,6 +16,7 @@ import { AsyncResult, Atom } from "effect/unstable/reactivity";
16import { EnvironmentRegistry } from "../connection/registry.ts";16import { EnvironmentRegistry } from "../connection/registry.ts";
17import { connectionProjectionPhase } from "../connection/model.ts";17import { connectionProjectionPhase } from "../connection/model.ts";
18import { EnvironmentSupervisor } from "../connection/supervisor.ts";18import { EnvironmentSupervisor } from "../connection/supervisor.ts";
19import { safeErrorLogAttributes } from "../errors/safeLog.ts";
19import { EnvironmentCacheStore } from "../platform/persistence.ts";20import { EnvironmentCacheStore } from "../platform/persistence.ts";
20import { subscribe } from "../rpc/client.ts";21import { subscribe } from "../rpc/client.ts";
21import { applyShellStreamEvent } from "./shellReducer.ts";22import { applyShellStreamEvent } from "./shellReducer.ts";
...@@ -42,11 +43,7 @@ function shellStatusForSnapshot(...@@ -42,11 +43,7 @@ function shellStatusForSnapshot(
42 return Option.isSome(snapshot) ? "cached" : "empty";43 return Option.isSome(snapshot) ? "cached" : "empty";
43}44}
4445
45function formatShellError(error: unknown): string {46const SHELL_SYNCHRONIZATION_ERROR_MESSAGE = "Could not synchronize environment data.";
46 return error instanceof Error && error.message.trim().length > 0
47 ? error.message
48 : "Could not synchronize environment data.";
49}
5047
51export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make")(function* () {48export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make")(function* () {
52 const supervisor = yield* EnvironmentSupervisor;49 const supervisor = yield* EnvironmentSupervisor;
...@@ -57,7 +54,7 @@ export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make")...@@ -57,7 +54,7 @@ export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make")
57 Effect.logWarning("Could not load cached environment shell.").pipe(54 Effect.logWarning("Could not load cached environment shell.").pipe(
58 Effect.annotateLogs({55 Effect.annotateLogs({
59 environmentId,56 environmentId,
60 error: error.message,57 ...safeErrorLogAttributes(error),
61 }),58 }),
62 Effect.as(Option.none<OrchestrationShellSnapshot>()),59 Effect.as(Option.none<OrchestrationShellSnapshot>()),
63 ),60 ),
...@@ -78,7 +75,7 @@ export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make")...@@ -78,7 +75,7 @@ export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make")
78 Effect.logWarning("Could not persist environment shell cache.").pipe(75 Effect.logWarning("Could not persist environment shell cache.").pipe(
79 Effect.annotateLogs({76 Effect.annotateLogs({
80 environmentId,77 environmentId,
81 error: error.message,78 ...safeErrorLogAttributes(error),
82 }),79 }),
83 ),80 ),
84 ),81 ),
...@@ -110,11 +107,19 @@ export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make")...@@ -110,11 +107,19 @@ export const makeEnvironmentShellState = Effect.fn("EnvironmentShellState.make")
110 },107 },
111 );108 );
112 const setStreamError = (error: unknown) =>109 const setStreamError = (error: unknown) =>
113 SubscriptionRef.update(state, (current) => ({110 Effect.logWarning("Could not synchronize the environment shell.").pipe(
114 ...current,111 Effect.annotateLogs({
115 status: shellStatusForSnapshot(current.snapshot),112 environmentId,
116 error: Option.some(formatShellError(error)),113 ...safeErrorLogAttributes(error),
117 }));114 }),
115 Effect.andThen(
116 SubscriptionRef.update(state, (current) => ({
117 ...current,
118 status: shellStatusForSnapshot(current.snapshot),
119 error: Option.some(SHELL_SYNCHRONIZATION_ERROR_MESSAGE),
120 })),
121 ),
122 );
118123
119 const applyItem = Effect.fn("EnvironmentShellState.applyItem")(function* (124 const applyItem = Effect.fn("EnvironmentShellState.applyItem")(function* (
120 item: OrchestrationShellStreamItem,125 item: OrchestrationShellStreamItem,