Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
3 changes: 3 additions & 0 deletions .github/workflows/hub-metadata-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 26 additions & 0 deletions docs/scripts/aggregate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
35 changes: 35 additions & 0 deletions docs/scripts/tests/analytics-privacy.test.mjs
Original file line number Diff line number Diff line change
@@ -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(/<script>([\s\S]*?)<\/script>/g)].map(m => m[1]);
const bootstrap = scripts.find(s => s.includes("G-EZTLJDFS6J"));
assert.ok(bootstrap);
const calls = [];
const context = { dataLayer: calls, window: null, localStorage: { getItem: () => null }, location: { pathname:'/security-model.html', href:'https://docs.agent-assembly.com/core/?prompt=PRIVATE_CANARY', hostname:'docs.agent-assembly.com' }, document:{ readyState:'complete', title:'PRIVATE_CANARY', addEventListener(){}, querySelector(){return null;} }, console };
context.window = context;
vm.createContext(context);
// Sonar S1523: these are repository-owned constant scripts, never user or network input.
vm.runInContext(bootstrap, context);
context.window.gtag = (...args) => calls.push(args);
vm.runInContext(funnel, context);
const setIndex = calls.findIndex(x => x[0] === 'set');
assert.ok(setIndex >= 0);
const setPayload = calls[setIndex][1];
assert.equal(setPayload.page_location, 'https://docs.agent-assembly.com/');
assert.equal(setPayload.page_referrer, '');
assert.equal(setPayload.page_title, 'Agent Assembly documentation');
const config = calls.find(x => x[0] === 'config');
assert.ok(setIndex < calls.indexOf(config));
assert.equal(JSON.stringify(config[2]), JSON.stringify({ anonymize_ip:true, page_location:'https://docs.agent-assembly.com/', page_referrer:'', page_title:'Agent Assembly documentation', send_page_view:false }));
for (const call of calls) assert.equal(JSON.stringify(call).includes('PRIVATE_CANARY'), false);
assert.ok(calls.some(x => x[0] === 'js'));
const pageViews = calls.filter(x => x[0] === 'event' && x[1] === 'page_view');
assert.equal(pageViews.length, 1);
assert.equal(pageViews[0][2].page_location, 'https://docs.agent-assembly.com/');
assert.equal(pageViews[0][2].page_referrer, '');
assert.ok(calls.some(x => x[0] === 'event' && x[1] === 'docs_security_model_view')); console.log('analytics privacy VM checks passed');
23 changes: 18 additions & 5 deletions docs/theme/aa-funnel-events.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,21 @@
const GITHUB_EXAMPLES = 'github.com/ai-agent-assembly/examples';
const GITHUB_ISSUE_ANY = /^https?:\/\/github\.com\/ai-agent-assembly\/[^/]+\/issues/;

// Closed vocabulary: never send raw URL, title, hostname, query, fragment,
// or referrer values, which may contain pasted prompts or repository names.
function pageId() {
const path = location.pathname;
if (/quickstart-saas|installation/.test(path)) { return 'installation'; }
if (/security-model/.test(path)) { return 'security_model'; }
if (/python-sdk/.test(path)) { return 'python_sdk'; }
if (/node-sdk/.test(path)) { return 'node_sdk'; }
if (/go-sdk/.test(path)) { return 'go_sdk'; }
return 'docs';
}

function baseParams() {
return {
hostname: location.hostname,
page_path: location.pathname,
page_title: document.title,
page_id: pageId(),
surface: SURFACE
};
}
Expand Down Expand Up @@ -104,10 +114,13 @@
const href = a.getAttribute('href') || '';
if (!href || href.charAt(0) === '#') { return; }

let linkDomain = 'docs';
if (/github\.com/.test(a.href)) { linkDomain = 'github'; }
else if (/agent-assembly\.com/.test(a.href)) { linkDomain = 'agent_assembly'; }
else if (/horonomy\.dev/.test(a.href)) { linkDomain = 'horonomy'; }
const params = {
cta_location: ctaLocationFor(a),
link_url: a.href,
link_domain: a.hostname || '',
link_domain: linkDomain,
target_product: targetProductFor(a.href)
};

Expand Down
27 changes: 23 additions & 4 deletions docs/theme/head.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
window.gtag = gtag;
// Consent Mode v2 — deny analytics/ads storage until the visitor opts in.
gtag('consent', 'default', {
'analytics_storage': 'denied',
Expand All @@ -19,7 +20,25 @@
}
} catch (e) {}
gtag('js', new Date());
gtag('config', 'G-EZTLJDFS6J', { 'anonymize_ip': true });
gtag('set', {
'page_location': 'https://docs.agent-assembly.com/',
'page_referrer': '',
'page_title': 'Agent Assembly documentation'
});
// Suppress GA's automatic page_view and clear referrer attribution. Events
// use only the closed vocabulary in aa-funnel-events.js.
gtag('config', 'G-EZTLJDFS6J', {
'anonymize_ip': true,
'page_location': 'https://docs.agent-assembly.com/',
'page_referrer': '',
'page_title': 'Agent Assembly documentation',
'send_page_view': false
});
gtag('event', 'page_view', {
'page_location': 'https://docs.agent-assembly.com/',
'page_referrer': '',
'page_title': 'Agent Assembly documentation'
});
</script>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-EZTLJDFS6J"></script>

Expand Down Expand Up @@ -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);
}
Expand Down
Loading