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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Usage research is available at [`/report/0923`](https://sharehtml.zhenjia.dev/report/0923). The September 23, 2026 report uses a frozen snapshot and publishes only aggregated findings and anonymized use cases.

First-party analytics separates browser reports, HTTP/MCP business outcomes, acquisition signals, and bot evidence. See the [measurement contract](docs/analytics/measurement-contract.md) and [operations guide](docs/analytics/operations.md) for consent, data limits, retention, and GA4 configuration. Apply the analytics migrations before deploying with `ANALYTICS_ENABLED=true`. Consenting browser events use the separate Share HTML GA4 property; enhanced measurement is disabled and private share pages do not send Google events.
First-party analytics separates browser reports, HTTP/MCP business outcomes, acquisition signals, and bot evidence. See the [measurement contract](docs/analytics/measurement-contract.md) and [operations guide](docs/analytics/operations.md) for analytics preferences, data limits, retention, and GA4 configuration. Apply the analytics migrations before deploying with `ANALYTICS_ENABLED=true`. Browser analytics is enabled by default when the service is enabled and can be turned off in Analytics preferences; existing opt-outs, DNT/GPC, and unreadable preference storage prevent collection. Eligible browser events use the separate Share HTML GA4 property; enhanced measurement is disabled and private share pages do not send Google events.

<p align="center">
<a href="https://sharehtml.zhenjia.dev">
Expand Down
16 changes: 10 additions & 6 deletions docs/analytics/measurement-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ Version 1, prospective only. `ANALYTICS_ENABLED=true` activates first-party tele

## Browser contract

When service analytics is enabled, an unset browser preference defaults to enabled. Users can turn it off in Analytics preferences; stored opt-outs are preserved. DNT/GPC override the default, and unreadable preference storage prevents collection. Turning analytics off clears the tab session/acquisition context. This policy also gates browser GA, whose dedicated resource and private-share-page exclusions remain separate requirements.

The September 26, 2026 client change expands collection coverage from affirmative opt-in to default-on with opt-out. The confirmed production timestamp is recorded in Cloudflare deployment history and the release pull request. Comparisons of browser, WebMCP or GA counts across the confirmed deployment boundary must account for this coverage change and must not label the difference as product growth. Frozen historical reports are unchanged.

Same-origin `POST /api/analytics/events`, `Content-Type: application/json`, matching `Origin` required. Maximum streamed body is 4096 bytes. Body:

```json
{"event":"page_view","event_id":"aab11f0d-0baa-417b-8271-620fa97ef198","session_id":"bab11f0d-0baa-417b-8271-620fa97ef198","route":"/","acquisition":{"source":"google","medium":"organic","campaign":"launch","referrer_domain":"google.com"}}
```

Ordinary browser events allowlisted: `page_view`, `upload_started`, `upload_failed`, `share_link_copied`. The same bounded, rate-limited endpoint separately allows consented `webmcp_available`, `webmcp_registered`, `webmcp_call`, `webmcp_result` with the strict combinations below. UUID `event_id` is globally idempotent; UUID `session_id` denotes a sessionStorage tab session, never a person. All are client self-reports. Uploads may include JSON form field `analytics` with `{session_id, acquisition}`; invalid analytics never blocks upload. `source`, `medium`, `campaign` accept lowercase `[a-z0-9_-]` tokens, maximum 64 characters; referrer is a domain only. Everything else is dropped. Routes are allowlisted; `/s/<slug>` and `/v/<slug>/...` become `/s/:slug`, `/v/:slug`; unknown routes become `other`. No title, HTML, filename, raw IP/UA, key, claim/auth token, slug, full referrer URL, or arbitrary query parameter is stored in the new telemetry.
Ordinary browser events allowlisted: `page_view`, `upload_started`, `upload_failed`, `share_link_copied`. The same bounded, rate-limited endpoint separately allows preference-eligible `webmcp_available`, `webmcp_registered`, `webmcp_call`, `webmcp_result` with the strict combinations below. UUID `event_id` is globally idempotent; UUID `session_id` denotes a sessionStorage tab session, never a person. All are client self-reports. Uploads may include JSON form field `analytics` with `{session_id, acquisition}`; invalid analytics never blocks upload. `source`, `medium`, `campaign` accept lowercase `[a-z0-9_-]` tokens, maximum 64 characters; referrer is a domain only. Everything else is dropped. Routes are allowlisted; `/s/<slug>` and `/v/<slug>/...` become `/s/:slug`, `/v/:slug`; unknown routes become `other`. No title, HTML, filename, raw IP/UA, key, claim/auth token, slug, full referrer URL, or arbitrary query parameter is stored in the new telemetry.

The browser endpoint returns 204 on accepted or disabled telemetry; 400 invalid body; 403 origin; 413 oversized; 415 content type; 429 rate limit; 503 unavailable storage. The atomic PostgreSQL RPC limits 30 events per tab-session per minute and 120 per daily HMAC IP abuse bucket per minute across Worker isolates. Retries consume rate allowance, but duplicate event IDs never create additional event rows. Abuse buckets are separate from analytics, expire after one day via maintenance, and are never a people metric. Non-browser clients can forge Origin/session IDs; rate limits mitigate pollution, not establish identity.

Expand All @@ -27,7 +31,7 @@ MCP initialize stores only an allowlisted client family (`claude`, `codex`, `cur

## Event meaning

`page_served` counts successful server GET responses for the HTML homepage, marketing pages and `/s/:slug` wrappers. It is independent of browser consent, so a crawler that only reads ordinary HTML is included with the same actor/evidence caveats. HEAD, error responses, uploaded `/v` content, report pages and discovery documents are excluded from this event. A private wrapper is only a generic shell: serving it does not prove access to protected HTML. It is a response-count denominator, not a person, rendered page, browser session or conversion. `page_view` is a consenting browser's self-reported view; one navigation can emit both, while cached/client-only navigation and missing consent can make coverage differ. Never add these two event counts together. Use server `page_served` with server outcomes for coarse request activity and consented tab-session events for the separate browser funnel. The `page_served` CHECK-constraint migration must be applied before deploying this event.
`page_served` counts successful server GET responses for the HTML homepage, marketing pages and `/s/:slug` wrappers. It is independent of browser analytics preferences, so a crawler that only reads ordinary HTML is included with the same actor/evidence caveats. HEAD, error responses, uploaded `/v` content, report pages and discovery documents are excluded from this event. A private wrapper is only a generic shell: serving it does not prove access to protected HTML. It is a response-count denominator, not a person, rendered page, browser session or conversion. `page_view` is an eligible browser's self-reported view; one navigation can emit both, while cached/client-only navigation and opt-outs or privacy/storage restrictions can make coverage differ. Never add these two event counts together. Use server `page_served` with server outcomes for coarse request activity and eligible tab-session events for the separate browser funnel. The `page_served` CHECK-constraint migration must be applied before deploying this event.

`share_created` means the server completed share metadata, object storage, asset metadata, and scan-state updates. It includes automatically blocked uploads (HTTP 202), which are not successful public delivery. HTTP status and outcome retain that distinction. `share_create_failed` includes validation/rate-limit/server failures. MCP tool result `isError` determines tool failure; HTTP status alone cannot determine MCP success. `preview_served` means GET passed access checks and obtained an HTML object, not that a person rendered or read it. HEAD is excluded. `discovery_read` means a successful GET of a discovery document; it is not a user, lead, install, or conversion. MCP initialize is not an upload. Browser upload_failed can overlap a server failure and must not be summed as unique failures.

Expand All @@ -47,13 +51,13 @@ order by start_time desc limit 10;
select event_name, transport, actor_category, count(*) from public.analytics_events group by 1,2,3;
```

No GA Measurement Protocol requests originate from the server. If enabled separately, browser GA4 remains a separate, consent/privacy-dependent data source and is never injected into uploaded `/v` HTML. Historical traffic without this instrumentation remains unknown.
No GA Measurement Protocol requests originate from the server. If enabled separately, browser GA4 remains a separate, preference/privacy-dependent data source and is never injected into uploaded `/v` HTML. Historical traffic without this instrumentation remains unknown.

## Browser WebMCP observations

The WebMCP migration expands the existing event/transport/tool constraints and the same ingestion RPC's fixed combinations. It adds no tables, privileges, public reads, new retention policy or alternate rate limits. Apply it before the client release. Existing HTML tool definitions are extracted into `src/client/webmcp.ts` and wrapped without changing their inputs, HTTP requests, returned content or thrown errors.

All four events require affirmative optional analytics consent, enabled first-party analytics, and no DNT/GPC. Tools work even when telemetry is off. Availability and registration state can be reported once per consented tab context after the user grants consent; prior tool calls are never replayed. Each invocation rechecks consent, so revoking before a result suppresses that result.
All four events require enabled first-party analytics, an enabled browser preference, readable preference storage, and no DNT/GPC. An unset preference defaults to enabled; a stored opt-out remains disabled. Tools work even when telemetry is off. Availability and registration state can be reported once per eligible tab context when analytics is enabled; prior tool calls are never replayed. Each invocation rechecks eligibility, so turning analytics off before a result suppresses that result.

| Event | Meaning | Required outcome/tool |
| --- | --- | --- |
Expand All @@ -64,7 +68,7 @@ All four events require affirmative optional analytics consent, enabled first-pa

Transport is `webmcp`, legacy_source is fixed `webmcp`, and tool is one of `describe_share_html`, `get_public_share`, `access_private_share`, `create_share`. MCP method/client are null. No arguments, response text, HTML, title, slug, key, raw exception, claimed agent name or other dynamic tool metadata is sent. These events never use the GA sender. Actor evidence remains the request's coarse UA/Cloudflare evidence, independent of the client-reported transport.

Anyone can imitate these client reports; availability is not active usage, registration is not a tool call, and tool invocation is not proof of an AI agent or human. `webmcp_result=success` preserves the tool's existing `isError` semantics, so a server 202 automatically blocked creation can still be a successful tool response. Use the independent server `share_created` status/outcome to distinguish accepted-but-blocked storage from publicly usable creation. With consent, WebMCP creation attaches the same sanitized tab session/acquisition context as ordinary browser uploads, permitting tab-level association with server outcomes; denial/DNT/GPC omits it. Server HTTP creations remain `transport=http_api, legacy_source=webmcp`; do not sum them with WebMCP result counts. Source is now parsed immediately after form decoding so missing-file/extension validation failures retain the label; errors before form decoding cannot be reliably attributed.
Anyone can imitate these client reports; availability is not active usage, registration is not a tool call, and tool invocation is not proof of an AI agent or human. `webmcp_result=success` preserves the tool's existing `isError` semantics, so a server 202 automatically blocked creation can still be a successful tool response. Use the independent server `share_created` status/outcome to distinguish accepted-but-blocked storage from publicly usable creation. When browser analytics is eligible, WebMCP creation attaches the same sanitized tab session/acquisition context as ordinary browser uploads, permitting tab-level association with server outcomes; opt-out/DNT/GPC or unavailable preference storage omits it. Server HTTP creations remain `transport=http_api, legacy_source=webmcp`; do not sum them with WebMCP result counts. Source is now parsed immediately after form decoding so missing-file/extension validation failures retain the label; errors before form decoding cannot be reliably attributed.

All optional events are best effort, with no retries or guaranteed delivery. Rate limiting, unloads, denial, unavailable storage, or a lost request/result can produce unmatched counts. There is no durable per-call correlation ID: concurrent calls and server creations cannot be joined exactly from session/tool/time alone. Report aggregate calls/results with these gaps, never infer historical non-use from zero WebMCP tags.

Expand All @@ -74,4 +78,4 @@ All optional events are best effort, with no retries or guaranteed delivery. Rat

Raw events and daily aggregates retain this country dimension, with the existing 90-day/730-day retention and permission model. Pre-instrumentation rows receive `ZZ`; historical countries are never reconstructed from stored hashes or guesses. Rollups group country separately and continue aggregating all complete raw dates before deleting expired raw rows.

This describes the network request's apparent country or region, not citizenship, residence, language, ethnicity, age or a person. VPNs, corporate proxies, datacenters and AI/cloud fetchers can identify an exit location rather than the end user's location. Report country/region distributions of observed requests, separated by actor/transport and explicit internal traffic where appropriate; do not label them user population demographics or count requests as people. Optional browser/WebMCP events still require consent; coarse server events keep their existing service-measurement policy.
This describes the network request's apparent country or region, not citizenship, residence, language, ethnicity, age or a person. VPNs, corporate proxies, datacenters and AI/cloud fetchers can identify an exit location rather than the end user's location. Report country/region distributions of observed requests, separated by actor/transport and explicit internal traffic where appropriate; do not label them user population demographics or count requests as people. Optional browser/WebMCP events still follow the browser preference, storage and DNT/GPC eligibility checks; coarse server events keep their existing service-measurement policy.
Loading
Loading