Implement glob-based pattern discovery and matching in worker
Replace prefix-only merge logic with token-template analysis that discovers sandwich patterns (e.g. ph_phc_*_posthog). The worker now emits GLOB patterns, adoption uses globMatch, and validation enforces exactly one wildcard for GLOB patterns. Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -346,6 +346,26 @@ func (r *CreateTrackerPatternRequest) Validate() error {
|
|||||||
return s
|
return s
|
||||||
}(),
|
}(),
|
||||||
))
|
))
|
||||||
|
v.Check(r.Pattern, "pattern", func(value any) *validator.ValidationError {
|
||||||
|
s, _ := value.(string)
|
||||||
|
switch r.MatchType {
|
||||||
|
case coredata.TrackerPatternMatchTypeGlob:
|
||||||
|
if strings.Count(s, "*") != 1 {
|
||||||
|
return &validator.ValidationError{
|
||||||
|
Code: validator.ErrorCodeInvalidFormat,
|
||||||
|
Message: "glob pattern must contain exactly one *",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case coredata.TrackerPatternMatchTypeExact:
|
||||||
|
if strings.Contains(s, "*") {
|
||||||
|
return &validator.ValidationError{
|
||||||
|
Code: validator.ErrorCodeInvalidFormat,
|
||||||
|
Message: "exact pattern must not contain *",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
v.Check(r.DisplayName, "display_name", validator.Required(), validator.SafeTextNoNewLine(255))
|
v.Check(r.DisplayName, "display_name", validator.Required(), validator.SafeTextNoNewLine(255))
|
||||||
v.Check(r.Description, "description", validator.SafeText(1000))
|
v.Check(r.Description, "description", validator.SafeText(1000))
|
||||||
|
|
||||||
|
|||||||
@@ -165,15 +165,15 @@ func (h *patternAnalysisHandler) Process(ctx context.Context, banner coredata.Co
|
|||||||
|
|
||||||
source := bestSource(group)
|
source := bestSource(group)
|
||||||
|
|
||||||
prefixPattern := &coredata.TrackerPattern{
|
globPattern := &coredata.TrackerPattern{
|
||||||
ID: gid.New(banner.ID.TenantID(), coredata.TrackerPatternEntityType),
|
ID: gid.New(banner.ID.TenantID(), coredata.TrackerPatternEntityType),
|
||||||
OrganizationID: group[0].OrganizationID,
|
OrganizationID: group[0].OrganizationID,
|
||||||
CookieBannerID: banner.ID,
|
CookieBannerID: banner.ID,
|
||||||
CookieCategoryID: key.categoryID,
|
CookieCategoryID: key.categoryID,
|
||||||
TrackerType: key.trackerType,
|
TrackerType: key.trackerType,
|
||||||
Pattern: key.prefix,
|
Pattern: key.template,
|
||||||
MatchType: coredata.TrackerPatternMatchTypePrefix,
|
MatchType: coredata.TrackerPatternMatchTypeGlob,
|
||||||
DisplayName: key.prefix + "*",
|
DisplayName: key.template,
|
||||||
MaxAgeSeconds: maxAge,
|
MaxAgeSeconds: maxAge,
|
||||||
Description: "",
|
Description: "",
|
||||||
Source: source,
|
Source: source,
|
||||||
@@ -181,23 +181,23 @@ func (h *patternAnalysisHandler) Process(ctx context.Context, banner coredata.Co
|
|||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
inserted, err := prefixPattern.InsertIfNotExists(ctx, tx, scope)
|
inserted, err := globPattern.InsertIfNotExists(ctx, tx, scope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("cannot insert prefix pattern %q: %w", key.prefix, err)
|
return fmt.Errorf("cannot insert glob pattern %q: %w", key.template, err)
|
||||||
}
|
}
|
||||||
if !inserted {
|
if !inserted {
|
||||||
if err := prefixPattern.LoadByBannerIDTypeAndPattern(ctx, tx, scope, banner.ID, key.trackerType, key.prefix, maxAge); err != nil {
|
if err := globPattern.LoadByBannerIDTypeAndPattern(ctx, tx, scope, banner.ID, key.trackerType, key.template, maxAge); err != nil {
|
||||||
return fmt.Errorf("cannot load existing prefix pattern %q: %w", key.prefix, err)
|
return fmt.Errorf("cannot load existing glob pattern %q: %w", key.template, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if prefixPattern.CookieCategoryID != key.categoryID || prefixPattern.MatchType != coredata.TrackerPatternMatchTypePrefix {
|
if globPattern.CookieCategoryID != key.categoryID || globPattern.MatchType != coredata.TrackerPatternMatchTypeGlob {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, exactPattern := range group {
|
for _, exactPattern := range group {
|
||||||
var trackers coredata.DetectedTrackers
|
var trackers coredata.DetectedTrackers
|
||||||
if err := trackers.RelinkByTrackerPatternID(ctx, tx, scope, exactPattern.ID, prefixPattern.ID); err != nil {
|
if err := trackers.RelinkByTrackerPatternID(ctx, tx, scope, exactPattern.ID, globPattern.ID); err != nil {
|
||||||
return fmt.Errorf("cannot relink detected trackers from pattern %q: %w", exactPattern.Pattern, err)
|
return fmt.Errorf("cannot relink detected trackers from pattern %q: %w", exactPattern.Pattern, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,8 +212,8 @@ func (h *patternAnalysisHandler) Process(ctx context.Context, banner coredata.Co
|
|||||||
|
|
||||||
h.logger.InfoCtx(
|
h.logger.InfoCtx(
|
||||||
ctx,
|
ctx,
|
||||||
"merged exact patterns into prefix pattern",
|
"merged exact patterns into glob pattern",
|
||||||
log.String("prefix", key.prefix),
|
log.String("template", key.template),
|
||||||
log.Int("count", len(group)),
|
log.Int("count", len(group)),
|
||||||
log.String("banner_id", banner.ID.String()),
|
log.String("banner_id", banner.ID.String()),
|
||||||
)
|
)
|
||||||
@@ -242,7 +242,7 @@ func (h *patternAnalysisHandler) Process(ctx context.Context, banner coredata.Co
|
|||||||
type mergeGroupKey struct {
|
type mergeGroupKey struct {
|
||||||
categoryID gid.GID
|
categoryID gid.GID
|
||||||
trackerType coredata.TrackerType
|
trackerType coredata.TrackerType
|
||||||
prefix string
|
template string
|
||||||
durationBucket int
|
durationBucket int
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,29 +250,30 @@ func findMergeGroups(
|
|||||||
patterns coredata.TrackerPatterns,
|
patterns coredata.TrackerPatterns,
|
||||||
threshold int,
|
threshold int,
|
||||||
) map[mergeGroupKey][]*coredata.TrackerPattern {
|
) map[mergeGroupKey][]*coredata.TrackerPattern {
|
||||||
prefixCounts := make(map[mergeGroupKey][]*coredata.TrackerPattern)
|
templateCounts := make(map[mergeGroupKey][]*coredata.TrackerPattern)
|
||||||
for _, p := range patterns {
|
for _, p := range patterns {
|
||||||
bucket := durationBucket(p.MaxAgeSeconds)
|
bucket := durationBucket(p.MaxAgeSeconds)
|
||||||
for _, pfx := range separatorPrefixes(p.Pattern) {
|
for _, tmpl := range templateCandidates(p.Pattern) {
|
||||||
key := mergeGroupKey{categoryID: p.CookieCategoryID, trackerType: p.TrackerType, prefix: pfx, durationBucket: bucket}
|
key := mergeGroupKey{categoryID: p.CookieCategoryID, trackerType: p.TrackerType, template: tmpl, durationBucket: bucket}
|
||||||
prefixCounts[key] = append(prefixCounts[key], p)
|
templateCounts[key] = append(templateCounts[key], p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type candidate struct {
|
type candidate struct {
|
||||||
key mergeGroupKey
|
key mergeGroupKey
|
||||||
|
fixedChars int
|
||||||
patterns []*coredata.TrackerPattern
|
patterns []*coredata.TrackerPattern
|
||||||
}
|
}
|
||||||
|
|
||||||
var candidates []candidate
|
var candidates []candidate
|
||||||
for key, pats := range prefixCounts {
|
for key, pats := range templateCounts {
|
||||||
if len(pats) >= threshold {
|
if len(pats) >= threshold {
|
||||||
candidates = append(candidates, candidate{key, pats})
|
candidates = append(candidates, candidate{key, len(key.template) - 1, pats})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(candidates, func(i, j int) bool {
|
sort.Slice(candidates, func(i, j int) bool {
|
||||||
return len(candidates[i].key.prefix) > len(candidates[j].key.prefix)
|
return candidates[i].fixedChars > candidates[j].fixedChars
|
||||||
})
|
})
|
||||||
|
|
||||||
assigned := make(map[*coredata.TrackerPattern]bool)
|
assigned := make(map[*coredata.TrackerPattern]bool)
|
||||||
@@ -299,14 +300,47 @@ func findMergeGroups(
|
|||||||
return groups
|
return groups
|
||||||
}
|
}
|
||||||
|
|
||||||
func separatorPrefixes(name string) []string {
|
func templateCandidates(name string) []string {
|
||||||
var prefixes []string
|
var candidates []string
|
||||||
|
|
||||||
for i, ch := range name {
|
for i, ch := range name {
|
||||||
if ch == '_' || ch == '-' {
|
if ch == '_' || ch == '-' {
|
||||||
prefixes = append(prefixes, name[:i+1])
|
candidates = append(candidates, name[:i+1]+"*")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return prefixes
|
|
||||||
|
tokens, sep := splitTokens(name)
|
||||||
|
if len(tokens) >= 3 && sep != 0 {
|
||||||
|
s := string(sep)
|
||||||
|
for pos := 1; pos < len(tokens)-1; pos++ {
|
||||||
|
tmpl := strings.Join(tokens[:pos], s) + s + "*" + s + strings.Join(tokens[pos+1:], s)
|
||||||
|
candidates = append(candidates, tmpl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitTokens(name string) ([]string, byte) {
|
||||||
|
if idx := strings.IndexByte(name, '_'); idx >= 0 {
|
||||||
|
return strings.Split(name, "_"), '_'
|
||||||
|
}
|
||||||
|
if idx := strings.IndexByte(name, '-'); idx >= 0 {
|
||||||
|
return strings.Split(name, "-"), '-'
|
||||||
|
}
|
||||||
|
return []string{name}, 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func globMatch(pattern, name string) bool {
|
||||||
|
star := strings.Index(pattern, "*")
|
||||||
|
if star < 0 {
|
||||||
|
return pattern == name
|
||||||
|
}
|
||||||
|
prefix := pattern[:star]
|
||||||
|
suffix := pattern[star+1:]
|
||||||
|
return strings.HasPrefix(name, prefix) &&
|
||||||
|
strings.HasSuffix(name, suffix) &&
|
||||||
|
len(name) >= len(prefix)+len(suffix)
|
||||||
}
|
}
|
||||||
|
|
||||||
func bestSource(patterns []*coredata.TrackerPattern) *coredata.CookieSource {
|
func bestSource(patterns []*coredata.TrackerPattern) *coredata.CookieSource {
|
||||||
@@ -333,24 +367,24 @@ func (h *patternAnalysisHandler) adoptUncategorisedPatterns(
|
|||||||
return false, fmt.Errorf("cannot load uncategorised category: %w", err)
|
return false, fmt.Errorf("cannot load uncategorised category: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var prefixPatterns coredata.TrackerPatterns
|
var globPatterns coredata.TrackerPatterns
|
||||||
if err := prefixPatterns.LoadAllByCookieBannerID(
|
if err := globPatterns.LoadAllByCookieBannerID(
|
||||||
ctx,
|
ctx,
|
||||||
tx,
|
tx,
|
||||||
scope,
|
scope,
|
||||||
banner.ID,
|
banner.ID,
|
||||||
coredata.NewTrackerPatternFilter(new(coredata.TrackerPatternMatchTypePrefix), nil, new(false)),
|
coredata.NewTrackerPatternFilter(new(coredata.TrackerPatternMatchTypeGlob), nil, new(false)),
|
||||||
nil,
|
nil,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return false, fmt.Errorf("cannot load prefix patterns: %w", err)
|
return false, fmt.Errorf("cannot load glob patterns: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(prefixPatterns) == 0 {
|
if len(globPatterns) == 0 {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Slice(prefixPatterns, func(i, j int) bool {
|
sort.Slice(globPatterns, func(i, j int) bool {
|
||||||
return len(prefixPatterns[i].Pattern) > len(prefixPatterns[j].Pattern)
|
return len(globPatterns[i].Pattern) > len(globPatterns[j].Pattern)
|
||||||
})
|
})
|
||||||
|
|
||||||
exactMatchType := coredata.TrackerPatternMatchTypeExact
|
exactMatchType := coredata.TrackerPatternMatchTypeExact
|
||||||
@@ -370,9 +404,9 @@ func (h *patternAnalysisHandler) adoptUncategorisedPatterns(
|
|||||||
for _, ep := range uncategorisedExact {
|
for _, ep := range uncategorisedExact {
|
||||||
var match *coredata.TrackerPattern
|
var match *coredata.TrackerPattern
|
||||||
epBucket := durationBucket(ep.MaxAgeSeconds)
|
epBucket := durationBucket(ep.MaxAgeSeconds)
|
||||||
for _, pp := range prefixPatterns {
|
for _, gp := range globPatterns {
|
||||||
if ep.TrackerType == pp.TrackerType && strings.HasPrefix(ep.Pattern, pp.Pattern) && durationBucket(pp.MaxAgeSeconds) == epBucket {
|
if ep.TrackerType == gp.TrackerType && globMatch(gp.Pattern, ep.Pattern) && durationBucket(gp.MaxAgeSeconds) == epBucket {
|
||||||
match = pp
|
match = gp
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -393,9 +427,9 @@ func (h *patternAnalysisHandler) adoptUncategorisedPatterns(
|
|||||||
adopted = true
|
adopted = true
|
||||||
h.logger.InfoCtx(
|
h.logger.InfoCtx(
|
||||||
ctx,
|
ctx,
|
||||||
"adopted uncategorised exact pattern into prefix pattern",
|
"adopted uncategorised exact pattern into glob pattern",
|
||||||
log.String("exact_pattern", ep.Pattern),
|
log.String("exact_pattern", ep.Pattern),
|
||||||
log.String("prefix_pattern", match.Pattern),
|
log.String("glob_pattern", match.Pattern),
|
||||||
log.String("banner_id", banner.ID.String()),
|
log.String("banner_id", banner.ID.String()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user