Skip to content

feat(web): add passive semantic analytics attributes across frontend workflows - #1010

Merged
XiaoSeS merged 13 commits into
iflytek:mainfrom
xiaoyaner0201:feat/frontend-semantic-attrs-989
Oct 29, 2026
Merged

XiaoSeS merged 13 commits into
iflytek:mainfrom
xiaoyaner0201:feat/frontend-semantic-attrs-989

Conversation

@xiaoyaner0201

@xiaoyaner0201 xiaoyaner0201 commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Refs #989(本 PR 是两步交付中的第一步,合并后 #989 继续保持打开,等后续配置化接入 PR 完成并验证后再关闭)

给前端业务操作提供稳定、被动的 HTML 语义属性,外部采集器无需依赖中文/英文文案、CSS 或 DOM 层级。产品默认不加载采集脚本、不采集、不外发数据。

  • 从搜索/详情首批提案扩展到发现、发布、版本/文件、社交、Suite、审核/推广/举报、命名空间、GitHub 同步、通知、账号与管理端;包含可观察的确认、取消、重试和条件操作。
  • data-analytics-id 使用静态业务类别,必要时用字面量条件区分冻结/解冻/删除、申请/直接撤销等动作。资源、身份、Token、输入内容不进入属性。
  • 业务封装显式透传到真实控件及 portal 内的确认/取消/X;保留现有 handlers、权限、disabled/busy、i18n、test IDs 与生命周期。
  • 提供 AST 生成的清单、组件/事件回归及部署方可选的 Nginx 注入说明。按维护者评审补充了可配置规则的部署方示例与可跑通的 Umami 本地接入示例(docs/examples/analytics/),默认关闭、需显式同意,产品本身仍不加载任何采集脚本。
  • 按维护者决定分两个 PR 交付:本 PR 交付被动标签、通用 helper、示例修正和中文接入指南;只改配置就能开启的接入方式放到后续 PR。两边的任务拆分和接口对齐见 OpenSpec openspec/changes/add-configurable-frontend-analytics/。

当前源码清单为 54 个注册路由、804 个唯一静态 ID、821 处 literal JSX 属性位置(975 个条件字面量值、29 个导航定义、36 个透传位置)。这些是源码口径,不是运行时元素、业务成功事件、用户数或完整采集覆盖承诺。

Validation

  • Applicable checks selected from .trellis/spec/testing/index.md; exclusions explained
  • Backend tests passed when backend behavior changed — N/A,后端未修改
  • Frontend typecheck/build passed when frontend files changed
  • OpenAPI SDK regenerated or checked when API contracts changed — N/A,API 未修改
  • Smoke test run when relevant — 真实编译前端、Nginx 与 Chrome;API 使用明确合成夹具,不是生产/真实后端 E2E

验证工具链:Node 24.13.0、pnpm 10.33.0。本轮验证 HEAD 为 57275a6bfb53875cee7dc0d83f78b182627c8a14,开发基线仍冻结于 e88ebf57c2e9123ef9697ce9a08492fd7e18b7a8。

node web/scripts/analytics-manifest.mjs
corepack pnpm@10.33.0 -C web typecheck
corepack pnpm@10.33.0 -C web lint
corepack pnpm@10.33.0 -C web test
corepack pnpm@10.33.0 -C web build
corepack pnpm@10.33.0 -C web test analytics-example.test.ts analytics-umami-example.test.ts
docker compose -f docs/examples/analytics/compose.yaml config
  • 全量前端:261 文件、1,282 项测试通过;示例聚焦:2 文件、77 项通过;typecheck/lint/build/清单新鲜度/Compose 配置均通过。
  • 使用固定 Umami v3.4.1 真实源码的独立探针三组(6、11、12 项)均通过,父侧原样复跑也通过;仓库内回归测试模拟 SDK 的发送顺序,升级 SDK 时须重新做真实源码集成验证。
  • 最新真实 Chrome + Nginx + Umami + Postgres 集成:18 项通过,验证实际请求体白名单、数据库落行、新动作配置、连续点击、停止采集的微任务边界、默认关闭及平台故障隔离。仅 SkillHub API 响应是合成夹具,不是生产/真实后端业务 E2E。
  • 首发时另做过 25 项 HTTP 检查和 28 项 Chrome 检查(包含 namespace portal 和 /skillhub/ 子路径);这是首发证据,本轮没有重新跑子路径浏览器验收。
  • 从此前已发布 head 66c1dcf6 到当前 head,只有示例、文档和示例测试变化;产品代码、生成清单、Docker、CI 和依赖不变。
  • 新 head 的托管 CI 结果以 Checks 为准,不以本地验证代替。

Risk

  • User-facing impact: 仅新增被动属性与明确透传;不开启外部集成时没有分析事件或网络请求。
  • Deployment or migration impact: 无数据库/后端/默认部署变更。可选 Nginx 示例由部署方单独启用;已有 TLS、CSP、认证与安全头保留,禁止为注入放宽策略。浏览器脚本仍有页面权限,需由部署方审查和维护。
  • Rollback approach: 关闭 edge 注入并按文档清理旧 HTML 缓存即可停用集成;属性保留本身不产生采集。

Notes

  • 后端外发设计仍独立于 [Feature] RFC:默认关闭的厂商中立后端业务事件出口 / Opt-in backend business-event export #1009;本 PR 不提供下载完成、安装、运行或发布成功回执。
  • 原始八个兼容语义保留。可观察的点击/submit/keydown/选择意图不等于服务端成功;Escape、遮罩、路由卸载、原生文件选择器取消等边界见使用文档。
  • 外部 JS 示例默认关闭采集;默认规则包含原始 12 个动作,部署方可通过配置把其他已发布 ID 映射到 click/submit/card 三种有限规则。特殊交互留待扩展,部署方自行选择指标、同意机制、传输与保留策略。
  • 已知范围:共享 Markdown/目录浏览类别可用于 Skill、Suite 或审核内容,不能仅凭 skill.* 前缀推断宿主资源类型。
  • 可选 Nginx 示例对 UI fallback 统一 no-store;根目录 public 文件(例如 logo 和 /fonts/)也可能沿该缓存策略。正文过滤仍仅 text/html,部署方可按实际静态文件路由进一步保留缓存。
  • 文档入口:docs/frontend-analytics-integration.md(中文接入指南)、docs/frontend-analytics.md(英文契约)、docs/frontend-analytics-manifest.json、docs/examples/analytics/(原 docs/examples/nginx-analytics.conf 与 skillhub-intents.mjs 已移入此目录)。
  • 评审返修(追加提交 361a4d47、1936cee3、2aecb618、57275a6b)逐条说明见 PR 评论;后端提案 [Feature] RFC:默认关闭的厂商中立后端业务事件出口 / Opt-in backend business-event export #1009 独立,不在本轮实现范围。
  • 拆分后追加 900faf7d(合入 main 后刷新清单行号,内容不变)、238b7841(示例 edge 上传上限 100 MiB、默认上游对齐正式 Web 入口并补 host-gateway)、688b7ce1 与 3527a5ac(中文接入指南与 OpenSpec 拆分)。本轮验证:全量 1,357 项、示例 80 项测试及 typecheck/lint/build/清单/Compose 通过;真实 Chrome + Nginx + Umami + Postgres 21 项检查通过,包含 5 MiB 上传可通过、超过 100 MiB 被拒。
English summary

This frontend-only contribution provides passive semantic HTML attributes across the registered UI, including meaningful conditional actions, confirmations, cancellations and retries. Collection remains deployment-owned and opt-in: no SDK, reporting, app loader or runtime analytics dispatch is added. Static attributes never include resource/identity/credential/input contents.

The generated source inventory contains 54 registered routes, 804 unique static categories and 821 literal JSX attribute sites. These are source counts, not runtime elements, users, successful operations or exhaustive event coverage. Wrapper properties are explicitly forwarded to real controls and portal close buttons; existing interactions and permissions remain unchanged.

Split into two PRs per maintainer decision: this PR delivers passive markers, the generic helper, example fixes and a Chinese integration guide (docs/frontend-analytics-integration.md); configuration-only deployment follows in a separate PR, with tasks and interfaces aligned in openspec/changes/add-configurable-frontend-analytics/. Issue #989 stays open until that follow-up is verified. Validation at 3527a5ac: 1,357 frontend tests and 80 example-focused tests, typecheck, lint, build, manifest and Compose config passed; real Chrome/Nginx/Umami/Postgres passed 21 checks, including a 5 MiB upload passing and >100 MiB rejected. Earlier at 57275a6b: 1,282 frontend tests, 77 example-focused tests, typecheck, lint, build, manifest freshness and Compose config passed. Three sets of pinned-Umami-source integration probes (6, 11 and 12) passed and were replayed independently. The latest real Chrome/Nginx/Umami/Postgres integration passed 18 checks; only SkillHub API responses were synthetic. Earlier 25 HTTP and 28 Chrome checks, including sub-path adaptation, remain first-publication evidence; sub-path browser acceptance was not rerun this round. Current hosted CI is reported separately in Checks.

The optional deployment example documents compression, cache validators, MIME/404, CSP, root/sub-path URI handling, consent and rollback. Collection stays off by default. The default rule map contains the original 12 actions; deployers map other published IDs to the finite click/submit/card rules by config. A runnable local Umami example (docs/examples/analytics/) was added after maintainer review; it is off by default and requires explicit consent. See the review follow-up comment for per-request changes and current Checks for the reworked head. Shared content-browser categories can cross resource domains; the sample fallback's conservative no-store also affects some root public assets. Backend export stays separate in #1009; UI intent is not a completion receipt.

Infographic

前端被动语义属性与部署方按需采集

Passive frontend labels and deployment-owned optional collection

概念示意,不代表生产部署或业务成功回执。Conceptual illustration, not deployment or completion evidence.

Signed-off-by: 千乘妍 (Xiaoyaner) <258399167+xiaoyaner0201@users.noreply.github.com>
Signed-off-by: 千乘妍 (Xiaoyaner) <258399167+xiaoyaner0201@users.noreply.github.com>
Move the collector example test alongside existing root-level web tests so the web-only Docker context does not require repository documentation. Preserve all assertions and default Vitest discovery; document the boundary.

Verified 1217 tests, focused 12 tests, typecheck, lint, build and a fresh web-only build without repository docs. Original snapshot reproduced TS2307. Independent review passed. Full local image build was blocked by an uncached pnpm download with network disabled; remote image CI remains authoritative.

Signed-off-by: 千乘妍 (Xiaoyaner) <258399167+xiaoyaner0201@users.noreply.github.com>
@xiaoyaner0201

Copy link
Copy Markdown
Contributor Author

Fixed the web-image build failure in 66c1dcf.

The new docs-example test was inside the production TypeScript inputs, but the existing image builds from web/ alone and cannot resolve repository-level docs/. Moved that test alongside the existing root-level web tests and adjusted its relative import. Every assertion remains unchanged; default Vitest discovery still runs all 12 cases. No Dockerfile, dependency, build configuration or production code changes.

Verified locally: 1,217 tests; focused 12 cases; typecheck, lint and build. A fresh web-only snapshot reproduced the original TS2307, then passed after the move with repository docs absent. Independent review found no blocking issue. The full local Docker build attempt stopped at an uncached package-manager download with network disabled, so it is not counted as a passing image build. The new head's GitHub CI is now rerunning.

中文:容器构建失败已定位并修正,只调整文档示例测试的位置及相对路径,保留全部断言;新提交的远端 CI 结果以 Checks 为准。

@XiaoSeS

XiaoSeS commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator

我们认可当前的整体方案:SkillHub 提供稳定的页面动作标签,部署方按需加载采集脚本、自选统计平台,默认不采集、不外发、不绑定厂商 SDK。

参考成熟方案并核对当前实现后,建议进一步完善以下几点,主要集中在采集示例、配置、文档和测试,无需重做整体架构。

1. 支持通过配置启用新的普通动作

当前示例把 12 个动作及其识别规则写在脚本中。希望允许部署方配置“动作名称对应哪种已有规则”,例如:

skill.download.request → click
search.query.submit → submit
skill.example.open → click

目标是:新增普通按钮或表单时,添加页面标签并更新配置即可,不需要重复编写监听逻辑。

需要允许新动作复用已有规则,不只是从原有 12 个动作中选择开启哪些。规则类型保持明确、有限;特殊选择器、拖拽等交互可以后续单独扩展,本期不要求覆盖全部标签。

2. 明确字段含义和兼容规则

请简要说明:

  • data-analytics-id 表示动作类别,不是某次事件的唯一编号。
  • 当前 { id, event } 回调各字段的含义,以及后续增加可选字段的方式。
  • 已有动作改名、删除或改变含义时,如何通知和迁移。
  • 未知动作或不支持的规则如何安全跳过,不影响页面使用。

继续保持字段白名单,不自动抓取输入内容、Token、完整 URL 等信息,无需引入复杂的协议框架。

3. 补齐一个能跑通的接入示例

目前文档允许入口脚本导入 helper,但 Nginx 示例只开放入口文件,同目录其他模块会返回 404。请明确采用打包方式,或补充 helper 的精确静态路由。

建议选择一个成熟统计平台,跑通:

用户操作 → 采集回调 → 平台适配 → 接收端收到预期事件。

同时检查最终发送的数据。部分 SDK 会自动附加 URL、标题、来源等字段,不能只检查回调参数。

4. 补充必要测试并同步文档

测试重点覆盖配置生效、未知规则、重复初始化、表单和键盘重复计数,以及关闭采集和发送失败的情况,确保不影响正常操作。

请同步更新现有接入文档,说明如何配置规则、新增标签、接入统计平台及关闭采集,提供一个可复现的完整示例即可,无需另建一套文档体系。

继续明确:点击下载不代表下载或安装成功,提交发布不代表发布成功。

整体希望达到:普通新增动作加标签、改配置即可;特殊交互扩展规则;更换统计平台主要修改适配器。 本期无需增加后台配置页、统计数据库、持久化队列或多厂商 SDK 集成。

以上是我们的初步建议,不代表实现方式必须如此。如果你认为有更简单、成熟或更适合当前架构的方案,欢迎按你的判断提出替代设计,说明取舍后我们一起确认。重点是保持厂商中立、默认不采集、易于接入和扩展。

Signed-off-by: 千乘妍 (Xiaoyaner) <258399167+xiaoyaner0201@users.noreply.github.com>
Signed-off-by: 千乘妍 (Xiaoyaner) <258399167+xiaoyaner0201@users.noreply.github.com>
Signed-off-by: 千乘妍 (Xiaoyaner) <258399167+xiaoyaner0201@users.noreply.github.com>
Signed-off-by: 千乘妍 (Xiaoyaner) <258399167+xiaoyaner0201@users.noreply.github.com>
@xiaoyaner0201

Copy link
Copy Markdown
Contributor Author

Thanks for the review. All four points are addressed in four append-only commits on top of the previously green head: 361a4d47, 1936cee3, 2aecb618 and 57275a6b (final head: 57275a6bfb53875cee7dc0d83f78b182627c8a14). The overall architecture is unchanged. Product code under web/src, the generated manifest, Docker, CI and dependencies are untouched; every change is in docs/examples/analytics/, docs/frontend-analytics.md and the two web-root example tests.

1. Config-driven rules

observeSkillhubIntents({ root, rules, onIntent }) now takes a plain { "<action id>": "<rule type>" } map, for example { ...DEFAULT_RULES, 'skill.example.open': 'click' }. Any ID in the product grammar ^[a-z]+(?:\.[a-z]+){2,}$ can be configured, the same check web/scripts/analytics-manifest.mjs enforces. A parity test verifies that all 804 published IDs are accepted.

There are three finite rule types:

  • click: buttons and links
  • submit: FORM only, so button click and Enter converge on one native submit
  • card: the existing DIV role=link card with its Enter/Space handler

Special interactions such as select, file, drop and crop are left for a future rule type, and the docs say not to map them to click. resolveRules() is a pure function that returns { active, skipped } with a reason per invalid entry. Invalid config never throws and never touches the page.

2. Field meaning and compatibility

The doc now covers:

  • data-analytics-id is an action category, not a unique event ID.
  • { id, event } means the matched category plus the observed native DOM event type.
  • Fields change additively only and must be allowlisted static values. Breaking changes get a new versioned module path.
  • Published IDs are frozen. Renames and removals must show up in the manifest diff and release notes. Configured IDs that disappear simply stop firing, and new IDs stay inert until configured.
  • Unknown actions and unsupported rules are skipped silently.
  • The output allowlist is unchanged: no input values, tokens, URLs, query strings or text.
  • "Download click ≠ download/install success" and "publish submit ≠ publish success" are still stated.

3. Runnable integration (Umami v3.4.1, MIT, self-hosted)

docs/examples/analytics/ contains:

  • nginx.conf: the edge template
  • compose.yaml: a local demo
  • observe/v1/skillhub-intents.mjs: the vendor-neutral helper
  • observe/v1/umami-adapter.mjs: the only vendor-specific code
  • observe/v1/observer.mjs: the deployer entry, enabled: false and no consent by default

The helper 404 is fixed. One versioned prefix, /_skillhub-observe/v1/, serves the entry and its relative imports with try_files $uri =404, JavaScript MIME, nosniff, immutable caching and no directory listing. A missing module returns a non-HTML 404, never the SPA fallback.

The edge proxies exactly two Umami endpoints, the tracker script and /api/send, so script-src 'self'; connect-src 'self' still holds. The tracker is loaded with data-auto-track="false", which means no automatic pageview, history hook or click listener.

Swapping platforms means replacing only the adapter and these two edge routes.

To check the final data, the sanitizer works on the payload Umami is about to send, not on our callback. Only { website, name, data: { event } } passes. Pageview, identify and performance payloads, plus hostname, url, title, referrer, screen, language and any extra keys, are dropped.

The doc also discloses what still leaves the browser or is derived on the server: IP, User-Agent and Origin; Umami's x-umami-hostname and x-umami-cache headers; and server-side session hashing and browser/OS/geo derivation. The edge sets Referrer-Policy: no-referrer and strips Referer, Cookie and Authorization on the two collector routes.

Consent withdrawal: stop() removes the listeners and denies further payloads. Umami 3.4.1 awaits the before-send result before calling fetch, so the adapter also re-checks at serialization time (a non-enumerable toJSON). After stop() returns, no new request is dispatched; only a request whose fetch had already started cannot be recalled. This relies on the pinned SDK's ordering. The committed regression tests model that ordering; in this round, separate integration probes also exercised the actual pinned v3.4.1 source. Upgrading Umami requires rerunning source-level integration checks, not just the modeled tests.

End-to-end check, run locally and outside CI. The setup was this branch's built frontend behind the example edge, real Chrome, Umami 3.4.1 and Postgres; only the SkillHub API responses were synthetic. 18/18 checks passed:

  • Button search and Enter search each produce exactly one event.
  • Card keyboard open is recorded.
  • A download click is recorded even when the download request itself fails.
  • A newly configured action is received through config only; an unconfigured marker sends nothing.
  • Two back-to-back four-segment actions are both received.
  • The captured /api/send bodies contain only the allowlisted payload. Postgres rows show page_title, referrer, screen and language as NULL and url_path empty.
  • After stop(), nothing more is sent, including a click whose stop lands between Umami's before-send hook and its fetch. With enabled: false, the entry/helper modules may load but install no observer and load no Umami tracker; without injection, none of these scripts load. With Umami stopped, SkillHub keeps working with no page errors.

One upstream detail: Umami 3.4.1 stores an omitted query as an empty string rather than NULL. Nothing is transmitted for it. We documented it instead of patching Umami.

4. Tests

web/analytics-example.test.ts and web/analytics-umami-example.test.ts have 77 cases. They cover:

  • config enabling a new action
  • invalid IDs, unknown rules and non-string rules
  • unsupported hosts per rule
  • repeat initialization (the first config wins until stop()) and re-init
  • nested roots counting once
  • form and keyboard double counting, including repeat, IME and modifier keys
  • turning collection off
  • callback throw and rejection
  • the tracker missing, failing, rejecting or hanging
  • the pending cap while the script loads (at most 20, dropped on failure or stop)
  • stop() landing between before-send and fetch
  • the exact sanitizer output and the exact serialized request body, including values read only once even when the payload uses getters

They use no real network. Verified locally on the final head: 1,282 web tests, typecheck, lint, build, manifest freshness and docker compose config all pass. CI on the new head will confirm.

Trade-offs, open to adjustment:

  • The rule vocabulary is intentionally small.
  • Delivery is best-effort with no retry or durable queue, so counts can undercount. The only buffering is the bounded script-load wait described above.
  • Deduplication is per helper module instance.

The integration doc is still the single existing docs/frontend-analytics.md, with sections for configuring rules, adding a tag, connecting a platform and turning collection off. No admin page, analytics DB, persistent queue or multi-vendor SDK was added.

中文

感谢评审。四点都已处理,在原先 CI 全绿的 head 上追加了四个提交 361a4d47、1936cee3、2aecb618、57275a6b,整体架构不变。web/src 产品代码、生成清单、Docker、CI、依赖都没有改动,改动只在 docs/examples/analytics/、docs/frontend-analytics.md 和 web 根目录下的两个示例测试。

1. 配置规则

observeSkillhubIntents({ root, rules, onIntent }) 接受 { 动作ID: 规则类型 } 配置,例如 { ...DEFAULT_RULES, 'skill.example.open': 'click' }。可配置的 ID 规则与 web/scripts/analytics-manifest.mjs 完全一致,有测试逐个确认 804 个已发布 ID 都能配置。

规则类型只有三种:

  • click:按钮和链接
  • submit:仅限 FORM,按钮点击和回车都汇合为一次原生 submit
  • card:现有的 DIV role=link 卡片,及其 Enter/Space 处理

下拉、文件、拖拽、裁剪等特殊交互留待以后单独扩展规则,文档注明不要配成 click。resolveRules() 是纯函数,返回 { active, skipped },每个无效项都附原因。错误配置不会抛错,也不影响页面。

2. 字段含义与兼容

文档现在写明:

  • data-analytics-id 是动作类别,不是单次事件编号
  • { id, event } 是匹配到的类别加实际观察到的原生事件类型
  • 字段只做向后兼容的新增,新增字段必须是白名单内的静态值;不兼容的变更换新的版本路径
  • 已发布 ID 冻结,改名或删除必须体现在清单 diff 和发布说明里;部署方配置了但已消失的 ID 只是不再触发,新 ID 在配置之前不生效
  • 未知动作和不支持的规则直接跳过
  • 字段白名单不变:不采集输入内容、Token、URL、查询串、文本
  • 继续写明点击下载不代表下载或安装成功,提交发布不代表发布成功

3. 可跑通的接入示例(Umami 3.4.1,MIT,可自托管)

docs/examples/analytics/ 下包含:

  • nginx.conf:入口模板
  • compose.yaml:本地演示
  • observe/v1/skillhub-intents.mjs:厂商中立的 helper
  • observe/v1/umami-adapter.mjs:唯一与厂商相关的代码
  • observe/v1/observer.mjs:部署方入口,默认 enabled: false 且未获同意

helper 的 404 已修复:用一个带版本号的前缀 /_skillhub-observe/v1/ 同时提供入口及其相对 import,配置了 try_files $uri =404、JavaScript MIME、nosniff、不可变缓存,并关闭目录列表。缺失的模块返回非 HTML 的 404,不会落到 SPA 页面。

入口只代理 Umami 的两个接口(采集脚本和 /api/send),所以 script-src 'self'; connect-src 'self' 依然成立。采集脚本以 data-auto-track="false" 加载,不会自动发 pageview,也不会挂 history 或点击监听。

更换统计平台只需替换适配器和这两条入口路由。

为了核验最终发出的数据,过滤器检查的是 Umami 即将发送的最终 payload,不是我们的回调参数。只放行 { website, name, data: { event } };pageview、identify、performance,以及 hostname、url、title、referrer、screen、language 和多余字段都会被丢弃。

文档也如实列出仍会离开浏览器、或在服务端推导出的信息:IP、User-Agent、Origin;Umami 的 x-umami-hostname、x-umami-cache 请求头;服务端的会话哈希以及浏览器、系统、地域推导。入口为页面设置了 Referrer-Policy: no-referrer,并在两条采集路由上去掉 Referer、Cookie、Authorization。

撤回同意:stop() 会移除监听并拒绝后续 payload。Umami 3.4.1 会先等 before-send 的结果再调用 fetch,因此适配器在序列化那一刻再检查一次(不可枚举的 toJSON)。stop() 返回后不会再发出新请求,只有 fetch 已经开始的那一个无法撤回。这一点依赖所固定 SDK 版本的执行顺序。仓库内回归测试模拟该顺序;本轮另外用真实固定版本的源码跑过集成探针。升级 Umami 时,需要重新验证真实 SDK 源码,不能只依靠模拟测试。

端到端验证在本地进行,不在 CI 中:使用本分支构建出的前端、示例入口、真实 Chrome、Umami 3.4.1 和 Postgres,只有 SkillHub 的 API 响应是合成的。18 项全部通过:

  • 按钮搜索和回车搜索各产生恰好一条事件
  • 卡片的键盘打开被记录
  • 下载请求本身失败时,下载点击照样被记录
  • 只改配置就能收到新加的动作;未配置的标签不发送
  • 连续触发两个四段式动作,两条都收到
  • 抓到的 /api/send 请求体只含白名单字段;Postgres 中 page_title、来源、屏幕、语言为 NULL,url_path 为空
  • stop() 之后不再发送,包括停止恰好落在 Umami before-send 与 fetch 之间的那一次点击;enabled: false 时入口及 helper 模块可以加载,但不安装观察器、不加载 Umami 采集脚本;不注入时这些脚本都不加载;Umami 停掉后 SkillHub 正常可用,无页面报错

另有一个 Umami 的上游细节:3.4.1 在没传查询串时会存成空字符串而不是 NULL,实际上并没有发送任何查询内容。我们如实写进文档,没有修改 Umami。

4. 测试

web/analytics-example.test.ts 和 web/analytics-umami-example.test.ts 共 77 个用例,覆盖:

  • 配置启用新动作
  • 无效 ID、未知规则、非字符串规则
  • 各规则不支持的宿主元素
  • 重复初始化(stop() 之前以第一次配置为准)与重新初始化
  • 嵌套 root 只计一次
  • 表单和键盘的重复计数,包括按键重复、输入法、修饰键
  • 关闭采集
  • 回调抛错与 reject
  • 采集脚本缺失、加载失败、reject、挂起
  • 脚本加载期间的暂存上限(最多 20 条,加载失败或 stop 时丢弃)
  • stop() 落在 before-send 与 fetch 之间
  • 过滤器的精确输出,以及实际序列化出的请求体;即使 payload 带 getter,每个值也只读一次

测试不访问真实网络。最终 head 上本地验证通过:1,282 个前端测试、typecheck、lint、构建、清单新鲜度检查、docker compose config;以新 head 的 CI 为准。

可以再调整的取舍:

  • 规则类型刻意保持很少
  • 发送是尽力而为,不重试,也没有持久队列,统计可能偏少;只有上述脚本加载期间的有界暂存
  • 去重以 helper 模块实例为范围

接入文档仍是原有的 docs/frontend-analytics.md 一份,新增了配置规则、新增标签、接入平台、关闭采集几节。没有引入后台配置页、统计数据库、持久化队列或多厂商 SDK。

…urable-analytics

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>

# Conflicts:
#	web/src/app/layout.tsx
#	web/src/pages/login.tsx
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
@XiaoSeS
XiaoSeS marked this pull request as draft October 27, 2026 03:18
@XiaoSeS

XiaoSeS commented Oct 27, 2026 •

Copy link
Copy Markdown
Collaborator

前面四项返修已基本落实,现有“被动标签+通用 helper+平台适配器”的分层可以保留。进一步考虑生产接入后,我们新增以下交付目标,不视为原返修遗漏。

1. 部署体验

  • 所有接入参数通过配置提供,部署方不编辑 JS、Nginx 或其他源码,不手工复制采集文件。
  • 尽可能一条命令部署,分别说明复用已有统计平台、可选自托管组合;必要的平台信息和一次性初始化步骤明确列出。
  • 提供可独立照做的配置、部署、验收、故障排查、升级和关闭文档。

2. 建议方向与边界

建议评估复用现有 runtime-config.js:部署配置 → 可选加载入口 → 通用采集层 → 平台适配器。模块随 Web 发布、按需加载;如需同源代理,由部署自动生成独立路由,不依赖手工 HTML 注入或额外 Nginx。

通用层管理开关、规则、授权和生命周期;平台参数留在适配器。Umami 只是一个验证实现,第一版不要求集成多个平台。业务页面继续只提供标签,不调用厂商 SDK。

需要验证:默认关闭时不加载采集模块或发送事件;配置错误、脚本失败、统计服务或 DNS 不可用不阻塞 Web 和业务;保留上传、登录、下载、CSP、缓存及根/子路径行为。继续检查实际发送字段、启停和重复初始化边界,明确点击不代表业务成功。

3. 现有示例需要修正的两点

  • 新 edge 未设置请求体上限,采用默认 1 MiB,会阻止原 Web 上限 100 MiB 内的合法上传。
  • 默认 host.docker.internal:8080 与标准 Web 入口不匹配,原生 Linux 的宿主名映射也需说明。

可在最终接入方案中一并解决;若保留旧示例,也应修正。

请先反馈可行性、影响范围和建议拆分,再确定实现。以上不限定技术方案,若有更简单、可靠的方式,欢迎提出。新增接入能力超出原 PR 范围时,也可以作为后续 PR。

OpenSpec 讨论草案仅供参考,字段和组织方式尚可调整。

@xiaoyaner0201

xiaoyaner0201 commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor Author

@XiaoSeS 谢谢这么细的补充,也谢谢帮忙合了 main、写了 OpenSpec 草案 🙏 先说说我们的想法,看看方向对不对,再动手。

关于部署体验

复用 runtime-config.js,做成「填配置就能用」,技术上没问题。我们有一点顾虑:

不同团队用的埋点平台差别挺大,比如自建的 Umami、Plausible,还有 GA、神策、火山 DataFinder 等。它们加载 SDK 的方式、上报格式、合规要求都不太一样。

如果 SkillHub 内置一个 Umami 适配器,用 Umami 的团队会很方便;用其他平台的团队,还是得自己写适配和代理。而适配器、代理路由、Compose/Helm 配置这些,之后都要由 SkillHub 长期维护。

所以我们更倾向于两步走:

  1. 这个 PR:保持「页面标签 + 通用 helper + 示例」,再补一份照着就能做的接入指南,讲清楚怎么配置、怎么部署、怎么确认发出去的数据对不对、出问题怎么查、怎么升级和关闭。用已有统计平台和自己部署两种情况都会写到。
  2. 想请教一下:大家有没有比较希望内置的埋点平台?如果有明确的选择,我们再按草案的思路,把这一个平台做成配置化接入,单独开一个 PR,会更有针对性。

当然,如果你们更希望现在就做配置化入口,我们也可以按草案来。草案里定的几条边界我们都认同:默认关闭、不加载;配置写错只是不采集,不影响 Web;业务页面只放标签;不允许加载任意远程脚本。我们只建议第一版先做一个平台。

两处示例问题

你说得对,这个 PR 里会一起修:

  • 示例里的 Nginx 没设请求体上限,默认 1 MiB 会挡住正常上传。会改成和 Web 一样的 100 MiB。
  • 默认上游地址 host.docker.internal:8080 和标准 Web 入口对不上,会改过来。另外会补充说明:在原生 Linux 上要加 extra_hosts: host-gateway。

拆分建议

  • 这个 PR:页面标签、helper、示例修正、接入指南
  • 后续 PR(看大家的选择):配置化加载、单个平台适配、Compose/Helm 接入

看看这样可以吗?有不同意见随时说~

English

Thanks for the detailed follow-up, and for merging main and drafting the OpenSpec change. Here are our thoughts before implementing.

Deployment experience. Reusing runtime-config.js for a config-only setup is feasible. Our one concern: teams use quite different analytics platforms (self-hosted Umami/Plausible, GA, Sensors, Volcengine DataFinder, …), each with its own SDK loading, payload format and compliance needs. A built-in Umami adapter helps Umami users, but others would still write their own adapter and proxy, while SkillHub maintains the adapter, proxy routes and Compose/Helm wiring long term.

So we'd suggest two steps:

  1. This PR: keep "page markers + generic helper + example", and add a step-by-step integration guide: configuration, deployment, checking what is actually sent, troubleshooting, upgrade and shutdown, for both existing and self-hosted platforms.
  2. A question for you: is there a platform the community would like built in? If so, we can make that one platform config-driven following the draft, in a separate PR.

If you'd rather build the config-driven loader now, we're happy to follow the draft. We agree with its boundaries: off and not loaded by default, bad config only disables collection without affecting Web, pages only carry markers, no arbitrary remote scripts. We'd just suggest starting with one platform.

Example fixes (in this PR): set the edge body limit to 100 MiB to match Web; point the default upstream to the standard Web entry and document extra_hosts: host-gateway for native Linux.

Proposed split: this PR = markers, helper, example fixes, guide; follow-up PR (depending on the choice) = config-driven loading, one platform adapter, Compose/Helm wiring.

@XiaoSeS

XiaoSeS commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

可以按你建议拆成两个 PR。当前 PR 先完成基础埋点、通用采集能力及已知示例问题修复,并补齐接入指南。

后续 PR 的配置化接入方向也一并确定:部署方使用已支持的平台时,通过 Docker/Compose 或 Helm 配置即可开启,不需要修改 JS、复制采集文件或手工调整 Nginx。默认关闭,开启后按需加载;配置错误或采集服务故障不影响 SkillHub 正常使用。

第一版适配器可以沿用 Umami,但通用配置和加载机制不要绑定该平台,其他平台以后按需扩展,不要求任意平台都能纯配置接入。

请同步在 OpenSpec 中拆分任务,提前对齐两个 PR 的接口,并补齐部署文档和验收场景。两个 PR 可以分别合并,#989 保留到配置化接入完成并验证后再关闭,当前 PR 请避免自动关闭该 Issue。

若实现过程中发现更简单的方案,也欢迎提出讨论。

The merge of latest main shifted source lines in router and pages. Only
line numbers change; routes, IDs and sites are identical.

Refs iflytek#989

Signed-off-by: 千乘妍 (Xiaoyaner) <258399167+xiaoyaner0201@users.noreply.github.com>
Signed-off-by: 千乘妍 (Xiaoyaner) <258399167+xiaoyaner0201@users.noreply.github.com>
Refs iflytek#989

Signed-off-by: 千乘妍 (Xiaoyaner) <258399167+xiaoyaner0201@users.noreply.github.com>
Export the upstream override so later compose commands keep it, and
tell operators to bypass the immutable module cache when re-checking
an edited local entry.

Refs iflytek#989

Signed-off-by: 千乘妍 (Xiaoyaner) <258399167+xiaoyaner0201@users.noreply.github.com>
@xiaoyaner0201

Copy link
Copy Markdown
Contributor Author

@XiaoSeS 谢谢拍板 🙏 已按拆分方案把本 PR 这一步做完,麻烦有空再看看。

这次改了什么

1. 两个示例问题

  • 示例 Nginx 在 server 级加了 client_max_body_size 100M,和 Web 保持一致,应用路由不再被默认的 1 MiB 挡住。
  • 默认上游改成 host.docker.internal:80,对应 compose.release.yml 里 WEB_PORT 的默认值;edge 加了 extra_hosts: host-gateway,原生 Linux 也能直接用。
  • 加了 3 个测试锁住这两处,在旧配置上会失败。

2. 中文接入指南

新增 docs/frontend-analytics-integration.md,按步骤写了:接已有平台和本地 Umami 两种方式、规则配置、部署、验收(看实际发出去的字段)、故障排查、升级和关闭,以及已知限制。英文的 docs/frontend-analytics.md 保留作接口说明,顶部加了指向中文指南的链接。

3. OpenSpec 拆分

在你的草案上直接改:

  • tasks.md 分成 PR-1 / PR-2 两组,只勾了本 PR 实际做完的。
  • design.md 加了一张接口对齐表,写明 PR-2 要沿用 PR-1 已经定下的接口(ID 格式、observeSkillhubIntents、DEFAULT_RULES、resolveRules、三种规则、{ id, event }、adapter 的 send/stop),以及 enabled、consent、adapterConfig 这些配置项怎么对上。
  • spec.md 每条需求标了由哪个 PR 交付,补了两个示例修正和中文指南的验收场景。原来的要求没有删减。

本地没有 OpenSpec CLI,结构是对照 add-skill-suites 手工核对的。

4. 不会自动关闭 #989

PR 正文的 Closes #989 已改成 Refs #989,提交说明里也只写了 Refs。

顺带说一下

合入最新 main 后,源码行号整体往下移了两行,docs/frontend-analytics-manifest.json 里 66 个行号对不上,全量测试里的清单比对会失败。CI 那一轮前端测试是跳过的,所以没暴露出来。我们单独提交了 900faf7d 刷新行号,路由、ID 和位置数量都没变。

验证

  • 全量前端 1,357 项、示例 80 项测试,typecheck / lint / build / 清单 / Compose 配置都通过。
  • 本地用真实 Chrome + Nginx + Umami + Postgres 跑了 21 项检查,包括 5 MiB 上传能通过、超过 100 MiB 被拒,也照着指南从头走了一遍。前端用的是合成的 API 响应,不代表后端业务的端到端结果。

有哪里不对或者想调整的,随时说~

English

Thanks for the decision. This PR now covers the first step of the split.

  1. Example fixes: server-level client_max_body_size 100M on the example edge (matches Web); default upstream host.docker.internal:80 (release WEB_PORT default) plus extra_hosts: host-gateway for native Linux; three tests pin both and fail on the old config.
  2. Chinese integration guide: new docs/frontend-analytics-integration.md covering existing-platform and local Umami paths, rules, deployment, acceptance (actual emitted fields), troubleshooting, upgrade, shutdown and known limits. The English docs/frontend-analytics.md stays as the contract reference and links to it.
  3. OpenSpec split (edited in place): tasks grouped into PR-1 / PR-2 with only PR-1 work ticked; an interface-alignment table in design.md that PR-2 must adopt, including how enabled / consent / adapterConfig map onto it; each requirement in spec.md tagged with its delivering PR, plus scenarios for the example fixes and the guide. No original requirement removed. Checked manually against add-skill-suites (no OpenSpec CLI available).
  4. [Feature] 支持厂商中立的前端业务语义属性 #989 stays open: PR body now says Refs #989; commit messages use Refs only.

Note: after the main merge, source lines shifted, leaving 66 stale line numbers in docs/frontend-analytics-manifest.json and failing the manifest check in the full test suite (the frontend CI job was skipped on that commit). 900faf7d refreshes line numbers only.

Validation: 1,357 frontend tests and 80 example tests, typecheck, lint, build, manifest and Compose config pass; a local real Chrome/Nginx/Umami/Postgres run passed 21 checks, including a 5 MiB upload passing and >100 MiB rejected, and the guide was followed end to end. API responses were synthetic.

@XiaoSeS
XiaoSeS marked this pull request as ready for review October 29, 2026 06:27
@XiaoSeS
XiaoSeS merged commit 6952629 into iflytek:main Oct 29, 2026
21 of 22 checks passed
@XiaoSeS

XiaoSeS commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator

已完成复核并合并,感谢这轮补充。

我们在合入当前 main 的版本上完成了前端 1,357 项测试、14 项验收场景及真实服务验证:使用真实 SkillHub 后端、浏览器、Nginx、Umami 和 PostgreSQL,确认事件实际入库,并验证默认关闭、停止采集、重复初始化、撤回同意及采集服务故障不影响业务。维护者也已用正常浏览器人工操作,并在 Umami 图表中确认数据。相关 CI 已通过。

第一阶段按约定完成,#989 继续保持开放。后续请按 OpenSpec 中 PR-2 的方向推进配置化接入:复用运行时配置,Docker/Compose 和 Helm 用户通过配置即可开启;默认关闭、按需加载,配置错误或采集故障不影响主业务。首个适配器可用 Umami,通用配置和加载机制保持可扩展,并同步补齐部署文档及验收场景。若实现中有更简单、维护成本更低的方案,也欢迎讨论。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants