|
| 1 | +# Cookie consent runtime |
| 2 | + |
| 3 | +This tag implements the scripts handoff accompanying [Forms PR #5811](https://github.com/trysurface/surface_forms/pull/5811), reviewed at `7d525ac12caf9f8a8475551e907929d46dbc4dc0`. Deploy a compatible Forms API/renderer together with this bundle. This draft has automated coverage; staging browser/server QA below remains a release gate. |
| 4 | + |
| 5 | +## Customer integration |
| 6 | + |
| 7 | +Configure Cookie tracking, Ad & conversion tracking, and Surface analytics to **On consent** for Paddle's form. After both the tag and the host CMP are ready, report the CMP's current saved choices: |
| 8 | + |
| 9 | +```js |
| 10 | +window.SurfaceSetConsent({ |
| 11 | + cookieTracking: true, |
| 12 | + adTracking: true, |
| 13 | + surfaceAnalytics: true, |
| 14 | +}); |
| 15 | +``` |
| 16 | + |
| 17 | +Every call replaces the entire snapshot. Only literal `true` grants; omitted fields are false, including `cookieTracking` in older two-field integrations. Call again on changes and withdrawals. Replay saved choices every visit. Surface keeps consent in memory (`__SURFACE_CONSENT__` and a `surface:consent` DOM event); the CMP owns persistent consent and regional applicability. There is no built-in CMP or country detection. Do not reload the page on withdrawal. |
| 18 | + |
| 19 | +For three **On consent** policies: |
| 20 | + |
| 21 | +| Report | Host behavior | |
| 22 | +| --- | --- | |
| 23 | +| C false, any A/S | No optional cookie/storage reads, recognition, journey or parent advertising | |
| 24 | +| C only | Visitor recognition; no marketing or journey cookie snapshot | |
| 25 | +| C+A | Recognition, eligible marketing cookies, enabled Dreamdata identity, configured supported conversions | |
| 26 | +| C+S | Recognition, page journeys and Surface measurements | |
| 27 | +| C+A+S | All permitted purposes | |
| 28 | + |
| 29 | +Form rendering, explicit prefill, current-page URL/UTMs, validation and business submission continue independently. An operational `/lead/identify` request is expected for an external HTML form even without C: it uses a random interaction UUID and obtains a signed form-scoped capability, without a cached visitor/session/fingerprint. |
| 30 | + |
| 31 | +**Allowed** and missing modes on successfully loaded legacy settings preserve their Forms semantics. They can permit tracking without a positive reported answer. Use **On consent** to require explicit consent. An unavailable/malformed settings response denies optional work; it is never treated as legacy `{}`. |
| 32 | + |
| 33 | +## Runtime and data boundaries |
| 34 | + |
| 35 | +- `consent/runtime.ts` registers form contexts and fetches `{origin}/api/v1/public/forms/{formId}/runtime-config`, credential-free and `no-store`. It validates form/environment/settings, deduplicates concurrent loads, times out after eight seconds and revalidates on restored documents. Policy fetching never uses browser storage. |
| 36 | +- `store/store.ts` registers direct, dynamic and tag-created `/s/<formId>` frames on recognized origins, plus lazy embed declarations. Messages require the registered element's window and exact declared origin; detached, changed-src and unrelated windows cannot use that registration. Consent precedes store/readiness replies. Load handlers coexist with spinners. Each frame has separate explicit prefill and conversion deduplication state. |
| 37 | +- Shared host work uses the intersection of registered forms. Pending/error contexts deny; multiple environments/API origins deny because the existing browser keys have no tenant namespace. Per-frame snapshots are filtered again. Declared lazy forms participate before navigation. |
| 38 | +- Gates precede cookie getters, tracking storage and fingerprinting. Managed marketing snapshots permit only `hubspotutk`, `__hstc`, `__hssc`, `__hssrc`, `_ga`, `_ga_<alphanumeric>`, `_gid`, `_gcl_aw`, `_gcl_au`, `_fbp`, `_fbc` under C+A; only `surface_journey_id` is forwarded under C+S. `surface_recent_visit` remains internal. C alone reads neither snapshot. Explicit C replaces `trackCookie`; loaded legacy-only pages retain its cookie-map compatibility. A managed form on the page constrains legacy snapshots too. |
| 39 | +- Dreamdata requires `settings.dreamdata.enabled` and C+A. Source precedence is native localStorage, native cookie, `dd_aid`, compatibility localStorage, compatibility cookie. IDs are normalized and checked. The selected value travels in the renderer's `cookies.dd_anonymous_id` transport slot, never a new cookie. This transport does not add outbound Dreamdata calls. A denied `dd_aid` is removed from forwarded URL copies too. |
| 40 | +- Tag-created iframe URLs strip protocol-owned tracking identifiers and capabilities before first navigation. Current business query parameters/UTMs and explicit prefill stay available. Optional recognition cache includes verified form/API/environment scope, and never spreads an identify response/token into localStorage. Older unscoped cache records are ignored. |
| 41 | +- Native iframe renderers own their operational identity. The tag sends readiness with empty optional identity when denied, rather than creating a competing parent operational session. The tag's permitted recognition requests are form-scoped. |
| 42 | +- External forms allocate one in-memory interaction per document/form/API, deduplicate pending identity, await a capability, serialize writes using the returned response ID, and sample full reported consent immediately before sending. Established capabilities survive consent transitions and renew conservatively before the server's 12-hour expiry. Identity/write failures retain answers for retry and emit `surface:submit:error`. Repeated final clicks do not create another response. Tokens are never logged, cached, sent to analytics or put in URLs. |
| 43 | +- Journey events and external view/start measurements require C+S and carry actual form scope/full reported consent. Optional requests capture a generation and recheck before sending/applying results; revoke/regrant cannot restore obsolete results or replay denied-interval events. Functional route listeners remain active when journeys stop. |
| 44 | +- Registered valid conversion messages are acknowledged immediately even on denial to prevent the renderer's 250ms fallback. Provider/event configuration must match the requesting form. C/A consent-managed pages support only GA4 and Meta; X/LinkedIn/OpenAI remain off. Withdrawal sets owned GA opt-out flags and Meta revoke commands, and removes tag-owned queued commands during slow script loads. Meta conversions target the configured pixel. The tag does not add a HubSpot host loader; host-installed vendors remain the CMP's responsibility. GTM/Speed Insights restrictions inside the form belong to the renderer. |
| 45 | +- Withdrawal clears optional in-memory state and named Surface storage (`surfaceLeadData`, `LOCAL_SURFACE_LEAD_IDENTIFY_DATA`, journey cookies and `<formId>_pa`) at reachable host scopes. S withdrawal clears journeys while C may remain granted. Cross-origin iframe cleanup is owned by Forms. It does not clear all storage, CMP/login cookies or business state. Open-trigger config caching is now memory-only. |
| 46 | + |
| 47 | +## Release limits to resolve |
| 48 | + |
| 49 | +1. **Site-id-only pages have no form scope and therefore no optional host tracking.** An explicit declared journey/form scope requires a separate installer/API contract; the tag cannot infer a safe form from an environment. |
| 50 | +2. **Mixed SDK/tag pages need integration work in Forms** if page-wide policy intersection is required across SDK instances. PR #5811 shares consent but does not publish an SDK policy registry. This tag's intersection covers registered frames, lazy embeds and its external HTML forms. Do not enable overlapping SDK/tag journey producers without testing ownership and deduplication. |
| 51 | +3. **Direct iframe URLs must be authored cleanly.** The tag cannot undo identifiers already transmitted before it loaded. Scope validation uses the host's iframe `src` declaration; if embedded content navigates itself to another form without updating that declaration, the current protocol supplies no authenticated form-ID handshake. Use a new registered iframe for a new form, or add that handshake in Forms before supporting such navigation. |
| 52 | +4. **Previously sent requests cannot be recalled.** In particular, host withdrawal alone cannot cancel an already queued Dreamdata server job without a later response update. Capability invalidation/expiry also needs staging verification against the API: the scripts client never substitutes a persistent session on failure. |
| 53 | +5. **Separate legacy/standalone scripts are outside this bundle.** Audit existing installations of `surface_tracking.js` and `old/`; do not load an older tracking tag alongside this one. Duplicate loads of the new bundle reuse its active runtime and preserve sessions. |
| 54 | +6. **Settings fail closed here.** Reconcile any Forms SDK successful-fetch path that turns absent settings into `{}` before claiming identical error handling across every entry point. |
| 55 | + |
| 56 | +These limits and the customer's CMP/settings must be reviewed before claiming the Paddle installation is ready. This code alone does not establish legal compliance for the customer's full page or independently installed vendors. |
| 57 | + |
| 58 | +## Verification and rollout |
| 59 | + |
| 60 | +Run: |
| 61 | + |
| 62 | +```sh |
| 63 | +pnpm typecheck |
| 64 | +pnpm test |
| 65 | +pnpm test:unit |
| 66 | +pnpm build |
| 67 | +git diff --check |
| 68 | +cmp surface_tag.js surface_embed_v1.js |
| 69 | +``` |
| 70 | + |
| 71 | +The journey suite was migrated from the separate node runner into Vitest, so CI now exercises its existing attribution cases plus lifecycle/race checks. Policy expectations were generated from the pinned Forms source for 27 modes × 8 reports; the actual SDK bridge is a fixture. See [fixture provenance and sync procedure](../test/fixtures/README.md). |
| 72 | + |
| 73 | +Use `test/consent.html` against the compatible Forms staging deployment. It has all eight snapshots, a second-frame control and an external HTML form harness. Use test form IDs, destinations and credentials. Before release verify: |
| 74 | + |
| 75 | +- No announcement, reject, C-only, C+A, C+S, full grant, withdrawal/regrant, reload and a policy API failure. Check cookie/storage **reads** as well as final storage and network output. |
| 76 | +- All embed shapes, delayed CMP readiness, direct/dynamic frames, multiple forms with conflicting settings, remounts and custom origins. Confirm no pre-navigation identifier leakage and correct consent-first handshake. |
| 77 | +- Slow fingerprint, identify, runtime-config and vendor loads during withdrawal. Confirm no stale cookie, identity or conversion is restored afterward. |
| 78 | +- Native and HTML-form answers still submit with one current interaction; inspect saved response consent and minimized metadata. Exercise routing, booking and CRM sync with test destinations. Retry a failed/expired capability without losing draft answers. |
| 79 | +- Verify Dreamdata source precedence and saved private claims, then the documented server-job withdrawal boundary. |
| 80 | +- For Paddle, verify the CMP's mapping and saved-choice replay on a staging copy of `/demo`; inventory other vendors and standalone Surface assets. No production form submission or customer settings change is part of this draft. |
| 81 | + |
| 82 | +Publish a versioned compatible tag only after those checks and coordinate Forms deployment. Rollback must keep optional tracking disabled for managed forms while retaining business submission; do not restore an old allow-all tag to recover tracking. |
0 commit comments