fix(telemetry): dedupe plugin_loaded event to once per UTC day

The plugin_loaded event was emitted on every plugin reload, generating
high event volume on PostHog (proportional to opencode restarts per
user per day). With MAU > 60K and active power users restarting
frequently, this drove unnecessary event spend.

Add a separate daily dedup state field (lastPluginLoadedDayUTC) so the
plugin_loaded capture only fires once per UTC day per machine. The
existing daily activity dedup (lastActiveDayUTC, used by
omo_daily_active) is preserved as an independent gate so the two
dimensions cannot overwrite each other in the activity state file.
This commit is contained in:
YeonGyu-Kim
2026-04-28 15:01:40 +09:00
parent 58e4b8f519
commit 669e0667be
4 changed files with 207 additions and 11 deletions
+6
View File
@@ -104,6 +104,12 @@ function installModuleMocks(): void {
createPluginPostHog: mockCreatePluginPostHog,
getPostHogDistinctId: mockGetPostHogDistinctId,
}))
mock.module("./shared/posthog-activity-state", () => ({
getPluginLoadedCaptureState: () => ({
dayUTC: "2026-04-18",
capturePluginLoaded: true,
}),
}))
}
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 { startBackgroundCheck as startTmuxCheck } from "./tools/interactive-bash"
import { createPluginPostHog, getPostHogDistinctId } from "./shared/posthog"
import { getPluginLoadedCaptureState } from "./shared/posthog-activity-state"
const serverPlugin: Plugin = async (input, _options): Promise<Hooks> => {
installAgentSortShim()
@@ -43,19 +44,27 @@ const serverPlugin: Plugin = async (input, _options): Promise<Hooks> => {
} catch {
// telemetry failure is non-fatal, silently ignore
}
let pluginLoadedCaptureState: ReturnType<typeof getPluginLoadedCaptureState> | null = null
try {
posthog.capture({
distinctId,
event: "plugin_loaded",
properties: {
entry_point: "plugin",
has_openclaw: !!pluginConfig.openclaw,
tmux_enabled: isTmuxIntegrationEnabled(pluginConfig),
},
})
pluginLoadedCaptureState = getPluginLoadedCaptureState()
} catch {
// 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) {
await initializeOpenClaw(pluginConfig.openclaw)
}
+156 -1
View File
@@ -1,5 +1,5 @@
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 { tmpdir } from "node:os"
@@ -137,4 +137,159 @@ describe("getPostHogActivityCaptureState", () => {
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 })
})
})
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 })
})
})
+27 -1
View File
@@ -8,6 +8,7 @@ import { writeFileAtomically } from "./write-file-atomically"
type PostHogActivityState = {
lastActiveDayUTC?: string
lastPluginLoadedDayUTC?: string
}
type PostHogActivityCaptureState = {
@@ -15,6 +16,11 @@ type PostHogActivityCaptureState = {
captureDaily: boolean
}
type PluginLoadedCaptureState = {
dayUTC: string
capturePluginLoaded: boolean
}
const POSTHOG_ACTIVITY_STATE_FILE = "posthog-activity.json"
function getPostHogActivityStateFilePath(): string {
@@ -73,7 +79,8 @@ export function getPostHogActivityCaptureState(now: Date = new Date()): PostHogA
if (captureDaily) {
writePostHogActivityState({
lastActiveDayUTC: captureDaily ? dayUTC : state.lastActiveDayUTC,
...state,
lastActiveDayUTC: dayUTC,
})
}
@@ -82,3 +89,22 @@ export function getPostHogActivityCaptureState(now: Date = new Date()): PostHogA
captureDaily,
}
}
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,
}
}