-
Notifications
You must be signed in to change notification settings - Fork 0
fix(docs): restore canonical Umami analytics #815
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
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| import assert from 'node:assert/strict'; | ||
| import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; | ||
| import { tmpdir } from 'node:os'; | ||
| import { resolve } from 'node:path'; | ||
| import { test } from 'node:test'; | ||
|
|
||
| import { checkBuiltAnalytics } from './check-built-analytics.mjs'; | ||
| import { | ||
| DOCS_UMAMI_DOMAINS, | ||
| DOCS_UMAMI_SCRIPT_SRC, | ||
| DOCS_UMAMI_WEBSITE_ID, | ||
| docsAnalyticsConfig, | ||
| } from '../src/lib/analytics.mjs'; | ||
|
|
||
| test('analytics stays disabled outside the canonical released tree', () => { | ||
| assert.equal( | ||
| docsAnalyticsConfig({ | ||
| enabled: false, | ||
| websiteId: 'deployment-id', | ||
| scriptSrc: 'https://stats.example.invalid/script.js', | ||
| domains: 'example.invalid', | ||
| }), | ||
| null, | ||
| ); | ||
| }); | ||
|
|
||
| test('canonical release analytics uses source-controlled defaults', () => { | ||
| assert.deepEqual(docsAnalyticsConfig({ enabled: true }), { | ||
| websiteId: DOCS_UMAMI_WEBSITE_ID, | ||
| scriptSrc: DOCS_UMAMI_SCRIPT_SRC, | ||
| domains: DOCS_UMAMI_DOMAINS, | ||
| }); | ||
| assert.deepEqual( | ||
| docsAnalyticsConfig({ | ||
| enabled: true, | ||
| websiteId: ' ', | ||
| scriptSrc: '', | ||
| domains: ' ', | ||
| }), | ||
| { | ||
| websiteId: DOCS_UMAMI_WEBSITE_ID, | ||
| scriptSrc: DOCS_UMAMI_SCRIPT_SRC, | ||
| domains: DOCS_UMAMI_DOMAINS, | ||
| }, | ||
| ); | ||
| }); | ||
|
|
||
| async function builtRoot(t, body) { | ||
| const root = await mkdtemp(resolve(tmpdir(), 'registry-docs-analytics-')); | ||
| t.after(() => rm(root, { recursive: true, force: true })); | ||
| await mkdir(root, { recursive: true }); | ||
| await writeFile(resolve(root, 'index.html'), `<html><head>${body}</head></html>`); | ||
| return root; | ||
| } | ||
|
|
||
| test('built canonical root contains the exact Registry Docs tracker', async (t) => { | ||
| const root = await builtRoot( | ||
| t, | ||
| `<script defer src="${DOCS_UMAMI_SCRIPT_SRC}" data-website-id="${DOCS_UMAMI_WEBSITE_ID}" data-domains="${DOCS_UMAMI_DOMAINS}"></script>`, | ||
| ); | ||
| await checkBuiltAnalytics(root, { enabled: true }); | ||
| }); | ||
|
|
||
| test('built canonical root rejects a different website identity', async (t) => { | ||
| const root = await builtRoot( | ||
| t, | ||
| `<script defer src="${DOCS_UMAMI_SCRIPT_SRC}" data-website-id="wrong-id" data-domains="${DOCS_UMAMI_DOMAINS}"></script>`, | ||
| ); | ||
| await assert.rejects( | ||
| checkBuiltAnalytics(root, { enabled: true }), | ||
| /source-controlled Registry Docs tracker/, | ||
| ); | ||
| }); | ||
|
|
||
| test('noncanonical builds reject analytics', async (t) => { | ||
| const root = await builtRoot( | ||
| t, | ||
| `<script defer src="${DOCS_UMAMI_SCRIPT_SRC}" data-website-id="${DOCS_UMAMI_WEBSITE_ID}" data-domains="${DOCS_UMAMI_DOMAINS}"></script>`, | ||
| ); | ||
| await assert.rejects( | ||
| checkBuiltAnalytics(root, { enabled: false }), | ||
| /must not contain analytics outside the canonical released tree/, | ||
| ); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| import assert from 'node:assert/strict'; | ||
| import { readFile } from 'node:fs/promises'; | ||
| import { resolve } from 'node:path'; | ||
|
|
||
| import { parse } from 'parse5'; | ||
|
|
||
| import { | ||
| DOCS_UMAMI_DOMAINS, | ||
| DOCS_UMAMI_SCRIPT_SRC, | ||
| DOCS_UMAMI_WEBSITE_ID, | ||
| } from '../src/lib/analytics.mjs'; | ||
|
|
||
| function attributes(node) { | ||
| return Object.fromEntries((node.attrs ?? []).map(({ name, value }) => [name, value])); | ||
| } | ||
|
|
||
| function scriptAttributes(node, found = []) { | ||
| if (node.nodeName === 'script') found.push(attributes(node)); | ||
| for (const child of node.childNodes ?? []) scriptAttributes(child, found); | ||
| return found; | ||
| } | ||
|
|
||
| export async function checkBuiltAnalytics(root, { enabled }) { | ||
| const indexPath = resolve(root, 'index.html'); | ||
| const document = parse(await readFile(indexPath, 'utf8')); | ||
| const analyticsScripts = scriptAttributes(document).filter( | ||
| (attrs) => attrs.src === DOCS_UMAMI_SCRIPT_SRC || attrs['data-website-id'], | ||
| ); | ||
|
|
||
| if (!enabled) { | ||
| assert.deepEqual( | ||
| analyticsScripts, | ||
| [], | ||
| `${indexPath} must not contain analytics outside the canonical released tree`, | ||
| ); | ||
| return; | ||
| } | ||
|
|
||
| assert.equal( | ||
| analyticsScripts.length, | ||
| 1, | ||
| `${indexPath} must contain exactly one Umami tracker`, | ||
| ); | ||
| assert.deepEqual( | ||
| analyticsScripts[0], | ||
| { | ||
| defer: '', | ||
| src: DOCS_UMAMI_SCRIPT_SRC, | ||
| 'data-website-id': DOCS_UMAMI_WEBSITE_ID, | ||
| 'data-domains': DOCS_UMAMI_DOMAINS, | ||
| }, | ||
| `${indexPath} must contain the source-controlled Registry Docs tracker`, | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| --- | ||
| title: Privacy | ||
| description: Data collected when you use Registry Docs and how Registry Stack uses it. | ||
| status: current | ||
| owner: registry-docs | ||
| source_repos: | ||
| - registry-stack | ||
| last_reviewed: "2026-08-23" | ||
| doc_type: reference | ||
| locale: en | ||
| standards_referenced: [] | ||
| --- | ||
|
|
||
| Registry Docs uses self-hosted, cookie-free analytics to understand which documentation pages | ||
| help readers and where the documentation needs attention. | ||
| This page describes that processing and the choices available to you. | ||
|
|
||
| {/* Evidence: `docs/site/src/lib/analytics.mjs`, `DOCS_UMAMI_SCRIPT_SRC`, fixes the self-hosted | ||
| tracker endpoint; `docs/site/src/components/RegistryHead.astro`, `docsAnalyticsConfig()`, | ||
| includes that tracker in the canonical released tree; `docs/site/scripts/build-archives.mjs`, | ||
| `verifyAnalytics`, enforces the disabled versioned tree. Umami's official FAQ documents | ||
| cookie-free collection in the [Umami FAQ](https://docs.umami.is/docs/faq). */} | ||
|
|
||
| ## Who operates Registry Docs | ||
|
|
||
| Aubex Consulting LLC, 28 Geary St, Ste 650 #189, San Francisco, CA 94108, | ||
| United States, publishes Registry Stack and operates Registry Docs. | ||
| Aubex is the controller responsible for this site. | ||
| You can contact Aubex at [contact@registrystack.org](mailto:contact@registrystack.org). | ||
|
|
||
| ## Analytics data | ||
|
|
||
| Registry Docs loads self-hosted Umami analytics from `stats.registrystack.org` only on the | ||
| canonical documentation at `docs.registrystack.org/`. | ||
| The `/dev/` tree and versioned `/v/<version>/` archives do not load analytics. | ||
| Umami records page views, including URL query parameters, referral sources, general browser and | ||
| device information, approximate location, and selected outbound-link clicks. | ||
| The tracker does not set cookies or use a persistent visitor identifier. | ||
| Umami uses the request IP address to estimate location but does not store the address. | ||
|
|
||
| {/* Evidence: `docs/site/src/components/RegistryHead.astro`, `docsJourneyTargets`, implements the | ||
| selected outbound-link events; `docs/site/src/components/RegistryFooter.astro`, `showFeedback`, | ||
| contains the only other Umami call; neither component assigns a custom visitor identity. | ||
| Umami's official metric definitions document its page, query, referrer, browser, device, | ||
| location, IP-address, and rotating-session behavior in the | ||
| [Umami metric definitions](https://docs.umami.is/docs/metric-definitions). */} | ||
|
|
||
| Registry Stack uses these records to understand page use, improve navigation, and prioritize | ||
| documentation work. | ||
| The analytics records are not sold, used for advertising, or shared with advertising networks. | ||
|
|
||
| ## Hosting data | ||
|
|
||
| Registry Docs is hosted on GitHub Pages. | ||
| GitHub processes request and server-log data to deliver and protect the site under the | ||
| [GitHub Privacy Statement](https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement). | ||
|
|
||
| {/* Evidence: `.github/workflows/docs-pages.yml` uploads and deploys the static site through the | ||
| Pages deployment. */} | ||
|
|
||
| ## Legal basis and retention | ||
|
|
||
| Where the General Data Protection Regulation or UK General Data Protection Regulation applies, | ||
| Aubex relies on legitimate interests to operate aggregate documentation analytics. | ||
| Analytics records remain in the self-hosted Umami database while they are useful for improving | ||
| Registry Docs and are removed when they are no longer needed for that purpose. | ||
|
|
||
| ## Your choices and rights | ||
|
|
||
| You can block the analytics script with browser controls or a content blocker without losing access | ||
| to the documentation. | ||
| Email [contact@registrystack.org](mailto:contact@registrystack.org) to request access to, | ||
| correction of, deletion of, or restriction of personal data, or to object to its processing. | ||
| Umami analytics are not connected to a name or email address, so Aubex may not be able to identify | ||
| records associated with a particular visitor. | ||
| You can also complain to the data protection supervisory authority in your country. | ||
|
|
||
| Effective date: August 23, 2026. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| export const DOCS_UMAMI_WEBSITE_ID = '0a8aa090-83c5-4207-8c90-9fcc1e50bb78'; | ||
| export const DOCS_UMAMI_SCRIPT_SRC = 'https://stats.registrystack.org/script.js'; | ||
| export const DOCS_UMAMI_DOMAINS = 'docs.registrystack.org'; | ||
|
|
||
| function configuredValue(value, fallback) { | ||
| return value?.trim() || fallback; | ||
| } | ||
|
|
||
| /** | ||
| * @param {{ | ||
| * enabled?: boolean, | ||
| * websiteId?: string, | ||
| * scriptSrc?: string, | ||
| * domains?: string, | ||
| * }} [options] | ||
| */ | ||
| export function docsAnalyticsConfig({ | ||
| enabled = false, | ||
| websiteId, | ||
| scriptSrc, | ||
| domains, | ||
| } = {}) { | ||
| if (!enabled) return null; | ||
|
|
||
| return { | ||
| websiteId: configuredValue(websiteId, DOCS_UMAMI_WEBSITE_ID), | ||
| scriptSrc: configuredValue(scriptSrc, DOCS_UMAMI_SCRIPT_SRC), | ||
| domains: configuredValue(domains, DOCS_UMAMI_DOMAINS), | ||
| }; | ||
| } | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.