# Resonance measurement

`@sonordev/site-kit/resonance` connects the site's consent manager to scoped
measurement receipts and approved advertising destinations. `SiteKitLayout`
mounts its childless runtime alongside the existing analytics provider. Page
content and managed forms still render when measurement is unavailable. The
small shell blocks unresolved collection immediately; its controller loads
after the page reaches idle or the visitor interacts.

On dynamic routes with Next.js Cache Components, the layout gives the
pathname-dependent runtime its own Suspense boundary. Measurement stays
blocked while it's unresolved, and page content renders independently.

## First-party privacy preferences

For a site without a consent manager, mount `ResonanceConsentPreferences` once alongside `SiteKitLayout`.

```tsx
import { ResonanceConsentPreferences } from '@sonordev/site-kit/resonance'

<ResonanceConsentPreferences privacyUrl="/privacy" />
```

Visitors can choose visit measurement and advertising separately, decline optional purposes, and reopen Privacy choices to withdraw. Both optional choices start off. The component uses the existing adapter registration and keeps an already registered consent manager in charge.

A choice needs a fresh, enabled native configuration. The component waits while settings are unavailable. Stored preferences belong to the exact site, project, configuration and policy revisions. They expire after 180 days by default, with `maxAgeDays` configurable from 1 to 180. Native proof expiry is checked independently. If browser storage is unavailable, the choice lasts for the current tab. Withdrawal and cross-tab changes notify the existing runtime.

This component records preferences through the native consent owner. Configure the real page rules, destinations and approved visit feed before verifying advertising delivery.

### Branded footer settings

Mount the same leaf in the footer to keep one preference owner. Existing callers retain the floating launcher and automatic dialog. The optional presentation props are `launcherPlacement` (`floating` or `inline`), `launcherLabel`, `promptMode` (`dialog` or `panel`), and `className`.

```tsx
<ResonanceConsentPreferences
  launcherPlacement="inline"
  launcherLabel="Privacy settings"
  promptMode="panel"
  className="workshop-consent"
/>
```

Panel mode presents equal Accept optional, Reject optional and Customize actions when the native configuration is ready and no current preference exists. It opens without moving focus. Dismissal lasts for this mount and records no choice. Customize opens the existing native dialog. Closing returns focus to the initiating control, or the footer launcher if that control disappears after a choice. Unsaved settings remain visible when configuration becomes unavailable. Every save rechecks the owner, and a failed save stays open for review and retry. The saved message describes the local preference on this page.

Style the root with inherited `--sk-consent-surface`, `--sk-consent-foreground`, `--sk-consent-muted`, `--sk-consent-border`, `--sk-consent-accent`, `--sk-consent-focus`, `--sk-consent-radius`, and `--sk-consent-font` tokens. The font token accepts a complete CSS font value, including `inherit`. Prompt placement uses `--sk-consent-prompt-inset` and `--sk-consent-prompt-bottom`. Stable slot classes are `sk-consent__launcher`, `sk-consent__prompt`, `sk-consent__dialog`, `sk-consent__actions`, and `sk-consent__status`.

```css
.workshop-consent {
  --sk-consent-surface: #18201e;
  --sk-consent-foreground: #fff8e8;
  --sk-consent-muted: #d8d0bf;
  --sk-consent-border: #8b806a;
  --sk-consent-accent: #f0cd84;
  --sk-consent-focus: #f0cd84;
  --sk-consent-font: inherit;
}
```

For a site using the shared Echo chat, `--sk-echo-offset-bottom` is the existing placement input for the launcher and popup. A narrow layout can reserve the 60px launcher plus a gap beneath the privacy prompt by setting its bottom token from that value, safe-area inset and `--sk-vv-layout-gap`. Keep the existing CTA owner in charge of writing the Echo offset.

`[data-sk-echo-panel]` is the supported styling marker for an open Echo panel. At a narrow breakpoint, a site can hide only its nonmodal `sk-consent__prompt` while that marker exists, using a CSS `:has()` rule. The native privacy dialog remains independent. Verify chat open and closed states, short viewports, text zoom and focused content before shipping the placement.

## Connect your consent manager

Register one adapter for the consent manager your site already uses. The
adapter reads its actual current preferences and subscribes to changes. It
doesn't create a banner or choose preferences for the visitor.

```tsx
'use client'

import { useEffect } from 'react'
import {
  registerResonanceConsentAdapter,
  type ResonanceConsentAdapter,
} from '@sonordev/site-kit/resonance'

// Map these methods to your site's maintained consent manager.
export function ConsentConnection({ adapter }: { adapter: ResonanceConsentAdapter }) {
  useEffect(() => registerResonanceConsentAdapter(adapter), [adapter])
  return null
}
```

The adapter supplies `requiredPurposes`, `read()` and `subscribe(onChange)`.
`read()` may be asynchronous. It returns:

```ts
{
  valid: true,
  observedAt: '2026-10-24T12:00:00Z',
  source: 'cmp', // or 'user_preference'
  choices: {
    analytics_storage: 'granted',
    ad_storage: 'denied',
    ad_user_data: 'denied',
    ad_personalization: 'denied',
  },
}
```

Each purpose can be `unknown`, `granted`, `denied` or `withdrawn`. Return
`valid: false` when the manager can't establish current preferences. Include
`analytics_storage` in `requiredPurposes` when your site's policy requires it.
Notify subscribers as soon as a preference changes. Dispatch stops before an
asynchronous read completes.

The initial config read creates no measurement subject. A valid current
preference observation can create a new subject with server receipts. Configured
collection waits for that receipt response before activating. The
opaque proof stays in memory and never enters a URL or browser storage. Its
30-minute renewal doesn't prompt the visitor again or change consent. After
memory is lost, an actual valid preference observation can establish a new
subject. Visitor IDs and old receipt IDs can't recover the previous subject.

## What can send?

Advertising requires a current server proof and grants for `ad_storage`,
`ad_user_data` and `ad_personalization`, plus any additional configured purpose.
Explicit analytics denial stops first-party tracking. Required but unknown
analytics consent also stops it. Legacy sites without a configured requirement
keep their first-party analytics behavior, recorded as unknown policy rather
than a consent grant.

The existing analytics provider remains the pageview counter. Resonance
occurrences supply separate retargeting and experiment evidence. Resource
interest uses approved resource IDs. It doesn't submit raw query strings,
form values or DOM text. Accepted forms become conversions on the server.

Withdrawal clears pending occurrence retries and prevents delayed work from
crossing into a later grant. Granting again records the current page with a
new occurrence ID. It doesn't replay denied page history. Failed occurrence
requests can retry with the same event ID while their scope and consent remain
current. Already delivered provider requests can't be retracted.

## Advertising click lineage

After the current advertising grants and scoped proof are available, a new
page-view occurrence can carry one `gclid`, `gbraid` or `wbraid` from the current
URL. The live pathname must match the navigation being recorded. Duplicate or
mixed identifiers, whitespace, control characters and values longer than 1024
characters leave matching data absent. The length is an application transport
bound. Other occurrence kinds don't carry click identifiers.

This path works when analytics consent is denied and the configured advertising
policy doesn't additionally require it. The runtime doesn't retain a query from
before consent or recover one that's been removed. Retries use the same event
within its current consent and scope; withdrawal clears them. An already
recorded page isn't updated with a later identifier. No click identifier enters
browser storage or public status output.

An observed identifier doesn't prove a paid click. The server owns the link to
the actual accepted form and checks qualification and current advertising
consent before export. Deploy its accepting occurrence parser before enabling
the browser change.

## Same-page Meta cookie observation

After the required advertising grants and actual scoped consent proof are
available, the existing page-view occurrence can include an SDK-reported
`metaCookieObservation`. The runtime reads only actual `_fbp` and `_fbc`
cookies from the current page. It doesn't construct `fbc` from a click URL,
write cookies, retain a cookie jar, or install Meta's browser SDK.

Matching requires a unique configured page resource for the current path.
The occurrence uses that exact native resource reference and the same
navigation's `pagePath`. Missing or ambiguous mappings leave the base view
unmatched. Cookie reads don't run before persisted consent, for an unmatched
page, or after consent/proof/config expiry. The live pathname must match
before and after reading. Duplicate cookie names are omitted. The shared
occurrence parser rejects invalid matching values; valid values contain
1–1024 Unicode characters without whitespace or controls. Invalid optional
matching leaves the valid base occurrence intact.

This enriches one existing page view. It doesn't add another page-view
counter, create a business conversion or claim that a person rendered the
page. Existing content-interest evidence remains separate. Retries retain
the exact original event ID, navigation, time and captured body without
reading cookies again. Repeated consent notifications or configuration-cache
refreshes don't update an already recorded view. Withdrawal clears pending
work; a later grant can record only the current page with a new event.

Raw matching isn't forwarded through the provider event allowlist, public
status or browser storage. The accepting API must support the occurrence
shape and verify the actual owned page, current source rules and consent
before enabling this browser version. An accepted capture doesn't grant a
feed, prove native audience admission or establish provider delivery. Those
checks and the original grant/claim/time fences remain server-owned.

## Provider capabilities

Google's allowlisted tag loads after the required grants. Automatic pageviews
are disabled. Browser events use an explicit safe page location, empty
referrer and approved resource fields. The runtime has no browser conversion
action. A pre-existing Google tag or tag manager blocks installation until
compatibility is resolved, preventing a second installation.

## Meta browser pixel

The browser pixel exists for one reason: so Meta's own first-party cookies
(`_fbp`, and `_fbc` when a visitor arrives from a Meta ad click) exist on pages
you've approved. It sends no browser events. The only Meta event for a visit is
the server's Conversions API `PageView`, and the runtime exposes no `track`
capability. It passes no user data to the pixel, reads no form field, page title
or page metadata, and attaches no click or submit listener. It never writes
`_fbp` or `_fbc` itself and never builds one from a click URL.

**It's switched off in this release.** Site Kit 8.9.0 can't start the pixel.
This build implements no loader controls revision, so even a capability
witness from the server can't lift `meta_browser_collection_unverified`, and
`getResonanceMeasurementStatus()` keeps reporting it. A later release turns the
loader on. Everything below describes how it will behave then.

When it's on, a site's pixel starts only if the server's configuration carries
a capability witness for that site. The witness names the loader controls
revision and the policy revision it was issued under, and the runtime refuses a
revision it doesn't implement, a witness from an older policy, and a witness
issued more than 48 hours ago, and it withdraws a running pixel if the witness
lapses mid-session. A site without a witness loads nothing. The lift is per
site, and a configuration refresh that removes the witness, the destination or
`enabled` withdraws the pixel. The longest that can take is the configuration's
own expiry.

**What loads, and when.** Nothing exists before consent: no script, no
`window.fbq`, no cookie, no storage key. After the three advertising grants
(`ad_storage`, `ad_user_data`, `ad_personalization`) and a current scoped proof,
and on a page that's eligible and clean, the runtime queues consent revoked,
automatic configuration off and `init` with only the dataset ID from your
signed configuration, then inserts
`https://connect.facebook.net/en_US/fbevents.js` (async, no referrer). Once the
script has run, and the consent fence still holds, it grants consent. Each step
rechecks the fence. A grant that's queued ahead of the script doesn't release
the pixel's own lock, which is why the grant waits.

**Which pages.** A page is eligible only when its configured page resource has
`metaEligible: true`, which a person with owner or admin standing sets in
Sonor. It defaults to false, an agent can't set it, and the browser never
decides it. The same flag gates the cookie observation described above. The
runtime also refuses, with `meta_page_not_eligible`: any frame, a private
preview, a local host, a Sonor application host, a page carrying
`data-sk-authenticated`, and these paths and their children: `/admin`,
`/account`, `/dashboard`, `/login`, `/signin`, `/signup`, `/checkout`, `/cart`,
`/portal`, `/api` and `/preview`. Put `data-sk-authenticated` on the document in
any layout that sits behind a login.

**Clean URLs only.** The pixel reads the page URL and referrer on its own, so
the runtime doesn't start it, and pauses it on a single-page navigation, unless
the query holds only `fbclid`, `sonor_source` or `utm_*` keys with plain
values, there's no fragment, and the referrer is empty, an origin-only
cross-site address, or a same-site address whose path isn't on the denied list above and whose query follows the same rule.
Otherwise the status shows `meta_url_not_clean`. There's no rewriting.

**One pixel per page.** A page with its own `fbq`, a Meta script tag, a pixel
image or `noscript`, or a tag manager gets `meta_duplicate_installation` and no
second pixel. A tag manager clears only through an approved attestation in the
signed configuration. A changed dataset ID or scope under a running pixel shows
`meta_scope_change_requires_reload`, and more than one Meta destination shows
`meta_multiple_destinations`. A script that's blocked or fails shows
`meta_load_failed`. It isn't retried and no image or `noscript` fallback is
added.

**Withdrawal.** When consent, the proof or the configuration lapses, the runtime
sets the pixel's consent to revoked, stops issuing commands and, once the pixel has
run, expires `_fbp` and `_fbc` on the host and its parent domains, whether or not
consent was ever granted on that page. Code that has already run can't
be retracted, so the guarantee is that nothing further is sent. A later grant
starts fresh and never replays an earlier visit.

**For your site.** Your consent manager's vendor list and privacy notice must
name Meta, and the Conversions API user agent has its own consent wording.
Sites that ship a Content Security Policy need `script-src` for
`https://connect.facebook.net`, plus `connect-src` and `img-src` for
`https://connect.facebook.net` and `https://www.facebook.com`. Site-kit's
`securityHeaders` emits none of these. First-party receipts alone don't prove
provider audience admission, conversion matching or attribution.

Read `getResonanceMeasurementStatus()` to inspect the current policy and
provider blockers. `advertisingPermitted` describes consent eligibility. It
doesn't establish provider delivery or an installed campaign.

Managed experiments require the server's current assignment ID, experiment
revision and site. Without that witness, the existing experiment component
shows its control content. Its exposure reference records the assignment
actually rendered. Forms remain usable with measurement denied or unavailable.

## Other layouts and verification

A custom layout can mount `<ResonanceRuntime site="example.com" ... />` as a
childless sibling after publishing the existing browser-safe site credential.
Use the established server credential component. Don't expose a raw API key.
Mount one runtime per document.

Verify unknown, grant and withdrawal states against your actual consent
manager before enabling a campaign. Check preview suppression, navigation,
scope changes and failed requests using intercepted provider URLs. A built or
packed package isn't proof that a site installed it or that a provider accepted
an event.

### Current config and public source markers

Config freshness renews through a public read. An unchanged revision preserves
current consent; it doesn't create another subject or prompt. A changed config
or policy stops the previous scope and reads actual current preferences before
establishing the new scope. A failed lookup can't turn a configured requirement
into legacy permission. Advertising stops when proof or config expires.

The runtime reads only the bounded `sonor_source` parameter for campaign source
mapping. It forwards that marker on a current permitted occurrence. The server
verifies its signature and approved plan/destination scope. Other query fields
don't become measurement payload. A marker authenticates reviewed source
mapping, not a paid click or causal result. A visit made before consent isn't
replayed after the visitor has moved to another page.

The actual managed form submit refreshes its optional proof at each attempt.
Its accepted exposure reference comes from the measurement response. Analytics
and advertising purposes remain separate, and canonical form conversion stays
on the server. Compatible Google installations created by this runtime can be
reused for the same project/site and exact tag set. Changed scope or unknown
installations expose a blocker instead of initializing another tag.

The controller registry is shared by status observations, rendered experiment exposures and per-attempt form context. These readers use the same current policy and proof; none creates a grant. Deferred transport execution rechecks its current permission fence before sending, including after loading or credential refresh.
