Add option to skip recording GPC consent - #1784
Conversation
A visitor sending Global Privacy Control has the opt-out applied and recorded as a consent decision: a visitor id in localStorage, the consent cookie, and a consent record sent to the API. That happens even when no banner UI is mounted, so a detection-only embed writes consent records it never asked to manage, and a site running its own consent stack ends up with a second, narrower record of the same decision. Add gpcRecord to CookieBannerClientOptions, exposed as gpc-record="false" on the components and data-gpc-record on the script tag. When false the opt-out is still applied in the page -- categories denied, resources blocked, integrations updated -- so the flag cannot be used to ignore a GPC signal; only the persistence and the transmission are skipped. The "true"/"false" attribute parsing gcm-enabled introduced is now shared, since both flags need the same warn-and-default behaviour. Signed-off-by: Gren Paut <90505696+AnotherDork@users.noreply.github.com>
There was a problem hiding this comment.
3 issues found across 9 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/cookie-banner/src/client.ts">
<violation number="1" location="packages/cookie-banner/src/client.ts:201">
P3: The new tests only cover `resolveBooleanAttribute` parsing, not the actual behavior this PR introduces. There is no client-level test asserting that `gpcRecord: false` skips the consent cookie, visitor-id creation, and the `{bannerId}/consents` POST, or that the default `true` still records. Add a client test for the GPC branch so the privacy-relevant toggle can't silently regress.</violation>
<violation number="2" location="packages/cookie-banner/src/client.ts:204">
P2: When `gpc-record="false"` is used with the themed or headless banner, this branch leaves `CookieBannerClient.hasConsent` false, so the banner opens for a GPC visitor and can accept non-necessary categories after the opt-out was applied. Preserve the pre-change hidden/GPC-applied UI state through a non-persisting consent state instead of leaving the client indistinguishable from an unresolved visitor.</violation>
<violation number="3" location="packages/cookie-banner/src/client.ts:204">
P2: With `gpc-record="false"` in a deployment that renders visible banner UI (themed/IIFE always render), a GPC opt-out visitor now sees the consent banner. The new `else` branch calls `this.activate(gpcData)` instead of `this.gpc()`, so `this.consent` is never set and `hasConsent` stays `false`; `cookie-banner-root.initClient()` then falls through to `setState(initial_state)` instead of `setState("hidden")`. The default path hides the banner for GPC visitors (consent recorded), so the flag changes banner visibility for GPC visitors rather than only suppressing the record. If a GPC visitor should still have the banner suppressed, base the hide decision on `gpcApplied` as well as `hasConsent`.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| if (this.gpcRecord) { | ||
| this.gpc(); | ||
| } else { | ||
| this.activate(gpcData); |
There was a problem hiding this comment.
P2: When gpc-record="false" is used with the themed or headless banner, this branch leaves CookieBannerClient.hasConsent false, so the banner opens for a GPC visitor and can accept non-necessary categories after the opt-out was applied. Preserve the pre-change hidden/GPC-applied UI state through a non-persisting consent state instead of leaving the client indistinguishable from an unresolved visitor.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cookie-banner/src/client.ts, line 204:
<comment>When `gpc-record="false"` is used with the themed or headless banner, this branch leaves `CookieBannerClient.hasConsent` false, so the banner opens for a GPC visitor and can accept non-necessary categories after the opt-out was applied. Preserve the pre-change hidden/GPC-applied UI state through a non-persisting consent state instead of leaving the client indistinguishable from an unresolved visitor.</comment>
<file context>
@@ -194,7 +196,13 @@ export class CookieBannerClient {
+ if (this.gpcRecord) {
+ this.gpc();
+ } else {
+ this.activate(gpcData);
+ }
this._gpcApplied = true;
</file context>
| if (this.gpcRecord) { | ||
| this.gpc(); | ||
| } else { | ||
| this.activate(gpcData); |
There was a problem hiding this comment.
P2: With gpc-record="false" in a deployment that renders visible banner UI (themed/IIFE always render), a GPC opt-out visitor now sees the consent banner. The new else branch calls this.activate(gpcData) instead of this.gpc(), so this.consent is never set and hasConsent stays false; cookie-banner-root.initClient() then falls through to setState(initial_state) instead of setState("hidden"). The default path hides the banner for GPC visitors (consent recorded), so the flag changes banner visibility for GPC visitors rather than only suppressing the record. If a GPC visitor should still have the banner suppressed, base the hide decision on gpcApplied as well as hasConsent.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cookie-banner/src/client.ts, line 204:
<comment>With `gpc-record="false"` in a deployment that renders visible banner UI (themed/IIFE always render), a GPC opt-out visitor now sees the consent banner. The new `else` branch calls `this.activate(gpcData)` instead of `this.gpc()`, so `this.consent` is never set and `hasConsent` stays `false`; `cookie-banner-root.initClient()` then falls through to `setState(initial_state)` instead of `setState("hidden")`. The default path hides the banner for GPC visitors (consent recorded), so the flag changes banner visibility for GPC visitors rather than only suppressing the record. If a GPC visitor should still have the banner suppressed, base the hide decision on `gpcApplied` as well as `hasConsent`.</comment>
<file context>
@@ -194,7 +196,13 @@ export class CookieBannerClient {
+ if (this.gpcRecord) {
+ this.gpc();
+ } else {
+ this.activate(gpcData);
+ }
this._gpcApplied = true;
</file context>
| this.gpc(); | ||
| // GPC is honoured either way; gpcRecord only controls whether the | ||
| // decision is persisted and sent. | ||
| if (this.gpcRecord) { |
There was a problem hiding this comment.
P3: The new tests only cover resolveBooleanAttribute parsing, not the actual behavior this PR introduces. There is no client-level test asserting that gpcRecord: false skips the consent cookie, visitor-id creation, and the {bannerId}/consents POST, or that the default true still records. Add a client test for the GPC branch so the privacy-relevant toggle can't silently regress.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/cookie-banner/src/client.ts, line 201:
<comment>The new tests only cover `resolveBooleanAttribute` parsing, not the actual behavior this PR introduces. There is no client-level test asserting that `gpcRecord: false` skips the consent cookie, visitor-id creation, and the `{bannerId}/consents` POST, or that the default `true` still records. Add a client test for the GPC branch so the privacy-relevant toggle can't silently regress.</comment>
<file context>
@@ -194,7 +196,13 @@ export class CookieBannerClient {
- this.gpc();
+ // GPC is honoured either way; gpcRecord only controls whether the
+ // decision is persisted and sent.
+ if (this.gpcRecord) {
+ this.gpc();
+ } else {
</file context>
The gpcRecord: false path left this.consent null, so hasConsent stayed false and both visibility gates fell through to the layout's initial state -- showing a consent banner to a visitor who had just been opted out, and offering to accept the categories GPC had denied. Gate on gpcApplied as well. The default path is unchanged: it records, so hasConsent is already true there. Signed-off-by: Gren Paut <90505696+AnotherDork@users.noreply.github.com>
Problem
When a visitor's browser sends Global Privacy Control,
load()applies the opt-out and records it as a consent decision:ensureVisitorId()writes a visitor id tolocalStorage,setConsentCookie()writes the consent cookie, andrecordConsent()POSTs a consent record to the API.That is right for a banner deployment. It is surprising in two cases:
<probo-cookie-banner-root>with no UI children to run the tracker detectors also makes the SDK a consent manager for GPC visitors, writing consent records the operator never asked it to write.There is currently no way to opt out of it.
Change
CookieBannerClientOptions.gpcRecord?: boolean(defaulttrue— unchanged behaviour), exposed asgpc-record="false"on<probo-cookie-banner>/<probo-cookie-banner-root>anddata-gpc-record="false"on the IIFE script tag.activate(gpcData)denies non-necessary categories, blocked resources stay blocked, integrations are updated — so the flag cannot be used to ignore a GPC signal. Only the persistence and the transmission are skipped: no visitor id, no cookie, no consent record. That separation is deliberate; a flag that let operators drop the signal entirely would be the wrong thing to ship in a compliance SDK."true"/"false"attribute parsing added withgcm-enabledmoves to a sharedresolveBooleanAttribute()— both flags need the same warn-and-default-to-enabled behaviour, andresolveGcmEnabled()now delegates to it. Behaviour there is unchanged; its tests still pass untouched.On the shape
I have put this client-side to match
gcm-enabled, but unlike that flag it is not forced to be: GPC auto-consent runs after the config fetch, so a per-bannercapabilitiesfield in the manner ofresourceReportingwould also work. I did not go that way because it is a ~25-file change across Go, GraphQL, MCP, CLI, n8n and the console locales for one boolean, and because the operator settinggpc-recordis the same person settinggcm-enabledon the same embed. Happy to rework it as a server capability if you would rather have it there.Testing
npm --workspace @probo/cookie-banner run check/test/buildall pass (21 tests; new cases cover the shared resolver including the warning path).gpc-recordwiring verified present in the builtdist/cookie-banner.iife.js.lint-jssteps (@probo/skillsvalidate,make relay, root eslint + n8n lint) run locally: 0 errors.Summary by cubic
Adds an option to skip persisting and transmitting GPC consent decisions. When a visitor sends Global Privacy Control,
gpc-record="false"(orgpcRecord: false) still applies the opt-out in the page — categories denied, resources blocked, integrations updated — but no longer writes a visitor id, consent cookie, or API consent record. The banner also stays hidden for those visitors, since the opt-out is in effect without a stored consent. Default behavior is unchanged.Tests
+51-0"true"/"false", case/whitespace normalization, and the warn-and-default-to-enabled path.Package: cookie-banner
+87-20gpcRecordtoCookieBannerClientOptions, defaulting totrueto preserve current behavior.gpcRecordis false,activate(gpcData)runs directly instead ofgpc(), so the GPC signal still takes effect without producing a record.gpcApplied, so a GPC visitor is never shown a banner or offered categories that were already denied.gpc-recordwiring is exposed on<probo-cookie-banner>,<probo-cookie-banner-root>, the themed banner, anddata-gpc-recordon the IIFE script tag."true"/"false"parsinggcm-enabledintroduced is extracted intoresolveBooleanAttribute();resolveGcmEnabled()now delegates to it with unchanged behavior.Written for commit 6b55190. Summary will update on new commits.