bd867019f9
Adds a new Codex plugin component `telemetry` that emits a single omo_codex_daily_active event (reason: session_start) from every Codex SessionStart hook, with the same UTC-day deduplication, hashed installation identifier, and four-flag opt-out as the install-time event. Previously omo-codex telemetry only fired on install_completed, so DAU/WAU/MAU under-reported real Codex usage. - New plugin component packages/omo-codex/plugin/components/telemetry/ mirrors the rules/comment-checker/lsp pattern: own src/, tsc build, vitest tests, package.json (posthog-node dep), hooks/hooks.json. - src/codex-hook.ts wraps createPluginPostHog().trackActive(..., "session_start") with safeShutdown so Codex session startup never blocks on telemetry. - Plugin root hooks.json + workspaces register the new component alongside rules and ultrawork on SessionStart. - Aggregate test expectations updated to include the telemetry directory. - cross-package-equivalence.test.ts pins product-identity constants and shouldDisablePostHog behavior to stay byte-equivalent between the CLI installer (src/telemetry/) and the plugin runtime (plugin/components/telemetry/src/), so the two PostHog sources never drift on event name, distinct_id base, dedup file path, or opt-out flags. - PostHogActivityReason union in the CLI-side posthog.ts gains "session_start" so future CLI paths can emit the same reason without a type break.
70 lines
1.9 KiB
JavaScript
70 lines
1.9 KiB
JavaScript
#!/usr/bin/env node
|
|
import { stdin as processStdin, stdout as processStdout } from "node:process";
|
|
|
|
import { type CodexSessionStartInput, runSessionStartHook } from "./codex-hook.js";
|
|
|
|
const command = process.argv[2];
|
|
const subcommand = process.argv[3];
|
|
|
|
if (command === "hook" && subcommand === "session-start") {
|
|
await runHookCli();
|
|
} else {
|
|
process.stderr.write("Usage: codex-telemetry hook session-start\n");
|
|
process.exitCode = 1;
|
|
}
|
|
|
|
async function runHookCli(): Promise<void> {
|
|
const raw = await readStdin();
|
|
if (raw.trim().length === 0) return;
|
|
const parsed = parseHookInput(raw);
|
|
if (!isCodexSessionStartInput(parsed)) return;
|
|
const output = await runSessionStartHook(parsed);
|
|
if (output.length > 0) {
|
|
processStdout.write(output);
|
|
}
|
|
}
|
|
|
|
function parseHookInput(raw: string): unknown | undefined {
|
|
try {
|
|
const parsed: unknown = JSON.parse(raw);
|
|
return parsed;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function isCodexSessionStartInput(value: unknown): value is CodexSessionStartInput {
|
|
return (
|
|
isRecord(value) &&
|
|
value["hook_event_name"] === "SessionStart" &&
|
|
typeof value["session_id"] === "string" &&
|
|
isStringOrNull(value["transcript_path"]) &&
|
|
typeof value["cwd"] === "string" &&
|
|
typeof value["model"] === "string" &&
|
|
typeof value["permission_mode"] === "string" &&
|
|
typeof value["source"] === "string"
|
|
);
|
|
}
|
|
|
|
function isStringOrNull(value: unknown): value is string | null {
|
|
return typeof value === "string" || value === null;
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
}
|
|
|
|
function readStdin(): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
let data = "";
|
|
processStdin.setEncoding("utf8");
|
|
processStdin.on("data", (chunk: string) => {
|
|
data += chunk;
|
|
});
|
|
processStdin.once("error", reject);
|
|
processStdin.once("end", () => {
|
|
resolve(data);
|
|
});
|
|
});
|
|
}
|