Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.release.example
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ SKILLHUB_TRUST_FORWARDED_PROTO=false
# Leave empty so a fixed-base image keeps its baked base; set to a sub-path to override.
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=

POSTGRES_BIND_ADDRESS=127.0.0.1
POSTGRES_PORT=5432
POSTGRES_DB=skillhub
Expand Down
7 changes: 7 additions & 0 deletions charts/skillhub/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 专用 ConfigMap,只改统计配置时仅 Web 滚动更新、后端 Pod 不替换;旧标签页不会自动更新。字段、授权接口、初始化、隐私及验收见
[前端动作统计接入指南](../../docs/frontend-analytics-integration.md)。

### 高可用模式

```bash
Expand Down
2 changes: 1 addition & 1 deletion charts/skillhub/templates/validate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
{{- fail (printf "web.basePath must be '/' or a normalized sub-path that starts and ends with '/' and has no '.'/'..' or empty segments (matches the runtime and release-config checks): %s" $webBasePath) -}}
{{- end -}}
{{- $firstSegment := index (splitList "/" $webBasePath) 1 -}}
{{- if has $firstSegment (list "api" "oauth2" "login" "assets" "registry" "nginx-health" ".well-known" "runtime-config.js") -}}
{{- if has $firstSegment (list "api" "oauth2" "login" "assets" "registry" "nginx-health" ".well-known" "runtime-config.js" "_skillhub-analytics") -}}
{{- fail (printf "web.basePath must not start with a segment reserved by the SkillHub server (%s); it would shadow the server's own Nginx location: %s" $firstSegment $webBasePath) -}}
{{- end -}}
{{- $suffix := trimSuffix "/" $webBasePath -}}
Expand Down
13 changes: 13 additions & 0 deletions charts/skillhub/templates/web-analytics-configmap.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
10 changes: 10 additions & 0 deletions charts/skillhub/templates/web-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ spec:
configMapKeyRef:
name: {{ include "skillhub.fullname" . }}-config
key: web-api-base-url
- name: SKILLHUB_WEB_ANALYTICS_CONFIG
valueFrom:
configMapKeyRef:
name: {{ include "skillhub.fullname" . }}-web-analytics
key: config
- name: SKILLHUB_WEB_ANALYTICS_UPSTREAM
valueFrom:
configMapKeyRef:
name: {{ include "skillhub.fullname" . }}-web-analytics
key: upstream
{{- with .Values.web.extraEnv }}
{{- toYaml . | nindent 12 }}
{{- end }}
Expand Down
69 changes: 69 additions & 0 deletions charts/skillhub/tests/configuration-contracts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,75 @@ assert_rejected subpath-reserved-api --set-string web.basePath=/api/
assert_rejected subpath-reserved-assets --set-string web.basePath=/assets/
assert_rejected subpath-reserved-well-known --set-string web.basePath=/.well-known/
assert_rejected subpath-reserved-nested --set-string web.basePath=/api/nested/
assert_rejected subpath-reserved-analytics --set-string web.basePath=/_skillhub-analytics/
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 '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'
web:
analytics:
config: '{"enabled":true,"adapter":"umami","consent":"granted","adapterConfig":{"websiteId":"4398de8e-40df-4483-9eea-ed213643525f"}}'
upstream: https://stats.example.com
VALUES
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 -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 ' upstream: "https://stats.example.com"' "$TMP_DIR/analytics-enabled.yaml" || fail "missing analytics origin"

# Pod template annotations: "<deployment-name> <annotation> <value>" 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"
# A second adapter uses the same opaque JSON/ConfigMap contract, via a values file.
render analytics-default "$CHART_DIR" -f "$CHART_DIR/tests/datafinder-values.yaml" >"$TMP_DIR/analytics-datafinder.yaml"
grep -Fq 'appId\":123456' "$TMP_DIR/analytics-datafinder.yaml" || fail "DataFinder public config missing"
grep -Fq ' upstream: "https://collector.example.com"' "$TMP_DIR/analytics-datafinder.yaml" || fail "DataFinder origin missing"
assert_checksum "$TMP_DIR/analytics-default.yaml" "$TMP_DIR/analytics-datafinder.yaml" "$web" checksum/config: changed
diff <(pod_checksums "$TMP_DIR/analytics-default.yaml" | grep -v "^$web ") \
<(pod_checksums "$TMP_DIR/analytics-datafinder.yaml" | grep -v "^$web ") >/dev/null \
|| fail "DataFinder must not roll non-Web 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
Expand Down
4 changes: 4 additions & 0 deletions charts/skillhub/tests/datafinder-values.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
web:
analytics:
config: '{"enabled":true,"adapter":"datafinder","consent":"granted","adapterConfig":{"appId":123456,"product":"skillhub"}}'
upstream: https://collector.example.com
8 changes: 8 additions & 0 deletions charts/skillhub/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,14 @@
"service": { "$ref": "#/definitions/service" },
"basePath": { "type": "string", "pattern": "^$|^/$|^(/[A-Za-z0-9_~-][A-Za-z0-9._~-]*)+/$" },
"apiBaseUrl": { "type": "string" },
"analytics": {
"type": "object",
"additionalProperties": false,
"properties": {
"config": { "type": "string" },
"upstream": { "type": "string" }
}
},
"resources": { "$ref": "#/definitions/resources" },
"extraEnv": { "type": "array", "items": { "type": "object" } },
"podAnnotations": { "$ref": "#/definitions/stringMap" },
Expand Down
5 changes: 5 additions & 0 deletions charts/skillhub/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,11 @@ web:
basePath: ""
apiBaseUrl: ""

# 默认关闭。config 是与 Compose 相同的 JSON 字符串;不放平台密钥。
analytics:
config: ""
upstream: ""

service:
enabled: true
type: ClusterIP
Expand Down
32 changes: 32 additions & 0 deletions compose.analytics-umami.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Optional persistent collector. Add to compose.release.yml explicitly.
# The platform admin stays loopback-only; there is no additional Nginx.
services:
analytics-postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: ${UMAMI_DB_PASSWORD:?provide a separate database password}
volumes:
- analytics_postgres_data:/var/lib/postgresql/data
healthcheck:
test: [CMD-SHELL, pg_isready -U umami -d umami]
interval: 5s
timeout: 3s
retries: 20
umami:
image: ghcr.io/umami-software/umami:3.4.1
restart: unless-stopped
environment:
# Use a URL-safe password (letters/digits recommended), or percent-encode
# its password component in UMAMI_DATABASE_URL separately.
DATABASE_URL: ${UMAMI_DATABASE_URL:-postgresql://umami:${UMAMI_DB_PASSWORD:?provide a separate database password}@analytics-postgres:5432/umami}
APP_SECRET: ${UMAMI_APP_SECRET:?provide a separate random app secret}
ports:
- "127.0.0.1:${UMAMI_ADMIN_PORT:-8300}:3000"
depends_on:
analytics-postgres:
condition: service_healthy
volumes:
analytics_postgres_data:
2 changes: 2 additions & 0 deletions compose.release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,8 @@ services:
SKILLHUB_PUBLIC_BASE_URL: ${SKILLHUB_PUBLIC_BASE_URL:-}
SKILLHUB_WEB_AUTH_DIRECT_ENABLED: ${SKILLHUB_WEB_AUTH_DIRECT_ENABLED:-false}
SKILLHUB_WEB_AUTH_DIRECT_PROVIDER: ${SKILLHUB_WEB_AUTH_DIRECT_PROVIDER:-}
SKILLHUB_WEB_ANALYTICS_CONFIG: ${SKILLHUB_WEB_ANALYTICS_CONFIG:-}
SKILLHUB_WEB_ANALYTICS_UPSTREAM: ${SKILLHUB_WEB_ANALYTICS_UPSTREAM:-}
depends_on:
server:
condition: service_healthy
Expand Down
2 changes: 2 additions & 0 deletions docs/09-deployment.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# skillhub 部署架构与运维

对象存储可选部署、凭证和迁移说明见[对象存储手册](object-storage.md)。
可选前端动作统计默认关闭。Docker/Compose 和 Helm 只填配置即可接入 Umami,
详见[前端动作统计接入指南](frontend-analytics-integration.md)。

### CLI 设备授权地址

Expand Down
6 changes: 6 additions & 0 deletions docs/examples/analytics/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# 可选统计接入

旧的部署方脚本注入示例已迁入产品模块,不再作为部署入口。
请使用[配置接入指南](../../frontend-analytics-integration.md)。
唯一源码位于 `web/src/features/analytics/`。PR-1 的历史 edge 配置仅作为测试 fixture 保留,
不应部署。无需复制 JS,也不需要额外 Nginx。
26 changes: 0 additions & 26 deletions docs/examples/analytics/observe/v1/observer.mjs

This file was deleted.

Loading
Loading