diff --git a/.env.release.example b/.env.release.example index a8f96fa9a..71618de1e 100644 --- a/.env.release.example +++ b/.env.release.example @@ -33,6 +33,7 @@ SKILLHUB_WEB_BASE_PATH= # 可选前端动作统计,默认关闭。配置方法见 docs/frontend-analytics-integration.md。 # 只放公开参数,不放平台密钥。JSON 保持一行;Compose .env 中可用单引号包裹。 +# 修改后需重新创建 Web(up -d --force-recreate web),restart 不会重新读取本文件。 SKILLHUB_WEB_ANALYTICS_CONFIG= # 仅 Web 代理使用的 HTTP(S) origin,无路径、凭证、查询或末尾斜杠。 SKILLHUB_WEB_ANALYTICS_UPSTREAM= diff --git a/charts/skillhub/README.md b/charts/skillhub/README.md index 3cb6a041e..d73dfa8dd 100644 --- a/charts/skillhub/README.md +++ b/charts/skillhub/README.md @@ -109,7 +109,7 @@ Sentinel、实际 MinIO S3 连接,以及由 Kubernetes API 接受的 TLS Ingre 默认关闭。`web.analytics.config` 接受 JSON 字符串,`web.analytics.upstream` 接受仅部署可见的 HTTP(S) origin。支持 Umami,不需要编辑 JS 或 Nginx;先在平台创建网站并取得 UUID。 -配置变更触发 Web 滚动更新,旧标签页不会自动更新。字段、授权接口、初始化、隐私及验收见 +值放在 Web 专用 ConfigMap,只改统计配置时仅 Web 滚动更新、后端 Pod 不替换;旧标签页不会自动更新。字段、授权接口、初始化、隐私及验收见 [前端动作统计接入指南](../../docs/frontend-analytics-integration.md)。 ### 高可用模式 diff --git a/charts/skillhub/templates/configmap.yaml b/charts/skillhub/templates/configmap.yaml index 39c9a7f5c..409b70209 100644 --- a/charts/skillhub/templates/configmap.yaml +++ b/charts/skillhub/templates/configmap.yaml @@ -60,6 +60,4 @@ data: # Sub-path deployment (empty keeps a fixed-base image's baked base; set e.g. /portal/) web-base-path: {{ .Values.web.basePath | default "" | quote }} web-api-base-url: {{ .Values.web.apiBaseUrl | default "" | quote }} - web-analytics-config: {{ dig "analytics" "config" "" .Values.web | quote }} - web-analytics-upstream: {{ dig "analytics" "upstream" "" .Values.web | quote }} builtin-skills-enabled: {{ .Values.builtinSkills.enabled | quote }} diff --git a/charts/skillhub/templates/web-analytics-configmap.yaml b/charts/skillhub/templates/web-analytics-configmap.yaml new file mode 100644 index 000000000..8dc5f54a3 --- /dev/null +++ b/charts/skillhub/templates/web-analytics-configmap.yaml @@ -0,0 +1,13 @@ +{{- /* +Web-only analytics settings. Kept out of the shared ConfigMap so that changing +analytics only rolls Web; the server checksum hashes the shared ConfigMap. +*/}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "skillhub.fullname" . }}-web-analytics + labels: + {{- include "skillhub.web.labels" . | nindent 4 }} +data: + config: {{ dig "analytics" "config" "" .Values.web | quote }} + upstream: {{ dig "analytics" "upstream" "" .Values.web | quote }} diff --git a/charts/skillhub/templates/web-deployment.yaml b/charts/skillhub/templates/web-deployment.yaml index c87bce931..6a2b2e3a7 100644 --- a/charts/skillhub/templates/web-deployment.yaml +++ b/charts/skillhub/templates/web-deployment.yaml @@ -61,13 +61,13 @@ spec: - name: SKILLHUB_WEB_ANALYTICS_CONFIG valueFrom: configMapKeyRef: - name: {{ include "skillhub.fullname" . }}-config - key: web-analytics-config + name: {{ include "skillhub.fullname" . }}-web-analytics + key: config - name: SKILLHUB_WEB_ANALYTICS_UPSTREAM valueFrom: configMapKeyRef: - name: {{ include "skillhub.fullname" . }}-config - key: web-analytics-upstream + name: {{ include "skillhub.fullname" . }}-web-analytics + key: upstream {{- with .Values.web.extraEnv }} {{- toYaml . | nindent 12 }} {{- end }} diff --git a/charts/skillhub/tests/configuration-contracts.sh b/charts/skillhub/tests/configuration-contracts.sh index d28009f21..0685cef60 100755 --- a/charts/skillhub/tests/configuration-contracts.sh +++ b/charts/skillhub/tests/configuration-contracts.sh @@ -475,7 +475,12 @@ assert_rejected subpath-reserved-analytics --set-string web.basePath=/_skillhub- assert_rejected subpath-reserved-analytics-nested --set-string web.basePath=/_skillhub-analytics/nested/ render analytics-default "$CHART_DIR" >"$TMP_DIR/analytics-default.yaml" -grep -Fq 'web-analytics-config: ""' "$TMP_DIR/analytics-default.yaml" || fail "analytics must default off" +grep -Fq 'name: analytics-default-skillhub-web-analytics' "$TMP_DIR/analytics-default.yaml" \ + || fail "analytics must render its Web-only ConfigMap" +grep -Fq ' config: ""' "$TMP_DIR/analytics-default.yaml" || fail "analytics must default off" +if grep -Eq '^ web-analytics-(config|upstream):' "$TMP_DIR/analytics-default.yaml"; then + fail "analytics keys must stay out of the shared ConfigMap hashed by the server" +fi # --set treats {...} as a list, so pass the JSON string through a values file, # the same way the operator guide documents it. cat >"$TMP_DIR/analytics-values.yaml" <<'VALUES' @@ -484,11 +489,48 @@ web: config: '{"enabled":true,"adapter":"umami","consent":"granted","adapterConfig":{"websiteId":"4398de8e-40df-4483-9eea-ed213643525f"}}' upstream: https://stats.example.com VALUES -render analytics-enabled "$CHART_DIR" -f "$TMP_DIR/analytics-values.yaml" >"$TMP_DIR/analytics-enabled.yaml" +render analytics-default "$CHART_DIR" -f "$TMP_DIR/analytics-values.yaml" >"$TMP_DIR/analytics-enabled.yaml" for key in SKILLHUB_WEB_ANALYTICS_CONFIG SKILLHUB_WEB_ANALYTICS_UPSTREAM; do - grep -Fq "name: $key" "$TMP_DIR/analytics-enabled.yaml" || fail "missing analytics environment" + grep -A4 -F "name: $key" "$TMP_DIR/analytics-enabled.yaml" \ + | grep -Fq 'name: analytics-default-skillhub-web-analytics' \ + || fail "$key must come from the Web-only analytics ConfigMap" done -grep -Fq 'web-analytics-upstream: "https://stats.example.com"' "$TMP_DIR/analytics-enabled.yaml" || fail "missing analytics origin" +grep -Fq ' upstream: "https://stats.example.com"' "$TMP_DIR/analytics-enabled.yaml" || fail "missing analytics origin" + +# Pod template annotations: " " per line. +pod_checksums() { + awk ' + $1 == "kind:" { kind=$2; name="" } + kind == "Deployment" && name == "" && $1 == "name:" { name=$2 } + kind == "Deployment" && $1 ~ /^checksum\// { print name, $1, $2 } + ' "$1" | sort +} +checksum_of() { + pod_checksums "$1" | awk -v d="$2" -v k="$3" '$1 == d && $2 == k { print $3 }' +} +assert_checksum() { + local before=$1 after=$2 deployment=$3 annotation=$4 expect=$5 + local a b + a=$(checksum_of "$before" "$deployment" "$annotation") + b=$(checksum_of "$after" "$deployment" "$annotation") + [[ -n $a && -n $b ]] || fail "$deployment must render $annotation" + if [[ $expect == same && $a != "$b" ]]; then + fail "$deployment $annotation must not change" + elif [[ $expect == changed && $a == "$b" ]]; then + fail "$deployment $annotation must change" + fi +} +web=analytics-default-skillhub-web +server=analytics-default-skillhub-server +# Changing only analytics must roll Web and leave every other workload alone. +assert_checksum "$TMP_DIR/analytics-default.yaml" "$TMP_DIR/analytics-enabled.yaml" "$web" checksum/config: changed +diff <(pod_checksums "$TMP_DIR/analytics-default.yaml" | grep -v "^$web ") \ + <(pod_checksums "$TMP_DIR/analytics-enabled.yaml" | grep -v "^$web ") >/dev/null \ + || fail "analytics-only changes must not replace server or other pods" +# Backend configuration still rolls the server. +render analytics-default "$CHART_DIR" -f "$TMP_DIR/analytics-values.yaml" \ + --set-string bootstrapAdmin.displayName=Changed >"$TMP_DIR/analytics-server-change.yaml" +assert_checksum "$TMP_DIR/analytics-enabled.yaml" "$TMP_DIR/analytics-server-change.yaml" "$server" checksum/config: changed # publicBaseUrl is concatenated with paths (/cli/auth, /.well-known/clawhub.json), # so a query or fragment corrupts the generated URLs. Reject it independently of diff --git a/docs/frontend-analytics-integration.md b/docs/frontend-analytics-integration.md index 7e4053cd6..c2ca30ebf 100644 --- a/docs/frontend-analytics-integration.md +++ b/docs/frontend-analytics-integration.md @@ -112,7 +112,8 @@ kubectl -n skillhub logs deployment/skillhub-web --tail=100 ``` deployment 名称使用 Chart 默认 release 名 `skillhub`;自定义名称时替换命令中的名称。 -值进入 ConfigMap 和 Web 环境变量,checksum 改变会触发 Web 滚动更新。 +值写入 Web 专用的 `-web-analytics` ConfigMap,只改统计配置时仅 Web 滚动更新, +后端 Pod 不会被替换。 Chart 不自动部署 Umami。先在已有平台创建网站,并确认 Pod 出站网络、DNS 和证书可用。 集群内 origin 使用完整服务名,例如 `http://umami.stats.svc.cluster.local:3000`, Nginx 的运行时 DNS 不自动补 Pod 的搜索后缀。 @@ -177,8 +178,9 @@ await window.skillhubAnalytics.start() start() 是当前页的显式授权,支持初始 consent=denied。重复 start 不重复安装。 stop() 幂等;等待中的模块/adapter 不能迟到后重启采集。stop 返回后不新派发请求, -但已经开始的 fetch 无法撤回。规则变更使用 stop 再初始化;环境变量变化需重建或重启 Web。 -旧标签页不会自动收到新环境参数。修改 env 文件本身不会改变正在运行的容器。 +但已经开始的 fetch 无法撤回。规则变更使用 stop 再初始化。 +修改 `.env.release` 后必须重新创建 Web(`up -d --force-recreate web`);`docker compose restart` +只重启原容器,不会重新注入环境变量。旧标签页不会自动收到新配置,需要用户刷新或关闭。 ## 6. 根路径、子路径与验收 @@ -228,6 +230,7 @@ curl -i http://localhost/_skillhub-analytics/api/websites | SDK 失败后无事件 | 加载失败/超时后不重试;恢复平台后新开页面,或 stop 再 start。 | | 模块/资源返回 HTML | 检查部署版本、base path 和入口路由,不能改成任意远程脚本。 | | CSP 拦截 | 按原策略授权,不删除 CSP 或放宽通配符。 | +| send 返回 400 Website not found | websiteId 是合法 UUID,但 Umami 里没有这个网站。事件不会入库;配置校验只检查格式,通过不代表采集成功,需按第 6 节到平台核对入库。 | | 有点击但没有统计 | 规则未配置、宿主不支持、超过 Umami 名称 50 字符,或 best-effort 丢失。 | | 上传 413 | 检查整条外层代理链,正常 Web 仍是 100M,统计未降低它。 | @@ -262,8 +265,9 @@ Umami 仍可能由传输元数据推导浏览器、系统、设备、地域和 超时或 stop 会丢弃。就绪后的请求彼此独立。不要同时加载第二个 Umami tracker。 stop 的派发保护依赖固定版本在 fetch 前同步序列化的顺序,升级必须重新验证。 -自定义 adapter 属于开发流程,不是部署流程:在 `web/src/features/analytics/adapters/` -增加参数校验与 send/stop factory,注册到静态 registry,再增加部署端校验和精确代理端点。 +自定义 adapter 属于开发流程,不是部署流程。除前端 adapter 外,还要改部署侧: +在 `web/src/features/analytics/adapters/` 增加参数校验与 send/stop factory 并注册到静态 registry; +在 Web 启动脚本增加该平台的配置校验和精确代理端点;补充前端与 `web-analytics-test.py` 测试。 factory 检查取消信号后才创建 SDK。通用 controller 不认识厂商字段,业务组件只提供被动标签。 复用唯一的 `skillhub-intents.mjs`,不复制 helper,也不开放远程 JS URL。 新增标签时运行 `node web/scripts/analytics-manifest.mjs --write` 并审核语义和原生交互。 diff --git a/docs/frontend-analytics.md b/docs/frontend-analytics.md index d7cba572f..40a3f8581 100644 --- a/docs/frontend-analytics.md +++ b/docs/frontend-analytics.md @@ -113,7 +113,8 @@ The loader runs separately after the first React DOM commit, never blocking moun 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. +start. Deployment changes require recreating Web (`docker compose restart` does not +re-read the env file); old tabs do not update. ## Configuration and proxy diff --git a/openspec/changes/add-configurable-frontend-analytics/design.md b/openspec/changes/add-configurable-frontend-analytics/design.md index df3e60e8e..f5796bca2 100644 --- a/openspec/changes/add-configurable-frontend-analytics/design.md +++ b/openspec/changes/add-configurable-frontend-analytics/design.md @@ -51,7 +51,7 @@ Nginx 通过独立 include 片段选择已注册适配器的精确端点:Umami 现有平台接入:填写 JSON 和内部 upstream,更新 Web。可选自托管组合:在 release Compose 上叠加统计服务与其持久化数据库,使用显式凭证,不新增 Nginx;创建网站/取得 UUID 等一次性平台步骤必须诚实说明。入口和 Helm 均默认关闭。 -部署配置变更经重建/重启 Web 使新页面生效;旧标签页不自动接收新环境参数。动态停止通过当前页面接口生效;紧急全局停止可关闭独立采集路由。文档不得宣称仅修改 env 即刻撤销已在发送的请求。模块使用正常发布构建产物与缓存策略,不把可变配置放入 immutable JS。 +部署配置变更需重新创建 Web(Compose restart 不重新注入环境变量;Helm 统计值放在 Web 专用 ConfigMap,只滚动 Web)使新页面生效;旧标签页不自动接收新环境参数。动态停止通过当前页面接口生效;紧急全局停止可关闭独立采集路由。文档不得宣称仅修改 env 即刻撤销已在发送的请求。模块使用正常发布构建产物与缓存策略,不把可变配置放入 immutable JS。 ### 5. Serialization and loading implementation diff --git a/openspec/changes/add-configurable-frontend-analytics/tasks.md b/openspec/changes/add-configurable-frontend-analytics/tasks.md index e5aaea562..f83520499 100644 --- a/openspec/changes/add-configurable-frontend-analytics/tasks.md +++ b/openspec/changes/add-configurable-frontend-analytics/tasks.md @@ -20,7 +20,7 @@ ## 4. PR-2 (follow-up): deployment - [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.3 Wire Docker/Compose/Helm defaults and reserved sub-path validation. Helm keeps analytics in a Web-only ConfigMap. Template checks executed with helm 3.19.0: `helm lint --strict` and `configuration-contracts.sh`, which renders before/after values and asserts that analytics-only changes roll only Web while backend changes still roll the server. A real-cluster Helm install/upgrade with analytics enabled has not been run. - [x] 4.4 Provide optional persistent self-hosted Compose combination without another Nginx. ## 5. PR-2 (follow-up): documentation @@ -30,7 +30,7 @@ ## 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. (Worker matrix and local checks complete; independent review and Helm execution pending.) +- [ ] 6.2 Reviewed requirement/scenario/test matrix and targeted source/deployment checks. (Worker matrix, local checks and Helm template checks complete; maintainer verified the real image, backend, Umami, Postgres and browser path. Real-cluster Helm verification and independent review 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.