fix(telemetry): remove PostHog HAU tracking, keep DAU only

This commit is contained in:
YeonGyu-Kim
2026-04-26 15:05:32 +09:00
parent b32620ca47
commit e0230a435d
5 changed files with 158 additions and 44 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:
- 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
- Error diagnostics captured during failed CLI runs
- 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
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
+26 -8
View File
@@ -37,9 +37,7 @@ describe("getPostHogActivityCaptureState", () => {
// then
expect(result).toEqual({
dayUTC: "2026-04-11",
hourUTC: "2026-04-11T10",
captureDaily: true,
captureHourly: true,
})
rmSync(dataHomePath, { recursive: true, force: true })
@@ -60,9 +58,7 @@ describe("getPostHogActivityCaptureState", () => {
// then
expect(result).toEqual({
dayUTC: "2026-04-11",
hourUTC: "2026-04-11T10",
captureDaily: true,
captureHourly: true,
})
rmSync(dataHomePath, { recursive: true, force: true })
@@ -83,9 +79,7 @@ describe("getPostHogActivityCaptureState", () => {
// then
expect(result).toEqual({
dayUTC: "2026-04-11",
hourUTC: "2026-04-11T10",
captureDaily: true,
captureHourly: true,
})
rmSync(dataHomePath, { recursive: true, force: true })
@@ -112,9 +106,33 @@ describe("getPostHogActivityCaptureState", () => {
// then
expect(result).toEqual({
dayUTC: "2026-04-11",
hourUTC: "2026-04-11T10",
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 })
+1 -13
View File
@@ -8,14 +8,11 @@ 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"
@@ -28,10 +25,6 @@ function getUtcDayString(date: Date): string {
return date.toISOString().slice(0, 10)
}
function getUtcHourString(date: Date): string {
return date.toISOString().slice(0, 13)
}
function isPostHogActivityState(value: unknown): value is PostHogActivityState {
return value !== null && typeof value === "object" && !Array.isArray(value)
}
@@ -75,22 +68,17 @@ function writePostHogActivityState(nextState: PostHogActivityState): void {
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) {
if (captureDaily) {
writePostHogActivityState({
lastActiveDayUTC: captureDaily ? dayUTC : state.lastActiveDayUTC,
lastActiveHourUTC: captureHourly ? hourUTC : state.lastActiveHourUTC,
})
}
return {
dayUTC,
hourUTC,
captureDaily,
captureHourly,
}
}
+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")> {
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", () => {
beforeEach(() => {
mock.restore()
clearTelemetryEnv()
})
afterEach(() => {
mock.restore()
delete process.env.OMO_DISABLE_POSTHOG
delete process.env.OMO_SEND_ANONYMOUS_TELEMETRY
delete process.env.POSTHOG_API_KEY
delete process.env.POSTHOG_HOST
clearTelemetryEnv()
})
it("returns a no-op client when PostHog construction throws", async () => {
// given
process.env.OMO_DISABLE_POSTHOG = "0"
process.env.OMO_SEND_ANONYMOUS_TELEMETRY = "1"
process.env.POSTHOG_API_KEY = "test-api-key"
enableTelemetryEnv()
mock.module("posthog-node", () => ({
PostHog: class {
@@ -55,3 +86,73 @@ describe("posthog client creation", () => {
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 { 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_API_KEY = "phc_CFJhj5HyvA62QPhvyaUCtaq23aUfznnijg5VaaGkNk74"
@@ -117,7 +136,7 @@ function createPostHogClient(
})
},
trackActive: (distinctId, reason) => {
const activityState = getPostHogActivityCaptureState()
const activityState = resolveActivityState()
if (activityState.captureDaily) {
configuredClient.capture({
@@ -130,18 +149,6 @@ function createPostHogClient(
},
})
}
if (activityState.captureHourly) {
configuredClient.capture({
distinctId,
event: "omo_hourly_active",
properties: {
...sharedProperties,
hour_utc: activityState.hourUTC,
reason,
},
})
}
},
shutdown: async () => configuredClient.shutdown(),
}