Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 73 additions & 8 deletions apps/desktop/src/app/DesktopObservability.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, describe, it } from "@effect/vitest";
import * as Cause from "effect/Cause";
import * as Console from "effect/Console";
import * as Effect from "effect/Effect";
import * as Exit from "effect/Exit";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Schema from "effect/Schema";
Expand Down Expand Up @@ -36,40 +39,65 @@ const TraceRecordLine = Schema.Struct({

const decodeTraceRecordLine = Schema.decodeUnknownSync(Schema.fromJsonString(TraceRecordLine));

const environmentInput = (baseDir: string) =>
const consoleWithLog = (log: Console.Console["log"]): Console.Console =>
new Proxy(globalThis.console, {
get(target, property, receiver) {
if (property === "log") return log;
const value = Reflect.get(target, property, receiver) as unknown;
return typeof value === "function" ? value.bind(target) : value;
},
}) as Console.Console;

const environmentInput = (
baseDir: string,
options: { readonly development?: boolean; readonly platform?: NodeJS.Platform } = {},
) =>
({
dirname: "/repo/apps/desktop/dist-electron",
homeDirectory: baseDir,
platform: "darwin",
platform: options.platform ?? "darwin",
processArch: "arm64",
appVersion: "1.2.3",
appPath: "/repo",
isPackaged: false,
isPackaged: options.development === false,
resourcesPath: "/repo/resources",
runningUnderArm64Translation: false,
}) satisfies DesktopEnvironment.MakeDesktopEnvironmentInput;

const makeEnvironmentLayer = (baseDir: string) =>
DesktopEnvironment.layer(environmentInput(baseDir)).pipe(
const makeEnvironmentLayer = (
baseDir: string,
options: { readonly development?: boolean; readonly platform?: NodeJS.Platform } = {},
) =>
DesktopEnvironment.layer(environmentInput(baseDir, options)).pipe(
Layer.provide(
Layer.mergeAll(
NodeServices.layer,
DesktopConfig.layerTest({
T3CODE_HOME: baseDir,
VITE_DEV_SERVER_URL: "http://127.0.0.1:5733",
...(options.development === false
? {}
: { VITE_DEV_SERVER_URL: "http://127.0.0.1:5733" }),
}),
),
),
);

describe("DesktopObservability", () => {
it.effect("persists desktop Effect logs as span events in desktop.trace.ndjson", () =>
it.effect("keeps the packaged Windows console logger and tolerates a broken pipe", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-observability-test-",
});
const environmentLayer = makeEnvironmentLayer(baseDir);
const environmentLayer = makeEnvironmentLayer(baseDir, {
development: false,
platform: "win32",
});
let consoleLogCalls = 0;
const throwingConsole = consoleWithLog(() => {
consoleLogCalls += 1;
throw Object.assign(new Error("EPIPE: broken pipe, write"), { code: "EPIPE" });
});
const tracePath = yield* Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
return environment.path.join(environment.logDir, "desktop.trace.ndjson");
Expand All @@ -86,9 +114,12 @@ describe("DesktopObservability", () => {
}).pipe(
Effect.withSpan("desktop-observability-test"),
Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),
Effect.provideService(Console.Console, throwingConsole),
),
);

assert.equal(consoleLogCalls, 1);

const records = (yield* fileSystem.readFileString(tracePath))
.trim()
.split("\n")
Expand All @@ -112,6 +143,40 @@ describe("DesktopObservability", () => {
),
);

it.effect("does not hide unrelated packaged Windows console failures", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
prefix: "t3-desktop-observability-console-error-test-",
});
const environmentLayer = makeEnvironmentLayer(baseDir, {
development: false,
platform: "win32",
});
const unexpectedError = Object.assign(new Error("unexpected console failure"), {
code: "EINVAL",
});
const throwingConsole = consoleWithLog(() => {
throw unexpectedError;
});

const exit = yield* Effect.exit(
Effect.scoped(
Effect.logInfo("desktop console failure").pipe(
Effect.provide(DesktopObservability.layer.pipe(Layer.provideMerge(environmentLayer))),
Effect.provideService(Console.Console, throwingConsole),
),
),
);

assert(Exit.isFailure(exit));
assert.equal(Cause.squash(exit.cause), unexpectedError);
}).pipe(
Effect.scoped,
Effect.provide(Layer.mergeAll(NodeServices.layer, NodeHttpClient.layerUndici)),
),
);

it.effect("persists backend child output as structured JSON records in development", () =>
Effect.gen(function* () {
const fileSystem = yield* FileSystem.FileSystem;
Expand Down
37 changes: 34 additions & 3 deletions apps/desktop/src/app/DesktopObservability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,9 +438,40 @@ const backendOutputLogFactoryLayer = Layer.effect(
}),
);

const desktopLoggerLayer = Layer.mergeAll(
Logger.layer([Logger.consolePretty(), Logger.tracerLogger], { mergeWithExisting: false }),
Layer.succeed(References.MinimumLogLevel, "Info"),
const isBrokenPipeError = (error: unknown): boolean =>
typeof error === "object" && error !== null && "code" in error && error.code === "EPIPE";

const makeBrokenPipeSafeConsoleLogger = (): Logger.Logger<unknown, void> => {
const delegate = Logger.consolePretty();
let pipeBroken = false;

return Logger.make((options) => {
if (pipeBroken) return;

try {
delegate.log(options);
} catch (error) {
if (!isBrokenPipeError(error)) throw error;
pipeBroken = true;
}
});
};

const desktopLoggerLayer = Layer.unwrap(
Effect.gen(function* () {
const environment = yield* DesktopEnvironment.DesktopEnvironment;
// Packaged Windows launches can inherit a short-lived stdout pipe. Keep console logging active
// until that pipe closes, then disable only the unavailable sink while tracing continues.
const consoleLogger =
environment.platform === "win32" && !environment.isDevelopment
? makeBrokenPipeSafeConsoleLogger()
: Logger.consolePretty();

return Layer.mergeAll(
Logger.layer([consoleLogger, Logger.tracerLogger], { mergeWithExisting: false }),
Layer.succeed(References.MinimumLogLevel, "Info"),
);
}),
);

const tracerLayer = Layer.unwrap(
Expand Down
Loading