Friend macOS 发布健康指标规范:面向 Sentry 与 PostHog 的权威查询契约解析
【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend
导读
本文基于Friend开源仓库desktop/macos桌面端的《macOS Release-Health Metric Specification》展开,系统讲解一套服务于 macOS 桌面应用的发布健康(release-health)遥测指标规范。该规范回答了"一个指标何时算回归、何时算噪音、何时算未知"这类可观测性工程的核心问题,通过为每个信号定义精确的分子(numerator)、分母(denominator)、时间窗口、最小样本量(minimum cohort)、缺失数据处理规则与跨版本比较规则,确保中间生命周期事件和预期噪音不会被误读为客户可见的回归。读完本文,你将掌握 PTT 音频捕获漏斗、Realtime 令牌铸造、Provider 会话健康、回退(fallback)结果、崩溃率、主动建议投递、更新器投递与三类内存指标的具体查询口径,以及这套规范在Friend仓库中的实际代码落点。
一、规范的定位与权威性
release-health-metrics.md(仓库路径 desktop/macos/docs/release-health-metrics.md)是 macOS 发布健康遥测的权威查询契约(authoritative query contract)。它定义了每个信号的精确口径,目的是让中间生命周期事件和预期噪音不能被读成客户可见的回归。该文档是desktop/macos/AGENTS.md中 "Product analytics integrity"(产品分析完整性)与 "Fallback / resilience telemetry"(回退/韧性遥测)两个章节的完整展开版。
文档状态信息:Status:active ·Schema version:4 ·Owner:desktop/macos。当任何分子/分母定义、封闭枚举(closed enum)或字段名发生变化时,需要升级 Schema version 并在文档中显式记录变更。telemetry_schema_version(PTT 生命周期快照上)以及expected/outcome/mint_attempt_id/phase/close_attempt_id/turn_outcome等字段是本文档的机器可读伴生约定。
统一的发布身份(Release identity)
在 Sentry 与 PostHog 两个遥测面上,发布身份字段保持一致:
| 维度 | PostHog key | Sentry | 来源 |
|---|---|---|---|
| App 版本 | app_version | release(v{ver}+{build}-macos) | CFBundleShortVersionString |
| App 构建号 | app_build | release(同一标签) | CFBundleVersion |
| 发布渠道 | update_channel(stable/beta) | dist+update_channeltag | AppBuild.currentUpdateChannel |
| Bundle id | — | bundle_idtag | AppBuild.bundleIdentifier |
- PostHog 侧的
app_version/app_build/update_channel通过PostHogManager.register注册为超级属性(super-properties),因此每一个事件——包括floating_bar_ptt_ended——都携带发布身份。 - Sentry 侧的原生崩溃、App 挂起(app-hang)、看门狗(watchdog)事件通过
SentrySDK.start时设置的options.releaseName/options.dist实现构建可归属(build-attributable)。
跨切面规则(Cross-cutting rules)
- 中间事件不是失败(Intermediate events are not failures):指标的分子必须是"终态、有界的结果"(terminal, bounded outcome),绝不能是中间的生命周期转换。如果一个信号没有显式的 outcome 字段,它只是构建块(building block),不是发布健康指标。
- 预期生命周期从错误汇总中排除:带
expected = true(lifecycle_class = "expected")的实时事件——如空闲拆除(idle teardown)与计划内的会话轮换(planned session rotation)——可以单独检查,但必须从实时错误率和发布回归率中过滤掉。错误率只使用expected = false的事件。 - 最小样本量(Minimum cohort):低于最小样本量时,比率是
unknown(既不是100%也不是0%)。跨发布比较要求两侧都满足最小样本量。 - 比较基准:build 对 build、beta 对 stable 的比较使用相同的窗口与分母定义;只有当两个队列都满足最小样本量且方向对所关注的 outcomes 不利时,才判定为回归(regression)。
- 隐私边界:自定义指标负载只使用有界维度。任何 PostHog 属性或 Sentry tag 中都不允许出现转录文本、音频、提示词、自定义设备标识符或自由形式的本地错误文本。该边界由 DesktopDiagnosticsManagerTests.swift 与
TelemetryPrivacyBoundaryTests强制保证。
从源码侧看,事件名称的权威清单定义在 DesktopDiagnosticsManager.swift 的DesktopHealthEventName枚举中,包括pttAudioCaptureLifecycle、realtimeTokenMintFailed、realtimeProviderExpectedIdleTeardown、realtimeProviderPolicyClose、realtimeProviderSessionError、realtimeProviderCloseResolution、fallbackTriggered等,与本文档的指标一一对应。
二、指标总览与承载层
文档指出:每个指标只要存在对应项,就映射到desktop_release_doctor_report.METRIC_CONTRACTS名称;没有 doctor 条目桌面端 outcome 指标则是客户端输入,由发布证据层(release-evidence layer)消费。
指标清单速览:
| 指标 | 关键事件 | 最小样本 | 窗口 |
|---|---|---|---|
PTT 终态漏斗ptt_audio_capture_lifecycle | desktop_health_event | 每 build 50 次可判定尝试 | PT24H |
Realtime 令牌铸造realtime_token_mint_failed | desktop_health_event | 每 build 30 个铸造用户 | PT24H |
Realtime Provider 会话健康realtime_provider_* | 5 个相关事件 | 每 build 40 个活跃会话用户 | PT24H |
回退结果fallback_triggered(doctor:fallback_outcomes) | desktop_health_event | 每 build 50 个回退用户 | PT24H |
崩溃安全会话(doctor:crash_free_sessions) | Sentry 会话跟踪 | 每 build 100 个会话 | PT24H |
主动建议投递(doctor:proactive_delivery) | Advice Generated/Advice Delivery Outcome | 50 DAU / 10 个合格投递 | PT24H |
更新器投递(doctor:updater_delivery) | Update Check Started/Completed | 每 build 30 次尝试 | PT24H |
| 录音(客户端输入) | recording_error | 低于最小值 →unknown | — |
| 内存三漏斗(产品分析) | 三个独立事件 | — | — |
三、PTT 终态漏斗:ptt_audio_capture_lifecycle
这是文档中最详细、最能体现"中间事件不是失败"原则的指标。
来源事件:desktop_health_event,event = ptt_audio_capture_lifecycle(要求telemetry_schema_version >= 2;turn_kind从版本 3 起引入)。
分母(尝试次数):窗口内所有ptt_audio_capture_lifecycle事件,按failure_class分组。由于每一个终态处置——包括成功——都会远程上报,所以分母是可查询的。
分子(捕获失败):failure_class IN (capture_never_operational)。恢复类结果(recovery_outcome_recovered/_still_silent/_not_judgeable)通过recovery_attempt_id关联,不计入新的失败。
明确排除在失败分子之外(不算回归):
committed(成功)released_before_usable_audio/too_short_audible(轻点即松 / 过早松开)cancelled(用户取消)zero_or_near_zero_samples且turn_disposition = silent_rejected(安静丢弃 / 无语音)
其中,first_chunks_energy_bucket+turn_disposition两个字段用于把"真正的零采样捕获失败"与"有意的安静丢弃"区分开。
弃用事件:floating_bar_ptt_ended(携带had_transcript)把上述四类结果折叠成单个布尔值,绝不能作为 PTT 成功/失败的分母,仅保留用于向后兼容。
窗口与样本:PT24H 滚动窗口;每 build 最小队列 50 次可判定尝试;若某 build 没有任何ptt_audio_capture_lifecycle事件 → 判为unknown。
源码佐证:在 DesktopDiagnosticsManager.swift 中,PTT 看门狗(watchdog)逻辑有明确阈值(pttWatchdogThreshold = 3、15 分钟去重窗口、最小音频 0.35 秒),并且注释明确写着"cancel 是有界failure_class值,与capture_never_operational相区别",与规范口径完全一致。
四、Realtime 令牌铸造:realtime_token_mint_failed
来源事件:desktop_health_event,event = realtime_token_mint_failed。
阶段(warm vs active):phase是封闭集合——warm(后台预预热)与barge_in_replacement(活跃轮次中的 socket 替换);任何其他值被归入other。这是"暖 vs 活跃"维度。
即时结果(degraded/exhausted):铸造失败事件本身会记录控制器是启动了备用提供者回退(degraded)还是没有任何剩余的受管路径(exhausted)。后续的回退结果保留在关联的fallback_triggered(area = realtime_hub)事件上。当铸造触发故障切换(failover)时,通过mint_attempt_id关联两个事件,再用 provider 加有界时间窗口过滤。
分子(铸造耗尽、用户受影响):铸造失败事件上的outcome = exhausted(没有剩余受管路径)。详细的替换路径使用关联的realtime_hub回退事件。degraded(备用提供者回退已启动)是可恢复的,不是终态失败分子。
窗口与样本:PT24H;每 build 最小队列 30 个铸造尝试用户;无铸造事件 →unknown。
五、Realtime Provider 会话健康:realtime_provider_*
来源事件(5 个):
realtime_provider_expected_idle_teardown、realtime_provider_expected_session_rotation(两者expected = true)realtime_provider_policy_close、realtime_provider_session_error(两者expected = false)realtime_provider_close_resolution
客户回合(customer-turn)决策:每个 provider 关闭事件携带进程内close_attempt_id;配对的realtime_provider_close_resolution携带封闭集合的turn_outcome与即时的recovery_action/recovery_result。该 id 仅在同一个分析会话内有效,绝不是用户、设备、回合或 provider 会话标识符。
错误率分子:活跃回合的realtime_provider_session_error+realtime_provider_policy_close(expected = false),且配对 resolution 的turn_outcome = failed。pending_replacement是中间恢复状态,不是终态客户失败分子。分母:活跃 realtime 会话(以发出任意realtime_provider_*的独立会话为代理)。
排除项:两个expected_*事件(expected = true)——正常的空闲拆除和计划内的 60 分钟 OpenAI 会话轮换。它们仍可单独检查,但不得抬高 realtime 错误率或发布回归率。
窗口与样本:PT24H;每 build 最小队列 40 个活跃会话用户。
源码佐证:RealtimeProviderCloseTurnOutcome枚举定义在 DesktopDiagnosticsManager.swift,封闭集合为not_interrupted/failed/pendingReplacement,注释明确说明"关闭事件在控制器选择替换、故障切换或终态路径之前被捕获,而该封闭集合记录的是不含 provider 负载的决策"——这正是"close 与 close-resolution 分离"设计的实现。
六、回退结果:fallback_triggered(doctor 指标fallback_outcomes)
来源事件:desktop_health_event,event = fallback_triggered。
维度(全部为封闭枚举):area、reason、from、to、outcome(recovered/degraded/exhausted)。未知的area/reason归入other。
发布健康分子(客户可见降级):outcome IN (degraded, exhausted),按(area, reason, from, to)分组。recovered是静默的 UX 自愈(silent UX heal),不是失败。
已知良性抖动(known-benign flap):area = screen_capture、reason = capability_mismatch、from/to ∈ {screen_capture, capture_paused, recovery_poll}属于 ProactiveAssistants 屏幕捕获健康抖动(目标暂时不可用后恢复)。这是预期的能力抖动——应针对其速率告警,绝不能对绝对计数或recovered支路发 page。
area = other策略:剩余的other只折叠真正未分类的路径;非平凡的other比率是插桩缺陷(instrumentation defect),应进行 triage,而不是产品回归。命名所有者(screen_capture、memory_scope、desktop_update、tts_fallback、task_workflow、auth_storage、realtime_hub、ptt_cascade等)确保已知路径不落入other。
窗口与样本:PT24H;每 build 最小队列 50 个发出回退事件的用户。
源码佐证:DesktopFallbackOutcome枚举(recovered/degraded/exhausted)定义于 DesktopDiagnosticsManager.swift。此外,仓库中的memory_scope回退路径被用于内存操作可靠性跟踪(见后文"内存可靠性"一节),与文档"memory_scope仍是降级信号"的表述一致。
七、崩溃安全会话:doctor 指标crash_free_sessions
来源:Sentry release health(自动会话跟踪),以releaseName/dist为键。
分子:发生硬崩溃(hard crash)的会话数。分母:该 release 的总启动会话数。按release(version+build)与update_channel过滤;原生崩溃通过options.releaseName/options.dist实现构建可归属。
窗口与样本:PT24H;每 build 最小队列 100 个会话。
隐私:原生事件只携带update_channel/bundle_idtags,外加diagnostic_area/failure_class,无用户内容。
八、主动建议投递:doctor 指标proactive_delivery
这是唯一带"告警作业"(alarm job)描述的指标,规范对其自动化运行机制描述得最为详尽。
可用性(Availability)来源:发出Advice Generated的独立用户数 ÷ 同一滚动 PT24H 窗口内的 macOS DAU。两侧都限定$app_namespace = com.omi.computer-macos且$os_name = macOS。
投递(Delivery)来源:Advice Delivery Outcome终态事件。合格的投递结果只有delivered与failed;偏好/策略类抑制被排除。当合格结果 ≥ 10 时,若delivered恰好为 0 则视为不健康。
告警规则:
- 精确零 advice 用户且 macOS DAU ≥ 50,或精确零合格投递结果 →不健康。
- DAU < 50 或合格投递结果 < 10 →
unknown,既不是成功也不是失败。 - 计划任务
desktop_release_doctor.yml每小时运行一次:不健康期间保持一个持久的 GitHub issue 打开,测得恢复后关闭它,并将"PostHog 查询失败"当作告警处理而非静默通过。
监控凭据:计划任务读取仓库 Actions secretPOSTHOG_PERSONAL_API_KEY与 Actions 变量POSTHOG_PROJECT_ID、POSTHOG_HOST。它刻意不使用需要人工审批的prod环境(否则每小时检查将等待人来批准)。配置缺失时产生中性的unconfigured结果且不产生健康告警(因为没有发生测量);已配置但查询失败时产生monitor_error并打开持久 issue;两种状态都不会让指标变绿。
投递结果:每个Advice Generated事件携带不透明的进程内delivery_id;Advice Delivery Outcome记录封闭的outcome(delivered/suppressed/failed)与有界的reason。通知偏好只抑制投递,不抑制分析。排队的浮条项是中间态,在呈现边界接受之前绝不能算作已投递。
隐私:查询只导出聚合计数。两个事件的定制负载都不包含建议文本、屏幕内容、提示词、转录或设备标识符。PostHog 标准的person_id仅就地用于聚合基数计算,不由监控器返回。
九、更新器投递:doctor 指标updater_delivery
来源:Update Check Started通过不透明的attempt_id关联到其唯一的终态Update Check Completed。两者都携带触发器、源 app 版本/build 以及归一化更新渠道。
失败分子:终态result = failed。no_update与update_available是成功的检查结果。network_unavailable是自动后台检查,其 URL 错误专门是NSURLErrorNotConnectedToInternet(-1009);它单独上报,不是更新器缺陷。超时、DNS 与服务器可达性错误仍算失败,这样真实的更新服务故障不会被掩盖。离线时的手动检查仍为failed,以便用户获得反馈。
遗留事件:Update Check Failed仅保留诊断用途,绝不能作为分母或用户影响率。它现在遵守"每次检查一个终态"契约:Sparkle 对单次检查会重投didAbortWithError,而直到 2026-08,遗留事件对每个回调都会触发一次(在 0.12.187–0.12.212 构建上达到权威result = failed计数的3x–47x)。修复前的历史Update Check Failed数据量被放大了,不能与修复后的构建进行比较。
分母:不同的已开始尝试。缺失终态是独立的插桩健康缺陷(当下一次被 Sparkle 接纳的检查关闭一个陈旧身份时记为callback_missing)。开始记录仅在 Sparkle 序列化的mayPerform边界进行,周期结束 delegate 是省略 abort 回调路径的最终回退。被拒绝的请求不会产生幻影尝试,重复回调也不能产生额外终态。
窗口与样本:PT24H;每 build 最小队列 30 次尝试。
十、录音错误(客户端输入)
recording_errorPostHog 事件只携带error_class(无音频)。分子 = 错误数;分母 = 录音会话数。低于最小值 →unknown。
十一、内存指标:三个独立漏斗(产品分析)
文档明确指出,macOS 的"memory"面是三个独立漏斗,而非一个:录音用户不是主动内存的分母,Memory Created不是抽取内存的代理——它跟踪的是录音/会话与后端的对账(reconciliation)。以下三个指标就是让每个漏斗可测量的查询契约。所有负载只携带有界维度;不发送屏幕像素、OCR/窗口/App 名称、内存内容、提示词、Gemini 响应、原始模型材料、会话 id、转录文本或异常字符串(由MemoryAssistantTelemetryTests与test_conversation_memories_telemetry.py强制保证)。
1. 主动内存助手激活:Memory Assistant Setting Changed
- 来源事件:
Memory Assistant Setting Changed(桌面端,主动MemoryAssistant)。封闭属性:setting ∈ {enabled, notifications_enabled}、布尔value。 - 存在原因:分析被硬性门控在
enabled && notificationsEnabled,而通知默认关闭,因此通知开关处的激活悬崖(activation cliff)此前不可见。该事件让真正的分母可测量。 - 发射规则:对任一设置的每次用户主动发起的持久化变更恰好发射一个事件——远程设置同步、App 启动、默认读取、迁移或程序化重置时绝不发射。两个 UI 开关路径使用专用的用户意图 API,比较旧值新值并跳过无操作(no-op);裸 setter 故意静默。
- 激活指标:主动抽取用户(
Memory Extracted)÷ 监控用户(Monitoring Started)且notifications_enabled = true,限定$app_namespace='com.omi.computer-macos'AND$os_name='macOS'。不要再拿抽取量除以录音用户数。
源码佐证:MemoryAssistantTelemetry.swift 中Setting枚举(enabled/notifications_enabled)与settingChangeIsPersistedChange纯函数(oldValue != newValue)完全对应上述发射规则;文件头注释明确写着"通知开关是实际的分析门控(默认关)"。
2. 主动分析结果分布:Memory Assistant Analysis Run
- 来源事件:
Memory Assistant Analysis Run(桌面端)。封闭属性:outcome ∈ {synced, filtered_low_confidence, no_new_memory, sync_failed, local_persistence_failed, sync_state_persistence_failed, analysis_failed};可选的confidence_bucket(封闭十分位区间,如70_80)仅在模型返回了置信度的结果上出现。 - 发射规则:对每次实际的 Gemini 分析尝试恰好发射一个事件——不是每帧,也不在禁用/门控路径上。每个可达的终态映射到恰好一个 outcome。它补充——不替换、不改写——现有
Memory Extracted成功终态(后者仍只在本地 SQLite 插入后触发,包括synced、sync_failed、sync_state_persistence_failed;在local_persistence_failed后绝不触发)。 - 指标:分析尝试中的 outcome 分布。
synced是唯一完全成功的终态;sync_failed隔离后端创建丢失,local_persistence_failed隔离任何后端调用之前的 SQLite 持久化失败,sync_state_persistence_failed隔离后端成功后本地同步状态回执失败。filtered_low_confidence展示 0.70 置信度阈值的效果。
源码佐证:MemoryAssistantTelemetry.swift 的AnalysisOutcome枚举与confidenceBucket(把原始置信度钳制到[0,1]后向下取整到十分位,上限 90 以确保1.0落到90_100而不是100_100)。MemoryAssistantDurability.emitPersistenceTerminal(同文件 L221-L229)证明:持久化终态发射memoryAssistantAnalysisRun,且仅当shouldEmitMemoryExtracted(即!= .localPersistenceFailed)时才补发历史Memory Extracted成功事件——与文档"本地插入失败后 Memory Extracted 绝不触发"的表述精确一致。
3. 转录会话内存抽取成功:Conversation Memories Extracted(后端)
- 来源事件:
Conversation Memories Extracted(后端服务端转录内存路径;分析身份distinct_id= uid)。封闭属性:memory_count_bucket ∈ {1, 2, 3, 4_9, 10_plus}、source ∈ {transcription, external_integration}、path ∈ {canonical, legacy}。 - 存在原因:"这次录音是否产生了记忆?"步骤在服务端运行并写入 DB,但没有发出任何分析事件——这正是录音→内存可观测性缺口(observability gap)的根因。
- 发射规则:在持久化成功结果之后、于
extract_memories公共边界至多发射一次投递尝试。零抽取 → 不发射事件(无假成功)。持久化异常 → 传播,不发射。在权威的(uid, conversation)文档下的持久化、原子性 Firestore 标记允许在重新终结/重试之间至多一次PostHog 投递尝试,无缓存 TTL 或驱逐窗口。标记在 SDK 构建/捕获之前认领:由于 PostHog 捕获是排队而非投递确认,这明确是至多一次尝试语义——可选遥测可以丢失,但重试不能重复该值。若标记不可查询,这个可选指标失败关闭(不捕获)而终结流程继续。会话 id 仅是标记路径组件,绝不是 PostHog 属性。认领、PostHog 构建与捕获降级都记录共享的有界回退信号;任一失败都不能撤销持久化的抽取。 - 指标:转录内存抽取成功 = 发出
Conversation Memories Extracted的用户 ÷ 已终结会话(分母:Memory Created,录音对账代理)。按 uid + 窗口关联。
源码佐证:后端实现位于 memory_extraction_telemetry.py,docstring 完整复述了上述契约:封闭枚举、零抽取不发射、持久化失败跳过、(uid, conversation_id)维度下的原子 Firestore 标记实现幂等、失败关闭语义;CONVERSATION_MEMORIES_EXTRACTED常量与封闭的_VALID_SOURCES集合直接对应文档口径。配套测试 test_conversation_memories_telemetry.py 与抽取主流程 process_conversation.py 覆盖了该发射路径。
十二、内存可靠性(客户端输入,保持不变)
内存操作可靠性仍通过memory_scope回退结果跟踪(设备作用域拒绝);不发射任何内存内容。上面三个指标是激活/漏斗/价值契约,memory_scope仍是降级信号。
十三、功能路径成功与后端错误率:doctor 指标feature_path_success/backend_error_rate
这两个指标分别归 doctor 报告与后端所有;本规范只要求桌面客户端喂给 doctor 的feature_path_success分子(聊天终态结果、PTT 漏斗、回退结果)使用上述 outcome 语义,使其永远不会是中间事件。
十四、版本控制
当任何分子/分母定义、封闭枚举或字段名发生变化时,升级Schema version并在文档中显式说明变更。PTT 生命周期快照上的telemetry_schema_version以及expected/outcome/mint_attempt_id/phase/close_attempt_id/turn_outcome字段是本文档的机器可读伴生约定。
结语:把规范落成可执行的查询
这份规范的可贵之处在于"可执行":每一个信号都有精确的分子、分母、窗口、最小样本、缺失数据规则与比较规则,配合 DesktopDiagnosticsManager.swift 中的事件枚举与failure_class约束、MemoryAssistantTelemetry.swift 中的封闭 outcome 集合、memory_extraction_telemetry.py 中的至多一次发射语义,以及 Sentry/PostHog 两侧统一发布身份的注册方式,团队可以据此写出稳定、可复现、不会把预期噪音误报为客户回归的发布健康查询。阅读 AGENTS.md 可以了解这些指标背后的产品分析完整性与韧性遥测原则,而 integration-connect-telemetry.md 展示了同一遥测基础设施在其他功能面上的应用方式。
【免费下载链接】FriendAI that sees your screen, listens to your conversations and tells you what to do项目地址: https://gitcode.com/GitHub_Trending/fr/Friend
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考