Skip to content

Fix GDPR cookie consent manager#1072

Open
BenjaminMichaelis wants to merge 3 commits intomainfrom
fix/gdpr-consent-manager
Open

Fix GDPR cookie consent manager#1072
BenjaminMichaelis wants to merge 3 commits intomainfrom
fix/gdpr-consent-manager

Conversation

@BenjaminMichaelis
Copy link
Copy Markdown
Member

Summary

Comprehensive overhaul of the cookie consent manager to fix GDPR compliance issues, bugs, and a broken Google Analytics integration.

Changes

Strategy change

  • Show consent banner to all visitors — removes unreliable timezone-based region detection (VPN bypass gaps, missing timezones, ongoing maintenance burden). One-time banner click for non-EEA users is a worthwhile tradeoff for zero compliance liability.

Bug fixes

  • Clarity denied signal for new visitorsupdateClarityConsent() was never called for first-time visitors (only on returning visitors and after interaction). Now called unconditionally in init() so Clarity always receives the current consent state on page load.
  • personalization_storage mis-grouped — was bound to analyticsChecked in saveCustomPreferences(), should be advertisingChecked.
  • gtag('config') firing twice — removed duplicate call; now fires unconditionally once per Google Consent Mode v2 "Advanced" pattern (GA handles denied state with cookieless modeled pings).
  • Duplicate gtag('consent', 'default') — removed from initGoogleConsentMode(); _Layout.cshtml is the single owner (required to fire before gtag.js loads).
  • clearTrackingCookies() didn't clear domain-scoped cookies — now deletes on exact hostname, root domain, and no-domain for full GA/Clarity cookie coverage.
  • revokeAllConsent() called conflicting Clarity v1 API — removed clarity('consent', false); consentv2 handles it.

Improvements

  • wait_for_update: 500 added to gtag('consent', 'default') — prevents GA from firing pings before the consent banner has a chance to render.
  • CONSENT_VERSION = '2' — bumping this constant re-prompts all existing users. Stale cookies are invalidated on load.
  • _timestamp + _version stored in consent cookie — GDPR Art. 7(1) audit trail.
  • Secure flag on consent cookie (HTTPS only).
  • Privacy policy link added to consent banner text.
  • functionality_storage/security_storage protected — can no longer be overridden via cookie tampering.
  • openConsentPreferences() setTimeout removed — direct synchronous call.

Testing

Use ?testConsent=true to force the banner regardless of existing consent cookie.

- Show consent banner to all visitors (remove unreliable timezone-based
  region detection — VPN gaps, missing timezones, maintenance burden)
- Add CONSENT_VERSION with invalidation: bumping re-prompts all users
- Store _timestamp and _version in consent cookie for GDPR audit trail
- Always send Clarity denied signal on init for new visitors (was never
  receiving a consent signal before user interaction)
- Fix personalization_storage bound to advertising, not analytics
- Add wait_for_update: 500 to gtag consent defaults (prevents GA pings
  before banner renders)
- Fire gtag('config') unconditionally per Consent Mode v2 Advanced pattern
  (GA handles denied state with cookieless modeled pings)
- Remove duplicate gtag('consent', 'default') from initGoogleConsentMode()
- Fix clearTrackingCookies() to delete domain-scoped GA/Clarity cookies
- Remove conflicting Clarity v1 API call from revokeAllConsent()
- Add Secure flag to consent cookie on HTTPS
- Add privacy policy link to consent banner
- Replace fragile setTimeout in openConsentPreferences() with direct call
- Prevent functionality_storage/security_storage from being user-overridden
Copilot AI review requested due to automatic review settings May 7, 2026 06:07
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the site’s cookie consent implementation to improve GDPR compliance and correct Consent Mode v2 / Clarity signaling behavior, including moving Consent Mode “default” initialization into _Layout.cshtml (before gtag.js loads) and updating the client-side consent manager to show the banner globally and persist auditable consent state.

Changes:

  • Remove timezone-based consent gating and show the consent banner to all visitors until valid consent is stored.
  • Align Google Consent Mode v2 + GA initialization to the “default before gtag.js” pattern and avoid duplicate config/consent calls.
  • Add consent versioning/audit metadata, improve cookie deletion behavior, and fix consent key mappings (e.g., personalization_storage).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
EssentialCSharp.Web/wwwroot/js/consent-manager.js Updates consent persistence/versioning, Clarity updates, banner logic, and cookie clearing behavior.
EssentialCSharp.Web/Views/Shared/_Layout.cshtml Sets Consent Mode default values before loading gtag.js and configures GA in a single place.

Comment on lines 10 to 13
this.COOKIE_DURATION = 365; // days
this.CONSENT_VERSION = '2'; // Bump this to re-prompt all users when consent terms change
this.GOOGLE_ANALYTICS_ID = options.googleAnalyticsId || 'G-761B4BMK2R';
this.consentState = {
Comment on lines +265 to +272

// Notify layout scripts so they can fire gtag('config') on interactive consent
document.dispatchEvent(new CustomEvent('consentUpdated', {
detail: {
hasAnalyticsConsent: this.hasAnalyticsConsent(),
hasAdvertisingConsent: this.hasAdvertisingConsent()
}
}));
Comment on lines 464 to 468
// GA and Clarity cookies are often set on the root domain, so attempt deletion on both the
// exact hostname and the parent domain (e.g. .essentialcsharp.com)
const hostname = window.location.hostname;
const rootDomain = '.' + hostname.split('.').slice(-2).join('.');
trackingCookies.forEach(cookieName => {
Comment on lines 41 to 52
hasAdvertisingConsent: this.hasAdvertisingConsent()
}
});
document.dispatchEvent(event);
Comment on lines 98 to 104
shouldShowConsentBanner() {
// Show banner if in EEA/UK/Switzerland and no consent stored
return this.requiresConsent && !this.getCookie(this.COOKIE_NAME);
// Allow forcing the banner via URL param (useful for testing)
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('testConsent') === 'true') return true;
// Show to all visitors who haven't given valid consent yet
return !this.getCookie(this.COOKIE_NAME);
}
- Remove dead GOOGLE_ANALYTICS_ID option (gtag config is in _Layout.cshtml)
- Remove dead consentManagerReady and consentUpdated event dispatches
- Delete malformed consent cookie so banner re-shows on parse error
- Fix clearTrackingCookies() to handle multi-part TLDs by iterating all
  parent domain suffixes instead of assuming a 2-label registrable domain
The ad_storage/ad_user_data/ad_personalization signals are Google Consent
Mode v2 modeling signals, not for serving ads. The previous label and
description were factually wrong for a site that serves no advertisements.
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