Merge pull request #3698 from code-yeongyu/fix/posthog-cost-reduction

fix(telemetry): cut PostHog cost via HAU removal + plugin_loaded daily dedupe
This commit is contained in:
YeonGyu-Kim
2026-04-28 15:08:55 +09:00
committed by GitHub
7 changed files with 365 additions and 55 deletions
+2 -2
View File
@@ -16,7 +16,7 @@ We collect limited non-personal information needed to operate and improve the Se
When anonymous telemetry is enabled, the Application may collect: When anonymous telemetry is enabled, the Application may collect:
- Anonymous usage events, including `run_started`, `run_completed`, `run_failed`, `install_completed`, `install_failed`, `plugin_loaded`, `omo_daily_active`, and `omo_hourly_active` - Anonymous usage events, including `run_started`, `run_completed`, `run_failed`, `install_completed`, `install_failed`, `plugin_loaded`, and `omo_daily_active`
- Application metadata such as package version, plugin name, runtime, and command or entry-point context - Application metadata such as package version, plugin name, runtime, and command or entry-point context
- Error diagnostics captured during failed CLI runs - Error diagnostics captured during failed CLI runs
- A pseudonymous installation identifier derived from a one-way hash of the local hostname - A pseudonymous installation identifier derived from a one-way hash of the local hostname
@@ -25,7 +25,7 @@ We do not intentionally collect prompt contents, source files, repository conten
### Configuration and local state ### Configuration and local state
The Application stores local configuration and telemetry deduplication state on your machine to support installation, configuration, and anonymous daily or hourly active tracking. The Application stores local configuration and telemetry deduplication state on your machine to support installation, configuration, and anonymous daily active tracking.
## 2. How Telemetry Works ## 2. How Telemetry Works
+6
View File
@@ -104,6 +104,12 @@ function installModuleMocks(): void {
createPluginPostHog: mockCreatePluginPostHog, createPluginPostHog: mockCreatePluginPostHog,
getPostHogDistinctId: mockGetPostHogDistinctId, getPostHogDistinctId: mockGetPostHogDistinctId,
})) }))
mock.module("./shared/posthog-activity-state", () => ({
getPluginLoadedCaptureState: () => ({
dayUTC: "2026-04-18",
capturePluginLoaded: true,
}),
}))
} }
describe("oh-my-openagent telemetry isolation", () => { describe("oh-my-openagent telemetry isolation", () => {
+18 -9
View File
@@ -18,6 +18,7 @@ import { installAgentSortShim } from "./shared/agent-sort-shim"
import { detectExternalSkillPlugin, getSkillPluginConflictWarning } from "./shared/external-plugin-detector" import { detectExternalSkillPlugin, getSkillPluginConflictWarning } from "./shared/external-plugin-detector"
import { startBackgroundCheck as startTmuxCheck } from "./tools/interactive-bash" import { startBackgroundCheck as startTmuxCheck } from "./tools/interactive-bash"
import { createPluginPostHog, getPostHogDistinctId } from "./shared/posthog" import { createPluginPostHog, getPostHogDistinctId } from "./shared/posthog"
import { getPluginLoadedCaptureState } from "./shared/posthog-activity-state"
const serverPlugin: Plugin = async (input, _options): Promise<Hooks> => { const serverPlugin: Plugin = async (input, _options): Promise<Hooks> => {
installAgentSortShim() installAgentSortShim()
@@ -43,19 +44,27 @@ const serverPlugin: Plugin = async (input, _options): Promise<Hooks> => {
} catch { } catch {
// telemetry failure is non-fatal, silently ignore // telemetry failure is non-fatal, silently ignore
} }
let pluginLoadedCaptureState: ReturnType<typeof getPluginLoadedCaptureState> | null = null
try { try {
posthog.capture({ pluginLoadedCaptureState = getPluginLoadedCaptureState()
distinctId,
event: "plugin_loaded",
properties: {
entry_point: "plugin",
has_openclaw: !!pluginConfig.openclaw,
tmux_enabled: isTmuxIntegrationEnabled(pluginConfig),
},
})
} catch { } catch {
// telemetry failure is non-fatal, silently ignore // telemetry failure is non-fatal, silently ignore
} }
if (pluginLoadedCaptureState?.capturePluginLoaded) {
try {
posthog.capture({
distinctId,
event: "plugin_loaded",
properties: {
entry_point: "plugin",
has_openclaw: !!pluginConfig.openclaw,
tmux_enabled: isTmuxIntegrationEnabled(pluginConfig),
},
})
} catch {
// telemetry failure is non-fatal, silently ignore
}
}
if (pluginConfig.openclaw) { if (pluginConfig.openclaw) {
await initializeOpenClaw(pluginConfig.openclaw) await initializeOpenClaw(pluginConfig.openclaw)
} }
+182 -9
View File
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it } from "bun:test" import { afterEach, describe, expect, it } from "bun:test"
import { mkdirSync, rmSync, writeFileSync } from "node:fs" import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
import { join } from "node:path" import { join } from "node:path"
import { tmpdir } from "node:os" import { tmpdir } from "node:os"
@@ -37,9 +37,7 @@ describe("getPostHogActivityCaptureState", () => {
// then // then
expect(result).toEqual({ expect(result).toEqual({
dayUTC: "2026-04-11", dayUTC: "2026-04-11",
hourUTC: "2026-04-11T10",
captureDaily: true, captureDaily: true,
captureHourly: true,
}) })
rmSync(dataHomePath, { recursive: true, force: true }) rmSync(dataHomePath, { recursive: true, force: true })
@@ -60,9 +58,7 @@ describe("getPostHogActivityCaptureState", () => {
// then // then
expect(result).toEqual({ expect(result).toEqual({
dayUTC: "2026-04-11", dayUTC: "2026-04-11",
hourUTC: "2026-04-11T10",
captureDaily: true, captureDaily: true,
captureHourly: true,
}) })
rmSync(dataHomePath, { recursive: true, force: true }) rmSync(dataHomePath, { recursive: true, force: true })
@@ -83,9 +79,7 @@ describe("getPostHogActivityCaptureState", () => {
// then // then
expect(result).toEqual({ expect(result).toEqual({
dayUTC: "2026-04-11", dayUTC: "2026-04-11",
hourUTC: "2026-04-11T10",
captureDaily: true, captureDaily: true,
captureHourly: true,
}) })
rmSync(dataHomePath, { recursive: true, force: true }) rmSync(dataHomePath, { recursive: true, force: true })
@@ -112,11 +106,190 @@ describe("getPostHogActivityCaptureState", () => {
// then // then
expect(result).toEqual({ expect(result).toEqual({
dayUTC: "2026-04-11", dayUTC: "2026-04-11",
hourUTC: "2026-04-11T10",
captureDaily: false, captureDaily: false,
captureHourly: false, })
rmSync(dataHomePath, { recursive: true, force: true })
})
it("reads legacy hourly state without crashing", async () => {
// given
const dataHomePath = createDataHomePath()
const cachePath = join(dataHomePath, "oh-my-opencode")
mkdirSync(cachePath, { recursive: true })
writeFileSync(
join(cachePath, "posthog-activity.json"),
`${JSON.stringify({
lastActiveHourUTC: "2026-04-11T10",
})}\n`,
)
process.env.XDG_DATA_HOME = dataHomePath
const { getPostHogActivityCaptureState } = await importPostHogActivityStateModule()
// when
const result = getPostHogActivityCaptureState(new Date("2026-04-11T10:15:00.000Z"))
// then
expect(result).toEqual({
dayUTC: "2026-04-11",
captureDaily: true,
})
rmSync(dataHomePath, { recursive: true, force: true })
})
it("preserves lastPluginLoadedDayUTC when writing lastActiveDayUTC", async () => {
// given
const dataHomePath = createDataHomePath()
const cachePath = join(dataHomePath, "oh-my-opencode")
mkdirSync(cachePath, { recursive: true })
writeFileSync(
join(cachePath, "posthog-activity.json"),
`${JSON.stringify({
lastActiveDayUTC: "2026-04-10",
lastPluginLoadedDayUTC: "2026-04-11",
})}\n`,
)
process.env.XDG_DATA_HOME = dataHomePath
const { getPostHogActivityCaptureState } = await importPostHogActivityStateModule()
// when
getPostHogActivityCaptureState(new Date("2026-04-11T10:15:00.000Z"))
// then
const persistedState = JSON.parse(
readFileSync(join(cachePath, "posthog-activity.json"), "utf-8"),
)
expect(persistedState).toEqual({
lastActiveDayUTC: "2026-04-11",
lastPluginLoadedDayUTC: "2026-04-11",
}) })
rmSync(dataHomePath, { recursive: true, force: true }) rmSync(dataHomePath, { recursive: true, force: true })
}) })
}) })
describe("getPluginLoadedCaptureState", () => {
it("returns capturePluginLoaded=true when activity file does not exist", async () => {
// given
const dataHomePath = createDataHomePath()
process.env.XDG_DATA_HOME = dataHomePath
const { getPluginLoadedCaptureState } = await importPostHogActivityStateModule()
// when
const result = getPluginLoadedCaptureState(new Date("2026-04-11T10:15:00.000Z"))
// then
expect(result).toEqual({
dayUTC: "2026-04-11",
capturePluginLoaded: true,
})
rmSync(dataHomePath, { recursive: true, force: true })
})
it("returns capturePluginLoaded=false when lastPluginLoadedDayUTC matches today", async () => {
// given
const dataHomePath = createDataHomePath()
const cachePath = join(dataHomePath, "oh-my-opencode")
mkdirSync(cachePath, { recursive: true })
writeFileSync(
join(cachePath, "posthog-activity.json"),
`${JSON.stringify({
lastPluginLoadedDayUTC: "2026-04-11",
})}\n`,
)
process.env.XDG_DATA_HOME = dataHomePath
const { getPluginLoadedCaptureState } = await importPostHogActivityStateModule()
// when
const result = getPluginLoadedCaptureState(new Date("2026-04-11T10:15:00.000Z"))
// then
expect(result).toEqual({
dayUTC: "2026-04-11",
capturePluginLoaded: false,
})
rmSync(dataHomePath, { recursive: true, force: true })
})
it("returns capturePluginLoaded=true when lastPluginLoadedDayUTC is from a previous day", async () => {
// given
const dataHomePath = createDataHomePath()
const cachePath = join(dataHomePath, "oh-my-opencode")
mkdirSync(cachePath, { recursive: true })
writeFileSync(
join(cachePath, "posthog-activity.json"),
`${JSON.stringify({
lastPluginLoadedDayUTC: "2026-04-10",
})}\n`,
)
process.env.XDG_DATA_HOME = dataHomePath
const { getPluginLoadedCaptureState } = await importPostHogActivityStateModule()
// when
const result = getPluginLoadedCaptureState(new Date("2026-04-11T10:15:00.000Z"))
// then
expect(result).toEqual({
dayUTC: "2026-04-11",
capturePluginLoaded: true,
})
rmSync(dataHomePath, { recursive: true, force: true })
})
it("preserves lastActiveDayUTC when writing lastPluginLoadedDayUTC", async () => {
// given
const dataHomePath = createDataHomePath()
const cachePath = join(dataHomePath, "oh-my-opencode")
mkdirSync(cachePath, { recursive: true })
writeFileSync(
join(cachePath, "posthog-activity.json"),
`${JSON.stringify({
lastActiveDayUTC: "2026-04-11",
lastPluginLoadedDayUTC: "2026-04-10",
})}\n`,
)
process.env.XDG_DATA_HOME = dataHomePath
const { getPluginLoadedCaptureState } = await importPostHogActivityStateModule()
// when
getPluginLoadedCaptureState(new Date("2026-04-11T10:15:00.000Z"))
// then
const persistedState = JSON.parse(
readFileSync(join(cachePath, "posthog-activity.json"), "utf-8"),
)
expect(persistedState).toEqual({
lastActiveDayUTC: "2026-04-11",
lastPluginLoadedDayUTC: "2026-04-11",
})
rmSync(dataHomePath, { recursive: true, force: true })
})
it("does not rewrite state when lastPluginLoadedDayUTC matches today", async () => {
// given
const dataHomePath = createDataHomePath()
const cachePath = join(dataHomePath, "oh-my-opencode")
mkdirSync(cachePath, { recursive: true })
const initialPayload = `${JSON.stringify({
lastActiveDayUTC: "2026-04-10",
lastPluginLoadedDayUTC: "2026-04-11",
})}\n`
writeFileSync(join(cachePath, "posthog-activity.json"), initialPayload)
process.env.XDG_DATA_HOME = dataHomePath
const { getPluginLoadedCaptureState } = await importPostHogActivityStateModule()
// when
getPluginLoadedCaptureState(new Date("2026-04-11T10:15:00.000Z"))
// then
const persistedPayload = readFileSync(join(cachePath, "posthog-activity.json"), "utf-8")
expect(persistedPayload).toBe(initialPayload)
rmSync(dataHomePath, { recursive: true, force: true })
})
})
+28 -14
View File
@@ -8,14 +8,17 @@ import { writeFileAtomically } from "./write-file-atomically"
type PostHogActivityState = { type PostHogActivityState = {
lastActiveDayUTC?: string lastActiveDayUTC?: string
lastActiveHourUTC?: string lastPluginLoadedDayUTC?: string
} }
type PostHogActivityCaptureState = { type PostHogActivityCaptureState = {
dayUTC: string dayUTC: string
hourUTC: string
captureDaily: boolean captureDaily: boolean
captureHourly: boolean }
type PluginLoadedCaptureState = {
dayUTC: string
capturePluginLoaded: boolean
} }
const POSTHOG_ACTIVITY_STATE_FILE = "posthog-activity.json" const POSTHOG_ACTIVITY_STATE_FILE = "posthog-activity.json"
@@ -28,10 +31,6 @@ function getUtcDayString(date: Date): string {
return date.toISOString().slice(0, 10) return date.toISOString().slice(0, 10)
} }
function getUtcHourString(date: Date): string {
return date.toISOString().slice(0, 13)
}
function isPostHogActivityState(value: unknown): value is PostHogActivityState { function isPostHogActivityState(value: unknown): value is PostHogActivityState {
return value !== null && typeof value === "object" && !Array.isArray(value) return value !== null && typeof value === "object" && !Array.isArray(value)
} }
@@ -75,22 +74,37 @@ function writePostHogActivityState(nextState: PostHogActivityState): void {
export function getPostHogActivityCaptureState(now: Date = new Date()): PostHogActivityCaptureState { export function getPostHogActivityCaptureState(now: Date = new Date()): PostHogActivityCaptureState {
const state = readPostHogActivityState() const state = readPostHogActivityState()
const dayUTC = getUtcDayString(now) const dayUTC = getUtcDayString(now)
const hourUTC = getUtcHourString(now)
const captureDaily = state.lastActiveDayUTC !== dayUTC const captureDaily = state.lastActiveDayUTC !== dayUTC
const captureHourly = state.lastActiveHourUTC !== hourUTC
if (captureDaily || captureHourly) { if (captureDaily) {
writePostHogActivityState({ writePostHogActivityState({
lastActiveDayUTC: captureDaily ? dayUTC : state.lastActiveDayUTC, ...state,
lastActiveHourUTC: captureHourly ? hourUTC : state.lastActiveHourUTC, lastActiveDayUTC: dayUTC,
}) })
} }
return { return {
dayUTC, dayUTC,
hourUTC,
captureDaily, captureDaily,
captureHourly, }
}
export function getPluginLoadedCaptureState(now: Date = new Date()): PluginLoadedCaptureState {
const state = readPostHogActivityState()
const dayUTC = getUtcDayString(now)
const capturePluginLoaded = state.lastPluginLoadedDayUTC !== dayUTC
if (capturePluginLoaded) {
writePostHogActivityState({
...state,
lastPluginLoadedDayUTC: dayUTC,
})
}
return {
dayUTC,
capturePluginLoaded,
} }
} }
+109 -8
View File
@@ -1,23 +1,54 @@
import { afterEach, describe, expect, it, mock } from "bun:test" import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"
type CapturedPostHogMessage = {
distinctId: string
event: string
properties?: Record<string, unknown>
}
async function importPostHogModule(): Promise<typeof import("./posthog")> { async function importPostHogModule(): Promise<typeof import("./posthog")> {
return import(`./posthog?test=${Date.now()}-${Math.random()}`) return import(`./posthog?test=${Date.now()}-${Math.random()}`)
} }
function enableTelemetryEnv(): void {
process.env.OMO_DISABLE_POSTHOG = "0"
process.env.OMO_SEND_ANONYMOUS_TELEMETRY = "1"
process.env.POSTHOG_API_KEY = "test-api-key"
}
function clearTelemetryEnv(): void {
delete process.env.OMO_DISABLE_POSTHOG
delete process.env.OMO_SEND_ANONYMOUS_TELEMETRY
delete process.env.POSTHOG_API_KEY
delete process.env.POSTHOG_HOST
}
function mockPostHogNode(capturedMessages: CapturedPostHogMessage[]): void {
mock.module("posthog-node", () => ({
PostHog: class {
capture(message: CapturedPostHogMessage): void {
capturedMessages.push(message)
}
captureException(): void {}
async shutdown(): Promise<void> {}
},
}))
}
describe("posthog client creation", () => { describe("posthog client creation", () => {
beforeEach(() => {
mock.restore()
clearTelemetryEnv()
})
afterEach(() => { afterEach(() => {
mock.restore() mock.restore()
delete process.env.OMO_DISABLE_POSTHOG clearTelemetryEnv()
delete process.env.OMO_SEND_ANONYMOUS_TELEMETRY
delete process.env.POSTHOG_API_KEY
delete process.env.POSTHOG_HOST
}) })
it("returns a no-op client when PostHog construction throws", async () => { it("returns a no-op client when PostHog construction throws", async () => {
// given // given
process.env.OMO_DISABLE_POSTHOG = "0" enableTelemetryEnv()
process.env.OMO_SEND_ANONYMOUS_TELEMETRY = "1"
process.env.POSTHOG_API_KEY = "test-api-key"
mock.module("posthog-node", () => ({ mock.module("posthog-node", () => ({
PostHog: class { PostHog: class {
@@ -100,3 +131,73 @@ describe("posthog client creation", () => {
await expect(pluginPostHog.shutdown()).resolves.toBeUndefined() await expect(pluginPostHog.shutdown()).resolves.toBeUndefined()
}) })
}) })
describe("posthog trackActive emission contract", () => {
let resetActivityStateProvider: (() => void) | null = null
beforeEach(() => {
mock.restore()
clearTelemetryEnv()
})
afterEach(() => {
resetActivityStateProvider?.()
resetActivityStateProvider = null
mock.restore()
clearTelemetryEnv()
})
it("emits exactly one omo_daily_active and never omo_hourly_active when captureDaily is true", async () => {
// given
enableTelemetryEnv()
const captured: CapturedPostHogMessage[] = []
mockPostHogNode(captured)
const posthogModule = await importPostHogModule()
posthogModule.__setActivityStateProviderForTesting(() => ({
dayUTC: "2026-04-18",
captureDaily: true,
}))
resetActivityStateProvider = posthogModule.__resetActivityStateProviderForTesting
const client = posthogModule.createCliPostHog()
// when
client.trackActive("distinct-cli", "run_started")
// then
expect(captured).toHaveLength(1)
const emittedEvents = captured.map((message) => message.event)
expect(emittedEvents).not.toContain("omo_hourly_active")
const [dailyEvent] = captured
expect(dailyEvent?.event).toBe("omo_daily_active")
expect(dailyEvent?.distinctId).toBe("distinct-cli")
expect(dailyEvent?.properties).toMatchObject({
day_utc: "2026-04-18",
reason: "run_started",
source: "cli",
})
expect(dailyEvent?.properties).not.toHaveProperty("hour_utc")
})
it("emits nothing and never omo_hourly_active when captureDaily is false", async () => {
// given
enableTelemetryEnv()
const captured: CapturedPostHogMessage[] = []
mockPostHogNode(captured)
const posthogModule = await importPostHogModule()
posthogModule.__setActivityStateProviderForTesting(() => ({
dayUTC: "2026-04-18",
captureDaily: false,
}))
resetActivityStateProvider = posthogModule.__resetActivityStateProviderForTesting
const client = posthogModule.createPluginPostHog()
// when
client.trackActive("distinct-plugin", "plugin_loaded")
// then
expect(captured).toHaveLength(0)
const emittedEvents = captured.map((message) => message.event)
expect(emittedEvents).not.toContain("omo_daily_active")
expect(emittedEvents).not.toContain("omo_hourly_active")
})
})
+20 -13
View File
@@ -5,6 +5,25 @@ import packageJson from "../../package.json" with { type: "json" }
import { PLUGIN_NAME, PUBLISHED_PACKAGE_NAME } from "./plugin-identity" import { PLUGIN_NAME, PUBLISHED_PACKAGE_NAME } from "./plugin-identity"
import { getPostHogActivityCaptureState } from "./posthog-activity-state" import { getPostHogActivityCaptureState } from "./posthog-activity-state"
/** @internal test-only seam: keep null in production to use the real implementation. */
let activityStateProviderOverride: typeof getPostHogActivityCaptureState | null = null
function resolveActivityState(): ReturnType<typeof getPostHogActivityCaptureState> {
return (activityStateProviderOverride ?? getPostHogActivityCaptureState)()
}
/** @internal test-only */
export function __setActivityStateProviderForTesting(
provider: typeof getPostHogActivityCaptureState,
): void {
activityStateProviderOverride = provider
}
/** @internal test-only */
export function __resetActivityStateProviderForTesting(): void {
activityStateProviderOverride = null
}
const DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com" const DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com"
const DEFAULT_POSTHOG_API_KEY = "phc_CFJhj5HyvA62QPhvyaUCtaq23aUfznnijg5VaaGkNk74" const DEFAULT_POSTHOG_API_KEY = "phc_CFJhj5HyvA62QPhvyaUCtaq23aUfznnijg5VaaGkNk74"
@@ -128,7 +147,7 @@ function createPostHogClient(
}) })
}, },
trackActive: (distinctId, reason) => { trackActive: (distinctId, reason) => {
const activityState = getPostHogActivityCaptureState() const activityState = resolveActivityState()
if (activityState.captureDaily) { if (activityState.captureDaily) {
configuredClient.capture({ configuredClient.capture({
@@ -141,18 +160,6 @@ function createPostHogClient(
}, },
}) })
} }
if (activityState.captureHourly) {
configuredClient.capture({
distinctId,
event: "omo_hourly_active",
properties: {
...sharedProperties,
hour_utc: activityState.hourUTC,
reason,
},
})
}
}, },
shutdown: async () => configuredClient.shutdown(), shutdown: async () => configuredClient.shutdown(),
} }