Derive consent mode from geolocation, not banner config

The consent mode is now determined dynamically by the visitor's
country and its applicable regulation. The configured consent_mode
column is dropped from cookie_banners and added to
cookie_consent_records to persist the geo-derived mode at
consent-recording time. When no regulation matches, the default
is OPT_OUT.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-05-13 12:17:34 +04:00
parent e9d786c5d6
commit e739473bcd
30 changed files with 68 additions and 246 deletions

View File

@@ -134,8 +134,8 @@ func RegulationForCountry(cc coredata.CountryCode) Regulation {
// the visitor gives explicit consent; OPT_OUT means cookies may fire
// immediately but the visitor must be offered a way to opt out.
//
// When the regulation is unknown or RegulationNone, it returns an empty
// string so the caller can fall back to the banner's configured default.
// When the regulation is unknown or RegulationNone, it defaults to
// OPT_OUT (cookies may fire immediately, visitor can opt out).
func ConsentModeForRegulation(r Regulation) string {
switch r {
case RegulationGDPR,
@@ -157,6 +157,6 @@ func ConsentModeForRegulation(r Regulation) string {
return ConsentModeOptOut
default:
return ""
return ConsentModeOptOut
}
}

View File

@@ -51,7 +51,6 @@ type (
PrivacyPolicyURL *string
CookiePolicyURL string
ConsentExpiryDays int
ConsentMode coredata.CookieConsentMode
}
CreateCookieCategoryRequest struct {
@@ -68,7 +67,6 @@ type (
PrivacyPolicyURL *string
CookiePolicyURL *string
ConsentExpiryDays *int
ConsentMode *coredata.CookieConsentMode
DefaultLanguage *string
}
@@ -107,6 +105,7 @@ type (
SdkVersion string
Regulation *Regulation
CountryCode *coredata.CountryCode
ConsentMode *coredata.CookieConsentMode
}
DetectedCookie struct {
@@ -231,7 +230,6 @@ func (r *CreateCookieBannerRequest) Validate() error {
v.Check(r.PrivacyPolicyURL, "privacy_policy_url", validator.URL())
v.Check(r.CookiePolicyURL, "cookie_policy_url", validator.Required(), validator.URL())
v.Check(r.ConsentExpiryDays, "consent_expiry_days", validator.Required(), validator.Min(1))
v.Check(r.ConsentMode, "consent_mode", validator.Required(), validator.OneOfSlice(coredata.CookieConsentModes()))
return v.Error()
}
@@ -244,7 +242,6 @@ func (r *UpdateCookieBannerRequest) Validate() error {
v.Check(r.PrivacyPolicyURL, "privacy_policy_url", validator.URL())
v.Check(r.CookiePolicyURL, "cookie_policy_url", validator.URL())
v.Check(r.ConsentExpiryDays, "consent_expiry_days", validator.Min(1))
v.Check(r.ConsentMode, "consent_mode", validator.OneOfSlice(coredata.CookieConsentModes()))
v.Check(r.DefaultLanguage, "default_language", validator.OneOfSlice(SupportedLanguages))
return v.Error()
@@ -581,7 +578,6 @@ func (s *Service) CreateCookieBanner(
PrivacyPolicyURL: req.PrivacyPolicyURL,
CookiePolicyURL: req.CookiePolicyURL,
ConsentExpiryDays: req.ConsentExpiryDays,
ConsentMode: req.ConsentMode,
ShowBranding: s.showBranding,
DefaultLanguage: "en",
CreatedAt: now,
@@ -857,10 +853,9 @@ func (s *Service) UpdateCookieBanner(
privacyChanged := req.PrivacyPolicyURL != nil && !ptrEqual(req.PrivacyPolicyURL, banner.PrivacyPolicyURL)
cookiePolicyChanged := req.CookiePolicyURL != nil && *req.CookiePolicyURL != banner.CookiePolicyURL
expiryChanged := req.ConsentExpiryDays != nil && *req.ConsentExpiryDays != banner.ConsentExpiryDays
consentModeChanged := req.ConsentMode != nil && *req.ConsentMode != banner.ConsentMode
defaultLangChanged := req.DefaultLanguage != nil && *req.DefaultLanguage != banner.DefaultLanguage
snapshotChanged := privacyChanged || cookiePolicyChanged || expiryChanged || consentModeChanged || defaultLangChanged
snapshotChanged := privacyChanged || cookiePolicyChanged || expiryChanged || defaultLangChanged
if !nameChanged && !snapshotChanged {
return nil
@@ -878,9 +873,6 @@ func (s *Service) UpdateCookieBanner(
if req.ConsentExpiryDays != nil {
banner.ConsentExpiryDays = *req.ConsentExpiryDays
}
if req.ConsentMode != nil {
banner.ConsentMode = *req.ConsentMode
}
if req.DefaultLanguage != nil {
banner.DefaultLanguage = *req.DefaultLanguage
}
@@ -1609,11 +1601,9 @@ func (s *Service) GetActiveBannerConfig(
}
config.Regulation = regulation
if cm := ConsentModeForRegulation(regulation); cm != "" {
config.ConsentMode = cm
}
config.ConsentMode = ConsentModeForRegulation(regulation)
if !isLegacySDK(sdkVersion) {
remapTextsForConsentMode(config.Texts, config.ConsentMode, regulation)
remapTextsForConsentMode(config.Texts, config.ConsentMode)
}
return config, nil
@@ -1677,7 +1667,6 @@ func buildBannerConfig(
PrivacyPolicyURL: privacyPolicyURL,
CookiePolicyURL: snapshot.CookiePolicyURL,
ConsentExpiryDays: snapshot.ConsentExpiryDays,
ConsentMode: snapshot.ConsentMode,
ShowBranding: banner.ShowBranding,
Categories: categories,
Texts: texts,
@@ -1687,25 +1676,17 @@ func buildBannerConfig(
// remapTextsForConsentMode overrides the generic banner text keys with
// mode-specific variants so the client renders the appropriate copy
// without needing consent-mode awareness itself.
func remapTextsForConsentMode(texts map[string]string, consentMode string, regulation Regulation) {
func remapTextsForConsentMode(texts map[string]string, consentMode string) {
if texts == nil {
return
}
switch {
case consentMode == ConsentModeOptOut:
if consentMode == ConsentModeOptOut {
remapTextKey(texts, "banner_title_opt_out", "banner_title")
remapTextKey(texts, "banner_description_opt_out", "banner_description")
remapTextKey(texts, "button_acknowledge", "button_accept_all")
remapTextKey(texts, "button_opt_out", "button_reject_all")
texts["button_customize"] = ""
case regulation == RegulationNone:
remapTextKey(texts, "banner_title_notice", "banner_title")
remapTextKey(texts, "banner_description_notice", "banner_description")
remapTextKey(texts, "button_dismiss", "button_accept_all")
texts["button_reject_all"] = ""
texts["button_customize"] = ""
}
}
@@ -1964,6 +1945,7 @@ func (s *Service) RecordConsent(
SdkVersion: req.SdkVersion,
Regulation: req.Regulation,
CountryCode: req.CountryCode,
ConsentMode: req.ConsentMode,
CreatedAt: time.Now(),
}

View File

@@ -34,7 +34,6 @@ func TestSnapshotsEqual(t *testing.T) {
PrivacyPolicyURL: &policy,
CookiePolicyURL: "https://example.com/cookies",
ConsentExpiryDays: 180,
ConsentMode: "OPT_IN",
DefaultLanguage: "en",
Categories: []coredata.CookieBannerVersionSnapshotCategory{
{
@@ -248,7 +247,6 @@ func TestBuildSnapshot_RankInvariant(t *testing.T) {
ID: bannerID,
CookiePolicyURL: "https://example.com/cookies",
ConsentExpiryDays: 365,
ConsentMode: coredata.CookieConsentModeOptIn,
DefaultLanguage: "en",
}
@@ -298,14 +296,15 @@ func TestRemapTextsForConsentMode(t *testing.T) {
}
}
t.Run("no regulation clears reject and customize", func(t *testing.T) {
t.Run("opt in mode keeps all buttons", func(t *testing.T) {
t.Parallel()
texts := baseTexts()
remapTextsForConsentMode(texts, ConsentModeOptIn, RegulationNone)
remapTextsForConsentMode(texts, ConsentModeOptIn)
assert.Empty(t, texts["button_reject_all"])
assert.Empty(t, texts["button_customize"])
assert.Equal(t, "Accept All", texts["button_accept_all"])
assert.Equal(t, "Reject All", texts["button_reject_all"])
assert.Equal(t, "Customize", texts["button_customize"])
})
t.Run("opt out mode maps opt out to reject and clears customize", func(t *testing.T) {
@@ -313,7 +312,7 @@ func TestRemapTextsForConsentMode(t *testing.T) {
texts := baseTexts()
texts["button_opt_out"] = "Do Not Sell"
remapTextsForConsentMode(texts, ConsentModeOptOut, RegulationCCPA)
remapTextsForConsentMode(texts, ConsentModeOptOut)
assert.Equal(t, "Do Not Sell", texts["button_reject_all"])
assert.Empty(t, texts["button_customize"])

View File

@@ -116,7 +116,6 @@ func buildSnapshot(
PrivacyPolicyURL: banner.PrivacyPolicyURL,
CookiePolicyURL: banner.CookiePolicyURL,
ConsentExpiryDays: banner.ConsentExpiryDays,
ConsentMode: string(banner.ConsentMode),
DefaultLanguage: banner.DefaultLanguage,
Categories: snapshotCategories,
}