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);
}