import { Effect, Layer, ManagedRuntime } from "effect"; import { Analytics, defaultLayer as analyticsDefaultLayer, withExecutionAnalytics, type AnalyticsService, } from "@executor-js/analytics"; import { EngineDecorator } from "@executor-js/api/server"; import packageJson from "../package.json" with { type: "json" }; import { resolveDataDir } from "./config"; // --------------------------------------------------------------------------- // Self-host product analytics: one anonymous per-install service for the // server lifetime. The anonymous id persists next to the instance's other // first-boot state (secret.key) under the data dir, so an instance counts // once across restarts. Module-singleton runtime; dispose flushes at shutdown. // --------------------------------------------------------------------------- // Deployed self-host builds are published releases; the prerelease heuristic // mirrors apps/local/src/installation.ts. const VERSION: string = packageJson.version; const CHANNEL = VERSION.includes("-") ? ("beta" as const) : ("stable" as const); let analyticsRuntime: ManagedRuntime.ManagedRuntime | null = null; const getAnalyticsRuntime = (): ManagedRuntime.ManagedRuntime => { if (analyticsRuntime) return analyticsRuntime; analyticsRuntime = ManagedRuntime.make( analyticsDefaultLayer({ surface: "selfhost", version: VERSION, channel: CHANNEL, dataDir: resolveDataDir(), }), ); return analyticsRuntime; }; /** * Lazy facade over the instance's shared service. `record` forks into the * module runtime (fire-and-forget) so no request path ever waits on the * analytics layer; ordering within the runtime preserves event order. */ export const selfHostAnalytics: AnalyticsService = { record: (name, properties) => Effect.sync(() => { getAnalyticsRuntime().runFork( Effect.flatMap(Analytics.asEffect(), (analytics) => analytics.record(name, properties)), ); }), flush: Effect.promise(() => getAnalyticsRuntime().runPromise( Effect.flatMap(Analytics.asEffect(), (analytics) => analytics.flush), ), ), }; /** Flush and dispose the analytics runtime (server shutdown). */ export const disposeAnalytics = async (): Promise => { const runtime = analyticsRuntime; if (!runtime) return; analyticsRuntime = null; await Effect.runPromise(Effect.promise(() => runtime.dispose()).pipe(Effect.ignoreCause())); }; /** * The execution-analytics `EngineDecorator`: every engine the shared stack * builds gets wrapped once, with the plane derived from what the stack was * built to serve — an MCP session carries its `mcpResource`, the HTTP * executions plane has none. The wrap point IS the plane, so misattribution * is structurally impossible. */ export const SelfHostAnalyticsEngineDecorator: Layer.Layer = Layer.succeed( EngineDecorator, )({ decorate: (engine, _identity, context) => withExecutionAnalytics(engine, selfHostAnalytics, { plane: context.mcpResource === undefined ? "api" : "mcp", toolkit: context.mcpResource?.kind === "toolkit", }), });