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: 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
+26 -8
View File
@@ -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,9 +106,33 @@ 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 }) rmSync(dataHomePath, { recursive: true, force: true })
+1 -13
View File
@@ -8,14 +8,11 @@ import { writeFileAtomically } from "./write-file-atomically"
type PostHogActivityState = { type PostHogActivityState = {
lastActiveDayUTC?: string lastActiveDayUTC?: string
lastActiveHourUTC?: string
} }
type PostHogActivityCaptureState = { type PostHogActivityCaptureState = {
dayUTC: string dayUTC: string
hourUTC: string
captureDaily: boolean captureDaily: boolean
captureHourly: boolean
} }
const POSTHOG_ACTIVITY_STATE_FILE = "posthog-activity.json" const POSTHOG_ACTIVITY_STATE_FILE = "posthog-activity.json"
@@ -28,10 +25,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 +68,17 @@ 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, lastActiveDayUTC: captureDaily ? dayUTC : state.lastActiveDayUTC,
lastActiveHourUTC: captureHourly ? hourUTC : state.lastActiveHourUTC,
}) })
} }
return { return {
dayUTC, dayUTC,
hourUTC,
captureDaily, 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")> { 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 {
@@ -55,3 +86,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"
@@ -117,7 +136,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({
@@ -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(), shutdown: async () => configuredClient.shutdown(),
} }