Scope reset-trackers by keyword and report progress
The reset-trackers operator command reset every uncategorised, non-excluded pattern of a banner and printed only a single summary line once the transaction committed, giving no feedback during long rebuilds. Add a --keyword flag that scopes both the glob decomposition and the mapping reset to patterns whose pattern or display name contains the substring. The match lives in a new TrackerPatternFilter.WithPatternKeyword field so it runs in SQL and is shared by the glob load and the ResetAndRequestMappingByCookieCategoryID update, keeping the two in lockstep. The banner-wide pattern-analysis re-arm is left unscoped. Thread an optional progress callback through ResetBannerTrackers so the command streams per-phase updates (category load, per-glob decomposition, mapping reset, analysis re-arm) as the work runs. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -33,6 +33,21 @@ type ResetTrackersResult struct {
|
||||
AnalysisRequested bool
|
||||
}
|
||||
|
||||
// ResetProgressFunc receives human-readable progress messages emitted as
|
||||
// a banner reset advances through its phases. It is optional: pass nil to
|
||||
// run silently. Messages are emitted inside the reset transaction, so a
|
||||
// later failure that rolls the transaction back may leave already-printed
|
||||
// progress describing work that did not commit.
|
||||
type ResetProgressFunc func(message string)
|
||||
|
||||
func (p ResetProgressFunc) report(format string, args ...any) {
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
|
||||
p(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// ResetBannerTrackers re-arms the tracker pipeline for a banner's
|
||||
// uncategorised, non-excluded patterns. It is an operator action
|
||||
// (proboctl), tenant-scoped via the provided Scoper.
|
||||
@@ -50,39 +65,57 @@ type ResetTrackersResult struct {
|
||||
// every detection it covers becomes (or rejoins) an exact pattern keyed
|
||||
// by its identifier - and the now-empty glob is deleted. User-categorised
|
||||
// and excluded patterns are never touched.
|
||||
//
|
||||
// When keyword is non-nil and non-empty, the reset is scoped to patterns
|
||||
// whose pattern or display name contains it (case-insensitive): only
|
||||
// matching globs are decomposed and only matching patterns are re-armed
|
||||
// for mapping. The banner-wide pattern-analysis re-arm is unaffected.
|
||||
//
|
||||
// progress receives human-readable phase updates as the reset runs; pass
|
||||
// nil to run silently.
|
||||
func ResetBannerTrackers(
|
||||
ctx context.Context,
|
||||
pgClient *pg.Client,
|
||||
scope coredata.Scoper,
|
||||
bannerID gid.GID,
|
||||
mappingOnly bool,
|
||||
keyword *string,
|
||||
progress ResetProgressFunc,
|
||||
) (ResetTrackersResult, error) {
|
||||
var result ResetTrackersResult
|
||||
|
||||
err := pgClient.WithTx(
|
||||
ctx,
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
progress.report("Loading uncategorised category for banner %s...", bannerID)
|
||||
|
||||
var uncategorised coredata.CookieCategory
|
||||
if err := uncategorised.LoadUncategorisedByCookieBannerID(ctx, tx, scope, bannerID); err != nil {
|
||||
return fmt.Errorf("cannot load uncategorised category: %w", err)
|
||||
}
|
||||
|
||||
if !mappingOnly {
|
||||
if err := decomposeGlobs(ctx, tx, scope, bannerID, uncategorised.ID, &result); err != nil {
|
||||
if err := decomposeGlobs(ctx, tx, scope, bannerID, uncategorised.ID, keyword, &result, progress); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
progress.report("Resetting links and re-arming mapping on matching patterns...")
|
||||
|
||||
var patterns coredata.TrackerPatterns
|
||||
|
||||
reset, err := patterns.ResetAndRequestMappingByCookieCategoryID(ctx, tx, scope, uncategorised.ID)
|
||||
reset, err := patterns.ResetAndRequestMappingByCookieCategoryID(ctx, tx, scope, uncategorised.ID, keyword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot reset and request mapping: %w", err)
|
||||
}
|
||||
|
||||
result.PatternsReset = reset
|
||||
|
||||
progress.report("Reset %d pattern(s) and re-armed mapping.", reset)
|
||||
|
||||
if !mappingOnly {
|
||||
progress.report("Re-arming pattern analysis on banner %s...", bannerID)
|
||||
|
||||
banner := coredata.CookieBanner{ID: bannerID}
|
||||
if err := banner.SetPatternAnalysisRequested(ctx, tx); err != nil {
|
||||
return fmt.Errorf("cannot request pattern analysis: %w", err)
|
||||
@@ -110,7 +143,9 @@ func decomposeGlobs(
|
||||
scope coredata.Scoper,
|
||||
bannerID gid.GID,
|
||||
uncategorisedID gid.GID,
|
||||
keyword *string,
|
||||
result *ResetTrackersResult,
|
||||
progress ResetProgressFunc,
|
||||
) error {
|
||||
globMatchType := coredata.TrackerPatternMatchTypeGlob
|
||||
notExcluded := false
|
||||
@@ -121,18 +156,22 @@ func decomposeGlobs(
|
||||
tx,
|
||||
scope,
|
||||
bannerID,
|
||||
coredata.NewTrackerPatternFilter(&globMatchType, &uncategorisedID, ¬Excluded),
|
||||
coredata.NewTrackerPatternFilter(&globMatchType, &uncategorisedID, ¬Excluded).WithPatternKeyword(keyword),
|
||||
nil,
|
||||
); err != nil {
|
||||
return fmt.Errorf("cannot load glob patterns: %w", err)
|
||||
}
|
||||
|
||||
for _, glob := range globs {
|
||||
progress.report("Decomposing %d glob pattern(s) into exact patterns...", len(globs))
|
||||
|
||||
for i, glob := range globs {
|
||||
var detections coredata.DetectedTrackers
|
||||
if err := detections.LoadAllByTrackerPatternID(ctx, tx, scope, glob.ID); err != nil {
|
||||
return fmt.Errorf("cannot load detections for glob %q: %w", glob.Pattern, err)
|
||||
}
|
||||
|
||||
progress.report("[%d/%d] Decomposing glob %q (%d detection(s))...", i+1, len(globs), glob.Pattern, len(detections))
|
||||
|
||||
for _, detection := range detections {
|
||||
exactID, created, err := ensureExactPattern(ctx, tx, scope, glob, uncategorisedID, detection)
|
||||
if err != nil {
|
||||
|
||||
@@ -85,7 +85,7 @@ func TestResetBannerTrackers_FullRebuild(t *testing.T) {
|
||||
return nil
|
||||
}))
|
||||
|
||||
result, err := ResetBannerTrackers(ctx, client, fx.scope, fx.banner.ID, false)
|
||||
result, err := ResetBannerTrackers(ctx, client, fx.scope, fx.banner.ID, false, nil, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, result.GlobsDecomposed)
|
||||
|
||||
@@ -1390,14 +1390,19 @@ WHERE
|
||||
// iterating on the mapping agent. Excluded patterns are left untouched -
|
||||
// exclusion is a deliberate suppression. The cookie_category_id key
|
||||
// scopes the reset to the uncategorised category the caller resolves;
|
||||
// the Scoper keeps it tenant-isolated. Returns the number of patterns
|
||||
// reset.
|
||||
// the Scoper keeps it tenant-isolated. When keyword is non-nil and
|
||||
// non-empty, the reset is further restricted to patterns whose pattern or
|
||||
// display name contains it (case-insensitive). Returns the number of
|
||||
// patterns reset.
|
||||
func (tps *TrackerPatterns) ResetAndRequestMappingByCookieCategoryID(
|
||||
ctx context.Context,
|
||||
tx pg.Tx,
|
||||
scope Scoper,
|
||||
cookieCategoryID gid.GID,
|
||||
keyword *string,
|
||||
) (int64, error) {
|
||||
filter := NewTrackerPatternFilter(nil, nil, nil).WithPatternKeyword(keyword)
|
||||
|
||||
q := `
|
||||
UPDATE tracker_patterns
|
||||
SET
|
||||
@@ -1410,12 +1415,14 @@ WHERE
|
||||
%s
|
||||
AND cookie_category_id = @cookie_category_id
|
||||
AND excluded = false
|
||||
AND %s
|
||||
`
|
||||
|
||||
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||
q = fmt.Sprintf(q, scope.SQLFragment(), filter.SQLFragment())
|
||||
|
||||
args := pgx.StrictNamedArgs{"cookie_category_id": cookieCategoryID}
|
||||
maps.Copy(args, scope.SQLArguments())
|
||||
maps.Copy(args, filter.SQLArguments())
|
||||
|
||||
result, err := tx.Exec(ctx, q, args)
|
||||
if err != nil {
|
||||
|
||||
@@ -24,6 +24,7 @@ type TrackerPatternFilter struct {
|
||||
cookieCategoryID *gid.GID
|
||||
excluded *bool
|
||||
query *string
|
||||
patternKeyword *string
|
||||
source *CookieSource
|
||||
trackerType *TrackerType
|
||||
thirdPartyID *gid.GID
|
||||
@@ -47,6 +48,16 @@ func (f *TrackerPatternFilter) WithQuery(query *string) *TrackerPatternFilter {
|
||||
return f
|
||||
}
|
||||
|
||||
// WithPatternKeyword restricts the result to patterns whose pattern or
|
||||
// display name contains keyword (case-insensitive). It differs from
|
||||
// WithQuery, which matches display name or description: this targets the
|
||||
// raw pattern so operators can select by the matched domain or cookie
|
||||
// name. A nil or empty keyword disables the filter.
|
||||
func (f *TrackerPatternFilter) WithPatternKeyword(keyword *string) *TrackerPatternFilter {
|
||||
f.patternKeyword = keyword
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *TrackerPatternFilter) WithSource(source *CookieSource) *TrackerPatternFilter {
|
||||
f.source = source
|
||||
return f
|
||||
@@ -102,6 +113,13 @@ func (f *TrackerPatternFilter) SQLFragment() string {
|
||||
ELSE TRUE
|
||||
END
|
||||
AND
|
||||
CASE
|
||||
WHEN @filter_pattern_keyword::text IS NOT NULL AND @filter_pattern_keyword::text != '' THEN
|
||||
(pattern ILIKE '%' || @filter_pattern_keyword || '%'
|
||||
OR display_name ILIKE '%' || @filter_pattern_keyword || '%')
|
||||
ELSE TRUE
|
||||
END
|
||||
AND
|
||||
CASE
|
||||
WHEN @has_source_filter::boolean = false THEN TRUE
|
||||
WHEN @has_source_filter::boolean = true THEN
|
||||
@@ -148,6 +166,7 @@ func (f *TrackerPatternFilter) SQLArguments() pgx.StrictNamedArgs {
|
||||
"has_excluded_filter": false,
|
||||
"filter_excluded": nil,
|
||||
"filter_query": nil,
|
||||
"filter_pattern_keyword": nil,
|
||||
"has_source_filter": false,
|
||||
"filter_source": nil,
|
||||
"has_tracker_type_filter": false,
|
||||
@@ -177,6 +196,10 @@ func (f *TrackerPatternFilter) SQLArguments() pgx.StrictNamedArgs {
|
||||
args["filter_query"] = *f.query
|
||||
}
|
||||
|
||||
if f.patternKeyword != nil {
|
||||
args["filter_pattern_keyword"] = *f.patternKeyword
|
||||
}
|
||||
|
||||
if f.source != nil {
|
||||
args["has_source_filter"] = true
|
||||
args["filter_source"] = string(*f.source)
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
func newCmdResetTrackers(f *cmdutil.Factory) *cobra.Command {
|
||||
var (
|
||||
flagMappingOnly bool
|
||||
flagKeyword string
|
||||
flagDryRun bool
|
||||
flagYes bool
|
||||
)
|
||||
@@ -38,11 +39,14 @@ func newCmdResetTrackers(f *cmdutil.Factory) *cobra.Command {
|
||||
"non-excluded patterns it clears catalog/vendor links, rebuilds the raw exact " +
|
||||
"patterns from detected_trackers (decomposing derived globs), and re-arms the " +
|
||||
"pattern-analysis and mapping workers. User-categorised and excluded patterns are " +
|
||||
"preserved. With --mapping-only it skips the rebuild and only re-arms mapping.",
|
||||
"preserved. With --mapping-only it skips the rebuild and only re-arms mapping. " +
|
||||
"With --keyword the rebuild and mapping reset are scoped to patterns whose pattern " +
|
||||
"or display name contains the substring.",
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVar(&flagMappingOnly, "mapping-only", false, "Only re-arm mapping (skip the detection rebuild and analysis)")
|
||||
cmd.Flags().StringVar(&flagKeyword, "keyword", "", "Only reset patterns whose pattern or display name contains this substring")
|
||||
cmd.Flags().BoolVar(&flagDryRun, "dry-run", false, "Print the target banner without writing")
|
||||
cmd.Flags().BoolVar(&flagYes, "yes", false, "Skip confirmation")
|
||||
|
||||
@@ -68,6 +72,12 @@ func newCmdResetTrackers(f *cmdutil.Factory) *cobra.Command {
|
||||
mode = "mapping-only reset"
|
||||
}
|
||||
|
||||
var keyword *string
|
||||
if flagKeyword != "" {
|
||||
keyword = &flagKeyword
|
||||
mode = fmt.Sprintf("%s scoped to keyword %q", mode, flagKeyword)
|
||||
}
|
||||
|
||||
if flagDryRun {
|
||||
_, _ = fmt.Fprintf(out, "Would run %s on banner %s.\n", mode, bannerID.String())
|
||||
return nil
|
||||
@@ -77,7 +87,13 @@ func newCmdResetTrackers(f *cmdutil.Factory) *cobra.Command {
|
||||
return fmt.Errorf("about to run %s on banner %s; pass --yes to proceed or --dry-run to preview", mode, bannerID.String())
|
||||
}
|
||||
|
||||
result, err := cookiebanner.ResetBannerTrackers(ctx, pgClient, scope, bannerID, flagMappingOnly)
|
||||
_, _ = fmt.Fprintf(out, "Running %s on banner %s.\n", mode, bannerID.String())
|
||||
|
||||
progress := func(message string) {
|
||||
_, _ = fmt.Fprintf(out, " %s\n", message)
|
||||
}
|
||||
|
||||
result, err := cookiebanner.ResetBannerTrackers(ctx, pgClient, scope, bannerID, flagMappingOnly, keyword, progress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot reset banner %s: %w", bannerID, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user