From 591a051e0abd93322d11cf3c3c74001b23629d74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Mon, 8 Jun 2026 18:38:45 +0200 Subject: [PATCH] Split tracker patterns on colon and dot separators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The pattern-analysis tokenizer split names only on underscore first, then on dash. A key like "name_done:" left the colon glued to the leading hex group, so the trailing UUID was never recognised as a single token and instead shredded on dash into short hex anchors. The derived template kept those anchors fixed, producing a distinct glob per UUID and preventing any merge. Treat colon and dot as primary separators alongside underscore so the embedded UUID is isolated, matched by isUUIDShape, and collapsed to a wildcard. Extend templateHasFixedAnchor to ignore the new separators so a separator-only template stays rejected by the anti-overmerge guard. Signed-off-by: Émile Ré --- pkg/cookiebanner/pattern_analysis_worker.go | 48 ++++++++++++++++--- .../pattern_analysis_worker_test.go | 32 +++++++++++++ 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/pkg/cookiebanner/pattern_analysis_worker.go b/pkg/cookiebanner/pattern_analysis_worker.go index be1e73695..8fe8aeda5 100644 --- a/pkg/cookiebanner/pattern_analysis_worker.go +++ b/pkg/cookiebanner/pattern_analysis_worker.go @@ -29,7 +29,17 @@ import ( "go.probo.inc/probo/pkg/gid" ) -const patternMergeThreshold = 3 +const ( + patternMergeThreshold = 3 + + // primarySeparators are the structural delimiters that always + // start a new token in splitTokens. `-` is intentionally excluded + // because it appears inside UUIDs, which splitTokens preserves as a + // single token; a value like "done:ecdd43d7-0193-4d24-b6ed-..." + // must split on ":" so the trailing UUID is isolated and collapsed + // to a wildcard rather than shredded into fixed hex anchors. + primarySeparators = "_:." +) // durationUnits mirrors the snap table from cookie-utils.ts. The same // tracker observed across different clients can have jitter in its @@ -565,16 +575,16 @@ func isUUIDShape(s string) bool { } func splitTokens(name string) ([]string, []byte) { - underscoreParts := strings.Split(name, "_") + primaryParts, primarySeps := splitOnAny(name, primarySeparators) var ( tokens []string seps []byte ) - for i, part := range underscoreParts { + for i, part := range primaryParts { if i > 0 { - seps = append(seps, '_') + seps = append(seps, primarySeps[i-1]) } if isUUIDShape(part) || !strings.Contains(part, "-") { @@ -597,6 +607,29 @@ func splitTokens(name string) ([]string, []byte) { return tokens, seps } +// splitOnAny splits s on every byte found in separators, returning the +// parts and the separator byte that preceded each part after the first. +// len(seps) == len(parts)-1. +func splitOnAny(s, separators string) ([]string, []byte) { + var ( + parts []string + seps []byte + start int + ) + + for i := 0; i < len(s); i++ { + if strings.IndexByte(separators, s[i]) >= 0 { + parts = append(parts, s[start:i]) + seps = append(seps, s[i]) + start = i + 1 + } + } + + parts = append(parts, s[start:]) + + return parts, seps +} + func joinTokens(tokens []string, seps []byte) string { var b strings.Builder @@ -616,10 +649,13 @@ func joinTokens(tokens []string, seps []byte) string { // "__*", "-*", "--*", "__*__" would merge unrelated third parties // (e.g. __support__, __darkreader__wasEnabledForHost, // __EXT_APP_REFRESH_BLACK_SUB_DOMAINS__) under a single glob, so -// candidates without any fixed alphanumeric anchor are rejected. +// candidates without any fixed alphanumeric anchor are rejected. The +// separators recognised here mirror primarySeparators (plus the +// UUID-internal "-") so a separator-only template such as ":.*" is +// rejected too. func templateHasFixedAnchor(tmpl string) bool { for _, ch := range tmpl { - if ch != '*' && ch != '_' && ch != '-' { + if ch != '*' && ch != '-' && !strings.ContainsRune(primarySeparators, ch) { return true } } diff --git a/pkg/cookiebanner/pattern_analysis_worker_test.go b/pkg/cookiebanner/pattern_analysis_worker_test.go index aa9058f09..037a6d985 100644 --- a/pkg/cookiebanner/pattern_analysis_worker_test.go +++ b/pkg/cookiebanner/pattern_analysis_worker_test.go @@ -243,6 +243,12 @@ func TestHeuristicTemplate(t *testing.T) { input: "__a1b2c3d4_e5f6g7h8", changed: false, }, + { + name: "colon-delimited trailing UUID collapses to wildcard", + input: "letaido.onboarding.invite_done:0a1b2c3d-4e5f-6789-abcd-ef0123456789", + template: "letaido.onboarding.invite_done:*", + changed: true, + }, } for _, tt := range tests { @@ -538,6 +544,12 @@ func TestSplitTokens(t *testing.T) { tokens: []string{"session", "550e8400-e29b-41d4-a716-446655440000", "data"}, seps: []byte{'_', '_'}, }, + { + name: "colon and dot separators isolate trailing UUID", + input: "letaido.onboarding.invite_done:0a1b2c3d-4e5f-6789-abcd-ef0123456789", + tokens: []string{"letaido", "onboarding", "invite", "done", "0a1b2c3d-4e5f-6789-abcd-ef0123456789"}, + seps: []byte{'.', '.', '_', ':'}, + }, } for _, tt := range tests { @@ -952,6 +964,26 @@ func TestFindMergeGroups(t *testing.T) { }, ) + t.Run( + "colon-delimited UUID keys merge under heuristic glob", + func(t *testing.T) { + t.Parallel() + + patterns := coredata.TrackerPatterns{ + makePattern("letaido.onboarding.invite_done:0a1b2c3d-4e5f-6789-abcd-ef0123456789", &oneYear), + makePattern("letaido.onboarding.invite_done:11111111-2222-3333-4444-555555555555", &oneYear), + makePattern("letaido.onboarding.invite_done:aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", &oneYear), + } + + groups := findMergeGroups(patterns, 3) + require.Len(t, groups, 1) + + group, ok := groups[mergeGroupKey{categoryID: gid.Nil, trackerType: coredata.TrackerTypeCookie, template: "letaido.onboarding.invite_done:*", durationBucket: durationBucket(&oneYear)}] + require.True(t, ok) + assert.Len(t, group, 3) + }, + ) + t.Run( "unrelated double-underscore keys do not merge under anchor-free glob", func(t *testing.T) {