feat(telemetry): add PostHog client factory with activity state tracking

This commit is contained in:
YeonGyu-Kim
2026-04-11 14:14:24 +09:00
parent 146be96716
commit 280a8a34e1
2 changed files with 203 additions and 10 deletions
+89
View File
@@ -0,0 +1,89 @@
import { existsSync, mkdirSync, readFileSync } from "node:fs"
import { join } from "node:path"
import { getDataDir } from "./data-path"
import { log } from "./logger"
import { CACHE_DIR_NAME } from "./plugin-identity"
import { writeFileAtomically } from "./write-file-atomically"
type PostHogActivityState = {
lastActiveDayUTC?: string
lastActiveHourUTC?: string
}
type PostHogActivityCaptureState = {
dayUTC: string
hourUTC: string
captureDaily: boolean
captureHourly: boolean
}
const POSTHOG_ACTIVITY_STATE_FILE = "posthog-activity.json"
function getPostHogActivityStateFilePath(): string {
return join(getDataDir(), CACHE_DIR_NAME, POSTHOG_ACTIVITY_STATE_FILE)
}
function getUtcDayString(date: Date): string {
return date.toISOString().slice(0, 10)
}
function getUtcHourString(date: Date): string {
return date.toISOString().slice(0, 13)
}
function readPostHogActivityState(): PostHogActivityState {
const stateFilePath = getPostHogActivityStateFilePath()
if (!existsSync(stateFilePath)) {
return {}
}
try {
const content = readFileSync(stateFilePath, "utf-8")
const parsed = JSON.parse(content) as PostHogActivityState
return parsed
} catch (error) {
log("[posthog-activity-state] Failed to read activity state", {
error: String(error),
stateFilePath,
})
return {}
}
}
function writePostHogActivityState(nextState: PostHogActivityState): void {
const stateFilePath = getPostHogActivityStateFilePath()
try {
mkdirSync(join(getDataDir(), CACHE_DIR_NAME), { recursive: true })
writeFileAtomically(stateFilePath, `${JSON.stringify(nextState, null, 2)}\n`)
} catch (error) {
log("[posthog-activity-state] Failed to write activity state", {
error: String(error),
stateFilePath,
})
}
}
export function getPostHogActivityCaptureState(now: Date = new Date()): PostHogActivityCaptureState {
const state = readPostHogActivityState()
const dayUTC = getUtcDayString(now)
const hourUTC = getUtcHourString(now)
const captureDaily = state.lastActiveDayUTC !== dayUTC
const captureHourly = state.lastActiveHourUTC !== hourUTC
if (captureDaily || captureHourly) {
writePostHogActivityState({
lastActiveDayUTC: captureDaily ? dayUTC : state.lastActiveDayUTC,
lastActiveHourUTC: captureHourly ? hourUTC : state.lastActiveHourUTC,
})
}
return {
dayUTC,
hourUTC,
captureDaily,
captureHourly,
}
}
+114 -10
View File
@@ -1,37 +1,140 @@
import os from "os"
import { createHash } from "node:crypto"
import { PostHog } from "posthog-node"
import packageJson from "../../package.json" with { type: "json" }
import { PLUGIN_NAME, PUBLISHED_PACKAGE_NAME } from "./plugin-identity"
import { getPostHogActivityCaptureState } from "./posthog-activity-state"
type PostHogClient = Pick<PostHog, "capture" | "captureException" | "shutdown">
const DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com"
const DEFAULT_POSTHOG_API_KEY = "phc_CFJhj5HyvA62QPhvyaUCtaq23aUfznnijg5VaaGkNk74"
type PostHogCaptureEvent = Parameters<PostHog["capture"]>[0]
type PostHogExceptionProperties = Parameters<PostHog["captureException"]>[2]
type PostHogSource = "cli" | "plugin"
type PostHogActivityReason = "run_started" | "plugin_loaded"
type PostHogClient = {
capture: (message: PostHogCaptureEvent) => void
captureException: (
error: unknown,
distinctId?: string,
additionalProperties?: PostHogExceptionProperties,
) => void
trackActive: (distinctId: string, reason: PostHogActivityReason) => void
shutdown: () => Promise<void>
}
const NO_OP_POSTHOG: PostHogClient = {
capture: () => undefined,
captureException: () => undefined,
trackActive: () => undefined,
shutdown: async () => undefined,
}
function isFalsy(value: string | undefined): boolean {
return value === "0" || value === "false" || value === "no"
}
function shouldDisablePostHog(): boolean {
return process.env.OMO_DISABLE_POSTHOG === "true"
if (process.env.OMO_DISABLE_POSTHOG === "true" || process.env.OMO_DISABLE_POSTHOG === "1") {
return true
}
return isFalsy(process.env.OMO_SEND_ANONYMOUS_TELEMETRY?.trim().toLowerCase())
}
function hasPostHogApiKey(): boolean {
return (process.env.POSTHOG_API_KEY ?? "").trim().length > 0
return getPostHogApiKey().length > 0
}
function createPostHogClient(options: ConstructorParameters<typeof PostHog>[1]): PostHogClient {
function getPostHogApiKey(): string {
return process.env.POSTHOG_API_KEY?.trim() || DEFAULT_POSTHOG_API_KEY
}
function getPostHogHost(): string {
return process.env.POSTHOG_HOST?.trim() || DEFAULT_POSTHOG_HOST
}
function getSharedProperties(source: PostHogSource): NonNullable<PostHogCaptureEvent["properties"]> {
return {
platform: "oh-my-opencode",
package_name: PUBLISHED_PACKAGE_NAME,
plugin_name: PLUGIN_NAME,
package_version: packageJson.version,
runtime: "bun",
source,
}
}
function createPostHogClient(
source: PostHogSource,
options: ConstructorParameters<typeof PostHog>[1],
): PostHogClient {
if (shouldDisablePostHog() || !hasPostHogApiKey()) {
return NO_OP_POSTHOG
}
return new PostHog(process.env.POSTHOG_API_KEY ?? "", options)
const configuredClient = new PostHog(getPostHogApiKey(), {
...options,
host: getPostHogHost(),
})
const sharedProperties = getSharedProperties(source)
return {
capture: (message) => {
configuredClient.capture({
...message,
properties: {
...sharedProperties,
...message.properties,
},
})
},
captureException: (error, distinctId, additionalProperties) => {
configuredClient.captureException(error, distinctId, {
...sharedProperties,
...additionalProperties,
})
},
trackActive: (distinctId, reason) => {
const activityState = getPostHogActivityCaptureState()
if (activityState.captureDaily) {
configuredClient.capture({
distinctId,
event: "omo_daily_active",
properties: {
...sharedProperties,
day_utc: activityState.dayUTC,
reason,
},
})
}
if (activityState.captureHourly) {
configuredClient.capture({
distinctId,
event: "omo_hourly_active",
properties: {
...sharedProperties,
hour_utc: activityState.hourUTC,
reason,
},
})
}
},
shutdown: async () => configuredClient.shutdown(),
}
}
export function getPostHogDistinctId(): string {
return os.hostname()
return createHash("sha256")
.update(`${PUBLISHED_PACKAGE_NAME}:${os.hostname()}`)
.digest("hex")
}
export function createCliPostHog(): PostHogClient {
return createPostHogClient({
host: process.env.POSTHOG_HOST,
return createPostHogClient("cli", {
enableExceptionAutocapture: true,
flushAt: 1,
flushInterval: 0,
@@ -39,8 +142,9 @@ export function createCliPostHog(): PostHogClient {
}
export function createPluginPostHog(): PostHogClient {
return createPostHogClient({
host: process.env.POSTHOG_HOST,
return createPostHogClient("plugin", {
enableExceptionAutocapture: true,
flushAt: 1,
flushInterval: 0,
})
}