-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Reimplement Plausible analytics #704
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| mkdocs-material[imaging]==9.7.5 | ||
| mkdocs-macros-plugin==1.5.0 | ||
| requests==2.33.1 | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,74 @@ | ||||||
| from __future__ import annotations | ||||||
|
|
||||||
| import json | ||||||
| import logging | ||||||
| import time | ||||||
| from pathlib import Path | ||||||
|
|
||||||
| import requests | ||||||
| from mkdocs.config.defaults import MkDocsConfig | ||||||
|
mrdarrengriffin marked this conversation as resolved.
|
||||||
|
|
||||||
| ALLOWLIST_URL = "https://www.openhomefoundation.org/allowed-referrers.json" | ||||||
| ALLOWLIST_FILE = Path(".cache/plausible/allowed-referrers.json") | ||||||
| ALLOWLIST_MAX_AGE = 3600 | ||||||
|
|
||||||
| # `strict: true` aborts the build on anything logged at WARNING or above under the | ||||||
| # "mkdocs" logger, and an unreachable allow list must never break the site, so the | ||||||
| # fallbacks below report at INFO instead. | ||||||
| log = logging.getLogger("mkdocs.hooks.plausible") | ||||||
|
|
||||||
|
|
||||||
| def normalize_referrers(payload: object) -> list[str]: | ||||||
| """Reduce the allow list payload to bare, lowercase domains.""" | ||||||
| if not isinstance(payload, list) or not all(isinstance(entry, str) for entry in payload): | ||||||
| raise ValueError("payload is not an array of strings") | ||||||
| return [domain for entry in payload if (domain := entry.strip().lower().removesuffix("."))] | ||||||
|
|
||||||
|
|
||||||
| def cached_referrers() -> list[str] | None: | ||||||
| """The allow list left behind by an earlier build, if it is still usable.""" | ||||||
| try: | ||||||
| return normalize_referrers(json.loads(ALLOWLIST_FILE.read_text())) | ||||||
| except FileNotFoundError: | ||||||
| return None | ||||||
| except (OSError, ValueError) as exception: | ||||||
| log.info(f"Discarding unusable allow list at {ALLOWLIST_FILE}: {exception}") | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
The same goes for your other loggers as well |
||||||
| return None | ||||||
|
|
||||||
|
|
||||||
| def download_referrers() -> list[str]: | ||||||
| """Download the allow list and cache it for subsequent builds.""" | ||||||
| response = requests.get(ALLOWLIST_URL, timeout=30) | ||||||
| response.raise_for_status() | ||||||
| referrers = normalize_referrers(response.json()) | ||||||
| ALLOWLIST_FILE.parent.mkdir(parents=True, exist_ok=True) | ||||||
| ALLOWLIST_FILE.write_text(json.dumps(referrers, indent=4, sort_keys=True) + "\n") | ||||||
| log.info(f"Fetched {len(referrers)} allowed referrers") | ||||||
| return referrers | ||||||
|
|
||||||
|
|
||||||
| def allowed_referrers() -> list[str]: | ||||||
| """The allow list, downloaded at most once per build.""" | ||||||
| if ( | ||||||
| ALLOWLIST_FILE.exists() | ||||||
| and time.time() - ALLOWLIST_FILE.stat().st_mtime < ALLOWLIST_MAX_AGE | ||||||
| and (cached := cached_referrers()) is not None | ||||||
| ): | ||||||
| return cached | ||||||
|
|
||||||
| try: | ||||||
| return download_referrers() | ||||||
| except (OSError, ValueError, requests.RequestException) as exception: | ||||||
| if (cached := cached_referrers()) is not None: | ||||||
| log.info(f"Could not refresh the allow list, reusing {ALLOWLIST_FILE}: {exception}") | ||||||
| return cached | ||||||
| log.info( | ||||||
| f"Could not fetch the allow list ({exception}), " | ||||||
| "every referrer will be reported to Plausible as unlisted" | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this have any value, should this not: |
||||||
| ) | ||||||
| return [] | ||||||
|
|
||||||
|
|
||||||
| def on_config(config: MkDocsConfig, **kwargs): | ||||||
| config.extra.setdefault("plausible", {})["allowed_referrers"] = allowed_referrers() | ||||||
| return config | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,6 +1,10 @@ | ||||||||||
| {% extends "main.html" %} | ||||||||||
|
|
||||||||||
| {% block content %} | ||||||||||
| <h1>404 - Not found</h1> | ||||||||||
| <script>document.addEventListener("DOMContentLoaded", function () { plausible("404"); });</script> | ||||||||||
| {% endblock %} | ||||||||||
| {% extends "main.html" %} | ||||||||||
|
|
||||||||||
| {% block content %} | ||||||||||
| <h1>404 - Not found</h1> | ||||||||||
| <script> | ||||||||||
| document.addEventListener("DOMContentLoaded", function () { | ||||||||||
| if (typeof window.plausible === "function") window.plausible("404"); | ||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||
| }); | ||||||||||
| </script> | ||||||||||
| {% endblock %} | ||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,5 @@ | ||
| <footer class="md-footer md-typeset"> | ||
| <p class="plausible-attribution"> | ||
| This website uses <a href="https://www.openhomefoundation.org/blog/making-our-web-analytics-open-source-with-plausible/" target="_blank" rel="noopener">privacy-first analytics</a> to help us improve the site. You can view all data in our <a href="https://plausible.openhomefoundation.org/hacs.xyz" target="_blank" rel="noopener">public dashboard</a>. | ||
| This website uses <a href="https://www.openhomefoundation.org/blog/making-our-web-analytics-open-source-with-plausible/" target="_blank" rel="noopener">privacy-first analytics</a> to help us improve the site. You can view all data in our <a href="{{ config.extra.plausible.dashboard }}" target="_blank" rel="noopener">public dashboard</a>. | ||
| </p> | ||
| </footer> | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| {#- | ||
| Plausible analytics. | ||
|
|
||
| Visitors arriving from their own Home Assistant or ESPHome instance send that | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ESPHome has HACS now? |
||
| private URL as the referrer, so the tracker is initialized with a transformRequest | ||
| that replaces every referrer outside the Open Home Foundation allow list with a | ||
| single aggregate bucket. source/hooks/plausible.py fetches the allow list once per | ||
| build and the loop below is the only thing that ever sees the real referrer. | ||
| -#} | ||
| <script async src="{{ config.extra.plausible.script }}"></script> | ||
| <script> | ||
| (function () { | ||
| var allowedReferrers = {{ config.extra.plausible.allowed_referrers | tojson }}; | ||
|
|
||
| window.plausible = window.plausible || function () { | ||
| (plausible.q = plausible.q || []).push(arguments); | ||
| }; | ||
| plausible.init = plausible.init || function (options) { | ||
| plausible.o = options || {}; | ||
| }; | ||
|
|
||
| plausible.init({ | ||
| transformRequest: function (payload) { | ||
| if (!payload.r) { | ||
| return payload; | ||
| } | ||
|
|
||
| var host = ""; | ||
| try { | ||
| host = new URL(payload.r).hostname.replace(/\.$/, ""); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should this also be lowercase? |
||
| } catch (error) { | ||
| // A referrer we cannot parse falls through and gets replaced. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should probably return early here then. |
||
| } | ||
|
|
||
| for (var index = 0; index < allowedReferrers.length; index++) { | ||
| var domain = allowedReferrers[index]; | ||
| if (host === domain || (host.length > domain.length && host.slice(-(domain.length + 1)) === "." + domain)) { | ||
| return payload; | ||
| } | ||
| } | ||
|
Comment on lines
+35
to
+40
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is no need to do this loop if the try/catch above failed as host will be "" |
||
|
|
||
| // One aggregate bucket, so we can see how much we filter without learning | ||
| // anything about individual visitors. RFC 2606 reserves .invalid, so this | ||
| // can never collide with a real domain. | ||
| payload.r = "https://unlisted.invalid/"; | ||
| return payload; | ||
| }, | ||
| }); | ||
| })(); | ||
| </script> | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not the current version?