Merge pull request #3750 from code-yeongyu/fix/posthog-keep-only-dau
fix(telemetry): keep only omo_daily_active for DAU/WAU/MAU
This commit is contained in:
+1
-1
@@ -117,7 +117,7 @@ curl -s https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/refs/head
|
||||
|
||||
**注記**: 公開されているパッケージおよびバイナリ名は `oh-my-opencode` を使用してください。`opencode.json` 内では、互換性レイヤーがプラグインエントリ `oh-my-openagent` を優先しますが、従来の `oh-my-opencode` エントリも警告付きで読み込まれます。プラグイン設定ファイルは依然として `oh-my-opencode.json` または `oh-my-opencode.jsonc` を使用するのが一般的で、移行期間中は従来のファイル名と改名後のファイル名の両方が認識されます。
|
||||
|
||||
匿名のテレメトリは、インストールとランタイムの信頼性向上のためにデフォルトで有効になっています。これは PostHog を使用し、生のホスト名ではなくハッシュ化されたインストール識別子を使用します。無効化するには `OMO_SEND_ANONYMOUS_TELEMETRY=0` または `OMO_DISABLE_POSTHOG=1` を設定してください。[プライバシーポリシー](docs/legal/privacy-policy.md)と[利用規約](docs/legal/terms-of-service.md)をご覧ください。
|
||||
匿名のテレメトリは、アクティブなインストール数(DAU/WAU/MAU)の集計のためにデフォルトで有効になっています。マシン1台につきUTC日あたり最大1回イベントが送信され、ハッシュ化されたインストール識別子を使用し、生のホスト名は使用せず、PostHog person profile も作成されません。無効化するには `OMO_SEND_ANONYMOUS_TELEMETRY=0` または `OMO_DISABLE_POSTHOG=1` を設定してください。[プライバシーポリシー](docs/legal/privacy-policy.md)と[利用規約](docs/legal/terms-of-service.md)をご覧ください。
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -111,7 +111,7 @@ curl -s https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/refs/head
|
||||
|
||||
**참고**: 배포된 패키지와 바이너리 이름은 `oh-my-opencode`를 사용하세요. `opencode.json` 내부에서는 호환성 레이어가 이제 플러그인 엔트리 `oh-my-openagent`를 우선시하며, 레거시 `oh-my-opencode` 엔트리는 경고와 함께 여전히 로드됩니다. 플러그인 설정 파일은 여전히 일반적으로 `oh-my-opencode.json` 또는 `oh-my-opencode.jsonc`를 사용하며, 전환 기간 동안 레거시와 변경된 basename 모두 인식됩니다.
|
||||
|
||||
익명 텔레메트리는 설치 및 런타임 안정성 개선을 위해 기본적으로 활성화되어 있습니다. PostHog를 사용하며 해시된 설치 식별자를 사용하고 원시 호스트명은 절대 사용하지 않습니다. `OMO_SEND_ANONYMOUS_TELEMETRY=0` 또는 `OMO_DISABLE_POSTHOG=1`로 비활성화할 수 있습니다. [개인정보처리방침](docs/legal/privacy-policy.md)과 [서비스 이용약관](docs/legal/terms-of-service.md)을 참조하세요.
|
||||
익명 텔레메트리는 활성 설치 수(DAU/WAU/MAU) 집계를 위해 기본적으로 활성화되어 있습니다. 머신당 UTC 하루에 최대 1회만 이벤트가 전송되며, 해시된 설치 식별자를 사용하고 원시 호스트명은 절대 사용하지 않으며 PostHog person profile은 생성되지 않습니다. `OMO_SEND_ANONYMOUS_TELEMETRY=0` 또는 `OMO_DISABLE_POSTHOG=1`로 비활성화할 수 있습니다. [개인정보처리방침](docs/legal/privacy-policy.md)과 [서비스 이용약관](docs/legal/terms-of-service.md)을 참조하세요.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ curl -s https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/refs/head
|
||||
|
||||
**Note**: Use the published package and binary name `oh-my-opencode`. Inside `opencode.json`, the compatibility layer now prefers the plugin entry `oh-my-openagent`, while legacy `oh-my-opencode` entries still load with a warning. Plugin config files still commonly use `oh-my-opencode.json` or `oh-my-opencode.jsonc`, and both legacy and renamed basenames are recognized during the transition.
|
||||
|
||||
Anonymous telemetry is enabled by default to help improve install and runtime reliability. It uses PostHog with a hashed installation identifier, never the raw hostname, and can be disabled with `OMO_SEND_ANONYMOUS_TELEMETRY=0` or `OMO_DISABLE_POSTHOG=1`. See [Privacy Policy](docs/legal/privacy-policy.md) and [Terms of Service](docs/legal/terms-of-service.md).
|
||||
Anonymous telemetry is enabled by default to track active installations (DAU/WAU/MAU). A single event is sent at most once per UTC day per machine using a hashed installation identifier, never the raw hostname, and PostHog person profiles are not created. Disable with `OMO_SEND_ANONYMOUS_TELEMETRY=0` or `OMO_DISABLE_POSTHOG=1`. See [Privacy Policy](docs/legal/privacy-policy.md) and [Terms of Service](docs/legal/terms-of-service.md).
|
||||
|
||||
---
|
||||
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ curl -s https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/refs/head
|
||||
|
||||
**Примечание**: Используйте опубликованное имя пакета и бинарника `oh-my-opencode`. Внутри `opencode.json` слой совместимости теперь предпочитает точку входа плагина `oh-my-openagent`, в то время как устаревшие записи `oh-my-opencode` все еще загружаются с предупреждением. Файлы конфигурации плагина по-прежнему часто используют `oh-my-opencode.json` или `oh-my-opencode.jsonc`, и как устаревшие, так и переименованные базовые имена распознаются во время переходного периода.
|
||||
|
||||
Анонимная телеметрия включена по умолчанию для улучшения надежности установки и работы. Она использует PostHog с хешированным идентификатором установки, никогда не используя исходное имя хоста, и может быть отключена с помощью `OMO_SEND_ANONYMOUS_TELEMETRY=0` или `OMO_DISABLE_POSTHOG=1`. См. [Политику конфиденциальности](docs/legal/privacy-policy.md) и [Условия обслуживания](docs/legal/terms-of-service.md).
|
||||
Анонимная телеметрия включена по умолчанию для подсчёта активных установок (DAU/WAU/MAU). Не более одного события на машину за UTC-сутки, использует хешированный идентификатор установки, никогда не использует исходное имя хоста, и не создаёт PostHog person profile. Можно отключить через `OMO_SEND_ANONYMOUS_TELEMETRY=0` или `OMO_DISABLE_POSTHOG=1`. См. [Политику конфиденциальности](docs/legal/privacy-policy.md) и [Условия обслуживания](docs/legal/terms-of-service.md).
|
||||
|
||||
------
|
||||
|
||||
|
||||
+1
-1
@@ -118,7 +118,7 @@ curl -s https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/refs/head
|
||||
|
||||
**注意**:请使用已发布的包名和二进制名 `oh-my-opencode`。在 `opencode.json` 中,兼容性层现在优先使用插件入口 `oh-my-openagent`,而旧的 `oh-my-opencode` 条目仍会加载并显示警告。插件配置文件通常仍使用 `oh-my-opencode.json` 或 `oh-my-opencode.jsonc`,在过渡期间新旧两种文件名都会被识别。
|
||||
|
||||
匿名遥测默认开启,用于帮助提升安装和运行时的可靠性。它使用 PostHog,并采用哈希化的安装标识符,绝不会使用原始主机名,可通过 `OMO_SEND_ANONYMOUS_TELEMETRY=0` 或 `OMO_DISABLE_POSTHOG=1` 禁用。详见 [隐私政策](docs/legal/privacy-policy.md) 和 [服务条款](docs/legal/terms-of-service.md)。
|
||||
匿名遥测默认开启,用于统计活跃安装数(DAU/WAU/MAU)。每台机器每个 UTC 日最多发送一次事件,使用哈希化的安装标识符,绝不会使用原始主机名,且不会创建 PostHog person profile。可通过 `OMO_SEND_ANONYMOUS_TELEMETRY=0` 或 `OMO_DISABLE_POSTHOG=1` 禁用。详见 [隐私政策](docs/legal/privacy-policy.md) 和 [服务条款](docs/legal/terms-of-service.md)。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ bunx oh-my-opencode install
|
||||
|
||||
Follow the prompts to configure your Claude, ChatGPT, and Gemini subscriptions. After installation, authenticate your providers as instructed.
|
||||
|
||||
Anonymous telemetry is enabled by default to help improve install and runtime reliability. It uses PostHog with a hashed installation identifier and can be disabled with `OMO_SEND_ANONYMOUS_TELEMETRY=0` or `OMO_DISABLE_POSTHOG=1`. See [Privacy Policy](../legal/privacy-policy.md) and [Terms of Service](../legal/terms-of-service.md).
|
||||
Anonymous telemetry is enabled by default to track active installations (DAU/WAU/MAU). A single event is sent at most once per UTC day per machine using a hashed installation identifier, and PostHog person profiles are not created. Disable with `OMO_SEND_ANONYMOUS_TELEMETRY=0` or `OMO_DISABLE_POSTHOG=1`. See [Privacy Policy](../legal/privacy-policy.md) and [Terms of Service](../legal/terms-of-service.md).
|
||||
|
||||
After you install it, you can read this [overview guide](./overview.md) to understand more.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Privacy Policy
|
||||
|
||||
Last updated: April 11, 2026
|
||||
Last updated: May 2, 2026
|
||||
|
||||
This Privacy Policy explains how oh-my-opencode and oh-my-openagent collect, use, and protect information related to the published CLI package, the OpenCode plugin, and the project website or repository materials where they apply.
|
||||
|
||||
@@ -14,14 +14,13 @@ We collect limited non-personal information needed to operate and improve the Se
|
||||
|
||||
### Automatically collected information
|
||||
|
||||
When anonymous telemetry is enabled, the Application may collect:
|
||||
When anonymous telemetry is enabled, the Application may collect a single anonymous usage event:
|
||||
|
||||
- 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
|
||||
- `omo_daily_active`, sent at most once per UTC day per machine when the plugin loads or when the `run` CLI is invoked, used to estimate daily, weekly, and monthly active installations
|
||||
- Anonymous machine metadata bundled with that event, such as package version, plugin name, runtime, OS family, locale, and timezone
|
||||
- A pseudonymous installation identifier derived from a one-way hash of the local hostname
|
||||
|
||||
We do not intentionally collect prompt contents, source files, repository contents, access tokens, API keys, or raw hostnames through this telemetry path.
|
||||
The Application does not create or update PostHog person profiles, and does not collect prompt contents, source files, repository contents, access tokens, API keys, raw hostnames, or runtime error diagnostics through this telemetry path.
|
||||
|
||||
### Configuration and local state
|
||||
|
||||
@@ -29,7 +28,7 @@ The Application stores local configuration and telemetry deduplication state on
|
||||
|
||||
## 2. How Telemetry Works
|
||||
|
||||
The Application uses PostHog for anonymous product analytics. Telemetry is enabled by default, following the same opt-out posture used in cmux, and is intended to help us understand installation success, runtime reliability, and broad usage patterns.
|
||||
The Application uses PostHog for anonymous product analytics. Telemetry is enabled by default, following the same opt-out posture used in cmux, and is intended only to estimate active installations (daily, weekly, and monthly) so we can understand broad adoption.
|
||||
|
||||
Telemetry can be disabled at any time by setting one of these environment variables before running the CLI or plugin host:
|
||||
|
||||
@@ -55,16 +54,15 @@ Each third-party service has its own terms and privacy practices.
|
||||
|
||||
We use collected information to:
|
||||
|
||||
- Measure installation and runtime health
|
||||
- Understand aggregate feature usage
|
||||
- Diagnose failures and improve reliability
|
||||
- Estimate daily, weekly, and monthly active installations
|
||||
- Understand aggregate adoption across operating systems and package versions
|
||||
- Maintain and evolve the Service
|
||||
|
||||
We do not sell personal information collected through this telemetry path.
|
||||
|
||||
## 5. Data Retention
|
||||
|
||||
Anonymous analytics and diagnostics are retained only as long as reasonably necessary for product, security, and operational analysis. Local telemetry state stored on your machine remains there until removed by you.
|
||||
Anonymous analytics are retained only as long as reasonably necessary for understanding adoption. Local telemetry state stored on your machine remains there until removed by you.
|
||||
|
||||
## 6. Your Choices
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import { afterEach, describe, expect, it, mock, spyOn } from "bun:test"
|
||||
import * as configManager from "./config-manager"
|
||||
import type { InstallArgs } from "./types"
|
||||
|
||||
describe("runCliInstaller telemetry isolation", () => {
|
||||
afterEach(() => {
|
||||
mock.restore()
|
||||
})
|
||||
|
||||
it("does not crash CLI install when telemetry shutdown throws", async () => {
|
||||
// given
|
||||
const restoreSpies = [
|
||||
spyOn(configManager, "detectCurrentConfig").mockReturnValue({
|
||||
isInstalled: false,
|
||||
installedVersion: null,
|
||||
hasClaude: false,
|
||||
isMax20: false,
|
||||
hasOpenAI: false,
|
||||
hasGemini: false,
|
||||
hasCopilot: false,
|
||||
hasOpencodeZen: false,
|
||||
hasZaiCodingPlan: false,
|
||||
hasKimiForCoding: false,
|
||||
hasOpencodeGo: false,
|
||||
hasVercelAiGateway: false,
|
||||
}),
|
||||
spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(true),
|
||||
spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.4.0"),
|
||||
spyOn(configManager, "addPluginToOpenCodeConfig").mockResolvedValue({
|
||||
success: true,
|
||||
configPath: "/tmp/opencode.jsonc",
|
||||
}),
|
||||
spyOn(configManager, "writeOmoConfig").mockReturnValue({
|
||||
success: true,
|
||||
configPath: "/tmp/oh-my-opencode.jsonc",
|
||||
}),
|
||||
]
|
||||
|
||||
mock.module("../shared/posthog", () => ({
|
||||
createCliPostHog: mock(() => ({
|
||||
trackActive: mock(() => {}),
|
||||
capture: mock(() => {}),
|
||||
captureException: mock(() => {}),
|
||||
shutdown: mock(async () => {
|
||||
throw new Error("shutdown failed")
|
||||
}),
|
||||
})),
|
||||
getPostHogDistinctId: mock(() => "install-distinct-id"),
|
||||
}))
|
||||
|
||||
const { runCliInstaller } = await import(`./cli-installer?telemetry=${Date.now()}-${Math.random()}`)
|
||||
const args: InstallArgs = {
|
||||
tui: false,
|
||||
claude: "no",
|
||||
openai: "yes",
|
||||
gemini: "no",
|
||||
copilot: "yes",
|
||||
opencodeZen: "no",
|
||||
zaiCodingPlan: "no",
|
||||
kimiForCoding: "no",
|
||||
opencodeGo: "no",
|
||||
}
|
||||
|
||||
// when
|
||||
const result = await runCliInstaller(args, "3.4.0")
|
||||
|
||||
// then
|
||||
expect(result).toBe(0)
|
||||
|
||||
for (const spy of restoreSpies) {
|
||||
spy.mockRestore()
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -23,11 +23,8 @@ import {
|
||||
validateNonTuiArgs,
|
||||
} from "./install-validators"
|
||||
import { getUnsupportedOpenCodeVersionMessage } from "./minimum-opencode-version"
|
||||
import { createCliPostHog, getPostHogDistinctId } from "../shared/posthog"
|
||||
|
||||
export async function runCliInstaller(args: InstallArgs, version: string): Promise<number> {
|
||||
const posthog = createCliPostHog()
|
||||
const distinctId = getPostHogDistinctId()
|
||||
const validation = validateNonTuiArgs(args)
|
||||
if (!validation.valid) {
|
||||
printHeader(false)
|
||||
@@ -65,16 +62,6 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
|
||||
const unsupportedVersionMessage = getUnsupportedOpenCodeVersionMessage(openCodeVersion)
|
||||
if (unsupportedVersionMessage) {
|
||||
printWarning(unsupportedVersionMessage)
|
||||
try {
|
||||
posthog.capture({ distinctId, event: "install_failed", properties: { command: "install", reason: "unsupported_opencode_version", is_update: isUpdate } })
|
||||
} catch {
|
||||
// telemetry failure is non-fatal, silently ignore
|
||||
}
|
||||
try {
|
||||
await posthog.shutdown()
|
||||
} catch {
|
||||
// telemetry failure is non-fatal, silently ignore
|
||||
}
|
||||
return 1
|
||||
}
|
||||
}
|
||||
@@ -90,16 +77,6 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
|
||||
const pluginResult = await addPluginToOpenCodeConfig(version)
|
||||
if (!pluginResult.success) {
|
||||
printError(`Failed: ${pluginResult.error}`)
|
||||
try {
|
||||
posthog.capture({ distinctId, event: "install_failed", properties: { command: "install", reason: "plugin_config_write_failed", is_update: isUpdate } })
|
||||
} catch {
|
||||
// telemetry failure is non-fatal, silently ignore
|
||||
}
|
||||
try {
|
||||
await posthog.shutdown()
|
||||
} catch {
|
||||
// telemetry failure is non-fatal, silently ignore
|
||||
}
|
||||
return 1
|
||||
}
|
||||
printSuccess(
|
||||
@@ -110,16 +87,6 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
|
||||
const omoResult = writeOmoConfig(config)
|
||||
if (!omoResult.success) {
|
||||
printError(`Failed: ${omoResult.error}`)
|
||||
try {
|
||||
posthog.capture({ distinctId, event: "install_failed", properties: { command: "install", reason: "omo_config_write_failed", is_update: isUpdate } })
|
||||
} catch {
|
||||
// telemetry failure is non-fatal, silently ignore
|
||||
}
|
||||
try {
|
||||
await posthog.shutdown()
|
||||
} catch {
|
||||
// telemetry failure is non-fatal, silently ignore
|
||||
}
|
||||
return 1
|
||||
}
|
||||
printSuccess(`Config written ${SYMBOLS.arrow} ${color.dim(omoResult.configPath)}`)
|
||||
@@ -169,29 +136,6 @@ export async function runCliInstaller(args: InstallArgs, version: string): Promi
|
||||
console.log(color.dim("oMoMoMoMo... Enjoy!"))
|
||||
console.log()
|
||||
|
||||
try {
|
||||
posthog.capture({
|
||||
distinctId,
|
||||
event: "install_completed",
|
||||
properties: {
|
||||
command: "install",
|
||||
is_update: isUpdate,
|
||||
has_claude: config.hasClaude,
|
||||
has_openai: config.hasOpenAI,
|
||||
has_gemini: config.hasGemini,
|
||||
has_copilot: config.hasCopilot,
|
||||
has_opencode_zen: config.hasOpencodeZen,
|
||||
},
|
||||
})
|
||||
} catch {
|
||||
// telemetry failure is non-fatal, silently ignore
|
||||
}
|
||||
try {
|
||||
await posthog.shutdown()
|
||||
} catch {
|
||||
// telemetry failure is non-fatal, silently ignore
|
||||
}
|
||||
|
||||
if ((config.hasClaude || config.hasGemini || config.hasCopilot) && !args.skipAuth) {
|
||||
printBox(
|
||||
`Run ${color.cyan("opencode auth login")} and select your provider:\n` +
|
||||
|
||||
@@ -64,8 +64,6 @@ describe("run telemetry isolation", () => {
|
||||
trackActive: () => {
|
||||
throw new Error("telemetry failed")
|
||||
},
|
||||
capture: mock(() => {}),
|
||||
captureException: mock(() => {}),
|
||||
shutdown: mock(async () => {
|
||||
throw new Error("shutdown failed")
|
||||
}),
|
||||
|
||||
@@ -58,20 +58,6 @@ export async function run(options: RunOptions): Promise<number> {
|
||||
} catch {
|
||||
// telemetry failure is non-fatal, silently ignore
|
||||
}
|
||||
try {
|
||||
posthog.capture({
|
||||
distinctId,
|
||||
event: "run_started",
|
||||
properties: {
|
||||
command: "run",
|
||||
agent: resolvedAgent,
|
||||
has_model: !!options.model,
|
||||
has_session_id: !!options.sessionId,
|
||||
},
|
||||
})
|
||||
} catch {
|
||||
// telemetry failure is non-fatal, silently ignore
|
||||
}
|
||||
|
||||
try {
|
||||
const resolvedModel = resolveRunModel(options.model)
|
||||
@@ -164,38 +150,6 @@ export async function run(options: RunOptions): Promise<number> {
|
||||
})
|
||||
}
|
||||
|
||||
if (exitCode === 0) {
|
||||
try {
|
||||
posthog.capture({
|
||||
distinctId,
|
||||
event: "run_completed",
|
||||
properties: {
|
||||
command: "run",
|
||||
agent: resolvedAgent,
|
||||
duration_ms: durationMs,
|
||||
message_count: eventState.messageCount,
|
||||
},
|
||||
})
|
||||
} catch {
|
||||
// telemetry failure is non-fatal, silently ignore
|
||||
}
|
||||
} else if (exitCode === 1) {
|
||||
try {
|
||||
posthog.capture({
|
||||
distinctId,
|
||||
event: "run_failed",
|
||||
properties: {
|
||||
command: "run",
|
||||
agent: resolvedAgent,
|
||||
exit_code: exitCode,
|
||||
duration_ms: durationMs,
|
||||
},
|
||||
})
|
||||
} catch {
|
||||
// telemetry failure is non-fatal, silently ignore
|
||||
}
|
||||
}
|
||||
|
||||
return exitCode
|
||||
} catch (err) {
|
||||
cleanup()
|
||||
@@ -210,25 +164,6 @@ export async function run(options: RunOptions): Promise<number> {
|
||||
if (err instanceof Error && err.name === "AbortError") {
|
||||
return 130
|
||||
}
|
||||
try {
|
||||
posthog.captureException(err, distinctId)
|
||||
} catch {
|
||||
// telemetry failure is non-fatal, silently ignore
|
||||
}
|
||||
try {
|
||||
posthog.capture({
|
||||
distinctId,
|
||||
event: "run_failed",
|
||||
properties: {
|
||||
command: "run",
|
||||
agent: resolvedAgent,
|
||||
error: serializeError(err),
|
||||
duration_ms: Date.now() - startTime,
|
||||
},
|
||||
})
|
||||
} catch {
|
||||
// telemetry failure is non-fatal, silently ignore
|
||||
}
|
||||
console.error(pc.red(`Error: ${serializeError(err)}`))
|
||||
return 1
|
||||
} finally {
|
||||
|
||||
@@ -34,8 +34,6 @@ const mockCreatePluginPostHog = mock(() => ({
|
||||
trackActive: () => {
|
||||
throw new Error("telemetry failed")
|
||||
},
|
||||
capture: mock(() => {}),
|
||||
captureException: mock(() => {}),
|
||||
shutdown: mock(async () => {}),
|
||||
}))
|
||||
const mockGetPostHogDistinctId = mock(() => "plugin-distinct-id")
|
||||
@@ -104,12 +102,6 @@ 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,7 +18,6 @@ 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()
|
||||
@@ -44,27 +43,6 @@ const serverPlugin: Plugin = async (input, _options): Promise<Hooks> => {
|
||||
} catch {
|
||||
// telemetry failure is non-fatal, silently ignore
|
||||
}
|
||||
let pluginLoadedCaptureState: ReturnType<typeof getPluginLoadedCaptureState> | null = null
|
||||
try {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ describe("getPostHogActivityCaptureState", () => {
|
||||
rmSync(dataHomePath, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it("preserves lastPluginLoadedDayUTC when writing lastActiveDayUTC", async () => {
|
||||
it("preserves unrelated state fields when writing lastActiveDayUTC", async () => {
|
||||
// given
|
||||
const dataHomePath = createDataHomePath()
|
||||
const cachePath = join(dataHomePath, "oh-my-opencode")
|
||||
@@ -168,128 +168,3 @@ describe("getPostHogActivityCaptureState", () => {
|
||||
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,7 +8,6 @@ import { writeFileAtomically } from "./write-file-atomically"
|
||||
|
||||
type PostHogActivityState = {
|
||||
lastActiveDayUTC?: string
|
||||
lastPluginLoadedDayUTC?: string
|
||||
}
|
||||
|
||||
type PostHogActivityCaptureState = {
|
||||
@@ -16,11 +15,6 @@ type PostHogActivityCaptureState = {
|
||||
captureDaily: boolean
|
||||
}
|
||||
|
||||
type PluginLoadedCaptureState = {
|
||||
dayUTC: string
|
||||
capturePluginLoaded: boolean
|
||||
}
|
||||
|
||||
const POSTHOG_ACTIVITY_STATE_FILE = "posthog-activity.json"
|
||||
|
||||
function getPostHogActivityStateFilePath(): string {
|
||||
@@ -89,22 +83,3 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ function mockPostHogNode(capturedMessages: CapturedPostHogMessage[]): void {
|
||||
capture(message: CapturedPostHogMessage): void {
|
||||
capturedMessages.push(message)
|
||||
}
|
||||
captureException(): void {}
|
||||
async shutdown(): Promise<void> {}
|
||||
},
|
||||
}))
|
||||
@@ -65,23 +64,9 @@ describe("posthog client creation", () => {
|
||||
const pluginPostHog = createPluginPostHog()
|
||||
|
||||
// then
|
||||
expect(() =>
|
||||
cliPostHog.capture({
|
||||
distinctId: "cli",
|
||||
event: "run_started",
|
||||
}),
|
||||
).not.toThrow()
|
||||
expect(() => cliPostHog.captureException(new Error("cli failure"), "cli")).not.toThrow()
|
||||
expect(() => cliPostHog.trackActive("cli", "run_started")).not.toThrow()
|
||||
await expect(cliPostHog.shutdown()).resolves.toBeUndefined()
|
||||
|
||||
expect(() =>
|
||||
pluginPostHog.capture({
|
||||
distinctId: "plugin",
|
||||
event: "plugin_loaded",
|
||||
}),
|
||||
).not.toThrow()
|
||||
expect(() => pluginPostHog.captureException(new Error("plugin failure"), "plugin")).not.toThrow()
|
||||
expect(() => pluginPostHog.trackActive("plugin", "plugin_loaded")).not.toThrow()
|
||||
await expect(pluginPostHog.shutdown()).resolves.toBeUndefined()
|
||||
})
|
||||
@@ -109,7 +94,6 @@ describe("posthog client creation", () => {
|
||||
mock.module("posthog-node", () => ({
|
||||
PostHog: class {
|
||||
capture() {}
|
||||
captureException() {}
|
||||
async shutdown() {}
|
||||
},
|
||||
}))
|
||||
@@ -120,13 +104,6 @@ describe("posthog client creation", () => {
|
||||
const pluginPostHog = createPluginPostHog()
|
||||
|
||||
// then
|
||||
expect(() =>
|
||||
pluginPostHog.capture({
|
||||
distinctId: "plugin",
|
||||
event: "plugin_loaded",
|
||||
}),
|
||||
).not.toThrow()
|
||||
expect(() => pluginPostHog.captureException(new Error("plugin failure"), "plugin")).not.toThrow()
|
||||
expect(() => pluginPostHog.trackActive("plugin", "plugin_loaded")).not.toThrow()
|
||||
await expect(pluginPostHog.shutdown()).resolves.toBeUndefined()
|
||||
})
|
||||
@@ -174,6 +151,7 @@ describe("posthog trackActive emission contract", () => {
|
||||
day_utc: "2026-04-18",
|
||||
reason: "run_started",
|
||||
source: "cli",
|
||||
$process_person_profile: false,
|
||||
})
|
||||
expect(dailyEvent?.properties).not.toHaveProperty("hour_utc")
|
||||
})
|
||||
|
||||
+1
-24
@@ -28,24 +28,15 @@ const DEFAULT_POSTHOG_HOST = "https://us.i.posthog.com"
|
||||
const DEFAULT_POSTHOG_API_KEY = "phc_CFJhj5HyvA62QPhvyaUCtaq23aUfznnijg5VaaGkNk74"
|
||||
|
||||
type PostHogCaptureEvent = Parameters<PostHog["capture"]>[0]
|
||||
type PostHogExceptionProperties = Parameters<PostHog["captureException"]>[2]
|
||||
type PostHogSource = "cli" | "plugin"
|
||||
type PostHogActivityReason = "run_started" | "plugin_loaded"
|
||||
|
||||
type PostHogClient = {
|
||||
capture: (message: PostHogCaptureEvent) => void
|
||||
captureException: (
|
||||
error: unknown,
|
||||
distinctId?: string,
|
||||
additionalProperties?: PostHogExceptionProperties,
|
||||
) => void
|
||||
trackActive: (distinctId: string, reason: PostHogActivityReason) => void
|
||||
shutdown: () => Promise<void>
|
||||
}
|
||||
|
||||
const NO_OP_POSTHOG: PostHogClient = {
|
||||
capture: () => undefined,
|
||||
captureException: () => undefined,
|
||||
trackActive: () => undefined,
|
||||
shutdown: async () => undefined,
|
||||
}
|
||||
@@ -131,21 +122,6 @@ function createPostHogClient(
|
||||
const sharedProperties = getSharedProperties(source)
|
||||
|
||||
return {
|
||||
capture: (message) => {
|
||||
configuredClient.capture({
|
||||
...message,
|
||||
properties: {
|
||||
...sharedProperties,
|
||||
...message.properties,
|
||||
},
|
||||
})
|
||||
},
|
||||
captureException: (error, distinctId, additionalProperties) => {
|
||||
configuredClient.captureException(error, distinctId, {
|
||||
...sharedProperties,
|
||||
...additionalProperties,
|
||||
})
|
||||
},
|
||||
trackActive: (distinctId, reason) => {
|
||||
const activityState = resolveActivityState()
|
||||
|
||||
@@ -155,6 +131,7 @@ function createPostHogClient(
|
||||
event: "omo_daily_active",
|
||||
properties: {
|
||||
...sharedProperties,
|
||||
$process_person_profile: false,
|
||||
day_utc: activityState.dayUTC,
|
||||
reason,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user