Skip to content
Merged
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
20 changes: 13 additions & 7 deletions docs/_overrides/main.html
Original file line number Diff line number Diff line change
Expand Up @@ -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 /<base>/<version>/, with
// versions.json one level up at /<base>/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 /<base>/<version>/, with versions.json
// one level up at /<base>/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() : []; })
Expand Down
27 changes: 27 additions & 0 deletions docs/_overrides/partials/integrations/analytics.html
Original file line number Diff line number Diff line change
@@ -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" %}
<script>"undefined"!=typeof __md_analytics&&__md_analytics()</script>
{% endif %}
102 changes: 102 additions & 0 deletions docs/_overrides/partials/integrations/analytics/google.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
{#-
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 %}
<script id="__analytics">
function __md_analytics() {
// Use the standard GA install pattern: assign the pusher to `window.gtag`
// (not a local) so the tag is detectable by Google's "Test your website"
// (typeof window.gtag === "function"), matching the other four doc sites.
// mkdocs-material's generated code uses a *local* pusher and never sets
// window.gtag, which is why python's tag was previously undetectable.
window.dataLayer = window.dataLayer || [];
window.gtag = window.gtag || function () { dataLayer.push(arguments); };

// Advanced Consent Mode v2 β€” default everything to denied BEFORE config so
// gtag sends cookieless pings until the visitor grants analytics consent.
gtag("consent", "default", {
analytics_storage: "denied",
ad_storage: "denied",
ad_user_data: "denied",
ad_personalization: "denied"
});

// Restore a granted state from Material's stored consent cookie, if present.
try {
var __aa_consent = __md_get("__consent");
if (__aa_consent && __aa_consent.analytics) {
gtag("consent", "update", { analytics_storage: "granted" });
}
} catch (e) { /* no stored consent yet β€” stay denied (cookieless) */ }

gtag("js", new Date());
gtag("config", "{{ property }}");

document.addEventListener("DOMContentLoaded", function () {
if (document.forms.search) {
document.forms.search.query.addEventListener("blur", function () {
if (this.value) gtag("event", "search", { search_term: this.value });
});
}
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();
var page = document.location.pathname;
var data = this.getAttribute("data-md-value");
gtag("event", "feedback", { page: page, data: data });
feedback.firstElementChild.disabled = true;
var note = feedback.querySelector(
".md-feedback__note [data-md-value='" + data + "']"
);
if (note) note.hidden = false;
});
}
feedback.hidden = false;
});
location$.subscribe(function (url) {
gtag("config", "{{ property }}", { page_path: url.pathname });
});
});

var script = document.createElement("script");
script.async = true;
script.src = "https://www.googletagmanager.com/gtag/js?id={{ property }}";
document.getElementById("__analytics").insertAdjacentElement("afterEnd", script);
}
</script>
14 changes: 14 additions & 0 deletions docs/_overrides/partials/javascripts/outdated.html
Original file line number Diff line number Diff line change
@@ -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.
-#}
<script>var el=document.querySelector("[data-md-component=outdated]"),base=new URL("{{ base_url }}",location),outdated=__md_get("__outdated",sessionStorage,base);!0===outdated&&el&&(el.hidden=!1)</script>
7 changes: 6 additions & 1 deletion scripts/ci/deploy-release-version-documentation.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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}."
42 changes: 42 additions & 0 deletions scripts/ci/templates/mike-redirect-with-analytics.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Redirecting</title>
<!--
Custom mike root-redirect template (AAASM-3558).

mike's default redirect.html carries no analytics, so Google's "Test your
website" cannot detect GA on the bare-root URL (/python-sdk/). This template
is byte-for-byte mike's default plus a self-contained gtag.js snippet using
advanced Consent Mode v2 (analytics_storage denied by default β†’ cookieless,
modelled pings until consent), matching the per-page posture and the other
four doc sites. Wired via `mike set-default --template` in
scripts/ci/deploy-release-version-documentation.sh.
-->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-L8JTJDG8G7"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag("consent", "default", {
analytics_storage: "denied",
ad_storage: "denied",
ad_user_data: "denied",
ad_personalization: "denied"
});
gtag("js", new Date());
gtag("config", "G-L8JTJDG8G7");
</script>
<noscript>
<meta http-equiv="refresh" content="1; url={{href}}" />
</noscript>
<script>
window.location.replace(
"{{href}}" + window.location.search + window.location.hash
);
</script>
</head>
<body>
Redirecting to <a href="{{href}}">{{href}}</a>...
</body>
</html>