From 7de90d79155b3f3bccc2791a9e02fce0a99b6b75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Tue, 16 Jun 2026 20:19:44 +0200 Subject: [PATCH] Keep attribution consistent with vendor links MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three paths could leave a catalog row's attribution out of step with its common_third_party_id. A FIRST_PARTY reclassification in the mapping worker kept a stale org ThirdParty link instead of clearing it. The upsert requeued terminal FIRST_PARTY rows for enrichment on a vendor they never adopt, since the vendor-preservation clause nulls it. And the proboctl upsert command did not normalize the verdict when an operator linked or unlinked a vendor without passing --attribution. Clear the org link on a first-party verdict, exclude FIRST_PARTY rows from the enrichment requeue, and have the CLI downgrade THIRD_PARTY to UNDETERMINED on unlink and promote UNDETERMINED to THIRD_PARTY on link. Signed-off-by: Émile Ré --- pkg/cookiebanner/tracker_mapping_worker.go | 9 +++++++- pkg/coredata/common_tracker_pattern.go | 13 +++++++---- pkg/proboctl/commontrackerpattern/upsert.go | 25 +++++++++++++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/pkg/cookiebanner/tracker_mapping_worker.go b/pkg/cookiebanner/tracker_mapping_worker.go index e582d9899..fd151a2a8 100644 --- a/pkg/cookiebanner/tracker_mapping_worker.go +++ b/pkg/cookiebanner/tracker_mapping_worker.go @@ -215,6 +215,7 @@ func (h *trackerMappingHandler) Process(ctx context.Context, tp coredata.Tracker commonPatternID := det.commonPatternID commonThirdPartyID := det.commonThirdPartyID directThirdPartyID := det.directThirdPartyID + firstParty := det.firstParty // Phase 2: tracker-mapping agent (no transaction). It runs only when // the deterministic signals could not resolve a catalog third party. @@ -251,6 +252,7 @@ func (h *trackerMappingHandler) Process(ctx context.Context, tp coredata.Tracker commonPatternID = firstNonNil(commonPatternID, match.commonPatternID) commonThirdPartyID = match.commonThirdPartyID + firstParty = match.firstParty return nil }, @@ -265,7 +267,12 @@ func (h *trackerMappingHandler) Process(ctx context.Context, tp coredata.Tracker // touches the database (in a short transaction). thirdPartyID := tp.ThirdPartyID - if thirdPartyID == nil { + // A first-party verdict is terminal: the artifact has no vendor, so + // any org ThirdParty link a prior mapping run left on the pattern is + // stale and must be cleared. + if firstParty { + thirdPartyID = nil + } else if thirdPartyID == nil { switch { case directThirdPartyID != nil: thirdPartyID = directThirdPartyID diff --git a/pkg/coredata/common_tracker_pattern.go b/pkg/coredata/common_tracker_pattern.go index 812f84075..5dcc3fbc7 100644 --- a/pkg/coredata/common_tracker_pattern.go +++ b/pkg/coredata/common_tracker_pattern.go @@ -309,16 +309,20 @@ SET -- re-queued for enrichment: the enrichment agent leaves descriptions -- blank when it cannot substantiate a purpose, and knowing the vendor -- gives it a second, better-informed attempt. The attempt counter is - -- reset so the re-armed row gets a fresh retry budget. + -- reset so the re-armed row gets a fresh retry budget. A terminal + -- FIRST_PARTY row never gains a vendor (the clause above discards the + -- incoming one), so it must never re-arm on a vendor it did not adopt. enrichment_requested_at = CASE - WHEN common_tracker_patterns.description = '' + WHEN common_tracker_patterns.attribution <> 'FIRST_PARTY' + AND common_tracker_patterns.description = '' AND common_tracker_patterns.common_third_party_id IS NULL AND EXCLUDED.common_third_party_id IS NOT NULL THEN NOW() ELSE common_tracker_patterns.enrichment_requested_at END, enrichment_attempts = CASE - WHEN common_tracker_patterns.description = '' + WHEN common_tracker_patterns.attribution <> 'FIRST_PARTY' + AND common_tracker_patterns.description = '' AND common_tracker_patterns.common_third_party_id IS NULL AND EXCLUDED.common_third_party_id IS NOT NULL THEN 0 @@ -330,7 +334,8 @@ SET -- unrecoverable: the stale-recovery sweep skips any row whose payload -- is non-null, so the re-claimed-but-never-finished row never requeues. enrichment = CASE - WHEN common_tracker_patterns.description = '' + WHEN common_tracker_patterns.attribution <> 'FIRST_PARTY' + AND common_tracker_patterns.description = '' AND common_tracker_patterns.common_third_party_id IS NULL AND EXCLUDED.common_third_party_id IS NOT NULL THEN NULL diff --git a/pkg/proboctl/commontrackerpattern/upsert.go b/pkg/proboctl/commontrackerpattern/upsert.go index 643dba378..730b7ea15 100644 --- a/pkg/proboctl/commontrackerpattern/upsert.go +++ b/pkg/proboctl/commontrackerpattern/upsert.go @@ -158,6 +158,18 @@ func newCmdUpsert(f *cmdutil.Factory) *cobra.Command { if cmd.Flags().Changed("common-third-party") { if flagCommonThirdParty == "" { pattern.CommonThirdPartyID = nil + + // Removing the vendor invalidates a THIRD_PARTY + // verdict, which by definition carries one. When the + // operator did not set an explicit --attribution, + // downgrade the now-stale verdict to UNDETERMINED so + // the mapping pipeline probes the vendor-free row + // again. A FIRST_PARTY row is already vendor-free and + // terminal, so it is left untouched. + if !cmd.Flags().Changed("attribution") && + pattern.Attribution == coredata.CommonTrackerPatternAttributionThirdParty { + pattern.Attribution = coredata.CommonTrackerPatternAttributionUndetermined + } } else { thirdPartyID, err := resolveCommonThirdPartyID(ctx, tx, flagCommonThirdParty) if err != nil { @@ -165,6 +177,19 @@ func newCmdUpsert(f *cmdutil.Factory) *cobra.Command { } pattern.CommonThirdPartyID = &thirdPartyID + + // A vendor-linked row is, by definition, attributed to + // a third party. When the operator did not set an + // explicit --attribution, normalize the verdict to + // THIRD_PARTY so the row never persists with an + // UNDETERMINED (or unset) verdict. A FIRST_PARTY row is + // terminal and stays vendor-free — the upsert discards + // the incoming vendor and keeps the verdict — so it is + // left untouched. + if !cmd.Flags().Changed("attribution") && + pattern.Attribution != coredata.CommonTrackerPatternAttributionFirstParty { + pattern.Attribution = coredata.CommonTrackerPatternAttributionThirdParty + } } }