# Private-page capture

An authenticated application can use readable replay or structure-only evidence. Confirm which experience the owner wants. Do not choose `privateCapture` merely because a page requires sign-in.

## Readable application replay

Use ordinary capture when the owner authorizes the page content that its privacy preset retains. The owner chooses Full, Balanced, or Strict in the dashboard; agent settings tools cannot weaken that choice. Full retains readable text, styling, and permitted images. Form values still need explicit input rules, and password, payment, and one-time-code fields remain excluded.

Load `t.js` without `data-project` or queued initialization. Check the required capabilities before passing a project ID, then initialize once with source-owned [route templates](TRACKER.md), without `privateCapture`. Require the hosted script's `routeTemplates` capability. If credential URLs occur in link or form attributes, require `privateUrls` too and mark those elements `data-hoot-private-url`. Use `data-hoot-private` for secret text, QR codes, and other credential displays. A safe page label does not hide a secret somewhere else in the DOM.

Recording permission must come from the site's existing recording controls or an established site-managed permission state. An owner's Hoot Lens login, a page requiring authentication, or a successful script load does not grant visitor permission. Preserve refusals, withdrawal, Global Privacy Control, and Do Not Track. An integration using existing site-managed permission must not write a new visitor choice or show a second recording prompt. Do not invent that permission for an installation test.

Mount the loader outside route-dependent providers. Let the ordinary SDK handle navigation and fresh snapshots. Verify the actual hosted bundle, outgoing synthetic payloads, permitted asset origins, and newly received recordings before claiming readable fidelity. Do not reconstruct old masked recordings from the current page.

## Structure-only profile

Private-page capture records a masked layout, approved workflow clicks, and scrolling with public route templates and fixed control IDs. It is opt-in integration code for an owner who explicitly wants authenticated pages included. It does not identify an application user or make a visit anonymous. Random visit identifiers, coarse browser/device information, layout dimensions, language, timing, and country can remain.

Use a site configured with strict privacy and fixed required visitor consent. Only sites whose custom block selectors are supported by this profile advertise `capabilities.privateCapture: 1` in their public project configuration. The tracker fetches that configuration fresh before observation. An absent capability, a failed request, a different preset, or implied/regional consent prevents this profile from starting. A supplied `options.config` is rejected for this profile, so a caller cannot bypass collector verification.

## Load before initializing

Load the script without `data-project`, `data-consent`, or any queued initialization. Check the capability before giving it a project ID. An old cached script must remain idle rather than ignore an option it does not understand.

```html
<script>
function startPrivateCapture() {
  if (window.Hootlens?.capabilities?.privateCapture !== 1) return;
  (window.hootlens = window.hootlens || []).push(
    ['notice', 'corner-right', 'Help us improve this website by recording a masked view of your visit. Form answers and personal page text stay hidden.'],
    ['consentSource', 'hootlens'],
    ['init', {
      projectId: 'YOUR_PUBLIC_PROJECT_ID',
      buildId: 'release-from-your-deployment',
      privateCapture: {
        routes: ['/', '/people', '/requests/:request'],
        controls: ['nav-home', 'open-report'],
        unmatchedRoute: '/unlisted-page'
      }
    }]
  );
}
</script>
<script src="https://hootlens.com/t.js" async crossorigin="anonymous" referrerpolicy="no-referrer" onload="startPrivateCapture()"></script>
```

For a framework, use its script-load callback and keep the same order. Mount one loader outside route-dependent providers, including error fallbacks that replace the main layout. Do not automatically grant consent. The queue adapter remembers the visitor's choice and applies changes from other open tabs. `['consent', false]` stops observation, aborts pending delivery, discards unsent data, and removes the visit's browser storage. It does not delete recordings already received. `stop` is a pause that can deliver captured data, so it is not a consent-withdrawal command.

## Routes and controls

`routes` holds at most 256 public templates, each no longer than 300 characters. A parameter such as `:request` matches one concrete path segment. Literal routes beat parameter routes. A different concrete record or query still starts a new page view, even when the public template is unchanged. Concrete values remain in browser memory only. Queries and fragments are not sent.

`unmatchedRoute` is an optional literal path used for unknown or not-found pages. Without it, unlisted pages suspend capture. Use a complete, source-derived route catalog and a constant fallback if every page should remain covered. Never build route templates or the fallback from a person, search term, token, or database value.

`controls` holds at most 256 fixed lowercase IDs containing letters, digits, and hyphens. These are an explicit allowlist of `data-hoot-id` values authored in source. They must not contain a person's or record's identity. Only exact, allowlisted workflow controls outside sensitive regions emit clicks. Unapproved clicks are dropped. Markers are not inherited from a container, and an approved marker cannot override a sensitive boundary. No visible text, accessibility label, title, alt text, native ID, class, source location, heading, or region name supplies a click label.

Clicks inside private regions, forms, fieldsets, labels, native answer controls, and ARIA answer controls/groups are dropped, including their descendants and approved markers. No click coordinates, replay node IDs, or offsets from those regions are sent. Keep personal forms, reports, directory details, and attachments inside appropriate private regions as additional defense. Portalled content needs the same policy as ordinary page content. Place nonsemantic custom answer groups and any answer-dependent decorations outside a standard form/group inside `data-hoot-private`; the tracker cannot recognize an arbitrary application-specific choice widget from its appearance.

The collector checks the canonical shape of templates and control IDs. It cannot prove that a site author's constant is semantically public. The source review and allowlist remain the owner's responsibility.

The page fingerprint uses safe structure and approved control markers through the existing page-version system. A private chosen root or any private/form/custom ancestor becomes one fixed token. Ordinary class, native-ID, visibility, dynamic-region, extension, and consent-banner hints do not contribute to this profile. `pageVersion`, DOM region/version strings, and variant overrides are not used by this profile. A CSS-only or state change with identical structure does not create a distinct safe fingerprint. Keep release identifiers tied to the actual deployment; do not use a person's or record's identity as a version workaround.

## Recorded layout and omitted channels

- All ordinary text becomes a fixed mask. Forms, fieldsets, labels, native answer controls, ARIA answer controls/groups, private regions, editable content, media, embedded pages, and custom-element internals become fixed empty placeholders without their original geometry, computed paint, or control markers. This includes checked-radio sibling labels and selected-option paint. Updates inside those boundaries are omitted.
- Class names, CSS selectors and stylesheets, URLs, native IDs, arbitrary data attributes, and input checked/selected state are dropped. Buttons with `aria-pressed` are answer controls and remain opaque after that attribute is removed. A closed vocabulary of computed layout and paint declarations preserves bounded geometry.
- Pointer, mouse, touch, drag, focus, and input replay events are omitted, along with movement heatmaps. Approved workflow clicks keep their real coordinates outside sensitive regions; document scrolling and safe structural layout remain.
- Only allowlisted control IDs outside opaque boundaries survive in replay attributes. The fingerprint reads structure and those approved IDs, not other instrumentation or page text. The release comes from a deployment constant or an automatic asset hash.
- Raw referrers, campaigns, experiment parameters, new-visitor flags, exit destinations, error details, business events, and conversion context are omitted. An enabled ordinary site error setting does not enable errors in this profile.
- The collector requires the site's strict/required policy, removes disallowed metadata channels, and rejects positional or mismatched click selectors, pointer/input replay, movement grids, form state, private-placeholder geometry, classes, stylesheets, and replay attributes before storage. Ordinary strict replay validation still applies.

Answer choices, focus paths, pointer trails, and interactions inside forms/private regions are unavailable for usability review. Surrounding layout and timing can still expose application state, including conditional content outside those boundaries, so review custom widgets and sensitive regions in source and with synthetic payloads. The result is a skeleton replay, not a faithful view of a person's page content. Safe navigation controls and layout behavior remain useful for usability review, while photos, charts, form contents, and readable personal content are deliberately unavailable. This profile does not change the behavior of existing ordinary full, balanced, or strict captures.


## Ordered intake and bounded privacy state

The collector remembers only compact node ancestry, opaque/tombstoned nodes, document roots, and approved control markers in internal recording state. Unknown, retired, or opaque targets cannot receive style/text updates, child additions, scroll positions, or spatial clicks. Approved clicks must identify the same active safe replay control. Observed private boundaries remain opaque for the rest of that recording, including when a role, private marker, or matching selector attribute is removed, or the DOM node is moved. The sender replaces newly opaque replay nodes with a fixed empty placeholder and clears their old paint and children. Removing a private marker, reusing a private node ID, or taking a later snapshot cannot erase that boundary or the privacy history of its prior descendants. Recording rotations reset bounded numeric identities but retain weak DOM privacy memory for the same tracker, including layout and idle rotations. Previously opaque DOM nodes cannot reappear merely because the viewport or session changes. Only document scroll replay is retained; scrolling inside elements is omitted. Controls that disappear or become private before their pending click is resolved or its batch is sealed are omitted. A successful action that removes its control can therefore omit that click while preserving its safe layout change.

This ledger is committed with the recording's existing atomic write. It is never included in recording listings or detail responses and does not require downloading prior replay batches. Private capture requires sequence zero with a full snapshot, then contiguous immutable batches. A missing predecessor returns retryable HTTP 429 with `private_sequence_pending` and `Retry-After: 1`. Normal SDK delivery and private unload delivery preserve this order. An unload budget never skips an older batch, so a page closed before acknowledgement can lose later unsent evidence.

The ledger retains at most 8,192 distinct node IDs, 100 replay ancestry levels, and 192 KiB for one page view. Privacy tombstones are never evicted. Reaching either bound stops capture with HTTP 413 and `private_replay_limit`, exposed as the `capture-limit` status reason. The SDK also stops with `capture-limit` if delivery would evict a private batch from its bounded queue or a batch is too large to send. These failures are diagnostic errors, not successful empty recordings. An existing ordinary recording cannot switch to this profile, and a private recording cannot downgrade to ordinary capture. Ordinary captures keep their existing delivery behavior.


Recording metadata exposes `captureCoverage: 'private-v1'` so a workflow click count or empty movement map cannot be mistaken for complete interaction coverage. Cohort summaries and aggregates expose the same value when every counted page view used this profile, or `captureCoverage: 'mixed'` when ordinary and private captures contribute together. Mixed movement maps cover only the ordinary subset. Ordinary/legacy-only responses omit the field. Existing historical aggregate counts retain this coverage when individual recordings expire or are deleted, and automation retraction removes the private count with its segment count.


## Supported custom block selectors

Private-v1 accepts each `privacy.blockSelectors` entry as a literal tag, or an optional literal tag followed by exactly one class, ID, or presence/equality attribute selector. Examples: `.private`, `#private-region`, `[data-private-answer]`, `[data-private-answer="yes"]`, and `section.private`. Names and values use the bounded ASCII grammar in `privateBlockSelector`; CSS escapes and nonliteral forms are unsupported. Multiple entries are an OR of independently observable boundaries.

Descendant selectors, combinators, comma lists, compound conditions such as `.answer-mode[data-private-answer]`, multiple attributes/classes, and pseudo-classes such as `:has()` are refused. Their transient history cannot be reconstructed from one attribute record. Put `data-hoot-private` on the containing element when a region needs a more complex privacy rule. Native controls, ARIA answer/group roles, and `data-hoot-private` retain their implicit protection.

An unsupported policy removes the collector's private capability, stops private initialization before DOM parsing or observation, and rejects private batches before storage. The required fresh public configuration request can still occur; no capture request follows. Private initialization never falls back to ordinary capture. Ordinary capture keeps its existing selector support.


Explicit private table bodies and dialog containers serialize as single opaque roots. Their hidden descendants do not consume the recording's node budget. A truly oversized initial public structure or a long-lived view that accumulates more than the hard identity/history bounds still ends with an explicit `capture-limit` diagnostic. Automatic high-water rollover is deferred: recording continuations need separate accounting so they cannot inflate page-view or quota counters.

Private metadata queries project the bounded replay ledger out at the Firestore read, using a small persisted coverage flag to retain notices. Private configuration requests use `cache: 'no-store'`, including policy refreshes. Invalid paint declarations are filtered individually, preserving other safe layout declarations.


These conservative boundaries also omit tabs carrying `aria-selected`, toolbars/button groups carrying `role="group"`, progress/stepper controls carrying `aria-valuenow`, and all custom-element hosts. Approved controls inside those opaque boundaries are not counted.
