Skip to content

feat: support new consent resolution strategy values #5798

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 77 additions & 13 deletions processor/consent.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import (
"fmt"
"sort"
"strings"

"github.com/samber/lo"

Expand All @@ -12,8 +14,15 @@
)

type ConsentManagementInfo struct {
AllowedConsentIDs []string
DeniedConsentIDs []string
Provider string
ResolutionStrategy string
}

type EventConsentManagementInfo struct {
AllowedConsentIDs interface{} `json:"allowedConsentIds"`
DeniedConsentIDs []string `json:"deniedConsentIds"`
AllowedConsentIDs interface{} `json:"allowedConsentIds"` // Not used currently but added for future use
Provider string `json:"provider"`
ResolutionStrategy string `json:"resolutionStrategy"`
}
Expand Down Expand Up @@ -47,28 +56,38 @@
proc.logger.Errorw("failed to get consent management info", "error", err.Error())
}

if len(consentManagementInfo.DeniedConsentIDs) == 0 {
// If the event does not have any consents, do not filter any destinations
if len(consentManagementInfo.AllowedConsentIDs) == 0 && len(consentManagementInfo.DeniedConsentIDs) == 0 {
return destinations
}

return lo.Filter(destinations, func(dest backendconfig.DestinationT, _ int) bool {
// Generic consent management
if cmpData := proc.getGCMData(sourceID, dest.ID, consentManagementInfo.Provider); len(cmpData.Consents) > 0 {

finalResolutionStrategy := consentManagementInfo.ResolutionStrategy

// For custom provider, the resolution strategy is to be picked from the destination config
if consentManagementInfo.Provider == "custom" {
finalResolutionStrategy = cmpData.ResolutionStrategy
}

// TODO: Remove "or" and "and" support once all the SDK clients stop sending it.
// Currently, it is added for backward compatibility.
switch finalResolutionStrategy {
// The user must consent to at least one of the configured consents in the destination
case "or":
case "any", "or":
if len(consentManagementInfo.AllowedConsentIDs) > 0 {
// The user must consent to at least one of the configured consents in the destination
return lo.Some(consentManagementInfo.AllowedConsentIDs, cmpData.Consents) || len(cmpData.Consents) == 0
}
// All of the configured consents should not be in denied
return !lo.Every(consentManagementInfo.DeniedConsentIDs, cmpData.Consents)

// The user must consent to all of the configured consents in the destination
default: // "and"
default: // "all" / "and"
if len(consentManagementInfo.AllowedConsentIDs) > 0 {
// The user must consent to all of the configured consents in the destination
return lo.Every(consentManagementInfo.AllowedConsentIDs, cmpData.Consents)
}
// None of the configured consents should be in denied
return len(lo.Intersect(cmpData.Consents, consentManagementInfo.DeniedConsentIDs)) == 0
}
}
Expand Down Expand Up @@ -185,10 +204,14 @@
consentsConfig := providerConfig.Consents

if len(consentsConfig) > 0 && providerConfig.Provider != "" {
consentIDs := lo.FilterMap(
consentsConfig,
func(consentsObj GenericConsentsConfig, _ int) (string, bool) {
return consentsObj.Consent, consentsObj.Consent != ""
consentIDs := lo.Map(consentsConfig, func(consentsObj GenericConsentsConfig, _ int) string {
return strings.TrimSpace(consentsObj.Consent)
})

consentIDs = lo.FilterMap(
consentIDs,
func(consentID string, _ int) (string, bool) {
return consentID, consentID != ""
},
)

Expand All @@ -205,22 +228,63 @@
}

func getConsentManagementInfo(event types.SingularEventT) (ConsentManagementInfo, error) {
consentManagementInfoFromEvent := EventConsentManagementInfo{}
consentManagementInfo := ConsentManagementInfo{}
if consentManagement, ok := misc.MapLookup(event, "context", "consentManagement").(map[string]interface{}); ok {
consentManagementObjBytes, mErr := jsonrs.Marshal(consentManagement)
if mErr != nil {
return consentManagementInfo, fmt.Errorf("error marshalling consentManagement: %v", mErr)
}

unmErr := jsonrs.Unmarshal(consentManagementObjBytes, &consentManagementInfo)
unmErr := jsonrs.Unmarshal(consentManagementObjBytes, &consentManagementInfoFromEvent)
if unmErr != nil {
return consentManagementInfo, fmt.Errorf("error unmarshalling consentManagementInfo: %v", unmErr)
}

consentManagementInfo.DeniedConsentIDs = consentManagementInfoFromEvent.DeniedConsentIDs
consentManagementInfo.Provider = consentManagementInfoFromEvent.Provider
consentManagementInfo.ResolutionStrategy = consentManagementInfoFromEvent.ResolutionStrategy

// This is to support really old version of the JS SDK v3 that sent this data as an object
// for OneTrust provider.
// Handle AllowedConsentIDs based on its type (array or map)
switch val := consentManagementInfoFromEvent.AllowedConsentIDs.(type) {
Copy link
Preview

Copilot AI May 1, 2025

Choose a reason for hiding this comment

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

[nitpick] Consider logging or handling cases where non-string values are encountered during conversion from []interface{} to []string, so that unexpected types are not silently ignored.

Copilot uses AI. Check for mistakes.

case []interface{}:
// Convert []interface{} to []string
consentManagementInfo.AllowedConsentIDs = make([]string, 0, len(val))
for _, v := range val {
if strVal, ok := v.(string); ok {
consentManagementInfo.AllowedConsentIDs = append(consentManagementInfo.AllowedConsentIDs, strVal)
}
}
case []string:
// Already a string array
consentManagementInfo.AllowedConsentIDs = val

Check warning on line 262 in processor/consent.go

View check run for this annotation

Codecov / codecov/patch

processor/consent.go#L260-L262

Added lines #L260 - L262 were not covered by tests
case map[string]interface{}:
// Use keys from the map (legacy OneTrust format)
// Also, sort the keys as the unmarshalling of map is not deterministic of the order of keys
// and it can cause flaky tests.
consentManagementInfo.AllowedConsentIDs = lo.Keys(val)
sort.Strings(consentManagementInfo.AllowedConsentIDs)
default:
consentManagementInfo.AllowedConsentIDs = []string{}
}

// Ideally, the clean up and filter is not needed for standard providers
// but useful for custom providers where users send this data directly
// to the SDKs.
sanitizePredicate := func(consent string, _ int) string {
return strings.TrimSpace(consent)
}

consentManagementInfo.AllowedConsentIDs = lo.Map(consentManagementInfo.AllowedConsentIDs, sanitizePredicate)
consentManagementInfo.DeniedConsentIDs = lo.Map(consentManagementInfo.DeniedConsentIDs, sanitizePredicate)

// Filter out empty values
filterPredicate := func(consent string, _ int) (string, bool) {
return consent, consent != ""
}

consentManagementInfo.AllowedConsentIDs = lo.FilterMap(consentManagementInfo.AllowedConsentIDs, filterPredicate)
consentManagementInfo.DeniedConsentIDs = lo.FilterMap(consentManagementInfo.DeniedConsentIDs, filterPredicate)
}

Expand Down
Loading
Loading