Strapi 服务端遥测(Telemetry)全解析:事件上报链路、命名规范与限流机制
本文以 Strapi monorepo 中的核心文档 docs/docs/docs/01-core/strapi/telemetry.md 为主线,完整讲解 Strapi 服务端遥测的实现机制:strapi.telemetry.send 的调用链路、事件命名与载荷(Payload)规范、各包中事件的发射位置,以及防止埋点“打爆”分析后端的五类流量控制策略。读完后你可以理解每个埋点事件从哪里产生、如何被组装与限流、何时会静默失效,并掌握为 Strapi 新增一个符合规范的 server-side 埋点所需的完整清单。
架构总览:从事件发射到 analytics 后端
Strapi 通过收集匿名使用数据来理解功能采纳情况并改进产品。服务端遥测的完整链路如下:
┌─────────────────────────────────────────────────────────────────┐
│ Feature code (controller, service, bootstrap, CLI, cron) │
│ strapi.telemetry.send('didSomething', { ...payload }) │
└────────────────────────────┬────────────────────────────────────┘
│
┌────────────────────────────▼────────────────────────────────────┐
│ packages/core/core/src/services/metrics/ │
│ index.ts → createTelemetryInstance, LIMITED_EVENTS │
│ sender.ts → POST body assembly + strapi.fetch() │
│ rate-limiter.ts → once-per-24h dedup for selected events │
│ middleware.ts → didReceiveRequest (REST, capped at 1000/d) │
└────────────────────────────┬────────────────────────────────────┘
│
▼
https://analytics.strapi.io/api/v2/track
(override: STRAPI_ANALYTICS_URL)
四个关键文件各司其职:
- index.ts — 工厂函数,暴露
send()、register()、isDisabled,并定义限流事件白名单LIMITED_EVENTS; - sender.ts — 组装 POST 请求体,通过
strapi.fetch()发送; - rate-limiter.ts — 对选定事件做“每 24 小时最多一次”的去重;
- middleware.ts — 为 REST 请求发射
didReceiveRequest,单进程上限 1000 次/天。
需要注意边界:Admin 面板(浏览器端)事件走的是另一条通道(React useTracking() 钩子),本文只覆盖 server-side 模式;MCP 及其他仅存在于服务端的功能也应使用下文的 server-side 模式。
注册:telemetry provider 如何接入容器
遥测通过 telemetry provider 挂载到 Strapi 容器,注册链路见 telemetry.ts:
export default defineProvider({
init(strapi) {
strapi.add('telemetry', () => createTelemetry(strapi));
},
async register(strapi) {
strapi.get('telemetry').register();
},
// bootstrap / destroy 同理透传
});
register() 阶段(前提是遥测未被禁用)会做两件事,对应 index.ts 中的源码:
- 每日 cron 心跳:注册
sendPingEvent定时任务,cron 表达式为0 0 12 * * *(每天中午 12 点发送ping事件); - REST 请求中间件:
strapi.server.use(createMiddleware({ sendEvent, strapi })),用于发射didReceiveRequest(限流细节见下文“流量控制”一节)。
发送一个事件
业务代码中的标准调用方式是:
strapi.telemetry.send('didCreateContentType', {
eventProperties: { kind: 'collectionType' },
userProperties: {
/* optional, per-user */
},
groupProperties: {
/* optional, project-level */
},
});
实现位于 sender.ts。每个事件就是一次 HTTP POST(无批处理 API),请求体字段如下:
| 字段 | 用途 |
|---|---|
event |
事件名(同时作为 X-Strapi-Event 请求头发送) |
userId |
可用时的哈希化管理员用户(generateAdminUserHash) |
installId |
由项目 uuid + package.json 生成的稳定安装标识 |
eventProperties |
该次行为特有的上下文 |
userProperties |
用户/环境特征(OS、Node 版本等) |
groupProperties |
项目特征(Strapi 版本、DB client、EE plan、各类计数等) |
sender 的源码级细节
阅读 sender.ts 可以看到若干文档层面没有展开但直接影响行为的关键实现:
- 默认元数据自动合并。
anonymousGroupProperties自动附带docker(process.env.DOCKER或is-docker检测结果)、isCI(ci-info)、version、useTypescriptOnServer/useTypescriptOnAdmin(懒加载@strapi/typescript-utils判断)、projectId、isHostedOnStrapiCloud(STRAPI_HOSTING === 'strapi.cloud');并额外注入projectType: strapi.EE ? 'Enterprise' : 'Community'。package.json中strapi键下的字段也会经_.defaults合并进groupProperties; - EE 字段条件注入:仅当
strapi.ee.subscriptionId/strapi.ee.planPriceId非空时才写入groupProperties; - installId 生成:调用 install-id.ts 中的
generateInstallId(uuid, installIdFromPackageJson)—— 若package.json已有strapi.installId则直接复用;否则以node-machine-id取机器标识,与 uuid 拼接后做 SHA-256;任一环节失败则回退为crypto.randomUUID(); userProperties只在存在 userId 时才发送(userId ? { ...anonymousUserProperties, ...payload.userProperties } : {}),保证匿名事件不带用户属性;- 网络与容错:默认
timeout: 1000(fetch 超时 1 秒);目标地址为env('STRAPI_ANALYTICS_URL', 'https://analytics.strapi.io') + '/api/v2/track',支持用环境变量覆盖端点(本地调试或自建收集器时可用);整个发送逻辑包在try/catch中,失败仅返回false——遥测绝不允许破坏应用行为,调用方通常还会追加.catch(() => {})或干脆不 await。
何时遥测处于禁用状态
由 index.ts 判定,满足任意一条即视为禁用:
const isDisabled =
!uuid || isTruthy(process.env.STRAPI_TELEMETRY_DISABLED) || isTruthy(telemetryDisabled);
- 配置中没有项目
uuid(典型场景:fresh clone、首次运行之前); - 环境变量
STRAPI_TELEMETRY_DISABLED为真值(1、true等); package.json中strapi.telemetryDisabled: true。
禁用时的行为特征:
strapi.telemetry.isDisabled返回该状态;暴露遥测数据的 admin 路由会走admin::isTelemetryEnabled策略;- sender 对象根本不会被创建(
isDisabled ? null : createSender(strapi)),send()直接返回true空转——这是源码注释中明确的设计意图:关闭时连 sender 及其@strapi/typescript-utils消费方都跳过; - 因此业务代码无需在发送前额外判断禁用状态(除非你在此之前要做昂贵的指标计算)。
另外,仓库提供了 strapi telemetry:disable CLI 命令来落盘禁用配置,执行时会发出 didOptOutTelemetry 事件(见下文 CLI 事件表)。
事件命名规范
服务端事件遵循统一词汇表,事件名是扁平的 camelCase 字符串,没有命名空间前缀(例如不存在 mcp.didCallTool 这种形式):
| 模式 | 含义 | 示例 |
|---|---|---|
did* |
已完成的动作 | didCreateContentType、didInviteUser |
didNot* |
失败或中断的动作 | didNotCreateContentType、didNotOpenTab |
didCreateFirst* |
项目生命周期内首次发生 | didCreateFirstAdmin、didCreateFirstContentType |
didInitialize* |
插件/功能 bootstrap 快照 | didInitializeI18n、didInitializePluginUpload |
did*ProcessStart/Finish/Fail |
长任务生命周期 | didDEITSProcessStart(数据导出/导入/传输) |
didSend*OnceAWeek |
每周聚合快照 | didSendUploadPropertiesOnceAWeek |
ping |
心跳 | 每日 cron |
will* |
意图表达(主要出现在 admin 前端,服务端少见) | — |
Payload 规范
eventProperties:行为上下文
只放“这次动作特有”的信息。代码库中的典型字段:
kind— 创建 content type 时的类型种类;model— 首个 entry 创建时的 content type UID;url、success、statusCode— REST 中间件;source、destination— 数据传输 provider;error— 失败信息。
隐私取向:优先类别(category)和计数(count),而不是标识符。存量事件中确有包含 UID 的(model、workflowId),但文档明确要求新特性在“类别足以表达”时避免重复用户级或内容级名称。
userProperties:用户/环境特征
- 服务端自动附带:
environment、os、osPlatform、osArch、osRelease、nodeVersion(见 sender 中的anonymousUserProperties); - 显式传入示例:
languagesInUse(didChangeInterfaceLanguage); - 部分事件显式匿名(空
userId),如didStartServer、didChangeInterfaceLanguage。
groupProperties:项目/安装级状态
通常是计数或配置:
numberOfAllContentTypes、numberOfComponents、database、plugins— 启动事件;numberOfLocales— i18n;uploadProvider、privateProvider— upload 初始化;numberOfActiveAdminUsers— admin cron;- 上传 / 审核工作流的周聚合指标对象。
启动事件 didStartServer 的 groupProperties 可以在 Strapi.ts 中逐项核对:database.connection.client、plugins 键列表、numberOfAllContentTypes、numberOfComponents、numberOfDynamicZones、numberOfConditionalFields、numberOfCustomControllers、environment。
服务端事件的发射位置
Core / 启动阶段
| 事件 | 位置 | 说明 |
|---|---|---|
didStartServer |
Strapi.ts → sendStartupTelemetry() |
每次进程启动触发一次;groupProperties 内容丰富 |
didOpenTab / didNotOpenTab |
Strapi.ts → openAdmin() |
仅开发环境自动打开浏览器时 |
ping |
metrics register() 的 cron |
每天中午 |
didReceiveRequest |
middleware.ts | 仅 REST API;不覆盖 MCP/GraphQL/admin |
其中 didStartServer 在 postListen() 中被触发(服务器监听成功、打印启动日志之后),且刻意不 await 以避免拖慢启动。didOpenTab/didNotOpenTab 成对出现,用于对比“自动打开浏览器”的成功率。
Admin(packages/core/admin/server)
| 事件 | 触发点 |
|---|---|
didCreateFirstAdmin |
首个管理员注册 |
didInviteUser |
用户邀请 |
didUpdateRolePermissions |
角色权限保存 |
didChangeInterfaceLanguage |
界面语言偏好变更 |
didUpdateProjectInformation |
bootstrap + 每日午夜 cron |
didUpdateSSOSettings |
EE SSO 设置(EE) |
didWatchAnAuditLog |
EE 审计日志查看(EE) |
metrics 服务:metrics.ts。
Content-type builder
| 事件 | 触发点 |
|---|---|
didCreateFirstContentType / didCreateContentType |
创建 content type(_.isEmpty(strapi.apis) 判定为“首次”) |
didNotCreateContentType |
创建失败 |
didCreateFirstComponent / didCreateComponent |
创建 component(以 registry 大小判定首次) |
Content manager
| 事件 | 触发点 |
|---|---|
didCreateFirstContentTypeEntry |
集合类型中的首个文档(totalEntries === 0) |
didConfigureListView |
保存列表视图配置 |
metrics 服务:metrics.ts。
Content releases
didCreateContentRelease、didUpdateContentRelease、didDeleteContentRelease、didPublishContentRelease — 位于 release.ts。
Review workflows(EE)
- 单动作事件:
didCreateStage、didEditStage、didDeleteStage、didChangeEntryStage、didCreateWorkflow、didEditWorkflow、didEditAssignee; - 周聚合事件:
didSendReviewWorkflowPropertiesOnceAWeek,由 weekly-metrics.ts 中的 cron 发送。
Upload
| 事件 | 触发点 |
|---|---|
didInitializePluginUpload |
插件 bootstrap(每日限流) |
didSaveMediaWithCaption / didSaveMediaWithAlternativeText |
媒体保存(每日限流) |
didEnableResponsiveDimensions / didDisableResponsiveDimensions |
设置变更(每日限流) |
didUploadImage |
图片上传 |
didBulkDeleteMediaLibraryElements / didBulkMoveMediaLibraryElements |
批量操作 |
didSendUploadPropertiesOnceAWeek |
每周 cron 聚合 |
metrics 封装:metrics.ts → trackUsage()。
i18n
| 事件 | 触发点 |
|---|---|
didInitializeI18n |
插件 bootstrap |
didUpdateI18nLocales |
Locale 增删改 |
metrics 服务:metrics.ts。
CLI(内嵌 Strapi 或独立运行)
| 事件 | 上下文 |
|---|---|
didDEITSProcessStart / didDEITSProcessFinish / didDEITSProcessFail |
数据导出(Export)、导入(Import)、传输(Transfer) |
didOptOutTelemetry |
strapi telemetry:disable |
will* / did* 项目创建事件 |
packages/cli/create-strapi-app |
| Cloud CLI 事件 | 经 Cloud API 代理(packages/cli/cloud) |
MCP(core)
MCP 是当前仓库中埋点设计最精细的模块,事件全部定义在 metrics.ts:
| 事件 | 触发点 |
|---|---|
didStartMcpServer |
server.mcp.enabled: true 时 MCP 服务器启动(eventProperties.path;groupProperties.numberOfTools / numberOfPrompts / numberOfResources;每日限流) |
didUseMcpServer |
认证通过的 MCP HTTP 请求(handlePost.ts;每日限流) |
didNotAuthenticateMcpRequest |
处理前被拒绝(eventProperties.errorClass: missing_token | invalid_token;每日限流) |
didNotHandleMcpRequest |
已认证请求在 connect/handle 阶段失败(eventProperties.errorClass: timeout | error;每日限流) |
didExecuteMcpCapability |
能力执行成功(eventProperties.type: tool | prompt | resource;另含 action、source) |
didNotExecuteMcpCapability |
能力返回 isError: true(同上结构 + errorClass: execution_error) |
两个值得注意的实现决策:
- 能力名归一化:工具/提示词/资源名被归一化为粗粒度的
action值(见 normalizeMcpCapability.ts),绝不使用 content type slug 等高基数字面量; - 双层去重:请求级事件走核心 rate limiter(事件名维度);
didExecuteMcpCapability/didNotExecuteMcpCapability则按 type + action + source 组合在metrics.ts内本地去重——这验证了后文“按类别先去重、再发送”的流量策略。对应测试见 metrics.test.ts。
对比 didStartMcpServer 与 didUseMcpServer 两个事件,可以识别出“启用了 MCP 但从未使用”的安装实例;prompt/resource 的执行埋点在归一化规则存在时激活。
包级 metrics 服务模式
多个包不把 strapi.telemetry.send 直接写在 controller 里,而是封装在专用的 metrics 服务中(仓库中均已确认存在):
packages/core/admin/server/src/services/metrics.ts
packages/core/content-manager/server/src/services/metrics.ts
packages/core/upload/server/src/services/metrics.ts
packages/core/core/src/services/mcp/metrics/metrics.ts
packages/plugins/i18n/server/src/services/metrics.ts
packages/core/review-workflows/server/src/services/metrics/index.ts
packages/core/admin/ee/server/src/services/metrics.ts
模式:薄服务 + sendDid…() 系列 helper;controller/service 只调用 metrics 服务,不直接触碰 strapi.telemetry。测试相应地 mock strapi.telemetry.send 或 mock 整个 metrics 服务。
流量控制(Volume Controls)
没有批处理 API —— 每个事件都是一次独立 HTTP 请求。Strapi 用五种互补策略控制总量:
1. 每日限流器(按事件名)
rate-limiter.ts 将 sender 包装一层,只对 LIMITED_EVENTS 中列出的事件生效。当前白名单(见 index.ts):
const LIMITED_EVENTS = [
'didSaveMediaWithAlternativeText',
'didSaveMediaWithCaption',
'didDisableResponsiveDimensions',
'didEnableResponsiveDimensions',
'didInitializePluginUpload',
...Object.values(MCP_LIMITED_TELEMETRY_EVENTS),
];
实现细节:一个 Map 缓存 + 滚动 24 小时窗口(cacheExpiresAt 到期后整体清空再重置)。关键限制:去重维度仅为事件名,不看 eventProperties。如果一个事件名覆盖多个类别(例如 didExecuteMcpCapability 有不同 type/action),必须在调用 strapi.telemetry.send 之前自行按类别去重(参考 MCP metrics.ts),而不是往 LIMITED_EVENTS 里堆一堆名字。
为高频新事件选择限流方案的四种路径:
- 把其精确事件名加入
LIMITED_EVENTS; - 每个类别使用独立事件名(各加入
LIMITED_EVENTS); - 发送前本地按类别去重(MCP 工具 action 的做法);
- 使用周聚合(见策略 3)。
2. REST 中间件上限
middleware.ts 中 didReceiveRequest 每个进程每 24 小时最多 1000 条,滚动窗口到期后计数器归零。匹配条件:
- URL 必须包含 REST API 前缀(
strapi.config.get('api.rest.prefix')); - 方法限于
GET/PUT/POST/DELETE; - URL 含
.的静态资源被跳过。
计数器在中间件入口处立即自增(防止竞态),实际发送推迟到 ctx.res 的 finish 或 close 事件,并按响应状态计算 success(2xx 为成功,无状态码时按 500 记)。发送用 Promise.resolve(...).catch(() => {}) 包裹,静默吞掉一切错误。MCP 请求(/mcp)不经过此中间件。
3. 周聚合 cron
对噪声大的领域,本地算好指标、每周只发一条事件:
- Upload:
didSendUploadPropertiesOnceAWeek— 文件夹深度统计、资源数量(weekly-metrics.ts+ 用strapi.store保存调度抖动); - Review workflows:
didSendReviewWorkflowPropertiesOnceAWeek— workflow/stage 计数。
调度时刻按安装实例随机化并持久化在 strapi.store 中,用于分摊后端压力。
4. “首次”与“后续”分离
不需要持久化 store 的“首用”模式:
didCreateFirstContentType:创建前_.isEmpty(strapi.apis);didCreateFirstComponent:component registry 为空;didCreateFirstAdmin:注册流程中触发;didCreateFirstContentTypeEntry:totalEntries === 0。
后续动作统一使用不带 First 的事件名(didCreateContentType、didCreateComponent 等),两类事件分开统计即可区分首用曲线与使用量曲线。
5. Fire-and-forget / 非阻塞
启动遥测与大量调用点都不 await 发送结果,失败被忽略;fetch 超时默认 1 秒。这条策略保证遥测在任何情况下都不成为业务链路上的阻塞点。
新增服务端事件检查清单
文档给出的 7 步清单可直接作为 PR 自检项:
- 命名 —
did*过去时;该用didCreateFirst*或didInitialize*时不要偷懒; - 载荷 — 动作上下文进
eventProperties,项目级计数进groupProperties; - 隐私 — 避免 PII、密钥与高基数字符串(content type UID、文档 ID 等),除非确有必要;
- 量级 — 可能高频触发的动作,第一天就接入限流或周聚合;
- 位置 — 优先放在所属包的
metrics服务中,在 controller/service 成功后调用; - 禁用守卫 —
strapi.telemetry.send禁用时自动空转,无需额外判断(除非发送前有昂贵的指标计算,可先查isDisabled); - 测试 — 单测中 mock
strapi.telemetry.send(可参考各包内现成的__tests__/metrics.test.ts,如 MCP metrics 测试)。
相关文档
- 采集数据的公开说明(Strapi 官网 Usage information 页面)——说明哪些数据被收集;
- Admin 前端遥测是独立通道(
useTracking()、插件包装器、生命周期事件),新 server-side 功能(如 MCP)应使用strapi.telemetry.send而非 React tracking hook; - API 参考:Telemetry Service;
- 关键类型定义:sender.ts 中的
Payload、Sender。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00