fix(telemetry): remove PostHog HAU tracking, keep DAU only
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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(),
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user