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
7 changes: 4 additions & 3 deletions CLOUDFLARE.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,9 @@ Cloudflare `request.cf.country`; browser-supplied geography headers are ignored.
No Google credentials or visitor geography belong in site artifacts.

See [the event, privacy, consent, and verification contract](scripts/docs-site/ANALYTICS.md).
The shared regional policy governs a Basic Consent Mode gate and persistent
Google Analytics choices. Enhanced Measurement remains enabled; automatic
Eligible public production visits start Google Analytics automatically, subject
to browser DNT/GPC and public-page safety. Startup does not consult old saved
choices or the geography endpoint; no analytics choice UI is rendered. Enhanced Measurement remains enabled; automatic
history pageviews are suppressed because the docs shell owns initial/PJAX views.
User-provided data and advertising features stay off. Existing server traffic
counts remain separate from these controls.
Expand All @@ -64,7 +65,7 @@ Run `node --test scripts/docs-site/analytics*.test.mjs workers/analytics-consent
The optional `DOCS_GA4_SDK_PATH` points to a coordinator-approved public SDK file;
fixtures intercept every request without forwarding collection. Browser transport
is not provider ingestion. Production activation requires the coordinated release
GO after hosting, consent, actual-SDK, and repository checks.
GO after the exact policy/diff, focused SDK, and repository checks.

## Required Cloudflare Access

Expand Down
93 changes: 40 additions & 53 deletions scripts/docs-site/ANALYTICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,18 @@ The site uses native `gtag.js`; existing hosting and analytics integrations rema
| `scroll`, `click`, `file_download`, `video_start/progress/complete`, `view_search_results`, `form_start/submit` | Native Enhanced Measurement, when supported | Preserve the capabilities; shared automatic **history pageviews** must be off. Native form submission is an attempt, not verified success. Native video needs a supported embedded player; the current docs sources contain only a video code example, not a live supported player, so no watched-video outcome is claimed. |
| `scroll_depth` | Actual scrolling: once per 25/50/75% threshold per view | `percent_scrolled`; native 90% remains separate. |
| `section_view` | A build-allowlisted heading stays at least half visible for one second | `section_id`, once per view; first 24 TOC headings. |
| `content_engagement` | 30/60 visible seconds on public documentation, excluding open search/chat/consent panels | `engagement_seconds=30/60`, `content_type=documentation`, `content_id`. |
| `content_engagement` | 30/60 visible seconds on public documentation, excluding open search/chat panels | `engagement_seconds=30/60`, `content_type=documentation`, `content_id`. |
| `select_content` | Public docs/site links, curated references, search results, code tabs, feedback choices | `content_type`, `content_id`, `ui_location`; code tabs may add `method`, `install_platform`. Public editorial/resource handles are allowed; visitor identifiers are not. |
| `popup_view`, `popup_dismiss` | Search dialog, community invitation, diagram expansion, assistant shell | `popup_id`, `ui_location`, `dismiss_method` on dismissal. Layout occlusion is not a user dismissal. |
| `form_attempt` | Accessible feedback issue-launch button | `form_id=docs_feedback`, `ui_location=page_feedback`; no published-issue or lead claim. |
| `copy_action` | Clipboard API or fallback has resolved | `content_type`, stable `content_id`, `action_result=success/error`, `ui_location`; optional `method`, `install_platform`. Never copied text. |
| `search` | A custom Pagefind result set is visibly stable for 1.2 seconds, selected, or closed | `search_context=docs`, `search_filter=none`, `search_status=complete/partial/error`, actual rendered `result_count` on complete/partial results (top 12 of N is 12/partial); error omits count; `search_term` only when every word is in public navigation/heading/result-title vocabulary and passes sensitive-value checks. No input-event emission. |
| `web_vital` | Official `web-vitals` LCP/INP/CLS reporting | `metric_name`, exactly one of `lcp_ms`, `inp_ms`, `cls_score` (first two milliseconds; CLS unitless), `metric_rating`, `navigation_type`, `release`. One report per metric instance. Only a valid saved grant covering the document from its start permits document metrics. Any private/unclassified/denied interval permanently invalidates remaining metrics, including pending imports; a late grant does not read buffered metrics. Public-only PJAX keeps initial document context. |
| `web_vital` | Official `web-vitals` LCP/INP/CLS reporting | `metric_name`, exactly one of `lcp_ms`, `inp_ms`, `cls_score` (first two milliseconds; CLS unitless), `metric_rating`, `navigation_type`, `release`. One report per metric instance. Only a public document allowed by browser privacy signals from its start permits document metrics. Any private/unclassified/denied interval permanently invalidates remaining metrics, including pending imports; a later allowed interval does not read buffered metrics. Public-only PJAX keeps initial document context. |
| `client_error` | Runtime/resource/rejection categories and owned search failures | Fixed `error_type`, fixed `error_code`, `release`; at most five distinct categories per view. No exception strings, stacks, filenames, or input values. |

All custom events carry safe public page context. `release` is the publishing
workflow's 12-character commit SHA, or `local` in local builds. Public static
All custom events carry safe public page context. `release` is the executing JavaScript identity
`js-<12 hex>`, derived from emitted runtime content before its identity is prepended.
Full build provenance is separate in the R2 publication catalog. Public static
content IDs can be high-cardinality: register them only when a report needs the
dimension. `result_count`, `lcp_ms`, `inp_ms`, and `cls_score` are metrics, not identifier dimensions. Zero results are derived only from `search_status=complete` with `result_count=0`.
Do not combine native outbound events with content selections as one interaction
Expand All @@ -32,9 +33,9 @@ total or sum distinct users across hosts. No docs event proves installation.
Preview builds, other origins, the hidden component fixture, unknown/error/API
routes, and unclassified history transitions cannot emit. Back/forward navigation
suspends measurement before its asynchronous fetch. A trusted BFCache restore
refreshes the persisted choice in a capture-phase `pageshow` handler before the
refreshes browser privacy signals in a capture-phase `pageshow` handler before the
native SDK, then uses the same owner/dedupe for one allowed restored view. Ordinary
initial `pageshow` adds none. An unmeasured A→B→A journey starts one current view on regrant; query-only DOM replacements rebind observers without another pageview or fresh event budgets. Document metrics stay invalid after a restore boundary. The tag keeps native
initial `pageshow` adds none. An unmeasured A→B→A journey starts one current view on browser-signal recovery; query-only DOM replacements rebind observers without another pageview or fresh event budgets. Document metrics stay invalid after a restore boundary. The tag keeps native
cross-domain/cookie behavior; configured Google linker behavior still requires
shared-SDK verification.

Expand All @@ -51,31 +52,23 @@ are therefore a deliberate measurement gap. Public documentation remains measure
for signed-in visitors. A session check is not a new login or signup.

Google automatic user-provided-data collection is off. Google signals and
ad-personalization signals are also disabled locally. The approved regional
policy is `2026-10-02.v2`: EEA + GB + CH and AX/GF/GP/MQ/RE/YT/MF (39 codes) require opt-in; other validated countries
use notice/opt-out; unknown geography remains off without explicit permission.
The public shell now uses the narrower existing-grant-only rule described below;
these region classes cannot enable collection. The existing Worker classifies trusted `request.cf.country` and returns only the
region class from uncached `/api/analytics-consent`. It never uses client-supplied
country headers or exposes country/IP in the response.

Frozen policy SHA-256: `96bb4b0abc931d856ce14aff0131320e2c8f520d0bb2632037f864fe79bd5e93`. Both client and endpoint require the v2 version. An old-version record is not absence: it stays off until a fresh explicit v2 choice, even in a notice/opt-out region. Visits without a valid stored grant remain off in every region.

The public shell no longer renders automatic analytics notices or footer on/off
controls. Collection now requires an existing, valid explicit grant in
`openclaw.analytics.consent`; no region can create a new grant. Existing choices
retain their 180-day expiry and the same validation/lifecycle handling. New visitors
and expired choices remain off, including notice/opt-out regions. No replacement
popup or new grant flow is provided. This reduces measurement coverage. DNT/GPC and explicit denial override
an allow; failed storage, failed/invalid region responses, expired records, and
unknown regions fail closed. No automatic regional default is enabled.

Basic Consent Mode holds the SDK until allowed. Global analytics/advertising
consent starts denied; only analytics storage is granted when allowed. Denial
closes the application gate and sets Google's measurement opt-out before the
consent update. Pre-choice/private actions are never replayed. Async copy/search outcomes and deferred visibility callbacks carry the original eligible interval; withdrawal or a private boundary invalidates them permanently. Already-consented queued/in-flight public batches may finish sending after denial; this is distinct from new denied-state collection. Persisted denial propagates to same-origin tabs. Failed persistence keeps this runtime denied and preserves drafts without claiming durable storage. Only the observed `_ga` and `_ga_3SK7X2YLSJ` cookies at the installation's host/parent root scope are removed; unrelated cookies remain. These controls
apply to Google Analytics only; existing server traffic counts remain separate.
No global legal-compliance claim is made.
ad-personalization signals are disabled locally. Eligible public production pages
start analytics automatically unless the browser sends a recognized DNT/GPC
signal. No saved analytics-choice record is read, written, migrated or consulted;
missing, granted, denied, expired, malformed and inaccessible old records all
have the same eligibility. No regional response is requested or awaited. The
existing geography endpoint is unused by analytics startup.

No analytics notice, banner, dialog or footer toggle is rendered. The minimal
shared Privacy policy and footer link remain unchanged. Basic Consent Mode keeps
all advertising consent denied; only analytics storage becomes granted on an
eligible public page. Browser privacy signals and private/unclassified boundaries
close the native measurement gate. Async outcomes and deferred callbacks from a
blocked interval cannot replay on recovery. Already-allowed queued/in-flight
public batches may finish sending; they are distinct from newly blocked activity.
Observed GA cookies are cleared when a browser privacy signal blocks an active
runtime; unrelated cookies and user drafts remain. Server traffic counts are
separate. This is a global public-page default, not a regional default.

## Verification and limits

Expand Down Expand Up @@ -105,32 +98,26 @@ not an assumption that `send_page_view: false` disables history measurement.
"chromium"`) without Playwright's `--disable-back-forward-cache` default. The
headless-shell delegate cannot prove this lifecycle. The fixture traverses real
history using CDP, checks trusted `pageshow.persisted` plus retained document
state, and tests other-tab denial plus first-ever grant while cached (both no-choice and saved-deny starts). Positive activation is deferred only between trusted pagehide/pageshow; ordinary background tabs still activate. Run with
state, and tests ignored old-record changes plus browser GPC across restore. Positive activation is deferred only between trusted pagehide/pageshow; ordinary background tabs still activate. Run with
`DOCS_GA4_REQUIRE_BFCACHE=1` and the approved SDK file to require actual cache
restoration; ordinary CI also checks correct behavior if Chromium chooses reload.
No browser network request to Google is forwarded. No synthetic pageshow is used
as native lifecycle proof.

## Two-stage deployment gate
## Deployment controls

`DOCS_SITE_GA4_ENABLED` must equal `1` at build time to produce analytics page
metadata. The existing R2 workflow reads this repository
variable with a default of `0`; CI explicitly builds an enabled artifact. No
Actions permission, required check, deployment queue, upload scope, or router
infrastructure changes are part of this gate.

1. After closed-gate publishing authorization, verify/initialize the resolved
repository/environment setting to `0`, land the reviewed head through normal
checks, and let the existing R2/router workflow publish it. Verify the served
router/asset commit, `/api/analytics-consent` decision and no-store headers,
spoofed-country-header immunity, absent analytics page metadata, and no SDK
loading in a bounded browser check.
2. After separate activation authorization, set `DOCS_SITE_GA4_ENABLED=1` and
dispatch the existing R2 workflow with `artifact_scope=shell` at the reviewed
current main. Verify the actual served metadata/assets and repeat bounded
consent/SDK request checks. Coordinate provider ingestion with the Google
collector owner; transport alone is not ingestion proof.

The removed UI previously displayed the approved withdrawal and configured
14-month retention addenda; retention configuration is unchanged. Retention configuration is not a claim that the
provider's 24-hour change window has elapsed or that historical data was restored.
metadata. The existing R2 workflow reads this repository variable with a default
of `0`; CI builds an enabled artifact. The current correction leaves the verified
production flag enabled. No Actions permission, required check, queue, upload
scope or hosting infrastructure is changed.

Land a reviewed exact head through normal checks and the existing publication.
Record first observed new JavaScript separately from complete HTML publication:
R2 uploads shared assets first, and cached/previously open documents may retain
an older runtime. Preserve the content-derived runtime ID and the independent
catalog build commit. Coordinate the effective collection scope with the source
and reader owners; reporting metadata holds must not be mislabeled as site gates.
All acceptance browser collection is intercepted, never forwarded as synthetic
QA. Provider settings, historical observations and unrecorded past visits are not
changed or reconstructed by this correction.
Loading
Loading