Backstage 插件级分析(Plugin Analytics):事件模型、自定义集成与埋点实践指南
【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage
本文基于 Backstage 仓库中的官方插件分析文档,系统讲解 Backstage 基于事件的 Analytics API:从 Events / Attributes / Context 三大核心概念出发,覆盖现成分析工具接入、自定义 AnalyticsApi 集成、useAnalytics()埋点、AnalyticsContext上下文注入、事件命名规范与单元测试,帮助开发者度量 Backstage 实例的真实使用情况,并为自己的插件标准化埋点。读完本文,你将能够为任意分析平台编写@backstage/analytics-module-*集成,并在插件中正确捕获可聚合、可下钻的事件。
说明:本文对应的官方文档属于旧版前端系统的插件文档,新版前端系统的对应版本参见 Plugin Analytics。本文描述的概念与事件在新旧两套前端系统中同样适用。
为什么 Backstage 需要 Analytics API
搭建、维护并持续迭代一个 Backstage 实例是一笔不小的投入。为了衡量这笔投入的回报,Backstage 内置了一个基于事件(event-based)的 Analytics API:它一方面给予应用集成方充分的灵活性,让他们可以把 Backstage 的使用数据收集到任意自选的分析工具中;另一方面又为插件开发者提供了一套标准接口,用来对关键用户交互进行埋点。
这套设计的核心在于"事件组合"(composition of events),它允许分析同时回答两类问题:
- 细粒度问题:例如"某个特定路由上被点击最多的元素是什么";
- 宏观问题:例如"我的 Backstage 实例中哪个插件使用率最高"。
在源码层面,这个能力由 AnalyticsApi 定义文件 提供:analyticsApiRef是 ID 为core.analytics的ApiRef,而AnalyticsApi接口只有一个方法captureEvent(event: AnalyticsEvent): void,所有事件最终都会汇聚到这个唯一的入口。
核心概念:Events、Attributes 与 Context
| 概念 | 含义 | 示例 |
|---|---|---|
| Events(事件) | 至少包含一个action(如click)和一个subject(如"被点击的东西") | click+"Deploy" |
| Attributes(属性) | 事件级的额外维度数据,键/值对形式 | 点击跳转的目标 URL:{ "to": "/a/page" } |
| Context(上下文) | 事件发生的更广阔背景,默认提供pluginId、extension、routeRef等信息 | { "pluginId": "catalog", "routeRef": "catalogIndexRouteRef" } |
从源码类型定义看,事件对象的完整结构如下(见 AnalyticsApi.ts):
export type AnalyticsEvent = { action: string; // 事件动作,如 view / click / filter / search subject: string; // 动作作用的对象,如页面路径、链接 URL value?: number; // 可选数值,如排名、进度百分比、耗时 attributes?: AnalyticsEventAttributes; // 可选维度数据 { [key]: string | boolean | number } context: AnalyticsContextValue; // 上下文元数据 };其中context的类型来自 analytics/types.ts:它由CommonAnalyticsContext(pluginId、routeRef、extension三个必填字段)与任意自定义键值组成。pluginId指事件被捕获处最近的父插件,routeRef是事件捕获时激活的路由引用 ID,extension是最近的父扩展。
官方支持的分析工具
消费并转发这些事件只需要一个 AnalyticsApi 的具体实现;不过常用集成已经被打包成插件提供,可直接选用:
| 分析工具 | 支持状态 |
|---|---|
| Google Analytics 4 | 官方支持 ✅ |
| New Relic Browser | 社区支持 ✅ |
| Matomo | 社区支持 ✅ |
| Quantum Metric | 社区支持 ✅ |
| Generic HTTP | 社区支持 ✅ |
想为你的组织使用的工具新增集成,可以提交 issue 建议,也可以直接阅读下文 Writing Integrations 章节,自己动手贡献一个集成。
关键事件一览
下表汇总了(取决于你所安装的插件)可能被捕获的标准事件。这些事件是各插件埋点的事实标准,也是你自定义事件命名时的参照基准。
| 动作(Action) | 主体(Subject) | 其他说明 |
|---|---|---|
navigate | 被导航到的页面 URL | 路由位置变化时立即触发;若关联的插件/路由数据不明确,则会在插件/路由数据就绪后、下一条事件或文档卸载前触发。当前路由的参数会作为 attributes 一并上报 |
click | 被点击链接的文本 | to属性表示点击跳转到的 URL |
create | 被创建软件的名称(name);若对应软件模板没有请求name属性,则使用字符串new {templateName} | context 中包含entityRef(模板引用,如template:default/template-name);value表示运行该模板节省的分钟数(基于模板的backstage.io/time-saved注解,若有) |
search | 在任意搜索栏组件中输入的搜索词 | context 中包含searchTypes(约束搜索范围的types);value为该查询的总结果数(若启用权限框架,该值可能不可见) |
discover | 被点击的搜索结果标题 | value为结果排名;同时提供to属性 |
not-found | 导致 404 页面出现的资源路径 | 至少由 TechDocs 触发 |
navigate事件的时序细节在 Tracker 实现 中有对应的源码逻辑:当路由变化发生在"gathered mountpoint"(一个路由节点下聚合多个插件的场景)时,navigate事件会被暂存到全局存储(mostRecentGatheredNavigation),待插件/路由数据确定后、或在下一条真实事件触发前、或页面beforeunload时补发,从而保证导航事件的准确性。相关行为在 Tracker.test.ts 中有系统性的单元测试覆盖(例如"绝不立即捕获_routeNodeType为gathered的 navigate 事件")。
Writing Integrations:编写自己的分析集成
分析事件转发在 Backstage 中实现为一个Utility API。就像你可以为错误处理或 SCM 认证提供自定义 API 实现一样,你也可以为分析提供自定义实现。所需的 API 只需提供一个方法captureEvent,接收一个AnalyticsEvent对象。
最小实现
旧版前端系统中,通过createApiFactory将自定义实现注册到analyticsApiRef:
import { analyticsApiRef, AnalyticsEvent, AnyApiFactory, createApiFactory, } from '@backstage/core-plugin-api'; export const apis: AnyApiFactory[] = [ createApiFactory(analyticsApiRef, { captureEvent: (event: AnalyticsEvent) => { window._AcmeAnalyticsQ.push(event); }, }), ];如果是在新版前端系统中构建,则使用AnalyticsImplementationBlueprint(其源码定义见 AnalyticsImplementationBlueprint.ts):
import { AnalyticsImplementationBlueprint } from '@backstage/frontend-plugin-api'; export const acmeAnalyticsImplementation = AnalyticsImplementationBlueprint.make({ name: 'acme', params: define => define({ deps: {}, factory() { return { captureEvent: event => { window._AcmeAnalyticsQ.push(event); }, }; }, }), });结合配置的完整实现
实际上,你通常需要封装实例化逻辑并从配置中读取参数。更完整的示例如下:
import { AnalyticsApi, analyticsApiRef, AnalyticsEvent, AnyApiFactory, configApiRef, createApiFactory, } from '@backstage/core-plugin-api'; import { AcmeAnalytics } from 'acme-analytics'; class AcmeAnalytics implements AnalyticsApi { private constructor(accountId: number) { AcmeAnalytics.init(accountId); } static fromConfig(config) { const accountId = config.getString('app.analytics.acme.id'); return new AcmeAnalytics(accountId); } captureEvent(event: AnalyticsEvent) { const { action, ...rest } = event; AcmeAnalytics.send(action, rest); } } export const apis: AnyApiFactory[] = [ createApiFactory({ api: analyticsApiRef, deps: { configApi: configApiRef }, factory: ({ configApi }) => AcmeAnalytics.fromConfig(configApi), }), ];新版前端系统的等价写法:
import { AnalyticsImplementationBlueprint } from '@backstage/frontend-plugin-api'; export const acmeAnalyticsImplementation = AnalyticsImplementationBlueprint.make({ name: 'acme', params: define => define({ deps: { configApi: configApiRef }, factory: ({ configApi }) => AcmeAnalytics.fromConfig(configApi), }), });命名约定:如果你是在与一个分析服务(而非内部工具)做集成,请考虑把该 API 实现作为插件贡献出来。按惯例,此类包命名为
@backstage/analytics-module-[name],相关配置键统一放在app.analytics.[name]之下。
处理用户身份(User Identity)
如果你所集成的分析平台具备"用户身份"的一等概念,可以(可选地)按如下约定支持它:
- 允许实现通过
fromConfig静态方法、以identityApi作为其中一个选项来实例化; - 使用
identityApi的getBackstageIdentity()方法解析出的userEntityRef作为发送到分析平台的用户 ID 基础。
import { AnalyticsApi, analyticsApiRef, AnyApiFactory, configApiRef, createApiFactory, identityApiRef, IdentityApi, } from '@backstage/core-plugin-api'; // 可选用 userId 初始化的实现。 class AcmeAnalytics implements AnalyticsApi { private constructor(accountId: number, identityApi?: IdentityApi) { if (identityApi) { identityApi.getBackstageIdentity().then(identity => { AcmeAnalytics.init(accountId, { userId: identity.userEntityRef, }); }); } else { AcmeAnalytics.init(accountId); } } static fromConfig(config, options) { const accountId = config.getString('app.analytics.acme.id'); return new AcmeAnalytics(accountId, options.identityApi); } } // 你的实现应这样实例化: export const apis: AnyApiFactory[] = [ createApiFactory({ api: analyticsApiRef, deps: { configApi: configApiRef, identityApi: identityApiRef }, factory: ({ configApi, identityApi }) => AcmeAnalytics.fromConfig(configApi, { identityApi, }), }), ];Capturing Events:在组件中捕获事件
要在组件中埋点,首先通过@backstage/core-plugin-api提供的useAnalytics()钩子获取一个分析追踪器(tracker)。追踪器包含captureEvent方法,接收action和subject两个参数:
import { useAnalytics } from '@backstage/core-plugin-api'; const analytics = useAnalytics(); analytics.captureEvent('deploy', serviceName);从 useAnalytics 实现 可以看到,该钩子通过useAnalyticsContext()读取当前 React 树中的上下文,并把它交给一个可复用的Tracker实例;即使analyticsApiRef未被注册(例如在测试环境中),它也会回退到空实现的{ captureEvent: () => {} },保证 API 对任何消费代码都是"真正可选"的。
提供额外属性(attributes)与数值(value)
在第三个options参数上可以附带额外的维度attributes以及数值型value:
analytics.captureEvent('merge', pullRequestName, { value: pullRequestAgeInMinutes, attributes: { org, repo, }, });上面的示例最终会捕获到如下事件对象:
{ "action": "merge", "subject": "Name of Pull Request", "value": 60, "attributes": { "org": "some-org", "repo": "some-repo" } }其中action、subject、value、attributes、context的字段约束均可在 AnalyticsApi.ts 的类型定义中找到对应依据。
为事件提供上下文:AnalyticsContext
attributes选项适合捕获组件内部就能拿到的细节。若要捕获 React 树更高层才可获得的元数据,或者希望帮助应用集成方按某个公共值聚合不同的事件,请使用<AnalyticsContext>:
import { AnalyticsContext, useAnalytics } from '@backstage/core-plugin-api'; const MyComponent = ({ value }) => { const analytics = useAnalytics(); const handleClick = () => analytics.captureEvent('check', value); return <SomeThing value={value} onClick={handleClick} />; }; const MyWrapper = () => { return ( <AnalyticsContext attributes={{ segment: 'xyz' }}> <MyComponent value={'Some Value'} /> </AnalyticsContext> ); };在上面的示例中,点击<SomeThing />会生成如下分析事件:
{ "action": "check", "subject": "Some Value", "context": { "segment": "xyz" } }注意:出于简洁,示例省略了 Backstage 核心提供的 context 键(pluginId、extension、routeRef),实际上报时这些细节会与自定义 context 一并携带。AnalyticsContext 可以嵌套,其值会沿 React 树向下合并,允许下层覆盖上层的键。
这一点在 AnalyticsContext 源码 中有明确的实现:它基于createVersionedContext创建版本化上下文,useMemo把父级值与当前 attributes 合并为{ ...parentValues, ...attributes },从而实现"向下合并、可覆盖"。当组件在根节点之下、没有任何 Provider 时,默认上下文为冻结的{ routeRef: 'unknown', pluginId: 'root', extension: 'App' }。
事件命名注意事项
事件被拆分为多个组成部分,正是为了在分析阶段支持不同粒度的分析。为了在分析时保持这种灵活性,务必让各层细节保持"未聚合"状态:
- 避免使用过于具体的
action。例如不要用filterEntityTable,而应使用filter作为 action,让EntityTable作为事件context的一部分(通常会自动由捕获filter事件时所在的extension提供)。 - 保持 attributes / context 语义一致。在向事件添加
attributes或围绕事件添加context时,先看看现有事件,判断你要捕获的数据是否与它们的attributes/context在意图、类型甚至内容上匹配。例如涉及 Catalog 的事件通常会包含entityRef上下文键——在自己的事件中使用相同的键和值,可以确保跨插件埋点的事件容易被聚合。
单元测试事件捕获
@backstage/test-utils包内置了MockAnalyticsApi实现,你可以在单元测试中用它来"监视"并对捕获到的任何分析事件做断言。其源码实现见 MockAnalyticsApi.ts:它内部维护一个事件数组,captureEvent时仅保留非空字段,getEvents()返回全部捕获的事件。注意该实现已标记为 deprecated,官方推荐改用@backstage/test-utils中mockApis.analytics命名空间下的新版本(当前文档示例仍以MockAnalyticsApi演示)。
使用方式如下:
import { render, fireEvent, waitFor } from '@testing-library/react'; import { analyticsApiRef } from '@backstage/core-plugin-api'; import { MockAnalyticsApi, TestApiProvider, wrapInTestApp, } from '@backstage/test-utils'; describe('SomeComponent', () => { it('should capture event on click', () => { // 使用 Mock Analytics API 监视事件捕获。 const apiSpy = new MockAnalyticsApi(); // 渲染被测组件 const { getByText } = render( wrapInTestApp( <TestApiProvider apis={[[analyticsApiRef, apiSpy]]}> <SomeComponentUnderTest /> </TestApiProvider>, ), ); // 触发会捕获事件的动作。 fireEvent.click(getByText('some component text')); // 断言事件以预期数据被捕获。 await waitFor(() => { expect(apiSpy.getEvents()[0]).toMatchObject({ action: 'expected action', subject: 'expected subject', attributes: { foo: 'bar', }, }); }); }); });事件在底层是如何流转的
理解事件流转的完整链路,有助于你写出更符合 Backstage 语义的埋点代码:
- 埋点:组件调用
useAnalytics()拿到Tracker,再调用tracker.captureEvent(action, subject, options); - 上下文合并:
useAnalytics通过useAnalyticsContext()读取当前 React 树(含各层AnalyticsContext)合并出的上下文,并通过tracker.setContext(context)注入(见 useAnalytics.tsx); - 特殊事件处理:
Tracker.captureEvent会剥离内部_routeNodeType键、拦截_ROUTABLE-EXTENSION-RENDERED内部事件、并在需要时把延迟的navigate事件优先补发(见 Tracker.ts); - 转发:最终组装出完整的
AnalyticsEvent(含context)调用analyticsApiRef所注册实现的captureEvent(event),由该实现转发到具体分析平台。
结语
Backstage 的 Analytics API 通过"标准事件 + 可插拔实现"的方式,把埋点接口与数据分析平台解耦:插件开发者只需遵循action/subject/attributes/context的约定即可贡献标准化事件;应用集成方则只需实现一个captureEvent方法,就能把全部数据接入任意工具。无论你是想度量整个实例的插件使用率,还是想追踪某个路由上的细粒度交互,这套事件模型都能给出结构化的答案。相关的类型定义、Tracker 实现与 Mock 工具分别位于 AnalyticsApi.ts、Tracker.ts 与 MockAnalyticsApi.ts,可作为进一步深挖的起点。
【免费下载链接】backstageBackstage is an open framework for building developer portals项目地址: https://gitcode.com/GitHub_Trending/ba/backstage
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考