Skip to content

Add option to skip recording GPC consent - #1784

Open
AnotherDork wants to merge 2 commits into
getprobo:mainfrom
AnotherDork:cookie-banner-gpc-record-opt-out
Open

Add option to skip recording GPC consent#1784
AnotherDork wants to merge 2 commits into
getprobo:mainfrom
AnotherDork:cookie-banner-gpc-record-opt-out

Conversation

@AnotherDork

@AnotherDork AnotherDork commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

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 to localStorage, setConsentCookie() writes the consent cookie, and recordConsent() POSTs a consent record to the API.

That is right for a banner deployment. It is surprising in two cases:

  • Detection-only embeds. Mounting <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.
  • Sites with their own consent stack. They end up with a second, narrower record of the same decision — one holding an anonymised IP and a user agent their own record may deliberately exclude.

There is currently no way to opt out of it.

Change

  • CookieBannerClientOptions.gpcRecord?: boolean (default true — unchanged behaviour), exposed as gpc-record="false" on <probo-cookie-banner> / <probo-cookie-banner-root> and data-gpc-record="false" on the IIFE script tag.
  • GPC is still honoured when the flag is off. The opt-out is applied in the page exactly as before — 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.
  • The "true"/"false" attribute parsing added with gcm-enabled moves to a shared resolveBooleanAttribute() — both flags need the same warn-and-default-to-enabled behaviour, and resolveGcmEnabled() now delegates to it. Behaviour there is unchanged; its tests still pass untouched.
  • README gains a Global Privacy Control section.

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-banner capabilities field in the manner of resourceReporting would 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 setting gpc-record is the same person setting gcm-enabled on 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 / build all pass (21 tests; new cases cover the shared resolver including the warning path).
  • gpc-record wiring verified present in the built dist/cookie-banner.iife.js.
  • lint-js steps (@probo/skills validate, 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" (or gpcRecord: 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

  • Covers the new shared boolean attribute resolver: absent, "true"/"false", case/whitespace normalization, and the warn-and-default-to-enabled path.

Package: cookie-banner +87 -20

  • Adds gpcRecord to CookieBannerClientOptions, defaulting to true to preserve current behavior.
  • When gpcRecord is false, activate(gpcData) runs directly instead of gpc(), so the GPC signal still takes effect without producing a record.
  • Visibility now also gates on gpcApplied, so a GPC visitor is never shown a banner or offered categories that were already denied.
  • gpc-record wiring is exposed on <probo-cookie-banner>, <probo-cookie-banner-root>, the themed banner, and data-gpc-record on the IIFE script tag.
  • The "true"/"false" parsing gcm-enabled introduced is extracted into resolveBooleanAttribute(); resolveGcmEnabled() now delegates to it with unchanged behavior.
  • README gains a Global Privacy Control section explaining the flag and that GPC remains honored.

Written for commit 6b55190. Summary will update on new commits.

Review in cubic

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>
Copilot AI lite review requested due to automatic review settings August 27, 2026 11:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);

@cubic-dev-ai cubic-dev-ai Bot Aug 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

if (this.gpcRecord) {
this.gpc();
} else {
this.activate(gpcData);

@cubic-dev-ai cubic-dev-ai Bot Aug 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

this.gpc();
// GPC is honoured either way; gpcRecord only controls whether the
// decision is persisted and sent.
if (this.gpcRecord) {

@cubic-dev-ai cubic-dev-ai Bot Aug 27, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants