feat(omo-codex): wire Codex SessionStart telemetry for DAU/WAU/MAU
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.
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import os from "node:os";
|
||||
|
||||
import { PostHog } from "posthog-node";
|
||||
|
||||
import { getPostHogApiKey, getPostHogHost, hasPostHogApiKey, shouldDisablePostHog } from "./env-flags.js";
|
||||
import { getPostHogActivityCaptureState } from "./posthog-activity-state.js";
|
||||
import {
|
||||
DEFAULT_POSTHOG_API_KEY,
|
||||
DEFAULT_POSTHOG_HOST,
|
||||
EVENT_NAME,
|
||||
PACKAGE_NAME,
|
||||
PRODUCT_NAME,
|
||||
getComponentVersion,
|
||||
} from "./product-identity.js";
|
||||
|
||||
export { DEFAULT_POSTHOG_API_KEY, DEFAULT_POSTHOG_HOST };
|
||||
|
||||
export type PostHogActivityReason = "session_start";
|
||||
|
||||
export type PostHogClient = {
|
||||
trackActive: (distinctId: string, reason: PostHogActivityReason) => void;
|
||||
shutdown: () => Promise<void>;
|
||||
};
|
||||
|
||||
type OsProvider = Pick<typeof os, "arch" | "cpus" | "hostname" | "platform" | "release" | "totalmem" | "type">;
|
||||
type ActivityStateProvider = typeof getPostHogActivityCaptureState;
|
||||
|
||||
let osProviderOverride: OsProvider | null = null;
|
||||
let activityStateProviderOverride: ActivityStateProvider | null = null;
|
||||
|
||||
const NO_OP_POSTHOG: PostHogClient = {
|
||||
trackActive: () => undefined,
|
||||
shutdown: async () => undefined,
|
||||
};
|
||||
|
||||
type PostHogCaptureEvent = Parameters<PostHog["capture"]>[0];
|
||||
|
||||
function resolveOsProvider(): OsProvider {
|
||||
return osProviderOverride ?? os;
|
||||
}
|
||||
|
||||
function resolveActivityStateProvider(): ActivityStateProvider {
|
||||
return activityStateProviderOverride ?? getPostHogActivityCaptureState;
|
||||
}
|
||||
|
||||
function getSafeCpuInfo(): { readonly count: number; readonly model: string | undefined } {
|
||||
try {
|
||||
const cpuInfo = resolveOsProvider().cpus();
|
||||
return {
|
||||
count: cpuInfo.length,
|
||||
model: cpuInfo[0]?.model,
|
||||
};
|
||||
} catch {
|
||||
return {
|
||||
count: 0,
|
||||
model: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function getSharedProperties(): NonNullable<PostHogCaptureEvent["properties"]> {
|
||||
const osProvider = resolveOsProvider();
|
||||
const cpuInfo = getSafeCpuInfo();
|
||||
|
||||
return {
|
||||
platform: "omo-codex",
|
||||
product_name: PRODUCT_NAME,
|
||||
package_name: PACKAGE_NAME,
|
||||
package_version: getComponentVersion(),
|
||||
runtime: "node",
|
||||
runtime_version: process.version,
|
||||
source: "plugin",
|
||||
$os: osProvider.platform(),
|
||||
$os_version: osProvider.release(),
|
||||
os_arch: osProvider.arch(),
|
||||
os_type: osProvider.type(),
|
||||
cpu_count: cpuInfo.count,
|
||||
cpu_model: cpuInfo.model,
|
||||
total_memory_gb: Math.round(osProvider.totalmem() / 1024 / 1024 / 1024),
|
||||
locale: Intl.DateTimeFormat().resolvedOptions().locale,
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
shell: process.env["SHELL"],
|
||||
ci: Boolean(process.env["CI"]),
|
||||
terminal: process.env["TERM_PROGRAM"],
|
||||
};
|
||||
}
|
||||
|
||||
export function createPluginPostHog(): PostHogClient {
|
||||
if (shouldDisablePostHog() || !hasPostHogApiKey()) {
|
||||
return NO_OP_POSTHOG;
|
||||
}
|
||||
|
||||
let client: PostHog;
|
||||
try {
|
||||
client = new PostHog(getPostHogApiKey(), {
|
||||
enableExceptionAutocapture: false,
|
||||
enableLocalEvaluation: false,
|
||||
strictLocalEvaluation: true,
|
||||
disableRemoteConfig: true,
|
||||
flushAt: 1,
|
||||
flushInterval: 0,
|
||||
host: getPostHogHost(),
|
||||
disableGeoip: false,
|
||||
});
|
||||
} catch {
|
||||
return NO_OP_POSTHOG;
|
||||
}
|
||||
|
||||
const sharedProperties = getSharedProperties();
|
||||
|
||||
return {
|
||||
trackActive: (distinctId, reason) => {
|
||||
const activityState = resolveActivityStateProvider()();
|
||||
if (!activityState.captureDaily) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.capture({
|
||||
distinctId,
|
||||
event: EVENT_NAME,
|
||||
properties: {
|
||||
...sharedProperties,
|
||||
$process_person_profile: false,
|
||||
day_utc: activityState.dayUTC,
|
||||
reason,
|
||||
},
|
||||
});
|
||||
},
|
||||
shutdown: async () => client.shutdown(),
|
||||
};
|
||||
}
|
||||
|
||||
export function getPostHogDistinctId(): string {
|
||||
return createHash("sha256").update(`omo-codex:${resolveOsProvider().hostname()}`).digest("hex");
|
||||
}
|
||||
|
||||
/** @internal test-only */
|
||||
export function __setOsProviderForTesting(provider: OsProvider): void {
|
||||
osProviderOverride = provider;
|
||||
}
|
||||
|
||||
/** @internal test-only */
|
||||
export function __resetOsProviderForTesting(): void {
|
||||
osProviderOverride = null;
|
||||
}
|
||||
|
||||
/** @internal test-only */
|
||||
export function __setActivityStateProviderForTesting(provider: ActivityStateProvider): void {
|
||||
activityStateProviderOverride = provider;
|
||||
}
|
||||
|
||||
/** @internal test-only */
|
||||
export function __resetActivityStateProviderForTesting(): void {
|
||||
activityStateProviderOverride = null;
|
||||
}
|
||||
Reference in New Issue
Block a user