# Site analytics privacy boundary

Status: accepted
Decision date: 2026-07-23

## Decision

TailMux website analytics uses two privacy levels in the same OpenPanel
project:

1. **Aggregate site statistics** run in production without waiting for the
   optional analytics choice. They are limited to statistics used to operate
   and improve the TailMux site.
2. **Identified product analytics** run only after the visitor selects
   `Accept` in the analytics banner.

Selecting `Reject`, or not making a choice, disables only the identified
stream. It does not disable technical processing required for authentication,
security, checkout, licensing, legal-acceptance evidence, or aggregate site
statistics.

This follows the Italian Garante's distinction between optional behavioral
tracking and analytics that can be treated like technical identifiers when
used only for aggregate statistics concerning one site, with minimized data
and no combination with other information:

- <https://www.garanteprivacy.it/faq/cookie>
- <https://www.garanteprivacy.it/web/guest/home/docweb/-/docweb-display/docweb/9677876%26nbsp>

## Aggregate stream

### Data flow

The browser sends a same-origin `POST /api/analytics/aggregate` request. The
request:

- uses `credentials: "omit"`, so authentication and consent cookies are not
  attached;
- uses an in-memory random session identifier that is discarded on page
  reload, tab close, or navigation away from the application;
- does not create or read cookies, `localStorage`, or `sessionStorage`;
- does not send query strings, full referrer paths, account identifiers,
  order identifiers, checkout identifiers, license identifiers, form values,
  names, or email addresses.

The TailMux origin validates a strict event/property allowlist, reduces the
request user-agent to coarse browser/OS/device families, and may add
Cloudflare's two-letter country code. It never forwards the raw visitor IP or
full user-agent to OpenPanel. The raw visitor IP is used only as an in-memory
rate-limit key and disappears when the process or fixed window resets.

The origin forwards the minimized event to the existing TailMux OpenPanel
project using the existing server-only credentials. OpenPanel sees the TailMux
server connection, not the visitor connection. The ephemeral browser session
UUID is prefixed with `aggregate:` and used only to group events from the
current page load. Every event also carries `privacy_mode: aggregate`.

### Allowed aggregate events

| Event                              | Exact trigger                                             | Allowed properties                                                                                |
| ---------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| `screen_view`                      | A client-side route becomes visible                       | canonical path, external referrer origin, current-page `utm_source`, `utm_medium`, `utm_campaign` |
| `signup_completed`                 | Account creation and legal acceptance both succeed        | none                                                                                              |
| `download_clicked`                 | A concrete release asset is selected                      | OS, architecture, release version                                                                 |
| `checkout_started`                 | The user requests checkout                                | signed-in boolean                                                                                 |
| `checkout_success_viewed`          | The validated checkout-success route is shown             | checkout-ID-present boolean, never the ID                                                         |
| `checkout_failed`                  | The checkout-failure route is shown                       | none                                                                                              |
| `portal_opened`                    | The customer portal URL is successfully obtained          | none                                                                                              |
| `seats_checkout_started`           | The additional-seat checkout URL is successfully obtained | none                                                                                              |
| `updates_renewal_checkout_started` | The updates-renewal checkout URL is successfully obtained | updates-window-active boolean                                                                     |
| `device_deactivated`               | Device deactivation succeeds                              | none                                                                                              |

Adding an aggregate event or property requires updating the runtime allowlist,
its tests, this table, and the Privacy Policy. Free text and stable business
object identifiers are prohibited.

### Aggregate-stream invariants

- Both privacy levels use the existing TailMux OpenPanel project.
- Aggregate delivery uses the existing server-only `OPENPANEL_CLIENT_ID` and
  `OPENPANEL_CLIENT_SECRET`; the secret must never use the `VITE_` prefix.
- TailMux must never call `identify` for an `aggregate:` profile, replace that
  profile with an account ID, or attach account/order/license identifiers to
  aggregate events.
- No OpenPanel `identify`, groups, revenue, refund, session replay, automatic
  element tracking, or outgoing-link tracking is permitted in this stream.
- Aggregate events are production-only. Localhost, previews, tests, and
  synthetic checks must not reach OpenPanel.
- OpenPanel data is retained for no longer than 13 months. OpenPanel Cloud does
  not currently enforce a per-event TTL, so the existing project data must be
  deleted or rotated operationally before any event exceeds that age.

## Identified stream

The same OpenPanel project also receives the identified stream. That stream is
disabled until the versioned `tailmux_analytics_consent` cookie contains
`granted`.

After consent, TailMux may:

- persist first-touch landing/referrer/UTM attribution in
  `tailmux:attribution` browser local storage;
- call OpenPanel `identify` with the internal TailMux account ID;
- connect consented browser events across sessions;
- send server-authoritative revenue and refund events with account, order,
  checkout, product, amount, currency, and timestamp fields.

The signed-in analytics preference is also stored in
`user_analytics_preference`. Verified Polar webhook events consult this row
before sending revenue or refund analytics. Missing, denied, or stale consent
must result in a skipped OpenPanel event, never a fallback send.

## Technical and contractual data

The analytics banner does not control data required to provide and protect the
service. Authentication sessions, security/rate-limit data, orders, payments,
refunds, licenses, device activations, transactional email, and versioned legal
acceptance are processed under their applicable contractual, legal-obligation,
or legitimate-interest basis and remain subject to minimization, access
control, retention, and the Privacy Policy.

These operational records must not be silently repurposed into the identified
analytics stream after an analytics rejection.

## Production configuration

The existing TailMux OpenPanel project uses:

- browser: `VITE_OPENPANEL_CLIENT_ID`
- server aggregate events and verified revenue/refunds:
  `OPENPANEL_CLIENT_ID` and `OPENPANEL_CLIENT_SECRET`
- API endpoint override, when needed: `OPENPANEL_API_URL`

No additional OpenPanel credentials are defined. Missing server credentials
cause the aggregate endpoint to accept and discard the event without exposing
credentials or falling back to browser-side identified tracking.

Before enabling aggregate delivery in production:

1. confirm the browser and server credentials belong to the existing TailMux
   OpenPanel project;
2. record the project-data deletion or rotation deadline;
3. verify representative raw events contain only the allowlisted properties;
4. confirm no `aggregate:` profile resolves to a TailMux account;
5. update the Privacy Policy version if the actual provider behavior differs
   from this decision.
