diff --git a/.github/workflows/hub-metadata-check.yml b/.github/workflows/hub-metadata-check.yml index 6edb9b0..9c3d315 100644 --- a/.github/workflows/hub-metadata-check.yml +++ b/.github/workflows/hub-metadata-check.yml @@ -118,6 +118,9 @@ jobs: - name: Validate every page's capability_ids against the capability manifest run: python3 docs/scripts/validate_capability_ids.py + - name: Run hosted analytics privacy VM checks + run: node docs/scripts/tests/analytics-privacy.test.mjs + - name: Run the claim-vocabulary self-test run: python3 docs/scripts/check_claim_vocabulary.py --selftest diff --git a/docs/scripts/aggregate.sh b/docs/scripts/aggregate.sh index 623219f..e8d21de 100755 --- a/docs/scripts/aggregate.sh +++ b/docs/scripts/aggregate.sh @@ -143,6 +143,32 @@ PY python3 docs/ci/build_versions.py latest latest "$out/versions.json" ) cp "$src/docs/site-root-index.html" "$out/index.html" + + # Core publishes historical version artifacts rebuilt from tags. Apply the + # Core-owned privacy hardener after every version and the root redirect are + # assembled, so older public snapshots receive the same fixed analytics + # identity without changing their product content. + local hardener="$src/docs/scripts/harden_published_analytics.mjs" + local temporary_hardener="" + if [[ ! -f "$hardener" ]]; then + # Pin the reviewed Core source until its hardener reaches the default branch. + local hardener_ref="5f37f5f1e97c089b1199ee170bba807dbca4583a" + git -C "$src" fetch --quiet origin "$hardener_ref" + temporary_hardener="$(mktemp "${TMPDIR:-/tmp}/aa-core-hardener.XXXXXX.mjs")" + hardener="$temporary_hardener" + git -C "$src" show "$hardener_ref:docs/scripts/harden_published_analytics.mjs" > "$hardener" + fi + local hardener_output hardener_status + set +e + hardener_output="$(node "$hardener" "$out" --public-prefix /core/ 2>&1)" + hardener_status=$? + set -e + [[ -z "$temporary_hardener" ]] || rm -f "$temporary_hardener" + printf '%s\n' "$hardener_output" + [[ "$hardener_status" -eq 0 ]] || fail "Core analytics hardener failed" + local hardened_count + hardened_count="$(printf '%s\n' "$hardener_output" | awk '/^Hardened analytics identity in [0-9]+ rendered HTML file\(s\)\.$/ {print $5}')" + [[ "${hardened_count:-0}" -gt 0 ]] || fail "Core analytics hardener changed no rendered pages" } build_python() { # mike-published version tree (gh-pages) -> public/python-sdk (AAASM-3752) diff --git a/docs/scripts/tests/analytics-privacy.test.mjs b/docs/scripts/tests/analytics-privacy.test.mjs new file mode 100644 index 0000000..e37db04 --- /dev/null +++ b/docs/scripts/tests/analytics-privacy.test.mjs @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import vm from 'node:vm'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); +const head = fs.readFileSync(path.join(root, 'theme/head.hbs'), 'utf8'); +const funnel = fs.readFileSync(path.join(root, 'theme/aa-funnel-events.js'), 'utf8'); +const scripts = [...head.matchAll(/ @@ -107,12 +126,12 @@ function sendFeedback(value) { // Consent Mode (AAASM-3554) governs whether this hit is stored; fire either way. if (typeof gtag === 'function') { - gtag('event', 'feedback', { 'value': value, 'page_path': location.pathname }); + gtag('event', 'feedback', { 'value': value, 'page_id': 'docs' }); } } function issueUrl() { - var title = 'Docs feedback: ' + location.pathname; - var body = 'Page: ' + location.href + '\n\nWhat could be improved?\n'; + var title = 'Docs feedback'; + var body = 'Page: documentation\n\nWhat could be improved?\n'; return 'https://github.com/ai-agent-assembly/' + REPO + '/issues/new?labels=docs,feedback' + '&title=' + encodeURIComponent(title) + '&body=' + encodeURIComponent(body); }