Skip to content

Commit cc577a7

Browse files
committed
fix(server): Make telemetry configurable and opt-in
Default telemetry off, honor explicit env opt-out, seed opt-in from T3CODE_TELEMETRY_ENABLED when unset, add settings/UI controls, and harden flush against mid-batch opt-out and settings read failures.
1 parent 82a9bcc commit cc577a7

13 files changed

Lines changed: 1074 additions & 33 deletions

File tree

apps/server/src/bin.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,13 @@ import * as WorkspacePaths from "./workspace/WorkspacePaths.ts";
3434
import * as ServerSecretStore from "./auth/ServerSecretStore.ts";
3535
import * as EnvironmentAuth from "./auth/EnvironmentAuth.ts";
3636
import { environmentAuthenticatedAuthLayer } from "./auth/http.ts";
37+
import * as ServerSettingsModule from "./serverSettings.ts";
3738

38-
const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer);
39+
const CliRuntimeLayer = Layer.mergeAll(
40+
NodeServices.layer,
41+
NetService.layer,
42+
ServerSettingsModule.ServerSettingsService.layerTest(),
43+
);
3944
class ProjectCliHttpApi extends HttpApi.make("environment").add(EnvironmentOrchestrationHttpApi) {}
4045

4146
const connectCli = makeCli({ cloudEnabled: true });

apps/server/src/bin.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,13 @@ import { hasCloudPublicConfig } from "./cloud/publicConfig.ts";
1313
import { sharedServerCommandFlags } from "./cli/config.ts";
1414
import { projectCommand } from "./cli/project.ts";
1515
import { runServerCommand, serveCommand, startCommand } from "./cli/server.ts";
16+
import * as ServerSettingsModule from "./serverSettings.ts";
1617

17-
const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer);
18+
const CliRuntimeLayer = Layer.mergeAll(
19+
NodeServices.layer,
20+
NetService.layer,
21+
ServerSettingsModule.ServerSettingsService.layerTest(),
22+
);
1823

1924
const connectPublicConfigMissingMessage =
2025
"T3 Connect commands are unavailable: this build is missing T3 Connect public configuration.";

apps/server/src/serverSettings.test.ts

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import * as Effect from "effect/Effect";
1212
import * as Duration from "effect/Duration";
1313
import * as FileSystem from "effect/FileSystem";
1414
import * as Layer from "effect/Layer";
15+
import * as Path from "effect/Path";
1516
import * as PlatformError from "effect/PlatformError";
1617
import * as Schema from "effect/Schema";
1718
import * as ServerSecretStore from "./auth/ServerSecretStore.ts";
@@ -486,6 +487,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
486487
const fileSystem = yield* FileSystem.FileSystem;
487488
const next = yield* serverSettings.updateSettings({
488489
addProjectBaseDirectory: "~/Development",
490+
automaticGitFetchInterval: Duration.seconds(10),
489491
observability: {
490492
otlpTracesUrl: "http://localhost:4318/v1/traces",
491493
otlpMetricsUrl: "http://localhost:4318/v1/metrics",
@@ -499,7 +501,8 @@ it.layer(NodeServices.layer)("server settings", (it) => {
499501
serverPassword: "secret-password",
500502
},
501503
},
502-
automaticGitFetchInterval: Duration.seconds(10),
504+
telemetryEnabled: true,
505+
telemetryPreferenceSet: true,
503506
});
504507

505508
assert.equal(next.providers.codex.binaryPath, "/opt/homebrew/bin/codex");
@@ -508,6 +511,7 @@ it.layer(NodeServices.layer)("server settings", (it) => {
508511
// @effect-diagnostics-next-line preferSchemaOverJson:off
509512
assert.deepEqual(JSON.parse(raw), {
510513
addProjectBaseDirectory: "~/Development",
514+
automaticGitFetchInterval: 10_000,
511515
observability: {
512516
otlpTracesUrl: "http://localhost:4318/v1/traces",
513517
otlpMetricsUrl: "http://localhost:4318/v1/metrics",
@@ -521,11 +525,117 @@ it.layer(NodeServices.layer)("server settings", (it) => {
521525
serverPassword: "secret-password",
522526
},
523527
},
524-
automaticGitFetchInterval: 10_000,
528+
telemetryEnabled: true,
529+
telemetryPreferenceSet: true,
525530
});
526531
}).pipe(Effect.provide(makeServerSettingsLayer())),
527532
);
528533

534+
it.effect("persists explicit telemetry opt-out marker", () =>
535+
Effect.gen(function* () {
536+
const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
537+
const serverConfig = yield* ServerConfig.ServerConfig;
538+
const fileSystem = yield* FileSystem.FileSystem;
539+
540+
yield* serverSettings.updateSettings({
541+
telemetryEnabled: false,
542+
});
543+
544+
const raw = yield* fileSystem.readFileString(serverConfig.settingsPath);
545+
// @effect-diagnostics-next-line preferSchemaOverJson:off
546+
assert.deepEqual(JSON.parse(raw), {
547+
telemetryPreferenceSet: true,
548+
});
549+
}).pipe(Effect.provide(makeServerSettingsLayer())),
550+
);
551+
552+
it.effect("loads persisted telemetryEnabled as an explicit preference", () =>
553+
Effect.gen(function* () {
554+
const fileSystem = yield* FileSystem.FileSystem;
555+
const path = yield* Path.Path;
556+
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
557+
prefix: "t3code-server-settings-telemetry-",
558+
});
559+
const settingsPath = path.join(baseDir, "userdata", "settings.json");
560+
yield* fileSystem.makeDirectory(path.dirname(settingsPath), { recursive: true });
561+
yield* fileSystem.writeFileString(settingsPath, '{ "telemetryEnabled": false }\n');
562+
563+
const layer = ServerSettingsModule.layer.pipe(
564+
Layer.provide(ServerSecretStore.layer),
565+
Layer.provideMerge(ServerConfig.layerTest(process.cwd(), baseDir)),
566+
);
567+
const settings = yield* Effect.gen(function* () {
568+
const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
569+
yield* serverSettings.start;
570+
return yield* serverSettings.getSettings;
571+
}).pipe(Effect.provide(layer));
572+
573+
assert.deepInclude(settings, {
574+
telemetryEnabled: false,
575+
telemetryPreferenceSet: true,
576+
});
577+
}),
578+
);
579+
580+
it.effect("loads malformed persisted telemetryEnabled as an explicit preference", () =>
581+
Effect.gen(function* () {
582+
const fileSystem = yield* FileSystem.FileSystem;
583+
const path = yield* Path.Path;
584+
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
585+
prefix: "t3code-server-settings-telemetry-malformed-",
586+
});
587+
const settingsPath = path.join(baseDir, "userdata", "settings.json");
588+
yield* fileSystem.makeDirectory(path.dirname(settingsPath), { recursive: true });
589+
yield* fileSystem.writeFileString(settingsPath, '{ "telemetryEnabled": "false" }\n');
590+
591+
const layer = ServerSettingsModule.layer.pipe(
592+
Layer.provide(ServerSecretStore.layer),
593+
Layer.provideMerge(ServerConfig.layerTest(process.cwd(), baseDir)),
594+
);
595+
const settings = yield* Effect.gen(function* () {
596+
const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
597+
yield* serverSettings.start;
598+
return yield* serverSettings.getSettings;
599+
}).pipe(Effect.provide(layer));
600+
601+
assert.deepInclude(settings, {
602+
telemetryEnabled: false,
603+
telemetryPreferenceSet: true,
604+
});
605+
}),
606+
);
607+
608+
it.effect("loads persisted telemetry opt-in from schema-invalid settings", () =>
609+
Effect.gen(function* () {
610+
const fileSystem = yield* FileSystem.FileSystem;
611+
const path = yield* Path.Path;
612+
const baseDir = yield* fileSystem.makeTempDirectoryScoped({
613+
prefix: "t3code-server-settings-telemetry-invalid-opt-in-",
614+
});
615+
const settingsPath = path.join(baseDir, "userdata", "settings.json");
616+
yield* fileSystem.makeDirectory(path.dirname(settingsPath), { recursive: true });
617+
yield* fileSystem.writeFileString(
618+
settingsPath,
619+
'{ "telemetryEnabled": true, "providers": null }\n',
620+
);
621+
622+
const layer = ServerSettingsModule.layer.pipe(
623+
Layer.provide(ServerSecretStore.layer),
624+
Layer.provideMerge(ServerConfig.layerTest(process.cwd(), baseDir)),
625+
);
626+
const settings = yield* Effect.gen(function* () {
627+
const serverSettings = yield* ServerSettingsModule.ServerSettingsService;
628+
yield* serverSettings.start;
629+
return yield* serverSettings.getSettings;
630+
}).pipe(Effect.provide(layer));
631+
632+
assert.deepInclude(settings, {
633+
telemetryEnabled: true,
634+
telemetryPreferenceSet: true,
635+
});
636+
}),
637+
);
638+
529639
it.effect("stores sensitive provider instance environment values outside settings.json", () =>
530640
Effect.gen(function* () {
531641
const serverSettings = yield* ServerSettingsModule.ServerSettingsService;

apps/server/src/serverSettings.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ import { writeFileStringAtomically } from "./atomicWrite.ts";
4747
import * as ServerConfig from "./config.ts";
4848
import { type DeepPartial, deepMerge } from "@t3tools/shared/Struct";
4949
import { fromJsonStringPretty, fromLenientJson } from "@t3tools/shared/schemaJson";
50-
import { applyServerSettingsPatch } from "@t3tools/shared/serverSettings";
50+
import {
51+
applyServerSettingsPatch,
52+
normalizeDecodedPersistedServerSettings,
53+
} from "@t3tools/shared/serverSettings";
5154
import * as ServerSecretStore from "./auth/ServerSecretStore.ts";
5255

5356
const encodeServerSettings = Schema.encodeEffect(ServerSettings);
@@ -306,9 +309,9 @@ const make = Effect.gen(function* () {
306309
issues: Cause.pretty(decoded.cause),
307310
cause: decoded.cause,
308311
});
309-
return DEFAULT_SERVER_SETTINGS;
312+
return normalizeDecodedPersistedServerSettings(DEFAULT_SERVER_SETTINGS, raw);
310313
}
311-
return decoded.value;
314+
return normalizeDecodedPersistedServerSettings(decoded.value, raw);
312315
});
313316

314317
const settingsCache = yield* Cache.make<typeof cacheKey, ServerSettings, ServerSettingsError>({

0 commit comments

Comments
 (0)