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:
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user