news 2026/9/13 14:55:47

Windmill 功能使用遥测(feature_usage)接入指南:从埋点设计到落库验证

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Windmill 功能使用遥测(feature_usage)接入指南:从埋点设计到落库验证

Windmill 功能使用遥测(feature_usage)接入指南:从埋点设计到落库验证

【免费下载链接】windmillOpen-source developer platform to power your entire infra and turn scripts into webhooks, workflows and UIs. Fastest workflow engine (13x vs Airflow). Open-source alternative to Retool and Temporal.项目地址: https://gitcode.com/GitHub_Trending/wi/windmill

feature_usage是 Windmill 的产品遥测累加器:以"天"为桶粒度的计数器,最终汇总进匿名使用统计载荷。它回答的是"这个功能有没有人用、大家选的是哪个变体",同时保证没有任何可识别身份的数据离开实例。本文以 docs/feature-telemetry.md 为骨架,结合开源仓库中的前端缓冲实现、后端注册与落库逻辑、迁移脚本与测试用例,完整讲解何时该埋点、如何设计词汇表、前端与后端两条接入路径,以及如何验证一条记录真的落进了数据库。

一、feature_usage 是什么:匿名、分桶、可聚合

在 Windmill 中,feature_usage是一张以天为分桶键的计数器表("day-bucketed counters"),所有计数最终滚入匿名的 usage-stats 载荷。它的设计目标很克制:只回答"有没有人用、选了哪种变体",不回答"谁在用、怎么用的"。

  • 目前注册了49 个动作(action),分布在18 个功能(feature)上:ai_sessionai_chatai_fixai_agentai_agent_evalapp_sandboxdatatableflow_editorflow_runflow_stephomerun_formdebuggertriggercommand_scripthub_scriptusage_metersso_groups_claim
  • 文档直言:产品中"几乎所有"功能都未埋点,因此每一个新的用户可见工作都是补上埋点的机会窗口。

从表结构可以最直观地理解这五个字段如何组合成"可按天、可按实体、可按 key 切分"的计数器。迁移脚本 backend/migrations/20260720081307_add_feature_usage.up.sql 定义了:

CREATE TABLE feature_usage ( feature VARCHAR(50) NOT NULL, kind VARCHAR(50) NOT NULL, key VARCHAR(100) NOT NULL DEFAULT '', entity_id VARCHAR(50) NOT NULL DEFAULT '', day DATE NOT NULL DEFAULT CURRENT_DATE, value BIGINT NOT NULL DEFAULT 0, updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (feature, kind, key, entity_id, day) ); -- 周期性的保留期删除只按 day 过滤;没有这个索引会全表扫描 -- (主键要经过另外四列才能到达 day)。 CREATE INDEX idx_feature_usage_day ON feature_usage (day);

注意两点实现细节:valueBIGINT且可累加,同一(feature, kind, key, entity_id, day)组合在一天内通过 UPSERT 不断累加;而idx_feature_usage_day索引专门服务于 60 天保留期的清理任务(详见后文 backend/src/monitor.rs 中的删除逻辑)。

二、什么时候该埋点,什么时候该保持沉默

埋点不是"每个功能都加"。文档给出了清晰的两分法:

应当提议埋点的场景——当一个新用户可见的交互留下了一个真实的问题:

  • 新的面板、模式、标签页、开关或入口:它到底有没有被发现、被使用?
  • 存在互相竞争的 UX 路径,或新的默认值:哪一条胜出?
  • 有 opt-in 或 beta 门槛:接受率(take rate)是多少?
  • 多步骤流程:用户在哪个环节流失?

应当保持沉默的场景

  • 纯 bugfix、重构、内部管道(plumbing);
  • 有用的信号必须依赖逐条标识数据(路径、名称、提示词、代码)才能表达——这些根本不允许记录,见"隐私规则"一节;
  • 如果答案不会改变任何决策,埋点就是过度设计,什么都不用说。

还有一个工程协作上的硬性约定:埋点要在方案(plan)阶段就提出来,把具体词汇表写成文字,让用户用一行话决定保留或删除,而不是把它当成一个独立的问答打断流程。

三、设计埋点词汇表:五个字段与封闭取值集合

这是整套机制的核心。五个字段各有含义与硬性限制:

字段含义限制
feature产品领域,如ai_chatflow_editor≤50 字符
kind该领域内的动作,如messagepanel_placement(feature, kind)是唯一被允许注册的组合≤50 字符
key动作的某个切面——模式、标签页类型、工具名、provider:model。聚合按(feature, kind, key)分组,因此它决定一个计数器被拆成多少个可比较的桶≤100 字符,identifier-shaped,可选
entity_id不透明的随机id(如会话 id),用于需要按实体分布而非扁平计数时≤50 字符,identifier-shaped,可选
value增量,默认 1被钳制在 1…1,000,000

Identifier-shaped的含义是:只允许 ASCII 字母数字加_ - : . /,不允许空格,其他任何字符都会被拒绝。

entity_id是解锁分布统计的关键:只有提供了它,载荷才会按(feature, kind, key)报告entity_counttotal_valuemedian_valuep90_valueinactive_3d_entity_count。省略它,就是朴素的"这件事发生了多少次"计数器。

key 词汇必须封闭且精简:应该在调用点旁边用 TS union 枚举全部取值,让整套集合能在同一个地方被审查。仓库里的 frontend/src/lib/components/flows/flowEditorTelemetry.ts 就是教科书式示例——它把 flow 编辑器的面板位置拆成恰好三个事件:

export type FlowPanelPlacementEvent = /** 宽度在 `auto` 模式下把面板挤进了 modal。 */ | 'breakpoint_modal' /** 用户在面板处于 modal 时把它固定进了窗格。 */ | 'force_attach' /** 用户在面板处于窗格时把它固定弹出了 modal。 */ | 'force_detach'

这个文件还展示了"什么时候不该计数"的判断力:forcedPlacementEvent只有在"用户钉住的偏好与面板当前所处位置不同"时才返回事件——auto不是被强制的放置,而钉在面板已处的位置则没有移动任何东西,计数它会与真正移动面板的覆盖操作无法区分。同样,createBreakpointTracker只在跨过 breakpoint 时计一次breakpoint_modal,因为mode在拖拽过程中会持续重新解析,按每次求值计数会把一次拖拽读成几百次。

四、接入配方:四步走,第 1 步和第 3 步跳过会"安静地失败"

文档给出了固定的四步配方,并特别警告:跳过第 1 步或第 3 步会安静地失败(fails quietly)

第 1 步:在FEATURE_USAGE_KINDS注册 (feature, kind) 组合

注册表位于backend/windmill-common/src/feature_usage_ee.rs(该文件托管在windmill-ee-private私有仓库)。未注册的(feature, kind)会被is_recordable_event用一个裸的continue丢弃——没有报错、没有日志,浏览器端依然是 204。也就是说:纯前端埋点看起来"一切正常",实际上什么都没记录。

第 2 步:从前端记录

import { logFeatureUsage } from '$lib/utils/featureUsage' logFeatureUsage('flow_editor', 'panel_placement', { key: 'force_detach' })

这是 fire-and-forget 调用。事件会按(workspace, feature, kind, key, entityId)在本地求和,然后:

  • 30 秒刷新一次;
  • visibilitychange→ hidden 时刷新;
  • pagehide时刷新;
  • 每个请求最多50 条事件
  • 失败的批次直接丢弃,不重试

这些常量的源码依据在 frontend/src/lib/utils/featureUsage.ts 中:FLUSH_INTERVAL_MS = 30_000MAX_EVENTS_PER_REQUEST = 50。缓冲实现用Map<string, { workspace, event }>(workspace, feature, kind, key, entityId)聚合,重复事件在本地累加value,这样即使 UI 很啰嗦,每次刷新也只产生一次 UPSERT。

值得注意的工程细节:发送端特意不用生成的 API 客户端,而是裸fetch并带keepalive: true(见 featureUsage.ts 中的flush实现),因为keepalive允许请求在标签页关闭/导航后完成——这正是最后一次 flush 发生的时机。而且每次 flush 会先同步发起所有分块请求再 await,因为 pagehide 刷新只能保护"已经发出的请求",keepalive无法拯救一个从未开始的 fetch。认证则依靠 token cookie(credentials: 'include'),请求目标是POST /w/{workspace}/workspaces/log_feature_usage,该路由在 backend/windmill-api/openapi.yaml 中定义为logFeatureUsage操作,实际挂在 backend/windmill-api-workspaces/src/workspaces.rs 的 workspaced 路由上。

对应的单元测试在 frontend/src/lib/utils/featureUsage.test.ts,覆盖了三个关键行为:重复事件按(feature, kind, key, entity)求和后只 flush 一批、按 workspace 拆分批次且没有 workspace 的事件被丢弃、pagehide flush 时在任一 send resolve 之前发起所有分块请求(否则关页瞬间会丢事件)。

第 3 步:更新披露文案

frontend/src/lib/components/InstanceSettings.svelte 列出了非 minimal 载荷包含的内容——这段文案出现了两次。任何新的计数器如果没有在披露里命名,实例就会"少披露了自己发送了什么"。文档特别提醒:这件事已经漂移过一次("This has already drifted once")。从该组件的披露文本可以看到 feature usage 的具体内容:"counts of which product features are used, including AI provider and model identifiers, the names of public hub scripts used, the languages debug sessions...";此外它还提供了 air-gapped(隔离网络)实例手动下载遥测数据的方式(windmill-telemetry-${date}-${signature}.json)。

第 4 步:验证有行落库

因为存在静默丢弃路径,"没有报错"证明不了任何事。验证方法是直接查库:

SELECT feature, kind, key, entity_id, day, value FROM feature_usage ORDER BY updated_at DESC LIMIT 10;

另一个关键前提:采集位于privatefeature 之后,公共构建(CE)从 HTTP 路由到 Rust helper 都不会记录任何东西。必须用--features enterprise,private运行后端,否则无论埋点多么正确,这张查询都会一直为空。公共构建里对应的是惰性实现 backend/windmill-common/src/feature_usage_oss.rs:is_recordable_event恒返回falselog_feature_usage是空操作,flush_feature_usage直接返回Ok(())——因为 CE 实例从不发送 stats 载荷,计数只会写出没人读的行。

五、隐私规则:什么可以离开实例

只有聚合计数能离开实例,且仅当遥测开启、minimal 模式关闭时。硬性禁止:

  • 永远不要把路径、提示词、脚本内容、workspace 名称、邮箱或任何用户标识符放进keyentity_id
  • entity_id必须是不透明随机 id,绝不能映射回某个用户或资源;
  • 如果想要的信号只能用标识数据表达,那它根本不能被采集——放弃它

数据生命周期:计数器聚合最近30 天的数据,行在60 天后被清理。清理逻辑并不依赖遥测发送器,而是独立跑在 backend/src/monitor.rs 的监控循环里,因此即使遥测被禁用、或构建没有 stats 调度器,保留期行也会被修剪:

// 60-day retention for anonymous feature-usage counters. Runs here (not only // in the telemetry sender) so rows are pruned even when telemetry is disabled // or the build has no stats scheduler. if let Err(e) = sqlx::query!("DELETE FROM feature_usage WHERE day < CURRENT_DATE - 60") .execute(db) .await { tracing::error!("Error deleting old feature_usage rows: {e}"); }

仓库在隐私上还有更细的实践。例如 featureUsage.ts 中的logHubScriptPickhubScriptUsageKey:公开 hub 的脚本被 slugify 后记录app/summary;而来自私有 hub 的脚本是客户自己的内容,因此在PRIVATE_HUB_MIN_VERSION及以上只记录"用了私有脚本"这个事实(key 塌缩为private)。同理,hubProjectUsageKey只有当 hub 域名与公共 hub 一致时才上报项目 slug,并且要求hubBaseUrlKnown已确认——因为 store 默认种入公共 hub 地址,一个读不到设置的实例否则会把自家项目名上报出去。

六、从后端记录:无 UI 的功能怎么埋

没有 UI 的功能用同样的方式从 Rust 记录:

windmill_common::feature_usage::log_feature_usage("trigger", "fired", kind.as_str());
  • 同一个注册表、同样的 key 规则、同样的"未注册即静默丢弃";
  • featurekind&'static str调用点无法传入计算出的组合——这是编译期对封闭词汇表的强制;
  • 该调用只递增一个内存计数器然后返回,由 monitor 循环统一 flush 累加器,因此对热路径足够便宜——但只是单次调用便宜,不是免费:一个基数无限的 key 会让 map 不断增长,直到撞上 per-action 上限并开始丢弃新 key;
  • 这条路径没有entity_id、没有显式value——它只数"发生了多少次"。

feature_usage_ee持有注册表与写入器;公共构建拿到的是惰性的feature_usage_oss(见 backend/windmill-common/src/feature_usage_oss.rs),因为 CE 实例从不发送 stats 载荷。这也解释了为何 flush 循环在 backend/src/monitor.rs 中不按 server_mode 门控:feature-usage 计数器在任何一个被埋点的调用点都会累加,一个从不 flush 的 worker 会在关停时丢掉计数,所以无论什么模式都要 flush。

仓库里现成的后端埋点调用点可以当模板参考,例如:

  • backend/windmill-api-debug/src/lib.rs 在调试会话创建时记录("debugger", "session", lang_key)
  • backend/windmill-api-workspaces/src/datatable_migrations.rs 记录("datatable", "migration_run" | "migration_rollback" | "migrations_toggled" | "migration_created", ...),并且特意在"未变更的重新推送"上计数,以免wmill sync push每次都同步全部迁移而淹没有意义的计数;
  • backend/windmill-api/src/ai_evals/run.rs 与 backend/windmill-api/src/ai_evals/datasets.rs 记录("ai_agent_eval", "run" | "dataset_created", ...),其中 run 的 key 表达"被测的是哪个状态的 agent"——部署版本、编辑中的改动还是旧版本。

七、端到端数据流:从一次点击到匿名统计载荷

把整条链路串起来看,一次前端埋点事件的生命周期是:

  1. 调用点logFeatureUsage('flow_editor', 'panel_placement', { key: 'force_detach' })(前端)或log_feature_usage("trigger", "fired", kind)(Rust);
  2. 本地聚合:前端按(workspace, feature, kind, key, entityId)在内存 Map 里累加value,最多攒 30 秒 / 50 条;Rust 端则递增内存计数器等待 monitor 循环 flush;
  3. 传输:前端用带keepalive的裸fetchPOST 到/w/{workspace}/workspaces/log_feature_usage,批次失败直接丢弃;Rust 端由 backend/src/monitor.rs 的循环调用flush_feature_usage写入数据库;
  4. 注册表过滤is_recordable_event校验(feature, kind)是否在FEATURE_USAGE_KINDS白名单内,未注册即静默丢弃(无日志、前端照样 204);
  5. 落库:按(feature, kind, key, entity_id, day)主键 UPSERT 累加;
  6. 汇总与清理:聚合最近 30 天数据进匿名 usage-stats 载荷(含entity_count/median_value/p90_value等分布统计),60 天前的行由 monitor 循环按day索引删除。

值得重复一遍的三条实操铁律:新埋点必须在 plan 阶段带上完整词汇表key 词汇用 TS union 封闭在调用点旁边每一步都必须用"查库有行"来验证,而不是"没有报错"——因为整套机制对未注册组合是默认静默的。最后,采集只存在于--features enterprise,private构建中,公共构建的 feature_usage_oss.rs 是彻底的空操作。

【免费下载链接】windmillOpen-source developer platform to power your entire infra and turn scripts into webhooks, workflows and UIs. Fastest workflow engine (13x vs Airflow). Open-source alternative to Retool and Temporal.项目地址: https://gitcode.com/GitHub_Trending/wi/windmill

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 14:54:10

蓝队防御生成式AI安全威胁的策略与技术

1. 项目概述&#xff1a;蓝队如何对抗生成式AI的安全威胁在生成式AI技术快速发展的今天&#xff0c;大语言模型(LLMs)和扩散模型等生成式AI系统正面临前所未有的安全挑战。作为防御方的蓝队&#xff0c;需要针对红队暴露的漏洞&#xff0c;构建有效的防御体系。这个对抗过程就像…

作者头像 李华
网站建设 2026/9/13 14:54:03

SQL Server OBJECT_ID函数详解:对象存在性判断与元数据管理实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 14:53:57

开源具身智能数据采集平台盘点:从Mobile ALOHA到UMI的高校落地方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 14:53:57

从日志采集到攻击链检测:主机安全态势感知系统实战

简介&#xff1a;这份主机安全态势感知系统毕业设计资源&#xff0c;面向计算机相关专业的学生或需要搭建安全监控原型的开发者&#xff0c;围绕实时监控、异常检测与威胁预警展开&#xff0c;帮助理解从数据采集到AI分析落地的完整流程。压缩包共662个文件&#xff0c;大小35.…

作者头像 李华
网站建设 2026/9/13 14:52:34

GD32F103手搓FreeRTOS内核:从启动文件到上下文切换全链路解析

1. 项目概述&#xff1a;这不是“点灯”&#xff0c;而是一次嵌入式系统认知的彻底重装“点灯大师进阶&#xff0c;从手搓操作系统开始&#xff08;10&#xff09;”——这个标题乍看像极了嵌入式新手教程里常见的“点亮LED”彩蛋&#xff0c;但括号里的“&#xff08;10&#…

作者头像 李华
网站建设 2026/9/13 14:51:06

ES9(ES2018)新特性详解:异步迭代器、对象展开与正则增强

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华