mirror of
https://github.com/iflytek/skillhub.git
synced 2026-11-14 09:01:16 +00:00
docs(analytics): document configuration-only deployment and acceptance
Replace manual injection instructions with Compose, Docker and Helm configuration, explicit platform initialization, consent controls, privacy boundaries and verified task status. Refs #989 Signed-off-by: 千乘妍 (Xiaoyaner) <258399167+xiaoyaner0201@users.noreply.github.com>
This commit is contained in:
parent
7fb74e81b2
commit
4661d2e1aa
7 changed files with 375 additions and 503 deletions
|
|
@ -105,6 +105,13 @@ PVC UID 与绑定 PV 在升级前后保持不变。四个场景分别覆盖默
|
|||
Sentinel、实际 MinIO S3 连接,以及由 Kubernetes API 接受的 TLS Ingress 路由。
|
||||
默认清理自己创建的 namespace;设置 `KEEP_HELM_SMOKE=true` 可保留现场用于排查。
|
||||
|
||||
### 可选前端动作统计
|
||||
|
||||
默认关闭。`web.analytics.config` 接受 JSON 字符串,`web.analytics.upstream` 接受仅部署可见的
|
||||
HTTP(S) origin。支持 Umami,不需要编辑 JS 或 Nginx;先在平台创建网站并取得 UUID。
|
||||
配置变更触发 Web 滚动更新,旧标签页不会自动更新。字段、授权接口、初始化、隐私及验收见
|
||||
[前端动作统计接入指南](../../docs/frontend-analytics-integration.md)。
|
||||
|
||||
### 高可用模式
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# skillhub 部署架构与运维
|
||||
|
||||
对象存储可选部署、凭证和迁移说明见[对象存储手册](object-storage.md)。
|
||||
可选前端动作统计默认关闭。Docker/Compose 和 Helm 只填配置即可接入 Umami,
|
||||
详见[前端动作统计接入指南](frontend-analytics-integration.md)。
|
||||
|
||||
### CLI 设备授权地址
|
||||
|
||||
|
|
|
|||
|
|
@ -1,80 +1,145 @@
|
|||
# 前端动作统计接入指南
|
||||
|
||||
## 1. 先了解边界
|
||||
## 1. 默认关闭,按需开启
|
||||
|
||||
SkillHub 页面只提供被动的 `data-analytics-id` 标签,默认不采集、不加载统计 SDK、不发送事件。可选 helper 把允许的原生交互转换成 `{ id, event }`,再交给部署方选择的 adapter。点击下载只表示下载意图,不表示下载、安装成功;提交发布也不表示发布成功。部署方负责授权、隐私合规、保留期限和平台安全。
|
||||
不配置时,不加载采集模块或 SDK,也不发送统计请求。正常业务不等待统计平台。
|
||||
现在 Docker、Compose、Helm 用户只需填写配置。不需要改 JS、复制文件或手写 Nginx。
|
||||
第一版支持 Umami 3.4.1。其他平台需要开发并发布新的注册 adapter,不是任意远程脚本插件。
|
||||
|
||||
本文对应 PR-1(#1010)的手工接入示例,不是产品内置开关。仅通过 Docker/Compose 或 Helm 配置开启、默认关闭和按需加载的能力计划在 PR-2 交付,见 [OpenSpec 变更](../openspec/changes/add-configurable-frontend-analytics/proposal.md)。Issue #989 要等 PR-2 完成并验证后才关闭。精确接口、事件边界和隐私约束以[英文契约](frontend-analytics.md)为准。
|
||||
点击下载只是下载意图,不是下载或安装成功。提交发布也不是发布成功。
|
||||
部署方负责授权策略、平台安全、数据保留和合规。接口和事件边界见[英文契约](frontend-analytics.md)。
|
||||
|
||||
## 2. 选择接入路径
|
||||
## 2. 复用已有 Umami
|
||||
|
||||
### 路径 A:复用已有统计平台
|
||||
前提:SkillHub 已正常部署;Web 镜像包含本功能;有可达的 Umami 3.4.1。
|
||||
先在 Umami 创建网站,拿到公开的网站 UUID。管理员密码、API token 不要放入前端配置。
|
||||
确认 Web 容器能够访问平台的 HTTP(S) origin。HTTPS 证书需由镜像的系统 CA 信任。
|
||||
自签名平台需先按组织规范配置受信任 CA,不要关闭证书验证。
|
||||
|
||||
先确认平台接收地址、公开站点标识、授权策略和 CSP。不要把平台管理密钥放到浏览器。平台不受支持时,需要部署方编写自己的 adapter;当前版本不能只填平台名字就接入任意平台。
|
||||
在受保护的 `.env.release` 中填写下面两项。把示例 UUID 换成自己网站的 UUID。
|
||||
JSON 保持一行。单引号用于 Compose 的 dotenv 文件,不是 JSON 的一部分。
|
||||
|
||||
1. 复用 [helper](examples/analytics/observe/v1/skillhub-intents.mjs),不要修改业务页面或引入厂商 SDK。
|
||||
2. 参照 [Umami adapter](examples/analytics/observe/v1/umami-adapter.mjs)实现 `send(intent) -> Promise<void>` 和 `stop()`。send 必须消化同步异常和异步拒绝,不向页面抛错;stop 后不能再派发请求,已经开始的 fetch 无法撤回。
|
||||
3. 由 adapter 验证平台参数、清洗最终发送字段、管理 SDK 加载和超时。不要只清洗 helper 回调后就信任 SDK 自动附加的 URL、标题或身份字段。
|
||||
4. 在部署方入口中连接 helper 与 adapter,只有策略允许时才初始化:
|
||||
|
||||
```js
|
||||
import { observeSkillhubIntents } from './skillhub-intents.mjs'
|
||||
// adapter 由部署方创建;必须先确认当前授权。
|
||||
const stopObserver = observeSkillhubIntents({
|
||||
root: document,
|
||||
onIntent: intent => adapter.send(intent),
|
||||
})
|
||||
function stop() { stopObserver(); adapter.stop() }
|
||||
```dotenv
|
||||
SKILLHUB_WEB_ANALYTICS_CONFIG='{"enabled":true,"adapter":"umami","consent":"granted","adapterConfig":{"websiteId":"4398de8e-40df-4483-9eea-ed213643525f"}}'
|
||||
SKILLHUB_WEB_ANALYTICS_UPSTREAM=https://stats.example.com
|
||||
```
|
||||
|
||||
入口交付、同源代理、授权回调和 CSP 由部署方维护。下面的 edge 仅演示 Umami 两个端点;使用其他平台时,需要自行审核并替换 adapter 和代理端点。不要把平台管理界面代理到公开采集前缀。
|
||||
`granted` 表示部署方确认当前策略允许采集,不表示每位用户都点击了同意。
|
||||
需要用户单独授权时,初始使用 `denied`,再接入第 5 节的公开接口。本功能不提供同意弹窗。
|
||||
|
||||
### 路径 B:本地自托管 Umami 演示
|
||||
|
||||
前提:已有能正常使用的 SkillHub Web;已安装 Docker 和 Compose;能使用示例指定的 Umami 3.4.1、Postgres 16、Nginx 1.30.6 镜像;本机 8300 和 8081 端口空闲。所有 shell 命令从仓库根目录运行。示例会创建独立演示网络和临时数据库,不用于生产持久化。请在隔离环境运行。
|
||||
|
||||
先检查配置,再启动:
|
||||
所有命令从仓库根目录运行。正常发布配置仍需包含数据库、存储和认证等参数。
|
||||
|
||||
```sh
|
||||
docker compose -f docs/examples/analytics/compose.yaml config
|
||||
docker compose -f docs/examples/analytics/compose.yaml up -d
|
||||
make validate-release-config
|
||||
docker compose --env-file .env.release -f compose.release.yml config --quiet
|
||||
docker compose --env-file .env.release -f compose.release.yml up -d --no-deps --force-recreate web
|
||||
docker compose --env-file .env.release -f compose.release.yml logs --tail=100 web
|
||||
```
|
||||
|
||||
默认上游是 `host.docker.internal:80`,对应 release Compose 的 `WEB_PORT` 默认 80。这里是容器访问宿主机的 Web 入口,不是后端端口,也不是 edge 自己的 8081。若 Web 实际发布在其他端口(比如 18080),先在当前终端导出,再启动。后面所有 compose 命令都要在这个终端里执行,否则重建 edge 时会回到默认的 80:
|
||||
不要公开分享完整的 Compose 渲染结果,其中可能包含业务凭证。
|
||||
Web 启动时自动生成同源代理,只开放 `/_skillhub-analytics/script.js` 和 `/_skillhub-analytics/api/send`。
|
||||
其他采集路径返回 404,不公开 Umami 管理界面。统计 DNS 在请求时解析,不是 Web 启动前提。
|
||||
统计配置错误只关闭统计。容器日志中的 `[analytics]` 提示字段,不回显原值。
|
||||
原有 API/auth 配置、100 MiB 上传上限、TLS、业务缓存及权限不变。不放宽 CSP。
|
||||
|
||||
直接运行 Docker 时,把同样两项用 `--env-file` 传给正常的 Web 容器即可。文件使用原始
|
||||
`KEY=value` 格式,JSON 外不要加 dotenv 单引号。保留原有 API、公开地址和端口配置。
|
||||
不要把 API 上游改成统计平台地址。
|
||||
|
||||
## 3. 可选自托管组合
|
||||
|
||||
需要 Docker Compose、可用的镜像网络及持久磁盘。这个组合增加 Umami 和它自己的
|
||||
Postgres,不增加 Nginx,也不修改 SkillHub 数据库。正式使用前配置备份和访问控制。
|
||||
|
||||
在 `.env.release` 追加独立随机凭证。先在本地生成,再把输出填入文件,不要提交文件。
|
||||
|
||||
```sh
|
||||
export SKILLHUB_WEB_UPSTREAM=host.docker.internal:18080
|
||||
docker compose -f docs/examples/analytics/compose.yaml up -d
|
||||
openssl rand -hex 24
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
原生 Linux Docker 需要 `host.docker.internal` 映射,示例 edge 已包含 `extra_hosts: ["host.docker.internal:host-gateway"]`。Docker Desktop/OrbStack 已提供这个名字。宿主机 Web 必须监听容器可达的地址;如果防火墙或仅回环监听阻止访问,先调整可达性,或填写容器可以访问的其他 `host:port`。不要填写 `http://` 或 URL 路径。
|
||||
```dotenv
|
||||
UMAMI_DB_PASSWORD=<第一条命令生成的值>
|
||||
UMAMI_APP_SECRET=<第二条命令生成的值>
|
||||
UMAMI_ADMIN_PORT=8300
|
||||
```
|
||||
|
||||
打开 `http://localhost:8300`。首次演示登录是 `admin` / `umami`,立即改密码。示例公开的数据库口令和 APP_SECRET 也只能用于演示;生产需要独立密钥、持久化、备份和访问控制。
|
||||
|
||||
在 Umami 创建一个演示网站,记录网站 UUID。打开 `docs/examples/analytics/observe/v1/observer.mjs`:
|
||||
|
||||
1. 将全零 `website` 改为刚创建的网站 UUID。
|
||||
2. 将 `enabled: false` 改为 `enabled: true`。
|
||||
3. 将 `hasConsent()` 接入自己的当前授权判断。仅在隔离演示、明确允许采集时可暂改为返回 true;正式接入不能把它当作用户同意。
|
||||
4. 按下一节配置 rules。示例额外的 `skill.example.open` 没有对应产品元素,不会凭空产生事件。
|
||||
|
||||
然后重建 edge,使单文件挂载更新可靠生效:
|
||||
数据库口令使用字母和数字最简单。包含特殊字符时,另填 `UMAMI_DATABASE_URL`,对 URL
|
||||
密码部分做百分号编码;Postgres 的 `UMAMI_DB_PASSWORD` 仍保留原值。
|
||||
先只启动平台,此时统计配置可以留空:
|
||||
|
||||
```sh
|
||||
docker compose -f docs/examples/analytics/compose.yaml up -d --force-recreate edge
|
||||
docker compose -f docs/examples/analytics/compose.yaml exec edge nginx -t
|
||||
docker compose -f docs/examples/analytics/compose.yaml exec edge nginx -T
|
||||
docker compose --env-file .env.release -f compose.release.yml -f compose.analytics-umami.yml config --quiet
|
||||
docker compose --env-file .env.release -f compose.release.yml -f compose.analytics-umami.yml up -d analytics-postgres umami
|
||||
```
|
||||
|
||||
打开 `http://localhost:8081`,不要从原 Web 入口验收注入。
|
||||
管理端口只绑定宿主机回环地址。远程机器需使用自己的 SSH 转发或受控管理入口。
|
||||
本机打开 `http://localhost:8300`。Umami 初始登录是 `admin` / `umami`,立即修改密码。
|
||||
创建网站,复制 UUID。此步骤不能省略,启动容器不会自动创建网站。
|
||||
将第 2 节 JSON 的 websiteId 换成该 UUID,再填写:
|
||||
|
||||
注意缓存:模块按 immutable 长期缓存,浏览器会一直用第一次拿到的 `observer.mjs`。本地演示每次改完入口文件,都要在开发者工具 Network 勾选「Disable cache」后再刷新,或者用一个新的无痕窗口打开。正式交付中不能原地改文件,要发布到新的版本目录(如 `v2`)并同步注入路径,不能覆盖旧版本字节。
|
||||
```dotenv
|
||||
SKILLHUB_WEB_ANALYTICS_UPSTREAM=http://umami:3000
|
||||
```
|
||||
|
||||
## 3. 配置规则和增加动作
|
||||
这里是 Compose 内部服务名。然后重建正常 Web:
|
||||
|
||||
省略 `rules` 使用冻结的 `DEFAULT_RULES`,共 12 条:
|
||||
```sh
|
||||
docker compose --env-file .env.release -f compose.release.yml -f compose.analytics-umami.yml up -d --no-deps --force-recreate web
|
||||
```
|
||||
|
||||
| ID | 规则 |
|
||||
同一 Compose 网络提供 DNS。Web 在独立容器或主机时需填写真正可达的内部 origin。
|
||||
不使用旧示例的 edge 或宿主机 Web 代理。统计平台初始化、改密、备份仍是运维步骤,
|
||||
不能把首次部署称为无需准备的一条命令。
|
||||
|
||||
## 4. Helm
|
||||
|
||||
保留已有生产 values 和 Secret。在 values 中增加以下内容:
|
||||
|
||||
```yaml
|
||||
web:
|
||||
analytics:
|
||||
config: '{"enabled":true,"adapter":"umami","consent":"granted","adapterConfig":{"websiteId":"4398de8e-40df-4483-9eea-ed213643525f"}}'
|
||||
upstream: https://stats.example.com
|
||||
```
|
||||
|
||||
```sh
|
||||
helm lint --strict charts/skillhub -f values-production.yaml
|
||||
helm upgrade --install skillhub ./charts/skillhub -n skillhub -f values-production.yaml
|
||||
kubectl -n skillhub rollout status deployment/skillhub-web
|
||||
kubectl -n skillhub logs deployment/skillhub-web --tail=100
|
||||
```
|
||||
|
||||
deployment 名称使用 Chart 默认 release 名 `skillhub`;自定义名称时替换命令中的名称。
|
||||
值进入 ConfigMap 和 Web 环境变量,checksum 改变会触发 Web 滚动更新。
|
||||
Chart 不自动部署 Umami。先在已有平台创建网站,并确认 Pod 出站网络、DNS 和证书可用。
|
||||
集群内 origin 使用完整服务名,例如 `http://umami.stats.svc.cluster.local:3000`,
|
||||
Nginx 的运行时 DNS 不自动补 Pod 的搜索后缀。
|
||||
|
||||
## 5. 字段、规则和动态授权
|
||||
|
||||
`SKILLHUB_WEB_ANALYTICS_CONFIG` 是可选 JSON 对象,最大 64 KiB,嵌套最多 32 层。
|
||||
空值等同关闭。重复 JSON 键拒绝,避免部署和浏览器解释不同。
|
||||
|
||||
| 字段 | 含义 |
|
||||
| --- | --- |
|
||||
| enabled | 布尔值,默认 false。总开关,start() 不能绕过它。 |
|
||||
| adapter | 静态注册名,目前只有 umami。 |
|
||||
| consent | denied 或 granted,默认 denied。 |
|
||||
| rules | 可选规则对象。省略用默认 12 条;显式提供会替换;空对象不采集。 |
|
||||
| adapterConfig | 适配器公开参数对象。Umami 只接受 websiteId UUID。 |
|
||||
|
||||
未知配置字段或不合法的平台参数关闭统计。rules 沿用纯 `resolveRules` 的 active/skipped
|
||||
语义:非法容器没有 active,非法条目跳过,其他合法规则继续工作。日志不输出非法 ID
|
||||
或原配置。开发者可本地检查 `resolveRules(rules).skipped`,不要发送诊断给平台。
|
||||
|
||||
`SKILLHUB_WEB_ANALYTICS_UPSTREAM` 只供部署代理使用,不进入浏览器配置。
|
||||
只接受 HTTP(S) origin,无凭证、路径、查询、fragment、空白或末尾斜杠。
|
||||
不要放入平台管理密钥。浏览器前缀由 Web 自动生成,不能配置任意脚本 URL。
|
||||
|
||||
默认规则保持冻结:
|
||||
|
||||
| ID | 类型 |
|
||||
| --- | --- |
|
||||
| search.query.submit | submit |
|
||||
| skill.card.open | card |
|
||||
|
|
@ -89,112 +154,116 @@ docker compose -f docs/examples/analytics/compose.yaml exec edge nginx -T
|
|||
| skill.delete.confirm | click |
|
||||
| skill.delete.cancel | click |
|
||||
|
||||
显式传入 rules 会替换默认值,空对象表示没有动作。要保留默认值再增加已存在的动作,先合并:
|
||||
例如只统计搜索和首页导航,可在 JSON 中增加:
|
||||
|
||||
```js
|
||||
import { DEFAULT_RULES, resolveRules } from './skillhub-intents.mjs'
|
||||
const rules = { ...DEFAULT_RULES, 'navigation.home.open': 'click' }
|
||||
const { active, skipped } = resolveRules(rules)
|
||||
console.table(skipped) // 只在本地检查,不发送这些诊断。
|
||||
```json
|
||||
{"rules":{"search.query.submit":"submit","navigation.home.open":"click"}}
|
||||
```
|
||||
|
||||
把 `active` 传给 helper 的 `rules`。也可在浏览器开发者工具 Console 中直接执行以下代码,无需编辑源码,就能查看无效项:
|
||||
这是 rules 字段示意,不是完整的开启配置。要保留默认值,需显式把默认条目也写进去。
|
||||
ID 匹配 `^[a-z]+(?:\.[a-z]+){2,}$`,只用静态动作类别,不拼用户或资源信息。
|
||||
`click` 用于普通按钮/链接,排除表单、选择器、文本框和非按钮 INPUT;`submit` 只用于
|
||||
FORM;`card` 只用于已有键盘处理的 DIV role=link。选择、文件、拖放、裁剪不支持这些规则,
|
||||
不要猜成 click。`skill.detail.render` 不是自动 pageview。
|
||||
|
||||
```js
|
||||
const { resolveRules } = await import('/_skillhub-observe/v1/skillhub-intents.mjs')
|
||||
console.table(resolveRules({ 'skill.example.open': 'drop' }).skipped)
|
||||
```
|
||||
|
||||
应看到 `id: skill.example.open`、`reason: unknown rule type`。非法 ID、非字符串规则也会跳过;非法容器返回 `id: null` 的诊断和空 active。该检查只验证配置,不能证明 DOM 元素支持某类事件。
|
||||
|
||||
仅支持三种规则:
|
||||
|
||||
- `click`:普通按钮、链接;忽略 FORM、SELECT、TEXTAREA、非按钮 INPUT(包括文件选择器)。
|
||||
- `submit`:只用于 FORM。点击提交和按 Enter 都归一到原生 submit;原生校验失败可能没有事件。
|
||||
- `card`:已有 Enter/Space 处理的 DIV role=link,观察 click 和对应 keydown。helper 不会替应用增加键盘行为。
|
||||
|
||||
不支持的选择器、拖放、文件和裁剪行为不要硬填 click。未配置的动作不采集。`skill.detail.render` 是结构标签,不是自动页面访问量。
|
||||
|
||||
新增产品动作时,开发者在正确的现有元素上增加静态字面量 ID,再从仓库根目录运行:
|
||||
|
||||
```sh
|
||||
node web/scripts/analytics-manifest.mjs --write
|
||||
node web/scripts/analytics-manifest.mjs
|
||||
```
|
||||
|
||||
审核生成的 [manifest](frontend-analytics-manifest.json) 差异和原生交互测试。部署方再加一条规则即可。ID 必须匹配 `^[a-z]+(?:\.[a-z]+){2,}$`:至少三段小写字母,不含数字、下划线或资源信息。已有 ID 含义冻结,不能把用户、技能名、版本、路径拼进去。manifest 是版本清单,不是运行时封闭枚举。
|
||||
|
||||
## 4. 部署、子路径和 CSP
|
||||
|
||||
edge 仅在 UI HTML 注入一个延迟执行的 module,不改变 API、登录、下载、静态资源或 runtime-config。server 级 `client_max_body_size 100M` 保留标准 Web 的 100 MiB 请求体上限;实际业务校验仍可能拒绝上传。不要在应用代理 location 覆盖成更小值。
|
||||
|
||||
正式启用前检查 Nginx 含 http_sub_module、`nginx -t` 成功,并审核 `nginx -T`。HTML 输入不能仍被上游 gzip/Brotli 压缩。示例禁用 HTML 缓存和旧校验器;启用或回退都应清理 CDN/浏览器旧 HTML。不要修改 API 或 immutable 资源的缓存规则。
|
||||
|
||||
根路径演示的 CSP 需要允许同源外部 module 和同源连接。保留原有 CSP;nonce-only 或 strict-dynamic 策略不能只靠同源满足。需要部署方协调每响应 nonce 或已批准的 hash/SRI;做不到就不要启用。不要加 unsafe-inline、unsafe-eval 或通配符。还要保留原安全头,注意 Nginx add_header 的 location 继承规则。
|
||||
|
||||
`/skillhub/` 等子路径不是只改一个 URL。先按正常部署方式配置 `SKILLHUB_WEB_BASE_PATH=/skillhub/` 和匹配的同源 API base,再同时调整 UI location、无尾斜线重定向、旁路正则、模块目录/注入 src、两个 Umami 精确端点和 adapter prefix。edge 到 Web 的 proxy_pass 不带尾斜线,保留原 URI,不能再次重写。逐项配置见[英文契约的子路径映射](frontend-analytics.md#supported-sub-path-mapping)。OAuth、公网 HTTPS 和资源地址仍遵守原部署要求。
|
||||
|
||||
## 5. 验收:照这个顺序检查
|
||||
|
||||
下面命令假定根路径和默认 edge 端口。子路径时为所有采集 URL 加相同前缀;自定义端口时统一替换地址。
|
||||
|
||||
1. 检查模块确实由 edge 提供,不是 SPA 返回的 HTML:
|
||||
|
||||
```sh
|
||||
curl -i http://localhost:8081/_skillhub-observe/v1/skillhub-intents.mjs
|
||||
curl -i http://localhost:8081/_skillhub-observe/v1/missing.mjs
|
||||
curl -I http://localhost:8081/skills
|
||||
```
|
||||
|
||||
第一个应为 200、JavaScript MIME、nosniff 和 immutable。第二个应为非 HTML 的 404。第三个应为 200、Cache-Control: no-store,并保留原 CSP(若原部署配置了 CSP)。
|
||||
|
||||
2. 打开 edge 的 SkillHub 页面,在开发者工具 Network 勾选「Preserve log」和「Disable cache」,筛选 `/_skillhub-umami/`。刷新后应加载 script.js,但没有自动 pageview 或 send。
|
||||
3. 搜索框输入任意本地测试文字,点击搜索。应恰好有一条 `/_skillhub-umami/api/send` POST,响应 200。再按 Enter 搜索一次,应新增一条,不是两条。切换 Network 的 Payload 查看真正出站 JSON,不要只看回调。
|
||||
4. JSON 只能包含 `{ type: 'event', payload: { website, name, data: { event } } }`。website 是配置的 UUID;搜索 name 为 search.query.submit,event 为 submit。不得出现搜索词、完整 URL、标题、referrer、身份或额外 data 字段。
|
||||
5. 打开 Umami 网站事件页,选择当前时间范围,应看到 search.query.submit 和对应数量。平台处理可能稍有延迟;不能把 HTTP 200 单独当作入库证明。
|
||||
6. 执行下面的 Console 命令后再搜索,页面仍能使用,但 Network 不得新增 send:
|
||||
当前页面挂载后提供接口,可接入已有同意组件;浏览器 Console 也可用于验收:
|
||||
|
||||
```js
|
||||
window.skillhubAnalytics.stop()
|
||||
// 仅在重新获准后:
|
||||
await window.skillhubAnalytics.start()
|
||||
```
|
||||
|
||||
stop 不能撤回已开始的 fetch。重新获准后才可调用 start();同一个 root 重复初始化保留第一次规则和回调,修改规则需要先 stop 再初始化。
|
||||
start() 是当前页的显式授权,支持初始 consent=denied。重复 start 不重复安装。
|
||||
stop() 幂等;等待中的模块/adapter 不能迟到后重启采集。stop 返回后不新派发请求,
|
||||
但已经开始的 fetch 无法撤回。规则变更使用 stop 再初始化;环境变量变化需重建或重启 Web。
|
||||
旧标签页不会自动收到新环境参数。修改 env 文件本身不会改变正在运行的容器。
|
||||
|
||||
7. 将入口恢复 `enabled: false`,重新创建 edge。保持「Disable cache」勾选(或换一个新的无痕窗口),刷新页面后再搜索:不得加载 script.js、产生采集回调或 send。完全不注入时也应零采集。最后在隔离演示中停止 Umami,再打开新页面搜索和打开技能卡片,应仍正常,无统计依赖导致的页面错误:
|
||||
## 6. 根路径、子路径与验收
|
||||
|
||||
根路径无需改路由。子路径只需保持现有三项一致:
|
||||
|
||||
```dotenv
|
||||
SKILLHUB_WEB_BASE_PATH=/skillhub/
|
||||
SKILLHUB_WEB_API_BASE_URL=/skillhub
|
||||
SKILLHUB_PUBLIC_BASE_URL=https://skills.example.com/skillhub
|
||||
```
|
||||
|
||||
浏览器采集路径自动变为 `/skillhub/_skillhub-analytics/`,正常入口只重写一次。
|
||||
`/_skillhub-analytics/` 是保留前缀,不能作为 Web base path。
|
||||
保留当前 CSP;需要允许已批准的同源外部脚本和连接。nonce-only/strict-dynamic 部署仍需
|
||||
按本身的安全策略授权,无法授权就不开启。不要增加 unsafe-inline、unsafe-eval 或通配符。
|
||||
|
||||
按顺序验收。下面用本机默认 Web 端口,实际环境替换 origin 和子路径:
|
||||
|
||||
```sh
|
||||
docker compose -f docs/examples/analytics/compose.yaml stop umami
|
||||
curl -i http://localhost/runtime-config.js
|
||||
curl -i http://localhost/_skillhub-analytics/script.js
|
||||
curl -i http://localhost/_skillhub-analytics/api/websites
|
||||
```
|
||||
|
||||
验收后如需继续演示,使用上面的 up 命令恢复。不要为了测试停止共享平台或生产服务。
|
||||
1. runtime-config 为 no-store;原 API/auth 键仍在;不得包含 upstream。
|
||||
2. 开启且 granted 时 script.js 应是 JS 200;未注册路径应为 404。
|
||||
3. 新开 SkillHub 页面,Network 保留日志。先看到应用挂载,再按需加载 controller、adapter
|
||||
和 script.js。不会自动发送 pageview。
|
||||
4. 搜索框输入本地测试文字,点击搜索,再按 Enter 搜索。每次应各有一条 api/send,页面正常跳转。
|
||||
5. 检查真正出站 JSON,只允许 `{type:'event',payload:{website,name,data:{event}}}`。
|
||||
搜索 name 为 search.query.submit,event 为 submit。不能有查询文字、URL、标题、身份或额外 data。
|
||||
6. 在 Umami 网站事件页核对事件和数量。HTTP 200 不能单独证明入库。
|
||||
7. 调用 stop,再搜索。业务继续,不能新增 send。重新 start 后允许继续。
|
||||
8. 关闭配置或 denied 后重建 Web,打开新页面:不得加载 controller/adapter、script.js 或 send。
|
||||
denied 页面只有显式 start 才会加载。
|
||||
9. 在隔离测试环境停止 Umami,或用不可解析的内部域名启动 Web。Web 仍应就绪,搜索、
|
||||
登录、卡片、下载继续遵守原业务行为。不要停止共享或生产平台来测试。
|
||||
10. 根/子路径都测试正常上传(大于 1 MiB 且业务有效)、登录与下载。统计不代表业务成功。
|
||||
|
||||
## 6. 故障排查
|
||||
## 7. 排查、升级和关闭
|
||||
|
||||
| 现象 | 常见原因 | 处理 |
|
||||
| --- | --- | --- |
|
||||
| module 404 | 未挂载模块,版本目录或子路径不一致 | 对照 nginx -T、挂载路径、注入 src 和相对 import,三份 mjs 都要可访问;不要回退到 index.html |
|
||||
| MIME 报错或返回 HTML | 模块误入 SPA fallback,缺少 mjs MIME | 使用示例独立前缀、try_files 404 和 JavaScript MIME,保留 nosniff |
|
||||
| CSP 拦截 | script-src/connect-src 不允许,或 nonce-only 策略 | 按原 CSP 的授权机制交付;不要删除 CSP 或放宽为通配符 |
|
||||
| 上传 413 | 外层代理仍默认 1 MiB,或请求超过 100 MiB | 用 nginx -T 检查整条代理链;应用代理继承 100M,超过标准上限仍应拒绝 |
|
||||
| 没有事件 | 走了原 Web 而非 edge,enabled/授权未开,UUID 错,规则未配置或宿主不支持 | 按第 5 节从模块、skipped、Network 到平台事件页逐步确认;检查时间范围;不要把结构标签当 pageview |
|
||||
| edge 502 | Web 上游地址/端口错误,宿主不可达 | 核对 WEB_PORT、host-gateway、防火墙和监听地址;不要把后端或 edge 端口填成 Web 上游 |
|
||||
| Umami 不可用 | SDK 404、超时、平台停止或网络故障 | 页面应继续正常;检查平台和代理日志,恢复后重新打开页面;没有重试补发保证 |
|
||||
| 现象 | 检查 |
|
||||
| --- | --- |
|
||||
| 没有模块/事件 | 总开关、授权、容器是否重建、是否新页面;默认关闭是正常情况。 |
|
||||
| 配置报错 | 看 `[analytics]` 字段诊断;JSON 用双引号,enabled 是布尔值,UUID 正确;不要发出原值。 |
|
||||
| script.js 502 | Web 到平台的 DNS、网络、端口或证书;origin 不带路径;业务应仍可用。 |
|
||||
| SDK 失败后无事件 | 加载失败/超时后不重试;恢复平台后新开页面,或 stop 再 start。 |
|
||||
| 模块/资源返回 HTML | 检查部署版本、base path 和入口路由,不能改成任意远程脚本。 |
|
||||
| CSP 拦截 | 按原策略授权,不删除 CSP 或放宽通配符。 |
|
||||
| 有点击但没有统计 | 规则未配置、宿主不支持、超过 Umami 名称 50 字符,或 best-effort 丢失。 |
|
||||
| 上传 413 | 检查整条外层代理链,正常 Web 仍是 100M,统计未降低它。 |
|
||||
|
||||
## 7. 升级、关闭和已知限制
|
||||
升级先比较[manifest](frontend-analytics-manifest.json),已有 ID 含义冻结,新 ID 不自动加入默认规则。
|
||||
消失的配置 ID 不再触发。SDK 固定 Umami 3.4.1;升级需重测真实 payload、stop、并发和
|
||||
根/子路径业务。回滚 Web 镜像要同时使用该版本支持的配置;先关闭统计最稳妥。
|
||||
|
||||
升级时先比较新旧 manifest。已有 ID 语义不变;含义变化需要新 ID,重命名/删除必须有发布说明。消失的 ID 不再触发,新 ID 默认不自动加入规则。模块/SDK 升级要重新验证真实出站字段、stop 派发边界、并发发送、根/子路径和业务行为,不能只看构建成功。
|
||||
持久关闭:把配置留空或 enabled 改 false,按第 2 节重新创建 Web。新页面应零模块/SDK/请求。
|
||||
老标签页不自动更新;需让用户关闭/刷新,或在现有同意组件调用 stop。
|
||||
紧急全局阻断可在平台或独立采集路由禁用接收,再处理旧页;不能撤回已发出的请求。
|
||||
|
||||
临时关闭当前页用 stop();持久关闭把入口改回 enabled:false,并按版本发布规则更新、清理 HTML 缓存和刷新。彻底关闭则撤掉注入/edge,让访问回到正常 Web 入口,确认新 HTML 不引用模块、Network 无采集。关闭平台网站或封锁采集路由是额外控制,不能代替授权撤回。
|
||||
|
||||
结束本地演示(会删除演示数据库,先确认无需要保留的数据):
|
||||
自托管只停止统计服务,不要对整个业务栈执行 down:
|
||||
|
||||
```sh
|
||||
docker compose -f docs/examples/analytics/compose.yaml down -v
|
||||
docker compose --env-file .env.release -f compose.release.yml -f compose.analytics-umami.yml stop umami analytics-postgres
|
||||
```
|
||||
|
||||
限制必须一起评估:
|
||||
数据库卷保留。删除统计数据需另行备份、确认后只处理 `analytics_postgres_data` 对应的卷,
|
||||
不要使用整个项目的 `down -v`。
|
||||
|
||||
- 尽力发送,无重试、无持久队列,可能少计。SDK 加载期最多在内存暂存 20 条,默认最多等三秒;超额、失败、超时或 stop 都丢弃。已就绪发送彼此独立,不因一个请求挂起而阻塞后续动作。
|
||||
- 示例固定 Umami 3.4.1。stop 的派发保护依赖此版本在 fetch 前同步序列化的顺序,升级必须重跑适配器测试和真实浏览器验收;不要同时加载第二个 Umami tracker。
|
||||
- JSON 字段少不等于匿名传输。IP、User-Agent、Origin 仍到平台;SDK 还带网站、实际 hostname 和后续会话 cache 头。edge 去掉采集代理的 Cookie、Authorization、Referer,并使用 no-referrer。Umami 仍可能据传输元数据推导浏览器、系统、设备、地域和会话。
|
||||
- SDK 在清洗前仍会在页面内读取标题、URL、referrer 和自身 localStorage 开关;通用 helper 不读取这些内容。平台日志、数据保留和权限需另审计,详见[隐私契约](frontend-analytics.md#privacy-and-integration-modes)。
|
||||
## 8. 隐私和自定义开发边界
|
||||
|
||||
JSON 只保留静态 ID 和原生 event。helper 不读内容、输入值、URL、Cookie、存储或身份。
|
||||
SDK 在清洗前仍会读取页面标题、URL、referrer 和自己的 localStorage 开关。
|
||||
少字段不等于匿名:IP、User-Agent、Origin 仍到平台;Umami 还有 website、实际 hostname、
|
||||
会话 cache 请求头。Nginx 在统计上游前移除 Cookie、Authorization、Referer,并隐藏 Set-Cookie。
|
||||
浏览器到 Web 本身仍可能带这些 HTTP 元数据,需按原 Web 日志和权限策略管理。
|
||||
Umami 仍可能由传输元数据推导浏览器、系统、设备、地域和会话。数据库缺失 URL 的回退
|
||||
是空 path/query 和 localhost,不是用户当前路径。访问日志、保留和权限要单独审核。
|
||||
|
||||
尽力发送,无重试或持久队列。SDK 加载期最多内存暂存 20 条,默认等三秒;超额、失败、
|
||||
超时或 stop 会丢弃。就绪后的请求彼此独立。不要同时加载第二个 Umami tracker。
|
||||
stop 的派发保护依赖固定版本在 fetch 前同步序列化的顺序,升级必须重新验证。
|
||||
|
||||
自定义 adapter 属于开发流程,不是部署流程:在 `web/src/features/analytics/adapters/`
|
||||
增加参数校验与 send/stop factory,注册到静态 registry,再增加部署端校验和精确代理端点。
|
||||
factory 检查取消信号后才创建 SDK。通用 controller 不认识厂商字段,业务组件只提供被动标签。
|
||||
复用唯一的 `skillhub-intents.mjs`,不复制 helper,也不开放远程 JS URL。
|
||||
新增标签时运行 `node web/scripts/analytics-manifest.mjs --write` 并审核语义和原生交互。
|
||||
测试命令为 `pnpm -C web test src/features/analytics` 和 `python3 scripts/tests/web-analytics-test.py`。
|
||||
|
|
|
|||
|
|
@ -1,392 +1,178 @@
|
|||
# Passive frontend analytics attributes
|
||||
# Passive frontend analytics contract
|
||||
|
||||
For step-by-step setup, see the [Chinese integration guide](frontend-analytics-integration.md).
|
||||
This document is the English contract reference. Configuration-only deployment is
|
||||
planned in [PR-2](../openspec/changes/add-configurable-frontend-analytics/proposal.md);
|
||||
the current examples remain deployment-owned and opt-in.
|
||||
|
||||
SkillHub exposes semantic HTML attributes for separately maintained, optional
|
||||
collectors. It does not collect or send analytics by default. There is no SDK,
|
||||
vendor dependency, vendor token, network reporter, app script loader or
|
||||
telemetry configuration switch. The application entrypoint and production Nginx
|
||||
configuration are unchanged. Deployment owners control consent, script delivery,
|
||||
retention, transport and compliance; this is not an official vendor integration
|
||||
or a durable business-event feed.
|
||||
For deployment, use the [Chinese configuration guide](frontend-analytics-integration.md).
|
||||
Docker/Compose and Helm ship optional analytics with Web. No configuration means no
|
||||
controller/adapter chunk, SDK or collection request. The small configuration gate is
|
||||
part of bootstrap; the controller and SDK are not. Business components remain passive.
|
||||
|
||||
## Contract and scope
|
||||
|
||||
`data-analytics-id` names an action category (what kind of action), not a unique
|
||||
event or instance ID, and describes intent, not its result. Many elements and
|
||||
many events share the same category. Lowercase dotted names are independent of
|
||||
language, visible text and layout.
|
||||
`data-analytics-id` names an action category, not a unique event, resource or user.
|
||||
It describes intent, not its result. IDs match `^[a-z]+(?:\.[a-z]+){2,}$`.
|
||||
Lowercase dotted names are independent of language, visible text and layout.
|
||||
Published meanings are frozen. A meaning change requires a new ID. Renames/removals
|
||||
need release notes and a reviewed [manifest](frontend-analytics-manifest.json) diff.
|
||||
The manifest is an AST source inventory, not a runtime closed enum or a count of
|
||||
users, pageviews, successful operations or exhaustive business coverage.
|
||||
|
||||
The example callback payload is `{ id, event }`: `id` is the matched category;
|
||||
`event` is the native DOM event type observed (`click`, `submit`, or `keydown`),
|
||||
not a business result. Evolution within this module version is additive only:
|
||||
new optional fields must be allowlisted static values, consumers ignore unknown
|
||||
keys, and existing field meanings never change. Breaking changes use a new
|
||||
versioned module path.
|
||||
The inventory includes landing/search, skills, suites, publishing, review,
|
||||
governance, namespaces, GitHub integration, notifications, preferences, account,
|
||||
authentication, administration and navigation. Literal branches distinguish finite
|
||||
actions. Never interpolate resource, slug, version, user, provider, role or backend
|
||||
values into IDs. Fixed navigation metadata is source-owned. Forwarding sites do
|
||||
not necessarily represent DOM elements; portals and repeated resources share IDs.
|
||||
|
||||
Published IDs are frozen. A meaning change needs a new ID; rename/removal must be
|
||||
called out in the PR and release notes and visible in the generated manifest diff
|
||||
(the CI drift test checks source against that manifest). Deployers should diff
|
||||
the manifest between versions: configured IDs that disappear simply never fire;
|
||||
new IDs remain inert until explicitly configured. The manifest is not a runtime
|
||||
closed enum.
|
||||
Literal conditional branches distinguish finite business actions (for example,
|
||||
namespace freeze/unfreeze/archive/restore/delete and each confirmation/cancel).
|
||||
No resource, slug, version, user, provider, role, label instance or backend string
|
||||
is interpolated into attributes. Fixed navigation metadata is source-owned.
|
||||
`data-analytics-event`, where present, describes a native observation boundary;
|
||||
it is a hint for a deployment collector, not an application event dispatcher.
|
||||
|
||||
The source inventory covers these domains:
|
||||
|
||||
- Landing/catalog/search: queries, sorting, filters, results, pagination and links.
|
||||
- Skills: detail structure, tabs/versions/comparison, documentation and files,
|
||||
acquisition/copy, social actions, labels, visibility and lifecycle confirmations.
|
||||
- Publishing: package/folder selection, preview/retry, submission/warnings and
|
||||
avatar editing, staging, retries and cancellation.
|
||||
- Suites: catalog/detail, members/versions, editor/import, publishing tasks,
|
||||
lifecycle and destructive actions.
|
||||
- Reviews, promotions, reports and governance: queues, detail inspection,
|
||||
dispositions, layered confirmations, rejection editing, cancellation and retry.
|
||||
- Namespaces: management, membership/import, invitations, ownership and lifecycle.
|
||||
- GitHub: collection/settings, discovery, credentials, scheduling/admission,
|
||||
repositories/sources/runs, conflicts and review inspection.
|
||||
- Notifications/preferences, account/profile/security/token flows, login,
|
||||
registration/reset/device/CLI authorization, administration and global navigation.
|
||||
|
||||
[The generated manifest](frontend-analytics-manifest.json) is the authoritative
|
||||
source index: registered routes and their implementations, literal JSX sites,
|
||||
conditional values, static navigation definitions and forwarding sites. Counts
|
||||
are recomputed from the TypeScript AST; they are not quotas. Unique IDs are
|
||||
semantic categories, JSX sites are source declarations, and forwarding sites
|
||||
may have no literal ID until a caller opts in. A wrapper prop is not itself a
|
||||
DOM element. Repeated resources/pages, desktop/mobile navigation and portal
|
||||
instances can share an ID; neither IDs nor source sites count runtime elements,
|
||||
unique users, pageviews, successful operations or exhaustive business coverage.
|
||||
|
||||
Development checks (from the repository root with web dependencies installed):
|
||||
Run from the repository root with dependencies already installed:
|
||||
|
||||
```sh
|
||||
node web/scripts/analytics-manifest.mjs
|
||||
# After reviewing intentional source changes:
|
||||
# Only after reviewing intentional source changes:
|
||||
node web/scripts/analytics-manifest.mjs --write
|
||||
pnpm -C web test src/app/analytics-manifest.test.ts
|
||||
pnpm -C web test src/app/analytics-manifest.test.ts src/features/analytics
|
||||
```
|
||||
|
||||
Do not ship the manifest as a runtime closed enum. A deployment collector should
|
||||
have its own small, explicit event/ID allowlist and update it deliberately.
|
||||
|
||||
### Original eight compatibility meanings
|
||||
|
||||
| ID | Meaning |
|
||||
| --- | --- |
|
||||
| `search.query.submit` | Search form submit; button and Enter converge here. |
|
||||
| `skill.card.open` | Existing card activation; click or its direct Enter/Space handler. |
|
||||
| `skill.detail.render` | Structural detail root after loading/error/missing/deleted guards; not a unique pageview or success. |
|
||||
| `skill.download.request` | Package download attempt; authentication/guards may reject it. |
|
||||
| `skill.command.copy` | Command-copy attempt for the displayed installation method; not clipboard or installation success. |
|
||||
| `skill.prompt.copy` | Agent-prompt-copy attempt; not execution or installation. |
|
||||
| `skill.publish.request` | Publish/update/resubmit attempt, never avatar-only save. |
|
||||
| `skill.publish.confirm` | Explicit publish warning continuation, never all generic dialogs or avatar-only save. |
|
||||
| search.query.submit | Native search form submission, not typing or search success. |
|
||||
| skill.card.open | Existing card click or direct Enter/Space activation. |
|
||||
| skill.detail.render | Structural loaded detail root, not an automatic pageview. |
|
||||
| skill.download.request | Download attempt; guards may reject it. |
|
||||
| skill.command.copy | Command-copy attempt, not clipboard or installation success. |
|
||||
| skill.prompt.copy | Prompt-copy attempt, not execution or installation success. |
|
||||
| skill.publish.request | Publish/update/resubmit attempt, not avatar-only save. |
|
||||
| skill.publish.confirm | Explicit publish warning continuation, not every dialog. |
|
||||
|
||||
Installation method tabs distinguish SkillHub and ClawHub; the original command
|
||||
copy ID remains compatible. Public/namespace/private choices and business modes
|
||||
use static categories, not data fetched from the selected object.
|
||||
Installation-method tabs distinguish SkillHub and ClawHub without changing the
|
||||
original command-copy meaning. Visibility choices and business modes use static
|
||||
categories, not data fetched from the selected object.
|
||||
|
||||
## Native observation policy and limits
|
||||
|
||||
Attributes alone never create events. Choose the native event per host, and
|
||||
count one semantic target per event rather than every marked ancestor:
|
||||
Attributes alone do not create events. Keep one nearest semantic target per event.
|
||||
Never change defaults, propagation, focus, history, permissions or app handlers.
|
||||
|
||||
- Forms: observe `submit`, not both submit-button click and submit. Enter and
|
||||
native validation follow existing app behavior. Rejected validation may produce
|
||||
no submit. Search typing/clear is not a search submission.
|
||||
- Buttons/links: observe click, including browser-generated keyboard click. Ignore
|
||||
disabled, aria-disabled and data-disabled controls. An enabled click can still
|
||||
fail app guards; busy/authorization semantics remain the application's.
|
||||
- Custom DIV cards with existing Enter/Space handlers need that keydown in addition
|
||||
to click. Native buttons must not count both. Tabs generate click from navigation
|
||||
keys; do not count Arrow/Home/End keydown again. Do not invent keyboard handlers
|
||||
for existing mouse-only rows or directories.
|
||||
- Native select: observe change. Where the static marker is on an option, read only
|
||||
`selectedOptions[0].getAttribute('data-analytics-id')`, never option value/text.
|
||||
A select marker describes its context, not the raw choice. Avoid click/change
|
||||
double counting. Checkbox/radio change is an intent, not saved server state.
|
||||
- Radix Select items are actual option DOM, sometimes in a portal. Pointerup or
|
||||
Enter/Space on an item is selection intent, not proof that onValueChange committed
|
||||
a new value. Opening the trigger and navigation/highlight are not committed
|
||||
selections. A click-only collector is insufficient. Validate event order with
|
||||
the deployed component/browser version rather than manufacturing app hooks.
|
||||
Trigger `*.interact` markers cover observable activation/keydown intent, including
|
||||
closed-trigger typeahead; never capture the typed key, infer the chosen value,
|
||||
or call that interaction a committed selection. Typeahead can change selection
|
||||
without opening an item. Keep trigger interaction and item selection-intent
|
||||
metrics separate instead of counting both as the same selection.
|
||||
- File inputs: change means a selection was offered, not upload success. Drop is
|
||||
offered files, not accepted/validated files. Picker-opening click differs from
|
||||
change; ignore programmatic input clicks as additional user intent. Never read
|
||||
filenames, paths or file metadata. Picker cancellation is not observed.
|
||||
- Crop surface pointer/keyboard interaction is adjustment intent, not proof the
|
||||
crop changed or an image upload succeeded; no coordinates or image data may leave.
|
||||
- Explicit cancel/close buttons, including X, preserve workflow-specific meaning.
|
||||
Escape, overlay dismissal, route teardown, browser Back, automatic close and
|
||||
native picker cancel are not reliably observable through these static attrs.
|
||||
Closing an editor is not proof an in-flight request was aborted.
|
||||
- FORM uses submit, not submit-button click plus submit. Enter converges here;
|
||||
rejected native validation may produce no submit. Typing/clear is not submission.
|
||||
- Ordinary buttons/links use click, including browser-generated keyboard click.
|
||||
Ignore disabled, aria-disabled and data-disabled controls. App guards still apply.
|
||||
- DIV role=link cards with existing Enter/Space handlers also use keydown. Do not
|
||||
synthesize activation. Ignore repeat, IME and modifier keydown. Native buttons
|
||||
must not count keydown and their generated click twice.
|
||||
- Tabs generate click from navigation keys; do not count Arrow/Home/End again.
|
||||
Do not invent keyboard support for existing mouse-only rows/directories.
|
||||
- Native selects, checkboxes and radio changes have different boundaries. An
|
||||
option marker can describe static selection intent, never its value/text. Do
|
||||
not infer committed selection from opening a trigger or highlighting an item.
|
||||
- Radix selection may use pointerup or Enter/Space in a portal. Trigger typeahead
|
||||
can change selection without opening an item. Trigger interaction is not proof
|
||||
of committed selection; never capture typed keys. Current finite rules do not
|
||||
implement select/file/drop/crop; do not misconfigure them as click.
|
||||
- File selection and dropping only offer files, not upload success. Do not read
|
||||
filename/path/metadata. Programmatic picker clicks are not extra user actions;
|
||||
cancellation is not observed. Crop interaction is not proof of changed pixels.
|
||||
- Explicit cancel/close retains workflow meaning. Escape, overlay, route teardown,
|
||||
browser Back, automatic close and native picker cancel are not reliably observed
|
||||
by these markers. Closing is not proof a pending request was aborted.
|
||||
|
||||
Use document-level delegation for portals; React ancestry is not DOM ancestry.
|
||||
Wrapper forwarding is caller-scoped, including confirmation/cancel and close X;
|
||||
generic UI primitives do not guess business IDs. Keep the nearest semantic target
|
||||
and reject nested unmarked interactive controls rather than attributing them to
|
||||
an ancestor card/form. Synthetic clicks (including temporary download anchors)
|
||||
are not new successful user actions. Do not block or change default behavior,
|
||||
propagation, focus, route history, permissions or app handlers.
|
||||
Use document delegation for portals; React ancestry is not DOM ancestry. Wrappers
|
||||
forward caller-scoped IDs, not guessed business IDs. Reject nested unmarked
|
||||
interactive controls rather than attributing them to an ancestor card/form.
|
||||
Synthetic temporary download anchors do not prove new successful user actions.
|
||||
SPA classification, consent transitions and view deduplication are separate
|
||||
policies; never derive them from full URLs/query strings.
|
||||
|
||||
`skill.detail.render` is not an automatic pageview event. SPA route classification,
|
||||
render retries, consent transitions, duplicate initialization and view deduplication
|
||||
are separate collector policies. Never derive these from full URLs/query strings.
|
||||
## Single product source and interfaces
|
||||
|
||||
### Privacy and integration modes
|
||||
The sole helper is `web/src/features/analytics/skillhub-intents.mjs`:
|
||||
|
||||
The recommended output allowlist is only `{ id, event }`, each validated against
|
||||
operator-owned static categories. Do not collect textContent/innerHTML, input or
|
||||
option values, clipboard contents, href/full URLs/query/fragment, cookies, storage,
|
||||
identity/account IDs, device codes, tokens, invitation links, private coordinates,
|
||||
filenames/paths, review/report reasons or crop data. Do not scrape arbitrary DOM
|
||||
attributes or reuse test IDs (some intentionally contain resource identifiers).
|
||||
- `observeSkillhubIntents({ root, rules, onIntent }) -> stop`.
|
||||
- Frozen `DEFAULT_RULES` has the original twelve rules listed in the guide.
|
||||
- `resolveRules(rules) -> { active, skipped: [{ id, reason }] }` is pure. Invalid
|
||||
containers produce id:null and no active rules. Unknown IDs are not rejected
|
||||
merely for being absent from the manifest. Invalid grammar/types are skipped.
|
||||
- Omitted rules use defaults. Explicit rules replace defaults; {} collects nothing.
|
||||
- click excludes FORM/SELECT/TEXTAREA/non-button INPUT; submit is FORM-only; card
|
||||
is DIV role=link only. Host validity is checked at observation, not resolution.
|
||||
- Intent is exactly `{ id, event }`; event is click/submit/keydown. Same-version
|
||||
evolution is additive optional allowlisted static fields; consumers ignore
|
||||
unknown keys. Breaking changes need a new module contract.
|
||||
- Importing does nothing; without callback no listeners are installed. Same root
|
||||
returns the same stop and retains the FIRST rules/callback. Stop then initialize
|
||||
to change configuration. Old disposers cannot remove a new installation. Stop
|
||||
is idempotent; nested observers share event deduplication.
|
||||
- Callback throws/rejections are swallowed without affecting business events.
|
||||
The helper itself has no network, cookies, storage, identity or history hooks.
|
||||
|
||||
Disabled integration means no injected script and zero analytics collection.
|
||||
Enabled delivery does not itself authorize collection: initialize only after
|
||||
applicable consent/policy approval, stop on withdrawal, and separately review any
|
||||
outbound adapter and CSP connect destinations. External JS has page privileges;
|
||||
serve reviewed versioned code, restrict access and treat compromise as a security
|
||||
incident. Vendor credentials and ingestion endpoints belong outside core.
|
||||
The controller owns authorization, rule resolution, generations and lifecycle.
|
||||
Static registry metadata validates public adapterConfig; factories lazily import
|
||||
adapters. Factory cancellation prevents late imports from creating an SDK after
|
||||
stop. Adapter exposes `send(intent) -> Promise<void>` (never rejects) and stop().
|
||||
The loader runs separately after the first React DOM commit, never blocking mount.
|
||||
Disabled, absent, invalid or denied configuration does not import the controller.
|
||||
Public `window.skillhubAnalytics.start()` explicitly grants current-page consent;
|
||||
stop() immediately disables observation. Enabled=false cannot be overridden by
|
||||
start. Deployment changes require recreating/restarting Web; old tabs do not update.
|
||||
|
||||
## Configure rules and add tags
|
||||
## Configuration and proxy
|
||||
|
||||
[The vendor-neutral helper](examples/analytics/observe/v1/skillhub-intents.mjs)
|
||||
exports `observeSkillhubIntents({ root, rules, onIntent })`, frozen `DEFAULT_RULES`
|
||||
(the original 12 categories), and pure `resolveRules(rules)`:
|
||||
`SKILLHUB_WEB_ANALYTICS_CONFIG` is JSON with enabled (boolean/default false),
|
||||
adapter (static registry name), consent (denied/granted/default denied), rules
|
||||
(optional finite rule map), adapterConfig (adapter-owned public object). Umami
|
||||
accepts only websiteId UUID. Invalid configuration disables only analytics with
|
||||
field-only diagnostics, never raw values. Rule resolution keeps its skip behavior.
|
||||
The startup parser limits JSON to 64 KiB/32 levels and rejects duplicate keys.
|
||||
|
||||
```js
|
||||
import { DEFAULT_RULES, resolveRules, observeSkillhubIntents } from './skillhub-intents.mjs'
|
||||
const rules = { ...DEFAULT_RULES, 'skill.example.open': 'click' }
|
||||
const { active, skipped } = resolveRules(rules) // Inspect locally; do not send config diagnostics.
|
||||
const stop = observeSkillhubIntents({ root: document, rules: active, onIntent: sendIntent })
|
||||
```
|
||||
`SKILLHUB_WEB_ANALYTICS_UPSTREAM` is a deploy-only HTTP(S) origin: no credentials,
|
||||
path, query, fragment, whitespace or injection characters. It is never put in
|
||||
browser configuration. Existing BusyBox tools validate deployment control fields;
|
||||
base64 transports JSON safely as data into the existing no-store runtime-config.js.
|
||||
No jq, Node runtime, extra package, CSP relaxation or HTML injection is required.
|
||||
|
||||
Omitting `rules` uses the defaults; passing it REPLACES them. Merge explicitly as
|
||||
above. The example accepts the same grammar the product manifest enforces:
|
||||
`^[a-z]+(?:\.[a-z]+){2,}$` (at least three dot-separated segments, lowercase letters
|
||||
only; no digits or underscores). For example, `skill.command.skillhub.select`
|
||||
can reuse `click`. Only finite rule types are supported:
|
||||
The reserved same-origin prefix is `/_skillhub-analytics/`. BASE_PATH is applied in
|
||||
the browser; existing Nginx rewrite strips it once. Enabled configurations generate
|
||||
exact script.js and api/send routes, with method restrictions; all other paths are
|
||||
404, never Umami admin. Consent-denied enabled configurations retain those routes
|
||||
for explicit dynamic start, but do not load/request them automatically.
|
||||
Variable proxy_pass uses the existing resolver at request time. HTTPS verifies
|
||||
certificates using the system trust bundle and correct SNI. Collector proxying
|
||||
strips Cookie/Authorization/Referer and hides Set-Cookie. API/auth routes, 100M
|
||||
uploads, TLS, business caching and permissions are not changed.
|
||||
|
||||
- `click`: ordinary buttons/links. FORM, SELECT, TEXTAREA and non-button INPUT
|
||||
hosts are ignored (including file inputs).
|
||||
- `submit`: FORM only; button click and Enter converge on native submit.
|
||||
- `card`: DIV role=link with its existing Enter/Space handler, plus click. The
|
||||
observer does not implement keyboard activation or synthesize clicks.
|
||||
## Privacy and integration modes
|
||||
|
||||
Select/file/drop/crop and other special interactions need a future rule type;
|
||||
never configure them as `click`. Unknown/unconfigured actions, invalid IDs,
|
||||
non-string rules and unsupported rule types/hosts are skipped without affecting
|
||||
the page. `resolveRules(...).skipped` gives `{ id, reason }` for invalid entries;
|
||||
an invalid rules container yields an `id: null` diagnostic and an empty active map.
|
||||
It validates configuration, not the live DOM host.
|
||||
Allowlist only static id/event, not text/HTML, input/option values, clipboard,
|
||||
URLs/query/fragment, cookies, storage, identities, codes/tokens, private paths,
|
||||
filenames, review reasons or crop data. Test IDs can contain resource identifiers;
|
||||
do not scrape them. The manifest is not a payload.
|
||||
|
||||
Importing the helper does nothing; without a callback it installs no listeners.
|
||||
The same root returns the existing stop function and keeps the FIRST config and
|
||||
callback. To change config, call `stop()` and initialize again. Nested observers
|
||||
from this module instance share event deduplication; only the nearest marked host
|
||||
is considered, never an ancestor behind an unmarked nested interactive control.
|
||||
Native-button keydown, repeated/IME/modifier card keydown are ignored. Sync throws
|
||||
and returned rejected promises from the callback are swallowed, with no retry or
|
||||
queue. No network, cookies, storage, identity or route patching occurs in the
|
||||
helper, and it never prevents defaults or stops propagation.
|
||||
Umami 3.4.1 uses manual-only script attributes and a final before-send sanitizer.
|
||||
Only `{type:'event',payload:{website,name,data:{event}}}` survives; website must
|
||||
match configuration, name must match product grammar and Umami's 50-character
|
||||
limit. Pageview/identify/performance/context/extra data are dropped. Payload values
|
||||
are read once. A non-enumerable toJSON guard rechecks stop synchronously at fetch
|
||||
dispatch; upgrading the SDK must reverify its ordering with dispatch-boundary tests.
|
||||
Stop cannot recall a fetch already started.
|
||||
|
||||
To add a normal action: product owners add a literal `data-analytics-id` on the
|
||||
appropriate existing host, run `node web/scripts/analytics-manifest.mjs --write`,
|
||||
review the manifest diff and test the host/native behavior. Deployers then add one
|
||||
rule line. No product listener or SDK is needed. Download click is NOT download or
|
||||
installation success; publish submission is NOT publish success.
|
||||
At most 20 valid intents wait in memory for up to three seconds while the SDK
|
||||
loads. Excess, failure, timeout or stop drops them. Once ready, requests are
|
||||
independent and time-bounded, with no retry/durable queue/post-load backlog.
|
||||
Do not load a second Umami tracker. Missing/blocked/offline collectors never reject
|
||||
into the application. SDK loading is trusted code with page privileges: review and
|
||||
pin its version, access and retention; it is not an arbitrary remote-JS plugin.
|
||||
|
||||
The repository-integration test lives at `web/analytics-example.test.ts`, alongside
|
||||
the existing root-level web tests. Vitest discovers it with `pnpm -C web test`;
|
||||
run it alone with `pnpm -C web test analytics-example.test.ts`. It stays outside
|
||||
the production TypeScript project's `src`/`test` inputs because the docs-only
|
||||
example is not present in the `web/` Docker build context.
|
||||
This is NOT anonymous transport. IP/User-Agent/Origin still reach the platform.
|
||||
The SDK sends website, actual hostname and session cache headers, reads
|
||||
URL/title/referrer before sanitization, and reads its own localStorage switch.
|
||||
Nginx strips sensitive headers before the collector, not before the browser's
|
||||
request reaches Web. Web logs require their own privacy policy. Umami can derive
|
||||
browser/OS/device/geo/session from transport metadata. With pinned 3.4.1 absent URL
|
||||
stores empty path/query and fallback hostname localhost; title/referrer/screen/
|
||||
language/distinct identity remain absent. Removing JSON fields does not disable
|
||||
transport-derived processing. Operators must review consent, logs, access and law.
|
||||
|
||||
## Connect a platform: local Umami v3.4.1 demo
|
||||
|
||||
[The Compose example](examples/analytics/compose.yaml) runs Umami v3.4.1 (MIT),
|
||||
Postgres 16 and an optional edge; SkillHub remains your existing web upstream.
|
||||
From the repository root:
|
||||
|
||||
```sh
|
||||
docker compose -f docs/examples/analytics/compose.yaml config
|
||||
docker compose -f docs/examples/analytics/compose.yaml up -d
|
||||
```
|
||||
|
||||
The default Web upstream is `host.docker.internal:80`, matching the release
|
||||
Compose `WEB_PORT` default of 80. Override `SKILLHUB_WEB_UPSTREAM` with the actual
|
||||
reachable host:port if different. The edge's `extra_hosts` maps this hostname to
|
||||
`host-gateway` on native Linux; Docker Desktop/OrbStack already provide it. The
|
||||
host Web listener must be reachable from containers. The edge inherits the standard
|
||||
server-level `client_max_body_size 100M` for application proxying (100 MiB); it does
|
||||
not reduce uploads to Nginx's default 1 MiB.
|
||||
|
||||
This creates a demo network and ephemeral database, not production storage. Use
|
||||
an isolated local Docker environment with available network pools. Open
|
||||
`http://localhost:8300` for local setup only. Umami's default `admin` / `umami`
|
||||
credentials, the public DB password and app secret are DEMO ONLY: change them.
|
||||
Create a synthetic website in Umami, copy its UUID into
|
||||
[`observer.mjs`](examples/analytics/observe/v1/observer.mjs), then explicitly change
|
||||
`enabled` and the consent hook for your local policy. Open SkillHub through
|
||||
`http://localhost:8081` and exercise an enabled action; inspect the request to
|
||||
`/_skillhub-umami/api/send` and Umami's event view. Tear down the local demo with
|
||||
`docker compose -f docs/examples/analytics/compose.yaml down -v` (demo data is removed).
|
||||
|
||||
[`umami-adapter.mjs`](examples/analytics/observe/v1/umami-adapter.mjs) is the only
|
||||
vendor-specific code. It loads `/script.js` with `data-auto-track="false"`, so
|
||||
v3.4.1 installs no pageview, history hook, click listener or performance observer.
|
||||
`data-host-url` is the same-origin `/_skillhub-umami` prefix, and
|
||||
`data-before-send` names its sanitizer. Do not load another Umami tracker on the
|
||||
same page (the SDK/global hook are singletons). It calls `umami.track(id, { event })`;
|
||||
at most 20 valid intents wait in memory for up to three seconds while the script
|
||||
loads (excess intents are dropped). They are released when ready, or dropped on
|
||||
load failure, timeout or `stop()`. Once ready, every valid intent calls the tracker
|
||||
independently: concurrent or hung requests do not block later intents. Each call's
|
||||
wait is time-bounded; missing, blocked, throwing or rejecting trackers resolve
|
||||
silently. There is no retry, durable queue or post-load backlog; best-effort counts
|
||||
can undercount. After `stop()` returns, no new request is dispatched; only a
|
||||
request whose fetch already started cannot be recalled (nor cancelled by a timeout).
|
||||
The before-send hook keeps its immediate stop check and adds a non-enumerable
|
||||
`toJSON` guard that re-checks consent when the SDK serializes at dispatch. In pinned
|
||||
v3.4.1 this runs synchronously in the fetch expression inside the SDK's try/catch:
|
||||
throwing on stop prevents fetch and is swallowed by the SDK. This relies on that
|
||||
ordering; a different Umami version must re-verify it with the dispatch-boundary
|
||||
regression tests in `web/analytics-umami-example.test.ts`.
|
||||
|
||||
The sanitizer allowlists the FINAL SDK payload, not just the callback. Only
|
||||
`{ type: 'event', payload: { website, name, data: { event } } }` survives; website
|
||||
must match configuration, name must match the ID shape (and Umami's 50-character
|
||||
limit), event must be click/submit/keydown. It returns null for pageviews (including
|
||||
unnamed `event` payloads), identify, performance and everything else. Hostname, URL,
|
||||
title, referrer, screen, language, tag, identity and extra data keys are removed.
|
||||
Values are read once, and only validated primitives are forwarded.
|
||||
The SDK still reads title/URL/referrer in-page before sanitizing; it also reads
|
||||
localStorage for its own `umami.disabled` switch. Those are SDK behaviors, not
|
||||
behaviors of the vendor-neutral helper.
|
||||
|
||||
This is NOT anonymous transport or zero server processing: IP, User-Agent and
|
||||
Origin still leave the browser. Umami sends `x-umami-website-id`,
|
||||
`x-umami-hostname` (the actual hostname) and, after a response, `x-umami-cache`
|
||||
(a session cache token). They are NOT sanitized JSON. The edge tightens UI
|
||||
Referrer-Policy to `no-referrer` and strips Referer, Cookie and Authorization on
|
||||
collector proxying; otherwise same-origin Referer can expose paths/queries.
|
||||
Umami v3.4.1 hashes website/IP/User-Agent/salt into a server-side session and derives
|
||||
browser/OS/device/geo from transport metadata. Removing JSON screen/language does
|
||||
not disable these derivations; review access logs, retention, consent and local
|
||||
law. Its missing-URL fallback stores empty url_path and fallback hostname
|
||||
`localhost`, not the current page; url_query is an empty string (not SQL NULL in
|
||||
v3.4.1), while title/referrer/screen/language stay null.
|
||||
|
||||
To replace the platform, replace the adapter and edge ingestion endpoints, keeping
|
||||
the helper and static payload contract. No admin configuration page, SkillHub
|
||||
analytics DB, persistent queue or multi-vendor SDK is introduced.
|
||||
|
||||
## Turn collection off
|
||||
|
||||
No injection means zero collection. `enabled: false` in the deployer's entry is
|
||||
also inert: no helper initialization or tracker load. On consent withdrawal call
|
||||
`window.skillhubAnalytics.stop()`; it removes listeners and denies SDK payloads,
|
||||
including those awaiting dispatch. After it returns, no new request is dispatched;
|
||||
only a request whose fetch already started cannot be recalled. The dispatch guard
|
||||
relies on pinned v3.4.1 serializing at dispatch; re-run the adapter regression tests
|
||||
to re-verify this ordering for a different Umami version. Resume only after approval
|
||||
via `start()`. Platform-side disabling/deleting the website, Umami's local
|
||||
`umami.disabled` switch, or blocking the two ingestion routes are additional
|
||||
operator controls, not substitutes for consent. Removing injection and purging
|
||||
HTML is the deployment rollback; default SkillHub never loads any of this.
|
||||
|
||||
## Optional deployment-owned Nginx injection
|
||||
|
||||
Prefer a separately maintained same-origin HTTPS module, optionally inserted by
|
||||
Nginx `sub_filter`, not an app entrypoint modification. Start with
|
||||
[the opt-in edge template](examples/analytics/nginx.conf). It is not loaded by the
|
||||
product. Compose substitutes only the two upstream host:port variables. Publish
|
||||
its HTTP listener on loopback behind your HTTPS terminator. Serve the supplied
|
||||
`observer.mjs`, helper and adapter outside the app bundle; only enable the entry
|
||||
after configuring website/rules/consent.
|
||||
|
||||
The sample filters only the UI fallback's default `text/html` type, once per
|
||||
response, inserting a module before `</head>`. Module loading is deferred and no
|
||||
application code depends on it; missing/failed JS must not stop SkillHub. The
|
||||
versioned prefix location overrides SPA fallback, sets JavaScript MIME and nosniff,
|
||||
and uses `try_files $uri =404` with directory listing off. The served directory
|
||||
contains only the three JS modules, never `.d.mts` or config. Missing modules are
|
||||
404, never index.html. Code is immutable: deploy changed bytes/config under a new
|
||||
version directory and update the injection path (breaking contracts need a new
|
||||
module version). The two exact Umami routes expose tracker and send only; all
|
||||
other paths under that prefix return 404. Same-origin script-src/connect-src are
|
||||
sufficient for this demo, subject to the existing policy below.
|
||||
|
||||
Deployment checklist:
|
||||
|
||||
1. Check `nginx -V` for http_sub_module (built with `--with-http_sub_module` or an
|
||||
equivalent loaded module), then `nginx -t`. Module absence is a blocker, not an
|
||||
invitation to weaken security. Review actual final config with `nginx -T`.
|
||||
2. Upstream `Accept-Encoding ""` requests uncompressed HTML. Ensure upstream
|
||||
gzip_static/Brotli/CDN compression cannot still send encoded bytes. Filtering
|
||||
compressed input does not work; downstream compression may run after filtering.
|
||||
Leave API, auth, download, asset and runtime-config locations unmodified.
|
||||
3. Filtered HTML must not reuse unfiltered validators or cached 304 responses. The
|
||||
example removes upstream ETag/Last-Modified/Content-MD5 and cache headers,
|
||||
suppresses conditional requests and disables proxy cache, serving no-store HTML.
|
||||
Purge old edge/CDN HTML on enable/rollback; test GET/HEAD, conditional headers,
|
||||
existing browser caches and 200/304 behavior. Do not cache injected HTML under
|
||||
the old unfiltered entity validator. API and immutable assets retain their policy.
|
||||
4. Preserve existing CSP and all security headers. The script must satisfy the
|
||||
current script-src policy (same-origin alone does not satisfy nonce-only or
|
||||
strict-dynamic policies). A per-response nonce or approved external hash/SRI
|
||||
policy needs deployment-specific coordination; otherwise do not enable injection.
|
||||
Never add unsafe-inline/unsafe-eval or broad wildcards. Any reporter needs an
|
||||
explicitly permitted connect-src endpoint; this example uses same-origin only.
|
||||
Nginx add_header inheritance is location-sensitive: merge/repeat existing edge
|
||||
security headers as necessary; do not drop or hide upstream CSP to make it work.
|
||||
5. Test actual built SkillHub HTML, not just a synthetic HTML fixture: exactly one
|
||||
script for UI routes, none for APIs/assets/downloads/auth endpoints; script MIME,
|
||||
missing-script 404, root/deep links, compression, CSP, disabled delivery and
|
||||
collector failure. Then browser-check native events, portals, guards and consent.
|
||||
A fixture filter probe is not final-config, SkillHub or production acceptance.
|
||||
|
||||
### Supported sub-path mapping
|
||||
|
||||
The sample is root deployment. For a supported `/skillhub/` deployment, keep that
|
||||
prefix on the edge-to-app proxy URI (proxy_pass without a trailing slash), and
|
||||
configure the app normally with SKILLHUB_WEB_BASE_PATH=/skillhub/ and matching
|
||||
same-origin API base. Its existing container entrypoint owns internal prefix
|
||||
rewriting; do not rewrite it a second time at the edge.
|
||||
|
||||
Adapt the sample's UI `location /` to `location /skillhub/`; add an exact
|
||||
`/skillhub` redirect to `/skillhub/`; scope the bypass regex to
|
||||
`^/skillhub/(api/|oauth2/|login/oauth2/|\.well-known/|assets/|registry/|runtime-config\.js$|nginx-health)`.
|
||||
Change the versioned module prefix location and injected src to
|
||||
`/skillhub/_skillhub-observe/v1/` and
|
||||
`/skillhub/_skillhub-observe/v1/observer.mjs`, plus the matching deny prefix.
|
||||
With `root /srv`, install the three JS files under
|
||||
`/srv/skillhub/_skillhub-observe/v1/`; relative imports follow automatically.
|
||||
Also scope BOTH exact Umami endpoints and the deny prefix to
|
||||
`/skillhub/_skillhub-umami/`, and change the entry's adapter `prefix` accordingly;
|
||||
upstream `/script.js` and `/api/send` stay unchanged. Keep other site locations
|
||||
unchanged; do not put sub_filter at http/server scope. Use any supported validated
|
||||
base path consistently, preserving original request URIs. HTTPS/public registry,
|
||||
OAuth callbacks and generated asset URLs still need the normal deployment setup.
|
||||
|
||||
Rollback: remove the sub_filter directives/injection location customization, test
|
||||
configuration, reload through the normal deployment procedure, and purge cached
|
||||
HTML. Existing cached versioned JS can remain temporarily but must not be referenced
|
||||
or initialized. Verify new HTML has no script, no callback/reporting occurs and
|
||||
SkillHub remains usable. This document does not authorize production changes.
|
||||
Supported-platform deployment, initialization, verification, upgrade, shutdown and
|
||||
custom adapter development are documented in the guide. Custom adapters require
|
||||
reviewed code, registry validation, cancellation, final payload cleaning and exact
|
||||
proxy routes; business components must remain vendor-neutral and passive.
|
||||
|
|
|
|||
|
|
@ -53,6 +53,14 @@ Nginx 通过独立 include 片段选择已注册适配器的精确端点:Umami
|
|||
|
||||
部署配置变更经重建/重启 Web 使新页面生效;旧标签页不自动接收新环境参数。动态停止通过当前页面接口生效;紧急全局停止可关闭独立采集路由。文档不得宣称仅修改 env 即刻撤销已在发送的请求。模块使用正常发布构建产物与缓存策略,不把可变配置放入 immutable JS。
|
||||
|
||||
### 5. Serialization and loading implementation
|
||||
|
||||
PR-2 不新增 jq 或 Node 运行时依赖。镜像已有 BusyBox awk 负责有界 JSON 语法和部署控制字段校验,base64 将 JSON 作为数据追加到现有 no-store runtime-config.js;浏览器用 JSON.parse 解码,不经 eval,也不把任意 JSON 插入 JS 字面量。配置最大 64 KiB、最多 32 层,重复键拒绝。非法 rules 仍按既有 resolveRules 跳过,不升级为整个配置失败。
|
||||
|
||||
bootstrap 仅静态包含小型 gate 和注册参数校验;controller/helper 与 adapter 保持独立懒加载 chunk。在首个 React DOM commit 后的独立任务中启动 gate,不把正常挂载与 SDK 绑定。enabled=true 且 consent=denied 时不加载模块,但代理保留精确端点,供当前页显式 start() 授权后使用。静态 adapter factory 在动态导入返回后检查取消信号,迟到的导入不能重新创建 SDK。
|
||||
|
||||
旧示例目录仅保留指南指针。helper/adapter 与全部断言迁入产品测试树;历史 edge/Compose 只保留为测试 fixture,不是部署路径,不维护第二份采集实现。
|
||||
|
||||
## Risks and verification
|
||||
|
||||
重点验证默认零 SDK/采集请求、错误配置降级、页面启动不等统计 SDK、DNS失败不阻断Web、stop/init竞争、适配器字段清洗、根/子路径、100M原业务上传、登录/下载及配置回滚。固定 SDK 升级须验证真实发送顺序;不能将静态配置或模拟API测试描述为真实业务E2E。正式测试矩阵在实现和独立Review收敛后生成。
|
||||
|
|
|
|||
|
|
@ -31,6 +31,6 @@ Issue #989 / PR #1010 已提供被动动作标签、通用 helper 和独立平
|
|||
|
||||
## Impact
|
||||
|
||||
影响按阶段区分:PR-1 仅涉及被动标签契约、示例、测试和接入文档;以下产品部署影响属于 PR-2。影响 Web 可选模块、运行时配置、容器启动、Compose/Helm、部署测试和文档。新增小型 jq 镜像依赖用于标准 JSON 校验与安全转义,避免用 envsubst 拼接任意配置。默认部署不启动统计服务、不加载采集模块、不外发事件。
|
||||
影响按阶段区分:PR-1 仅涉及被动标签契约、示例、测试和接入文档;以下产品部署影响属于 PR-2。影响 Web 可选模块、运行时配置、容器启动、Compose/Helm、部署测试和文档。复用镜像已有 BusyBox awk 校验有界 JSON,使用 base64 将 JSON 安全传入现有 runtime-config,避免 envsubst 拼接任意配置,不新增 jq 或其他镜像依赖。默认部署不启动统计服务、不加载采集模块、不外发事件。
|
||||
|
||||
范围外:后台采集配置页、SkillHub 分析数据库、通用远程代码插件平台、用户级同意弹窗、可靠消息/重试队列、服务端业务成功事件、生产环境部署和自动合并。统计平台自己的数据库属于可选外部服务,不修改 SkillHub schema。
|
||||
|
|
|
|||
|
|
@ -12,25 +12,25 @@
|
|||
- [ ] 2.7 Independent parent review and publication; keep Issue #989 open.
|
||||
|
||||
## 3. PR-2 (follow-up): optional frontend capability
|
||||
- [ ] 3.1 Move helper/adapter to a single product source under web/src/features/analytics and preserve all PR-1 assertions.
|
||||
- [ ] 3.2 Add default-closed runtime loader and vendor-neutral controller/registry; register Umami as the first adapter.
|
||||
- [ ] 3.3 Cover consent, validation, duplicate init, async stop races and adapter failures.
|
||||
- [x] 3.1 Move helper/adapter to a single product source under web/src/features/analytics and preserve all PR-1 assertions.
|
||||
- [x] 3.2 Add default-closed runtime loader and vendor-neutral controller/registry; register Umami as the first adapter.
|
||||
- [x] 3.3 Cover consent, validation, duplicate init, async stop races and adapter failures.
|
||||
- [ ] 3.4 Validate this OpenSpec change with the CLI when available and bind the follow-up PR/task.
|
||||
|
||||
## 4. PR-2 (follow-up): deployment
|
||||
- [ ] 4.1 Safely serialize and validate configuration without breaking existing runtime config.
|
||||
- [ ] 4.2 Add isolated proxy snippets with request-time DNS, certificate validation and exact endpoints.
|
||||
- [ ] 4.3 Wire Docker/Compose/Helm defaults and reserved sub-path validation.
|
||||
- [ ] 4.4 Provide optional persistent self-hosted Compose combination without another Nginx.
|
||||
- [x] 4.1 Safely serialize and validate configuration without breaking existing runtime config.
|
||||
- [x] 4.2 Add isolated proxy snippets with request-time DNS, certificate validation and exact endpoints.
|
||||
- [ ] 4.3 Wire Docker/Compose/Helm defaults and reserved sub-path validation. (Implemented; Compose/runtime checks passed. Helm CLI rendering remains unverified.)
|
||||
- [x] 4.4 Provide optional persistent self-hosted Compose combination without another Nginx.
|
||||
|
||||
## 5. PR-2 (follow-up): documentation
|
||||
- [ ] 5.1 Replace source-editing quickstart with configuration-only production paths for supported platforms; distinguish custom adapter development.
|
||||
- [ ] 5.2 Document fields, platform prerequisites, initialization, verification, troubleshooting, upgrade and shutdown.
|
||||
- [ ] 5.3 Link deployment entry points and document adapter extension and actual privacy boundaries.
|
||||
- [x] 5.1 Replace source-editing quickstart with configuration-only production paths for supported platforms; distinguish custom adapter development.
|
||||
- [x] 5.2 Document fields, platform prerequisites, initialization, verification, troubleshooting, upgrade and shutdown.
|
||||
- [x] 5.3 Link deployment entry points and document adapter extension and actual privacy boundaries.
|
||||
|
||||
## 6. PR-2 (follow-up): quality gates
|
||||
- [ ] 6.1 Consolidated independent implementation review, fixes and final base checkpoint.
|
||||
- [ ] 6.2 Reviewed requirement/scenario/test matrix and targeted source/deployment checks.
|
||||
- [ ] 6.3 Exact-SHA release Compose S3/auth smoke and Chromium normal/changed/failure scenarios, including default zero module/SDK/request and root/sub-path upload/login/download/CSP/cache compatibility.
|
||||
- [ ] 6.2 Reviewed requirement/scenario/test matrix and targeted source/deployment checks. (Worker matrix and local checks complete; independent review and Helm execution pending.)
|
||||
- [ ] 6.3 Exact-SHA release Compose S3/auth smoke and Chromium normal/changed/failure scenarios, including default zero module/SDK/request and root/sub-path upload/login/download/CSP/cache compatibility. (Local browser/real-collector and HTTP proxy checks passed; full backend S3/auth smoke remains pending.)
|
||||
- [ ] 6.4 CI/privacy/readiness, one validation report and retained preview for human acceptance.
|
||||
- [ ] 6.5 Close Issue #989 only after configuration-only deployment is complete and verified, not when PR-1 merges.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue