From b3afa49708104bad9d4c6ae5541e0b1886fcdb2f Mon Sep 17 00:00:00 2001 From: Bryant Date: Tue, 23 Jun 2026 09:55:41 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=93=88=20(docs):=20Load=20GA=20via=20?= =?UTF-8?q?advanced=20Consent=20Mode=20v2=20always?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Override Material's analytics partials so gtag.js loads on every page with analytics_storage denied by default (cookieless pings until consent, full hits after), restoring granted state from stored consent and assigning window.gtag so the tag is detectable. The feedback widget is revealed and its single consent-gated GA `feedback` event still flows through Material's own handler (no double-count). Refs AAASM-3558. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../partials/integrations/analytics.html | 27 ++++++ .../integrations/analytics/google.html | 96 +++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 docs/_overrides/partials/integrations/analytics.html create mode 100644 docs/_overrides/partials/integrations/analytics/google.html diff --git a/docs/_overrides/partials/integrations/analytics.html b/docs/_overrides/partials/integrations/analytics.html new file mode 100644 index 00000000..f9760b86 --- /dev/null +++ b/docs/_overrides/partials/integrations/analytics.html @@ -0,0 +1,27 @@ +{#- + Override of Material's partials/integrations/analytics.html (AAASM-3558). + + Material's generated partial only calls `__md_analytics()` once the analytics + consent cookie is granted (`__md_get("__consent").analytics`). With our opt-in + consent (cookies.analytics.checked: false, off by default) that call never + fires for ordinary visitors, so gtag.js is never loaded and GA never reports. + + We switch python to *advanced* Consent Mode v2 (matching the other four doc + sites): the overridden google.html partial declares `analytics_storage: + 'denied'` as the default before loading gtag, so calling `__md_analytics()` + unconditionally is GDPR-correct — GA sends cookieless, modelled pings until + the visitor grants analytics consent, then full hits. We therefore drop + Material's consent gate and always run the loader. + + The native `extra.consent` banner UI is kept; google.html reads the stored + consent on each load and updates Consent Mode to 'granted' when the analytics + cookie is enabled (the consent form reloads the page on submit, so reading + stored consent at load time is sufficient — no live form hook needed). +-#} +{% if config.extra.analytics %} + {% set provider = config.extra.analytics.provider %} +{% endif %} +{% if provider %} + {% include "partials/integrations/analytics/" ~ provider ~ ".html" %} + +{% endif %} diff --git a/docs/_overrides/partials/integrations/analytics/google.html b/docs/_overrides/partials/integrations/analytics/google.html new file mode 100644 index 00000000..7911082f --- /dev/null +++ b/docs/_overrides/partials/integrations/analytics/google.html @@ -0,0 +1,96 @@ +{#- + Override of Material's partials/integrations/analytics/google.html (AAASM-3558). + + Material's generated partial defines `__md_analytics()` (the gtag.js loader + + `config` + search/feedback wiring) but loads GA with *basic* Consent Mode — it + only runs once the analytics consent cookie is granted. With our opt-in consent + GA never loaded for ordinary visitors (window.gtag stayed undefined, zero + /g/collect hits). + + This override keeps Material's `__md_analytics()` body intact (so the feedback + widget is revealed + wired and its single GA `feedback` event still flows + through Material's own handler — no custom handler, no double-count) but turns + it into *advanced* Consent Mode v2, matching the other four doc sites: + + 1. Before `gtag('config', …)`, declare Consent Mode defaults as DENIED for + analytics_storage / ad_storage / ad_user_data / ad_personalization, so + gtag.js sends cookieless, modelled pings until the visitor consents. + 2. Read Material's stored consent (`__md_get('__consent')`, namespaced to the + deploy root) and, if the analytics cookie is enabled, immediately update + analytics_storage to 'granted' — upgrading from cookieless to full hits. + The consent banner reloads the page on submit, so reading stored consent + at load time covers accept/reject without a live form hook. + + `partials/integrations/analytics.html` (also overridden) now calls + `__md_analytics()` unconditionally, so this runs on every page load. The + Measurement ID + feedback markup still come from `extra.analytics` in + mkdocs.yml. Mirrors mkdocs-material 9.7.6; only the Consent Mode lines are added. +-#} +{% if config.extra.analytics %} + {% set property = config.extra.analytics.property | d("", true) %} +{% endif %} + From d8eb662ac66c99c4b0d6bb6bf2b1ae37957a86f5 Mon Sep 17 00:00:00 2001 From: Bryant Date: Tue, 23 Jun 2026 09:55:49 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=F0=9F=90=9B=20(docs):=20Fix=20outdated-ban?= =?UTF-8?q?ner=20URL=20construction=20crash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Override Material's outdated.html so the script passes `location` as the base to `new URL("{{ base_url }}", location)`. Material's generated code calls `new URL("..")` with no base, which throws `TypeError: Failed to construct 'URL'` on every page load. Refs AAASM-3558. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/_overrides/partials/javascripts/outdated.html | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 docs/_overrides/partials/javascripts/outdated.html diff --git a/docs/_overrides/partials/javascripts/outdated.html b/docs/_overrides/partials/javascripts/outdated.html new file mode 100644 index 00000000..a3062a0d --- /dev/null +++ b/docs/_overrides/partials/javascripts/outdated.html @@ -0,0 +1,14 @@ +{#- + Override of Material's partials/javascripts/outdated.html (AAASM-3558, carried + over from the closed PR #169 / AAASM-3557). + + Material's generated script calls `new URL("{{ base_url }}")` with no base + argument. On many pages `base_url` is a bare relative value such as `.` or + `..`, which is not an absolute URL, so `new URL("..")` throws + `TypeError: Failed to construct 'URL': Invalid URL` on every page load. + + The only change here is passing `location` as the base so the relative + `base_url` resolves against the current page. Behaviour is otherwise identical + to Material's generated script. +-#} + From 6c036165fc69aa1a33beaca4721d50ebfcfa8d7a Mon Sep 17 00:00:00 2001 From: Bryant Date: Tue, 23 Jun 2026 09:55:49 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=F0=9F=90=9B=20(docs):=20Resolve=20versions?= =?UTF-8?q?.json=20from=20deploy=20root=20at=20any=20depth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The version-warning banner derived versions.json by popping the page's last path segment, which 404s on deep pages. Resolve it from `base_url` (the deploy root) instead, so it always points one level above the version directory regardless of page depth. Refs AAASM-3558. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/_overrides/main.html | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/_overrides/main.html b/docs/_overrides/main.html index 5490c824..5f7a8b75 100644 --- a/docs/_overrides/main.html +++ b/docs/_overrides/main.html @@ -33,13 +33,19 @@ : document.querySelector("[data-aaasm-outdated]"); if (!host) return; - // Derive the current version directory and the versions.json URL from the - // page location: mike serves each version under ///, with - // versions.json one level up at //versions.json. - var path = window.location.pathname.replace(/\/+$/, ""); - var segments = path.split("/"); - var current = segments.pop() || ""; - var versionsUrl = segments.join("/") + "/versions.json"; + // mike serves each version under ///, with versions.json + // one level up at //versions.json. mkdocs renders `base_url` so it + // points at the deploy root (the current version directory) from *any* + // page depth, so the deploy-root versions.json is reliably one level + // above it. Resolving the bare relative `base_url` against `location` + // yields an absolute URL; popping only the page's last path segment (the + // previous approach) 404s on deep pages. + var versionRoot = new URL("{{ base_url }}/", window.location); + var versionsUrl = new URL("../versions.json", versionRoot).toString(); + + // The current version id is the last path segment of the deploy root. + var current = + versionRoot.pathname.replace(/\/+$/, "").split("/").pop() || ""; fetch(versionsUrl, { cache: "no-store" }) .then(function (r) { return r.ok ? r.json() : []; }) From 565de0af82cffa5a9e9d889bab479e4d1e23c1ef Mon Sep 17 00:00:00 2001 From: Bryant Date: Tue, 23 Jun 2026 09:55:57 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=93=88=20(docs):=20Carry=20GA=20snipp?= =?UTF-8?q?et=20on=20the=20mike=20root-redirect=20stub?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a custom mike redirect template with a self-contained gtag.js snippet (advanced Consent Mode v2, cookieless-until-consent) and pass it to `mike set-default --template` so Google's tag test detects GA on the bare-root URL (/python-sdk/) too. Refs AAASM-3558. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../deploy-release-version-documentation.sh | 7 +++- .../mike-redirect-with-analytics.html | 42 +++++++++++++++++++ 2 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 scripts/ci/templates/mike-redirect-with-analytics.html diff --git a/scripts/ci/deploy-release-version-documentation.sh b/scripts/ci/deploy-release-version-documentation.sh index 2eb5428e..b86cf141 100755 --- a/scripts/ci/deploy-release-version-documentation.sh +++ b/scripts/ci/deploy-release-version-documentation.sh @@ -153,8 +153,13 @@ fi # Set the default (root-redirect target) to the most authoritative channel that # exists, as decided by the resolver: stable, else pre-release, else latest. +# Use a custom redirect template that carries the GA snippet (advanced Consent +# Mode v2, cookieless-until-consent) so Google's "Test your website" detects the +# tag on the bare-root URL too (AAASM-3558). echo "🎯 Setting default channel (root redirect) to '${DEFAULT_CHANNEL}'" -mike set-default --push "${DEFAULT_CHANNEL}" +mike set-default --push \ + --template "${PROJECT_ROOT}/scripts/ci/templates/mike-redirect-with-analytics.html" \ + "${DEFAULT_CHANNEL}" echo "🍻 Release documentation deployed for ${RELEASE_TAG}: stable=${STABLE_VERSION:-(none)}," \ "pre-release=${PRERELEASE_VERSION:-(hidden)}, default=${DEFAULT_CHANNEL}." diff --git a/scripts/ci/templates/mike-redirect-with-analytics.html b/scripts/ci/templates/mike-redirect-with-analytics.html new file mode 100644 index 00000000..7d6d4a8d --- /dev/null +++ b/scripts/ci/templates/mike-redirect-with-analytics.html @@ -0,0 +1,42 @@ + + + + + Redirecting + + + + + + + + Redirecting to {{href}}... + + From a422f649a4a6a81cb3bc8079d407dc7b80e0f5c9 Mon Sep 17 00:00:00 2001 From: Bryant Date: Tue, 23 Jun 2026 10:21:08 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20(docs):=20Gate=20feedb?= =?UTF-8?q?ack=20widget=20on=20analytics=20consent=20(show=20on=20accept,?= =?UTF-8?q?=20hide=20on=20reject)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/_overrides/partials/integrations/analytics/google.html | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/_overrides/partials/integrations/analytics/google.html b/docs/_overrides/partials/integrations/analytics/google.html index 7911082f..239206b2 100644 --- a/docs/_overrides/partials/integrations/analytics/google.html +++ b/docs/_overrides/partials/integrations/analytics/google.html @@ -68,6 +68,12 @@ document$.subscribe(function () { var feedback = document.forms.feedback; if (feedback === undefined) return; + // Gate the feedback widget on analytics consent: reveal + wire its + // handlers only after the visitor Accepts analytics cookies. While + // undecided or on Reject the form stays hidden. GA itself keeps loading + // and sending cookieless pings regardless (handled above, ungated). + var __aa_fb_consent = __md_get("__consent"); + if (!(__aa_fb_consent && __aa_fb_consent.analytics)) return; for (var button of feedback.querySelectorAll("[type=submit]")) { button.addEventListener("click", function (e) { e.preventDefault();