Exclude uncategorised category from consent contract

The uncategorised category is an admin-side inbox for detected
cookies and should not be part of the visitor-facing consent
contract. Filter it out of snapshots so changes to uncategorised
patterns no longer trigger version bumps.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-05-02 18:33:07 +04:00
parent 887453fee2
commit 478ccf6785
4 changed files with 88 additions and 24 deletions

View File

@@ -2281,10 +2281,6 @@ func (s *Service) ReportDetectedCookies(
}
if inserted > 0 {
if _, err := s.ensureDraftVersionForBanner(ctx, tx, scope, banner.ID); err != nil {
return fmt.Errorf("cannot ensure draft version: %w", err)
}
if err := banner.SetPatternAnalysisRequested(ctx, tx); err != nil {
return fmt.Errorf("cannot request pattern analysis: %w", err)
}

View File

@@ -211,13 +211,15 @@ func TestBuildSnapshot_RankInvariant(t *testing.T) {
assert.True(t, snapshotsEqual(a, b))
})
t.Run("Necessary comes first and Uncategorised comes last", func(t *testing.T) {
t.Run("Necessary comes first and uncategorised is excluded", func(t *testing.T) {
t.Parallel()
snap := buildSnapshot(banner, mkCategories(0, 1, 2, 3), nil)
require.Len(t, snap.Categories, 4)
require.Len(t, snap.Categories, 3)
assert.Equal(t, coredata.CookieCategoryKindNecessary, snap.Categories[0].Kind)
assert.Equal(t, coredata.CookieCategoryKindUncategorised, snap.Categories[3].Kind)
for _, c := range snap.Categories {
assert.NotEqual(t, coredata.CookieCategoryKindUncategorised, c.Kind)
}
})
}

View File

@@ -25,13 +25,13 @@ import (
)
// resolveTranslations converts raw DB translations into the resolved map
// used by buildBannerConfig at serve time. Categories must be sorted in
// snapshot order so the positional category translations align.
// used by buildBannerConfig at serve time. Categories are filtered and sorted
// in snapshot order so the positional category translations align.
func resolveTranslations(
translations coredata.CookieBannerTranslations,
categories coredata.CookieCategories,
) map[string]coredata.CookieBannerVersionSnapshotTranslation {
return buildSnapshotTranslations(translations, sortCategoriesForSnapshot(categories))
return buildSnapshotTranslations(translations, consentCategories(categories))
}
// snapshotsEqual reports whether two version snapshots are visitor-identical.
@@ -43,34 +43,36 @@ func snapshotsEqual(a, b coredata.CookieBannerVersionSnapshot) bool {
}
// snapshotCategoryKindOrder returns a stable weight per Kind so the snapshot
// keeps the visitor-facing layout invariants (NECESSARY first, UNCATEGORISED
// last) without depending on the admin-controlled rank.
// keeps the visitor-facing layout invariants (NECESSARY first, then NORMAL
// sorted by ID) without depending on the admin-controlled rank.
func snapshotCategoryKindOrder(k coredata.CookieCategoryKind) int {
switch k {
case coredata.CookieCategoryKindNecessary:
return 0
case coredata.CookieCategoryKindNormal:
return 1
case coredata.CookieCategoryKindUncategorised:
return 2
default:
return 3
return 2
}
}
// sortCategoriesForSnapshot returns the categories ordered for snapshot
// rendering. The order is (Kind weight, ID byte order); rank is intentionally
// ignored so reordering is admin-only metadata and does not bump the version.
func sortCategoriesForSnapshot(categories coredata.CookieCategories) coredata.CookieCategories {
sorted := make(coredata.CookieCategories, len(categories))
copy(sorted, categories)
slices.SortStableFunc(sorted, func(a, b *coredata.CookieCategory) int {
// consentCategories returns the categories that are part of the consent
// contract (everything except UNCATEGORISED), sorted in snapshot order.
// UNCATEGORISED is an admin-side inbox and never shown to visitors.
func consentCategories(categories coredata.CookieCategories) coredata.CookieCategories {
filtered := make(coredata.CookieCategories, 0, len(categories))
for _, c := range categories {
if c.Kind != coredata.CookieCategoryKindUncategorised {
filtered = append(filtered, c)
}
}
slices.SortStableFunc(filtered, func(a, b *coredata.CookieCategory) int {
if d := snapshotCategoryKindOrder(a.Kind) - snapshotCategoryKindOrder(b.Kind); d != 0 {
return d
}
return bytes.Compare(a.ID[:], b.ID[:])
})
return sorted
return filtered
}
func buildSnapshot(
@@ -78,7 +80,7 @@ func buildSnapshot(
categories coredata.CookieCategories,
allPatterns coredata.CookiePatterns,
) coredata.CookieBannerVersionSnapshot {
categories = sortCategoriesForSnapshot(categories)
categories = consentCategories(categories)
cookiesByCategory := make(map[gid.GID]coredata.CookieItems)
for _, p := range allPatterns {