Skip to main content

Consent Gating

Opt-in, off by default for every company. When off, the loader behaves exactly as it always has (identification/enrichment, live view, and rrweb recording initialize immediately). When on, those features wait for an affirmative per-category consent signal. Chat is never gated.

Implemented in the loader (backend/public/widget.js, the ConsentGate object), configured per company via companies.consent_gating, edited in Settings → Widget → Install (frontend/src/views/settings/widget/tabs/InstallTab.vue).

Schema — companies.consent_gating (strict subdoc on CompanyModel)

FieldDefaultMeaning
enabledfalseMaster switch
block_until_decisiontruetrue: hold session creation + widget render until the visitor answers the banner. false: load immediately, default all-accepted, banner still shows
categories.identificationtrueAsk for the Identification category
categories.behaviourtrueAsk for the Behaviour category
categories.calls_recordingsfalseReserved — accepted and persisted by the endpoint, not yet enforced anywhere
texts.*built-in copytitle, banner_body, prefs_body, identification_label/_desc, behaviour_label/_desc — empty string falls back to loader defaults, max 1000 chars each
colors.*accent #111111, background #ffffff, text #111111Banner/modal theming; loader accepts valid hex only (bad values fall back — no CSS injection)

Only categories the company asks for gate their feature or appear in the Preferences modal (ask.<cat> = enabled && categories.<cat> in ConsentGate.configure()).

What each category gates

CategoryGates
identificationSnitcher/Apollo visitor identification — enforced server-side: session create sends tracking_consent: ConsentGate.identificationAllowed(); UserSessionController skips Snitcher (and therefore Apollo) when the company gates identification and consent isn't true
behaviourSession recording, live and replay — the loader only loads rrweb/live_session.js/recording scripts after Behaviour consent (ConsentGate.loadRecording()), so ungranted sessions capture nothing
calls_recordingsReserved, built later
(chat)Never gated — window.knockKnockConsent.chat is always true

A late grant is forward-only: ConsentGate.onConsentUpdated() calls POST /user-session/:session_id/grant-tracking (mid-session Snitcher/Apollo opt-in) and starts recording from that point; nothing is backfilled.

Endpoint

PUT /company/:_id/consent-gatingCompanyRoutes.jsCompanyController.updateConsentGating (JWT-guarded like the other company routes). Accepts partial { enabled, block_until_decision, categories, texts, colors }; writes dot-paths only (never clobbers the subdoc); rejects non-hex colors with 400.

Host-page contract (window.kkSetConsent)

Defined at the very top of the loader so a site's consent tool can call it as soon as the loader is present:

// boolean → applies to identification + behaviour
window.kkSetConsent(true)
// or partial object
window.kkSetConsent({ identification: true, behaviour: false })
  • State lives on window.knockKnockConsent = { chat: true, identification, behaviour }. The loader initializes it with window.knockKnockConsent || {...} — a host page may pre-seed the object before the loader loads (pre-init safe path); kkSetConsent itself only exists once the loader script has run.
  • Every call dispatches a kk-consent-updated event; the loader listens and applies mid-session changes (grant-tracking + recording load).
  • Visitor decisions from the bundled banner persist in localStorage under kk_consent (ConsentGate.STORAGE_KEY); returning visitors get their saved choice re-applied without a banner.

When enabled and no stored choice: banner (#kk-consent-banner, Accept All + Preferences) → optional preferences modal (#kk-consent-modal, per-category toggles; a non-asked category renders as a locked row). block_until_decision holds ConsentGate.awaitDecision() — session creation and widget render — until markDecided(). Non-blocking mode defaults both categories to accepted and still shows the banner so the visitor can change their mind. Sites with their own CMP drive kkSetConsent instead — any call fires kk-consent-updated, which marks the decision made and (once the session exists) replays it as a mid-session change.