diff --git a/apps/marketing/app/layout.tsx b/apps/marketing/app/layout.tsx
index a113b0a4c..d22b29f5f 100644
--- a/apps/marketing/app/layout.tsx
+++ b/apps/marketing/app/layout.tsx
@@ -1,7 +1,6 @@
import type React from "react";
import type { Metadata } from "next";
import { Nunito } from "next/font/google";
-import { Analytics } from "@vercel/analytics/next";
import "./globals.css";
const nunito = Nunito({ subsets: ["latin"], variable: "--font-nunito" });
@@ -60,10 +59,7 @@ export default function RootLayout({
}>) {
return (
-
- {children}
-
-
+ {children}
);
}
diff --git a/apps/marketing/package.json b/apps/marketing/package.json
index 6e303bee5..882effbd3 100644
--- a/apps/marketing/package.json
+++ b/apps/marketing/package.json
@@ -41,7 +41,6 @@
"@radix-ui/react-toggle": "1.1.1",
"@radix-ui/react-toggle-group": "1.1.1",
"@radix-ui/react-tooltip": "1.1.6",
- "@vercel/analytics": "1.3.1",
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
diff --git a/apps/server/integration/OrchestrationEngineHarness.integration.ts b/apps/server/integration/OrchestrationEngineHarness.integration.ts
index f0d266923..b59bfa6de 100644
--- a/apps/server/integration/OrchestrationEngineHarness.integration.ts
+++ b/apps/server/integration/OrchestrationEngineHarness.integration.ts
@@ -43,7 +43,6 @@ import { makeProviderServiceLive } from "../src/provider/Layers/ProviderService.
import { makeCodexAdapterLive } from "../src/provider/Layers/CodexAdapter.ts";
import { CodexAdapter } from "../src/provider/Services/CodexAdapter.ts";
import { ProviderService } from "../src/provider/Services/ProviderService.ts";
-import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts";
import { CheckpointReactorLive } from "../src/orchestration/Layers/CheckpointReactor.ts";
import { OrchestrationEngineLive } from "../src/orchestration/Layers/OrchestrationEngine.ts";
import { OrchestrationProjectionPipelineLive } from "../src/orchestration/Layers/ProjectionPipeline.ts";
@@ -284,12 +283,10 @@ export const makeOrchestrationIntegrationHarness = (
? makeProviderServiceLive().pipe(
Layer.provide(providerSessionDirectoryLayer),
Layer.provide(realCodexRegistry),
- Layer.provide(AnalyticsService.layerTest),
)
: makeProviderServiceLive().pipe(
Layer.provide(providerSessionDirectoryLayer),
Layer.provide(fakeRegistry!),
- Layer.provide(AnalyticsService.layerTest),
);
const checkpointStoreLayer = CheckpointStoreLive.pipe(Layer.provide(GitCoreLive));
diff --git a/apps/server/integration/providerService.integration.test.ts b/apps/server/integration/providerService.integration.test.ts
index 7ef61f4ac..7576896ba 100644
--- a/apps/server/integration/providerService.integration.test.ts
+++ b/apps/server/integration/providerService.integration.test.ts
@@ -12,7 +12,6 @@ import {
ProviderService,
type ProviderServiceShape,
} from "../src/provider/Services/ProviderService.ts";
-import { AnalyticsService } from "../src/telemetry/Services/AnalyticsService.ts";
import { SqlitePersistenceMemory } from "../src/persistence/Layers/Sqlite.ts";
import { ProviderSessionRuntimeRepositoryLive } from "../src/persistence/Layers/ProviderSessionRuntime.ts";
@@ -60,7 +59,6 @@ const makeIntegrationFixture = Effect.gen(function* () {
const shared = Layer.mergeAll(
directoryLayer,
Layer.succeed(ProviderAdapterRegistry, registry),
- AnalyticsService.layerTest,
).pipe(Layer.provide(SqlitePersistenceMemory));
const layer = makeProviderServiceLive().pipe(Layer.provide(shared));
diff --git a/apps/server/src/main.test.ts b/apps/server/src/main.test.ts
index caeb58625..0cdc05cb3 100644
--- a/apps/server/src/main.test.ts
+++ b/apps/server/src/main.test.ts
@@ -1,7 +1,6 @@
import * as Http from "node:http";
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it, vi } from "@effect/vitest";
-import type { OrchestrationReadModel } from "@okcode/contracts";
import * as ConfigProvider from "effect/ConfigProvider";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
@@ -10,11 +9,9 @@ import { FetchHttpClient } from "effect/unstable/http";
import { beforeEach } from "vitest";
import { NetService } from "@okcode/shared/Net";
-import { CliConfig, recordStartupHeartbeat, okcodeCli, type CliConfigShape } from "./main";
+import { CliConfig, okcodeCli, type CliConfigShape } from "./main";
import { ServerConfig, type ServerConfigShape } from "./config";
import { Open, type OpenShape } from "./open";
-import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery";
-import { AnalyticsService } from "./telemetry/Services/AnalyticsService";
import { Server, type ServerShape } from "./wsServer";
const start = vi.fn(() => undefined);
@@ -53,7 +50,6 @@ const testLayer = Layer.mergeAll(
openInFileManager: () => Effect.void,
revealInFileManager: () => Effect.void,
} satisfies OpenShape),
- AnalyticsService.layerTest,
FetchHttpClient.layer,
NodeServices.layer,
);
@@ -238,43 +234,6 @@ it.layer(testLayer)("server CLI command", (it) => {
}),
);
- it.effect("records a startup heartbeat with thread/project counts", () =>
- Effect.gen(function* () {
- const recordTelemetry = vi.fn(
- (_event: string, _properties?: Readonly>) => Effect.void,
- );
- const getSnapshot = vi.fn(() =>
- Effect.succeed({
- snapshotSequence: 2,
- projects: [{} as OrchestrationReadModel["projects"][number]],
- threads: [
- {} as OrchestrationReadModel["threads"][number],
- {} as OrchestrationReadModel["threads"][number],
- ],
- updatedAt: new Date(1).toISOString(),
- } satisfies OrchestrationReadModel),
- );
-
- yield* recordStartupHeartbeat.pipe(
- Effect.provideService(ProjectionSnapshotQuery, {
- getSnapshot,
- }),
- Effect.provideService(AnalyticsService, {
- record: recordTelemetry,
- flush: Effect.void,
- }),
- );
-
- assert.deepEqual(recordTelemetry.mock.calls[0], [
- "server.boot.heartbeat",
- {
- threadCount: 2,
- projectCount: 1,
- },
- ]);
- }),
- );
-
it.effect("does not start server for invalid --mode values", () =>
Effect.gen(function* () {
yield* runCli(["--mode", "invalid"]);
diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts
index 27efcfb6f..a48ac4b58 100644
--- a/apps/server/src/main.ts
+++ b/apps/server/src/main.ts
@@ -21,12 +21,9 @@ import { fixPath, resolveBaseDir } from "./os-jank";
import { Open } from "./open";
import * as SqlitePersistence from "./persistence/Layers/Sqlite";
import { makeServerProviderLayer, makeServerRuntimeServicesLayer } from "./serverLayers";
-import { ProjectionSnapshotQuery } from "./orchestration/Services/ProjectionSnapshotQuery";
import { ProviderHealthLive } from "./provider/Layers/ProviderHealth";
import { Server } from "./wsServer";
import { ServerLoggerLive } from "./serverLogger";
-import { AnalyticsServiceLayerLive } from "./telemetry/Layers/AnalyticsService";
-import { AnalyticsService } from "./telemetry/Services/AnalyticsService";
import { doctorCmd } from "./doctor";
export class StartupError extends Data.TaggedError("StartupError")<{
@@ -199,7 +196,6 @@ const LayerLive = (input: CliInput) =>
Layer.provideMerge(ProviderHealthLive),
Layer.provideMerge(SqlitePersistence.layerConfig),
Layer.provideMerge(ServerLoggerLive),
- Layer.provideMerge(AnalyticsServiceLayerLive),
Layer.provideMerge(ServerConfigLive(input)),
);
@@ -209,31 +205,6 @@ const isWildcardHost = (host: string | undefined): boolean =>
const formatHostForUrl = (host: string): string =>
host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
-export const recordStartupHeartbeat = Effect.gen(function* () {
- const analytics = yield* AnalyticsService;
- const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
-
- const { threadCount, projectCount } = yield* projectionSnapshotQuery.getSnapshot().pipe(
- Effect.map((snapshot) => ({
- threadCount: snapshot.threads.length,
- projectCount: snapshot.projects.length,
- })),
- Effect.catch((cause) =>
- Effect.logWarning("failed to gather startup snapshot for telemetry", { cause }).pipe(
- Effect.as({
- threadCount: 0,
- projectCount: 0,
- }),
- ),
- ),
- );
-
- yield* analytics.record("server.boot.heartbeat", {
- threadCount,
- projectCount,
- });
-});
-
const makeServerProgram = (input: CliInput) =>
Effect.gen(function* () {
const cliConfig = yield* CliConfig;
@@ -253,7 +224,6 @@ const makeServerProgram = (input: CliInput) =>
}
yield* start;
- yield* Effect.forkChild(recordStartupHeartbeat);
const localUrl = `http://localhost:${config.port}`;
const bindUrl =
diff --git a/apps/server/src/provider/Layers/ProviderService.test.ts b/apps/server/src/provider/Layers/ProviderService.test.ts
index 2f241715e..a639b956c 100644
--- a/apps/server/src/provider/Layers/ProviderService.test.ts
+++ b/apps/server/src/provider/Layers/ProviderService.test.ts
@@ -42,7 +42,6 @@ import {
makeSqlitePersistenceLive,
SqlitePersistenceMemory,
} from "../../persistence/Layers/Sqlite.ts";
-import { AnalyticsService } from "../../telemetry/Services/AnalyticsService.ts";
const asRequestId = (value: string): ApprovalRequestId => ApprovalRequestId.makeUnsafe(value);
const asEventId = (value: string): EventId => EventId.makeUnsafe(value);
@@ -251,7 +250,6 @@ function makeProviderServiceLayer() {
makeProviderServiceLive().pipe(
Layer.provide(providerAdapterLayer),
Layer.provide(directoryLayer),
- Layer.provideMerge(AnalyticsService.layerTest),
),
directoryLayer,
@@ -299,7 +297,6 @@ it.effect("ProviderServiceLive keeps persisted resumable sessions on startup", (
const providerLayer = makeProviderServiceLive().pipe(
Layer.provide(Layer.succeed(ProviderAdapterRegistry, registry)),
Layer.provide(directoryLayer),
- Layer.provide(AnalyticsService.layerTest),
);
yield* Effect.gen(function* () {
@@ -358,7 +355,6 @@ it.effect(
const firstProviderLayer = makeProviderServiceLive().pipe(
Layer.provide(Layer.succeed(ProviderAdapterRegistry, firstRegistry)),
Layer.provide(firstDirectoryLayer),
- Layer.provide(AnalyticsService.layerTest),
);
const updatedResumeCursor = {
threadId: asThreadId("thread-1"),
@@ -409,7 +405,6 @@ it.effect(
const secondProviderLayer = makeProviderServiceLive().pipe(
Layer.provide(Layer.succeed(ProviderAdapterRegistry, secondRegistry)),
Layer.provide(secondDirectoryLayer),
- Layer.provide(AnalyticsService.layerTest),
);
secondCodex.startSession.mockClear();
@@ -765,7 +760,6 @@ routing.layer("ProviderServiceLive routing", (it) => {
const firstProviderLayer = makeProviderServiceLive().pipe(
Layer.provide(Layer.succeed(ProviderAdapterRegistry, firstRegistry)),
Layer.provide(firstDirectoryLayer),
- Layer.provide(AnalyticsService.layerTest),
);
const initial = yield* Effect.gen(function* () {
@@ -797,7 +791,6 @@ routing.layer("ProviderServiceLive routing", (it) => {
const secondProviderLayer = makeProviderServiceLive().pipe(
Layer.provide(Layer.succeed(ProviderAdapterRegistry, secondRegistry)),
Layer.provide(secondDirectoryLayer),
- Layer.provide(AnalyticsService.layerTest),
);
secondClaude.startSession.mockClear();
diff --git a/apps/server/src/provider/Layers/ProviderService.ts b/apps/server/src/provider/Layers/ProviderService.ts
index 67deff5e6..ec1855f29 100644
--- a/apps/server/src/provider/Layers/ProviderService.ts
+++ b/apps/server/src/provider/Layers/ProviderService.ts
@@ -31,7 +31,6 @@ import {
type ProviderRuntimeBinding,
} from "../Services/ProviderSessionDirectory.ts";
import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts";
-import { AnalyticsService } from "../../telemetry/Services/AnalyticsService.ts";
export interface ProviderServiceLiveOptions {
readonly canonicalEventLogPath?: string;
@@ -145,7 +144,6 @@ function readPersistedCwd(
const makeProviderService = (options?: ProviderServiceLiveOptions) =>
Effect.gen(function* () {
- const analytics = yield* Effect.service(AnalyticsService);
const canonicalEventLogger =
options?.canonicalEventLogger ??
(options?.canonicalEventLogPath !== undefined
@@ -221,11 +219,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
);
if (existing) {
yield* upsertSessionBinding(existing, input.binding.threadId);
- yield* analytics.record("provider.session.recovered", {
- provider: existing.provider,
- strategy: "adopt-existing",
- hasResumeCursor: existing.resumeCursor !== undefined,
- });
return { adapter, session: existing } as const;
}
}
@@ -258,11 +251,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
}
yield* upsertSessionBinding(resumed, input.binding.threadId);
- yield* analytics.record("provider.session.recovered", {
- provider: resumed.provider,
- strategy: "resume-thread",
- hasResumeCursor: resumed.resumeCursor !== undefined,
- });
return { adapter, session: resumed } as const;
});
@@ -331,13 +319,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
modelOptions: input.modelOptions,
providerOptions: input.providerOptions,
});
- yield* analytics.record("provider.session.started", {
- provider: session.provider,
- runtimeMode: input.runtimeMode,
- hasResumeCursor: session.resumeCursor !== undefined,
- hasCwd: typeof input.cwd === "string" && input.cwd.trim().length > 0,
- hasModel: typeof input.model === "string" && input.model.trim().length > 0,
- });
return session;
});
@@ -377,13 +358,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
lastRuntimeEventAt: new Date().toISOString(),
},
});
- yield* analytics.record("provider.turn.sent", {
- provider: routed.adapter.provider,
- model: input.model,
- interactionMode: input.interactionMode,
- attachmentCount: input.attachments.length,
- hasInput: typeof input.input === "string" && input.input.trim().length > 0,
- });
return turn;
});
@@ -400,9 +374,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
allowRecovery: true,
});
yield* routed.adapter.interruptTurn(routed.threadId, input.turnId);
- yield* analytics.record("provider.turn.interrupted", {
- provider: routed.adapter.provider,
- });
});
const respondToRequest: ProviderServiceShape["respondToRequest"] = (rawInput) =>
@@ -418,10 +389,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
allowRecovery: true,
});
yield* routed.adapter.respondToRequest(routed.threadId, input.requestId, input.decision);
- yield* analytics.record("provider.request.responded", {
- provider: routed.adapter.provider,
- decision: input.decision,
- });
});
const respondToUserInput: ProviderServiceShape["respondToUserInput"] = (rawInput) =>
@@ -455,9 +422,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
yield* routed.adapter.stopSession(routed.threadId);
}
yield* directory.remove(input.threadId);
- yield* analytics.record("provider.session.stopped", {
- provider: routed.adapter.provider,
- });
});
const listSessions: ProviderServiceShape["listSessions"] = () =>
@@ -526,10 +490,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
allowRecovery: true,
});
yield* routed.adapter.rollbackThread(routed.threadId, input.numTurns);
- yield* analytics.record("provider.conversation.rolled_back", {
- provider: routed.adapter.provider,
- turns: input.numTurns,
- });
});
const runStopAll = () =>
@@ -563,10 +523,6 @@ const makeProviderService = (options?: ProviderServiceLiveOptions) =>
),
),
).pipe(Effect.asVoid);
- yield* analytics.record("provider.sessions.stopped_all", {
- sessionCount: threadIds.length,
- });
- yield* analytics.flush;
});
yield* Effect.addFinalizer(() =>
diff --git a/apps/server/src/serverLayers.ts b/apps/server/src/serverLayers.ts
index 245779315..3036b65ce 100644
--- a/apps/server/src/serverLayers.ts
+++ b/apps/server/src/serverLayers.ts
@@ -39,7 +39,6 @@ import { WorkflowEngineLive } from "./prReview/Layers/WorkflowEngine";
import { MergeConflictResolverLive } from "./prReview/Layers/MergeConflictResolver";
import { PrReviewLive } from "./prReview/Layers/PrReview";
import { PtyAdapter } from "./terminal/Services/PTY";
-import { AnalyticsService } from "./telemetry/Services/AnalyticsService";
type RuntimePtyAdapterLoader = {
layer: Layer.Layer;
@@ -61,7 +60,7 @@ const makeRuntimePtyAdapterLayer = () =>
export function makeServerProviderLayer(): Layer.Layer<
ProviderService,
ProviderUnsupportedError,
- SqlClient.SqlClient | ServerConfig | FileSystem.FileSystem | AnalyticsService
+ SqlClient.SqlClient | ServerConfig | FileSystem.FileSystem
> {
return Effect.gen(function* () {
const { providerEventLogPath } = yield* ServerConfig;
diff --git a/apps/server/src/telemetry/Identify.ts b/apps/server/src/telemetry/Identify.ts
deleted file mode 100644
index 15b87adbc..000000000
--- a/apps/server/src/telemetry/Identify.ts
+++ /dev/null
@@ -1,100 +0,0 @@
-import { Effect, FileSystem, Path, Random, Schema } from "effect";
-import * as Crypto from "node:crypto";
-import { homedir } from "node:os";
-import { ServerConfig } from "../config";
-
-const CodexAuthJsonSchema = Schema.Struct({
- tokens: Schema.Struct({
- account_id: Schema.String,
- }),
-});
-
-const ClaudeJsonSchema = Schema.Struct({
- userID: Schema.String,
-});
-
-class IdentifyUserError extends Schema.TaggedErrorClass()("IdentifyUserError", {
- message: Schema.String,
- cause: Schema.optional(Schema.Defect),
-}) {}
-
-const hash = (value: string) =>
- Effect.try({
- try: () => Crypto.createHash("sha256").update(value).digest("hex"),
- catch: (error) =>
- new IdentifyUserError({
- message: "Failed to hash identifier",
- cause: error,
- }),
- });
-
-const getCodexAccountId = Effect.gen(function* () {
- const fileSystem = yield* FileSystem.FileSystem;
- const path = yield* Path.Path;
-
- const authJsonPath = path.join(homedir(), ".codex", "auth.json");
- const authJson = yield* Effect.flatMap(
- fileSystem.readFileString(authJsonPath),
- Schema.decodeEffect(Schema.fromJsonString(CodexAuthJsonSchema)),
- );
-
- return authJson.tokens.account_id;
-});
-
-const getClaudeUserId = Effect.gen(function* () {
- const fileSystem = yield* FileSystem.FileSystem;
- const path = yield* Path.Path;
-
- const claudeJsonPath = path.join(homedir(), ".claude.json");
- const claudeJson = yield* Effect.flatMap(
- fileSystem.readFileString(claudeJsonPath),
- Schema.decodeEffect(Schema.fromJsonString(ClaudeJsonSchema)),
- );
-
- return claudeJson.userID;
-});
-
-const upsertAnonymousId = Effect.gen(function* () {
- const fileSystem = yield* FileSystem.FileSystem;
- const { anonymousIdPath } = yield* ServerConfig;
-
- const anonymousId = yield* fileSystem.readFileString(anonymousIdPath).pipe(
- Effect.catch(() =>
- Effect.gen(function* () {
- const randomId = yield* Random.nextUUIDv4;
- yield* fileSystem.writeFileString(anonymousIdPath, randomId);
- return randomId;
- }),
- ),
- );
-
- return anonymousId;
-});
-
-/**
- * getTelemetryIdentifier - Users are "identified" by finding the first match of the following, then hashing the value.
- * 1. ~/.codex/auth.json tokens.account_id
- * 2. ~/.claude.json userID
- * 3. ~/.okcode/telemetry/anonymous-id
- */
-export const getTelemetryIdentifier = Effect.gen(function* () {
- const codexAccountId = yield* Effect.result(getCodexAccountId);
- if (codexAccountId._tag === "Success") {
- return yield* hash(codexAccountId.success);
- }
-
- const claudeUserId = yield* Effect.result(getClaudeUserId);
- if (claudeUserId._tag === "Success") {
- return yield* hash(claudeUserId.success);
- }
-
- const anonymousId = yield* Effect.result(upsertAnonymousId);
- if (anonymousId._tag === "Success") {
- return yield* hash(anonymousId.success);
- }
-
- return null;
-}).pipe(
- Effect.tapError((error) => Effect.logWarning("Failed to get identifier", { cause: error })),
- Effect.orElseSucceed(() => null),
-);
diff --git a/apps/server/src/telemetry/Layers/AnalyticsService.test.ts b/apps/server/src/telemetry/Layers/AnalyticsService.test.ts
deleted file mode 100644
index 80caae5a2..000000000
--- a/apps/server/src/telemetry/Layers/AnalyticsService.test.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-import * as NodeHttpServer from "@effect/platform-node/NodeHttpServer";
-import * as NodeServices from "@effect/platform-node/NodeServices";
-import { assert, it } from "@effect/vitest";
-import { ConfigProvider, Effect, Layer } from "effect";
-import * as HttpServer from "effect/unstable/http/HttpServer";
-import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest";
-import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse";
-
-import { ServerConfig } from "../../config.ts";
-import { getTelemetryIdentifier } from "../Identify.ts";
-import { AnalyticsService } from "../Services/AnalyticsService.ts";
-import { AnalyticsServiceLayerLive } from "./AnalyticsService.ts";
-
-interface RecordedBatchRequest {
- readonly path: string;
- readonly body: {
- readonly batch?: ReadonlyArray<{
- readonly event?: string;
- readonly properties?: {
- readonly index?: number;
- readonly clientType?: string;
- };
- }>;
- } | null;
-}
-
-interface RecordedBatchBody {
- readonly batch: ReadonlyArray<{
- readonly event?: string;
- readonly properties?: {
- readonly index?: number;
- readonly clientType?: string;
- };
- }>;
-}
-
-it.layer(NodeServices.layer)("AnalyticsService test", (it) => {
- it.effect("flush drains all buffered events across multiple batches", () =>
- Effect.gen(function* () {
- const capturedRequests: Array = [];
- const serverConfigLayer = ServerConfig.layerTest(process.cwd(), {
- prefix: "t3-telemetry-base-",
- });
-
- const telemetryLayer = AnalyticsServiceLayerLive.pipe(Layer.provideMerge(serverConfigLayer));
- const configLayer = ConfigProvider.layer(
- ConfigProvider.fromUnknown({
- OKCODE_TELEMETRY_ENABLED: true,
- OKCODE_POSTHOG_KEY: "phc_test_key",
- OKCODE_POSTHOG_HOST: "",
- OKCODE_TELEMETRY_FLUSH_BATCH_SIZE: 20,
- }),
- );
- const batchServerLayer = HttpServer.serve(
- Effect.gen(function* () {
- const request = yield* HttpServerRequest.HttpServerRequest;
- if (request.method !== "POST") {
- return HttpServerResponse.empty({ status: 404 });
- }
-
- const payload = yield* request.json.pipe(
- Effect.map((body) => body as RecordedBatchRequest["body"]),
- Effect.catch(() => Effect.succeed(null)),
- );
-
- capturedRequests.push({ path: request.url, body: payload });
-
- return HttpServerResponse.jsonUnsafe({});
- }),
- );
- const runtimeLayer = telemetryLayer.pipe(
- Layer.provide(configLayer),
- Layer.provideMerge(NodeHttpServer.layerTest),
- );
-
- yield* Effect.gen(function* () {
- yield* Layer.launch(batchServerLayer).pipe(Effect.forkScoped);
- const telemetryIdentifier = yield* getTelemetryIdentifier;
- assert.equal(telemetryIdentifier !== null, true);
- const analytics = yield* AnalyticsService;
-
- for (let index = 0; index < 45; index += 1) {
- yield* analytics.record("test.flush.drain", { index });
- }
-
- yield* analytics.flush;
- }).pipe(Effect.provide(runtimeLayer));
-
- const batchRequests = capturedRequests.filter(
- (request): request is RecordedBatchRequest & { readonly body: RecordedBatchBody } =>
- Array.isArray(request.body?.batch),
- );
- assert.equal(batchRequests.length, 3);
- assert.equal(
- batchRequests.every((request) => request.path === "/batch/" || request.path === "/batch"),
- true,
- );
- const deliveredIndexes = batchRequests.flatMap((request) =>
- request.body.batch
- .filter((event) => event.event === "test.flush.drain")
- .map((event) => event.properties?.index)
- .filter((index): index is number => typeof index === "number"),
- );
-
- const sorted = deliveredIndexes.toSorted((a, b) => a - b);
- assert.equal(sorted.length, 45);
- assert.deepEqual(
- sorted,
- Array.from({ length: 45 }, (_, index) => index),
- );
- assert.equal(
- batchRequests.every((request) =>
- request.body.batch.every((event) => event.properties?.clientType === "cli-web-client"),
- ),
- true,
- );
- }),
- );
-});
diff --git a/apps/server/src/telemetry/Layers/AnalyticsService.ts b/apps/server/src/telemetry/Layers/AnalyticsService.ts
deleted file mode 100644
index f6b2ee425..000000000
--- a/apps/server/src/telemetry/Layers/AnalyticsService.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-/**
- * AnalyticsServiceLive - Anonymous PostHog telemetry layer.
- *
- * Persists a random installation-scoped anonymous id to state dir, buffers
- * events in memory, and flushes batches to PostHog over Effect HttpClient.
- *
- * @module AnalyticsServiceLive
- */
-
-import { Config, DateTime, Effect, Layer, Ref } from "effect";
-import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
-
-import { ServerConfig } from "../../config.ts";
-import { AnalyticsService, type AnalyticsServiceShape } from "../Services/AnalyticsService.ts";
-import { getTelemetryIdentifier } from "../Identify.ts";
-import { version } from "../../../package.json" with { type: "json" };
-
-interface BufferedAnalyticsEvent {
- readonly event: string;
- readonly properties?: Readonly>;
- readonly capturedAt: string;
-}
-
-const TelemetryEnvConfig = Config.all({
- posthogKey: Config.string("OKCODE_POSTHOG_KEY").pipe(
- Config.withDefault("phc_XOWci4oZP4VvLiEyrFqkFjP4CZn55mjYYBMREK5Wd6m"),
- ),
- posthogHost: Config.string("OKCODE_POSTHOG_HOST").pipe(
- Config.withDefault("https://us.i.posthog.com"),
- ),
- enabled: Config.boolean("OKCODE_TELEMETRY_ENABLED").pipe(Config.withDefault(true)),
- flushBatchSize: Config.number("OKCODE_TELEMETRY_FLUSH_BATCH_SIZE").pipe(Config.withDefault(20)),
- maxBufferedEvents: Config.number("OKCODE_TELEMETRY_MAX_BUFFERED_EVENTS").pipe(
- Config.withDefault(1_000),
- ),
-});
-
-const makeAnalyticsService = Effect.gen(function* () {
- const telemetryConfig = yield* TelemetryEnvConfig.asEffect();
- const httpClient = yield* HttpClient.HttpClient;
- const serverConfig = yield* ServerConfig;
- const identifier = yield* getTelemetryIdentifier;
- const bufferRef = yield* Ref.make>([]);
- const clientType = serverConfig.mode === "desktop" ? "desktop-app" : "cli-web-client";
-
- const enqueueBufferedEvent = (event: string, properties?: Readonly>) =>
- Effect.flatMap(DateTime.now, (now) =>
- Ref.modify(bufferRef, (current) => {
- const appended = [
- ...current,
- {
- event,
- ...(properties ? { properties } : {}),
- capturedAt: DateTime.formatIso(now),
- } satisfies BufferedAnalyticsEvent,
- ];
-
- const next =
- appended.length > telemetryConfig.maxBufferedEvents
- ? appended.slice(appended.length - telemetryConfig.maxBufferedEvents)
- : appended;
-
- return [
- {
- size: next.length,
- dropped: next.length !== appended.length,
- } as const,
- next,
- ] as const;
- }),
- );
-
- const sendBatch = (events: ReadonlyArray) =>
- Effect.gen(function* () {
- if (!telemetryConfig.enabled || !identifier) return;
-
- const payload = {
- api_key: telemetryConfig.posthogKey,
- batch: events.map((event) => ({
- event: event.event,
- distinct_id: identifier,
- properties: {
- ...event.properties,
- $process_person_profile: false,
- platform: process.platform,
- wsl: process.env.WSL_DISTRO_NAME,
- arch: process.arch,
- okCodeVersion: version,
- clientType,
- },
- timestamp: event.capturedAt,
- })),
- };
-
- yield* HttpClientRequest.post(`${telemetryConfig.posthogHost}/batch/`).pipe(
- HttpClientRequest.bodyJson(payload),
- Effect.flatMap(httpClient.execute),
- Effect.flatMap(HttpClientResponse.filterStatusOk),
- );
- });
-
- const flush: AnalyticsServiceShape["flush"] = Effect.gen(function* () {
- while (true) {
- const batch = yield* Ref.modify(bufferRef, (current) => {
- if (current.length === 0) {
- return [[] as ReadonlyArray, current] as const;
- }
- const nextBatch = current.slice(0, telemetryConfig.flushBatchSize);
- const remaining = current.slice(nextBatch.length);
- return [nextBatch, remaining] as const;
- });
-
- if (batch.length === 0) {
- return;
- }
-
- yield* sendBatch(batch).pipe(
- Effect.catch((error) =>
- Ref.update(bufferRef, (current) => [...batch, ...current]).pipe(
- Effect.flatMap(() => Effect.fail(error)),
- ),
- ),
- );
- }
- }).pipe(Effect.catch((cause) => Effect.logError("Failed to flush telemetry", { cause })));
-
- const record: AnalyticsServiceShape["record"] = Effect.fnUntraced(function* (event, properties) {
- if (!telemetryConfig.enabled || !identifier) return;
-
- const enqueueResult = yield* enqueueBufferedEvent(event, properties);
- if (enqueueResult.dropped) {
- yield* Effect.logDebug("analytics buffer full; dropping oldest event", {
- size: enqueueResult.size,
- event,
- });
- }
- });
-
- yield* Effect.forever(Effect.sleep(1000).pipe(Effect.flatMap(() => flush)), {
- disableYield: true,
- }).pipe(Effect.forkScoped);
-
- yield* Effect.addFinalizer(() => flush);
-
- return {
- record,
- flush,
- } satisfies AnalyticsServiceShape;
-});
-
-export const AnalyticsServiceLayerLive = Layer.effect(AnalyticsService, makeAnalyticsService);
diff --git a/apps/server/src/telemetry/Services/AnalyticsService.ts b/apps/server/src/telemetry/Services/AnalyticsService.ts
deleted file mode 100644
index 74f672626..000000000
--- a/apps/server/src/telemetry/Services/AnalyticsService.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * AnalyticsService - Anonymous telemetry capture contract.
- *
- * Provides a best-effort event API for runtime telemetry and a strict
- * `captureImmediate` method for call sites that need explicit error handling.
- *
- * @module AnalyticsService
- */
-import { Effect, Layer, ServiceMap } from "effect";
-
-export interface AnalyticsServiceShape {
- /**
- * Capture an event immediately; returns typed failure when capture fails.
- */
- readonly record: (
- event: string,
- properties?: Readonly>,
- ) => Effect.Effect;
-
- /**
- * Flush queued telemetry.
- */
- readonly flush: Effect.Effect;
-}
-
-export class AnalyticsService extends ServiceMap.Service()(
- "okcode/telemetry/Services/AnalyticsService",
-) {
- static readonly layerTest = Layer.succeed(AnalyticsService, {
- record: () => Effect.void,
- flush: Effect.void,
- });
-}
diff --git a/apps/server/src/wsServer.test.ts b/apps/server/src/wsServer.test.ts
index 43bb2bd84..2a76010b5 100644
--- a/apps/server/src/wsServer.test.ts
+++ b/apps/server/src/wsServer.test.ts
@@ -54,7 +54,6 @@ import type { GitCoreShape } from "./git/Services/GitCore.ts";
import { GitCore } from "./git/Services/GitCore.ts";
import { GitActionExecutionError, GitCommandError } from "./git/Errors.ts";
import { MigrationError } from "@effect/sql-sqlite-bun/SqliteMigrator";
-import { AnalyticsService } from "./telemetry/Services/AnalyticsService.ts";
const asEventId = (value: string): EventId => EventId.makeUnsafe(value);
const asProviderItemId = (value: string): ProviderItemId => ProviderItemId.makeUnsafe(value);
@@ -550,7 +549,6 @@ describe("WebSocket Server", () => {
Layer.provideMerge(providerHealthLayer),
Layer.provideMerge(openLayer),
Layer.provideMerge(serverConfigLayer),
- Layer.provideMerge(AnalyticsService.layerTest),
Layer.provideMerge(NodeServices.layer),
);
const runtimeServices = await Effect.runPromise(
diff --git a/apps/server/src/wsServer.ts b/apps/server/src/wsServer.ts
index 367fc5d53..610df1adb 100644
--- a/apps/server/src/wsServer.ts
+++ b/apps/server/src/wsServer.ts
@@ -78,7 +78,6 @@ import {
} from "./attachmentStore.ts";
import { parseBase64DataUrl } from "./imageMime.ts";
import { extractTextAttachmentContents } from "./attachmentText.ts";
-import { AnalyticsService } from "./telemetry/Services/AnalyticsService.ts";
import { expandHomePath } from "./os-jank.ts";
import { makeServerPushBus } from "./wsServer/pushBus.ts";
import { makeServerReadiness } from "./wsServer/readiness.ts";
@@ -289,7 +288,6 @@ export type ServerRuntimeServices =
| Keybindings
| SkillService
| Open
- | AnalyticsService
| EnvironmentVariables;
export class ServerLifecycleError extends Schema.TaggedErrorClass()(
diff --git a/bun.lock b/bun.lock
index 17780673f..1ff4b1133 100644
--- a/bun.lock
+++ b/bun.lock
@@ -67,7 +67,6 @@
"@radix-ui/react-toggle": "1.1.1",
"@radix-ui/react-toggle-group": "1.1.1",
"@radix-ui/react-tooltip": "1.1.6",
- "@vercel/analytics": "1.3.1",
"autoprefixer": "^10.4.20",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
@@ -1160,8 +1159,6 @@
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
- "@vercel/analytics": ["@vercel/analytics@1.3.1", "", { "dependencies": { "server-only": "^0.0.1" }, "peerDependencies": { "next": ">= 13", "react": "^18 || ^19" }, "optionalPeers": ["next", "react"] }, "sha512-xhSlYgAuJ6Q4WQGkzYTLmXwhYl39sWjoMA3nHxfkvG+WdBT25c563a7QhwwKivEOZtPJXifYHR1m2ihoisbWyA=="],
-
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
"@vitest/browser": ["@vitest/browser@4.1.2", "", { "dependencies": { "@blazediff/core": "1.9.1", "@vitest/mocker": "4.1.2", "@vitest/utils": "4.1.2", "magic-string": "^0.30.21", "pngjs": "^7.0.0", "sirv": "^3.0.2", "tinyrainbow": "^3.1.0", "ws": "^8.19.0" }, "peerDependencies": { "vitest": "4.1.2" } }, "sha512-CwdIf90LNf1Zitgqy63ciMAzmyb4oIGs8WZ40VGYrWkssQKeEKr32EzO8MKUrDPPcPVHFI9oQ5ni2Hp24NaNRQ=="],
@@ -2104,8 +2101,6 @@
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
- "server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="],
-
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],