Remap unmapped trackers on new vendor domains
When the common-third-party enrichment worker discovers owned domains for a vendor, tracker patterns that were detected and left unmatched before those domains were known had nothing to re-trigger them. Add RequestMappingForUnmappedByInitiatorDomains, a global re-arm that stamps mapping_requested_at on still-unmapped patterns whose detected trackers share one of the new domains, and call it from the enrichment worker's persist step for newly-inserted domains only. The mapping worker then re-resolves them through its existing domain-overlap path. Targeting is limited to patterns with no resolved vendor (no org third party and an absent or unlinked catalog row), so a pattern already attributed to this or any other vendor is never disturbed. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -1475,6 +1475,68 @@ WHERE
|
|||||||
return result.RowsAffected(), nil
|
return result.RowsAffected(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequestMappingForUnmappedByInitiatorDomains re-arms mapping on the
|
||||||
|
// still-unmapped org tracker patterns whose detected trackers share one
|
||||||
|
// of the given initiator domains, so the mapping worker re-resolves them
|
||||||
|
// via its domain-overlap signal. It is invoked by the common-third-party
|
||||||
|
// enrichment worker when a vendor gains new owned domains, a global
|
||||||
|
// catalog operation, so it is intentionally not tenant-scoped: a single
|
||||||
|
// catalog domain benefits all tenants' patterns.
|
||||||
|
//
|
||||||
|
// Only patterns with no resolved vendor are targeted (third_party_id IS
|
||||||
|
// NULL and an absent or unlinked catalog row), so a pattern already
|
||||||
|
// linked to the vendor that gained the domain - or to any other vendor -
|
||||||
|
// is never disturbed and never re-attributed. Extension-sourced patterns
|
||||||
|
// and patterns already queued for mapping are skipped. The
|
||||||
|
// common_tracker_patterns and detected_trackers subqueries are used only
|
||||||
|
// for filtering. Returns the number of patterns re-armed; an empty
|
||||||
|
// domains slice is a no-op.
|
||||||
|
func (tps *TrackerPatterns) RequestMappingForUnmappedByInitiatorDomains(
|
||||||
|
ctx context.Context,
|
||||||
|
tx pg.Tx,
|
||||||
|
domains []string,
|
||||||
|
) (int64, error) {
|
||||||
|
if len(domains) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
q := `
|
||||||
|
UPDATE tracker_patterns
|
||||||
|
SET
|
||||||
|
mapping_requested_at = NOW(),
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE
|
||||||
|
third_party_id IS NULL
|
||||||
|
AND mapping_requested_at IS NULL
|
||||||
|
AND (source IS NULL OR source != @extension_source)
|
||||||
|
AND (
|
||||||
|
common_tracker_pattern_id IS NULL
|
||||||
|
OR common_tracker_pattern_id IN (
|
||||||
|
SELECT id FROM common_tracker_patterns
|
||||||
|
WHERE common_third_party_id IS NULL
|
||||||
|
)
|
||||||
|
)
|
||||||
|
AND id IN (
|
||||||
|
SELECT DISTINCT tracker_pattern_id
|
||||||
|
FROM detected_trackers
|
||||||
|
WHERE initiator_domain = ANY(@domains)
|
||||||
|
AND tracker_pattern_id IS NOT NULL
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
args := pgx.StrictNamedArgs{
|
||||||
|
"extension_source": CookieSourceExtension,
|
||||||
|
"domains": domains,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := tx.Exec(ctx, q, args)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("cannot request mapping for unmapped tracker patterns by initiator domains: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.RowsAffected(), nil
|
||||||
|
}
|
||||||
|
|
||||||
// ResetAndRequestMappingByCookieCategoryID detaches every pattern in the
|
// ResetAndRequestMappingByCookieCategoryID detaches every pattern in the
|
||||||
// given category from its catalog row, org third party, and copied
|
// given category from its catalog row, org third party, and copied
|
||||||
// description, then re-arms mapping. Operators run this (via proboctl) on
|
// description, then re-arms mapping. Operators run this (via proboctl) on
|
||||||
|
|||||||
@@ -326,3 +326,186 @@ func TestResetStaleMappings(t *testing.T) {
|
|||||||
assert.Nil(t, load(fresh.ID).MappingRequestedAt, "recently claimed row must not be re-armed before the window elapses")
|
assert.Nil(t, load(fresh.ID).MappingRequestedAt, "recently claimed row must not be re-armed before the window elapses")
|
||||||
assert.Nil(t, load(completed.ID).MappingRequestedAt, "completed mapping (catalog row assigned) must never be re-armed")
|
assert.Nil(t, load(completed.ID).MappingRequestedAt, "completed mapping (catalog row assigned) must never be re-armed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRequestMappingForUnmappedByInitiatorDomains pins the new-domain
|
||||||
|
// re-map cascade: when a vendor gains owned domains, only still-unmapped
|
||||||
|
// org patterns whose detected trackers share one of those domains are
|
||||||
|
// re-armed. A pattern already linked to a vendor (via a catalog row that
|
||||||
|
// carries a common_third_party_id, or via third_party_id), an
|
||||||
|
// extension-sourced pattern, and a pattern whose trackers were seen on a
|
||||||
|
// different domain are all left untouched. An empty domain set is a
|
||||||
|
// no-op.
|
||||||
|
func TestRequestMappingForUnmappedByInitiatorDomains(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
client := test.PGClient(t)
|
||||||
|
ctx := context.Background()
|
||||||
|
fx := seedTrackerPatternFixture(t, ctx, client)
|
||||||
|
|
||||||
|
const (
|
||||||
|
vendorDomain = "vendor.example"
|
||||||
|
otherDomain = "unrelated.example"
|
||||||
|
)
|
||||||
|
|
||||||
|
now := time.Now().UTC().Truncate(time.Microsecond)
|
||||||
|
maxAge := 3600
|
||||||
|
|
||||||
|
// Cascade-deletes from the fixture's cookie_banner / organization
|
||||||
|
// drops cleanup take care of detected_trackers and third_parties; run
|
||||||
|
// these first (LIFO) so nothing is stranded if a FK lacks the cascade.
|
||||||
|
t.Cleanup(func() {
|
||||||
|
_ = client.WithTx(context.Background(), func(ctx context.Context, tx pg.Tx) error {
|
||||||
|
if _, err := tx.Exec(ctx, `DELETE FROM detected_trackers WHERE cookie_banner_id = $1`, fx.cookieBannerID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := tx.Exec(ctx, `DELETE FROM third_parties WHERE organization_id = $1`, fx.organizationID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// A catalog row already linked to a vendor: patterns pointing at it
|
||||||
|
// are considered mapped and must be left alone.
|
||||||
|
party := seedCommonThirdParty(t, ctx, client)
|
||||||
|
linkedCommon := coredata.CommonTrackerPattern{
|
||||||
|
ID: gid.New(gid.NilTenant, coredata.CommonTrackerPatternEntityType),
|
||||||
|
CommonThirdPartyID: &party.ID,
|
||||||
|
TrackerType: coredata.TrackerTypeCookie,
|
||||||
|
Pattern: "linked_catalog_" + gid.New(gid.NilTenant, coredata.CommonTrackerPatternEntityType).String(),
|
||||||
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
||||||
|
Confidence: 0.7,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
insertCommonTrackerPattern(t, ctx, client, linkedCommon)
|
||||||
|
|
||||||
|
// An org third party for the third_party_id-linked pattern.
|
||||||
|
orgThirdPartyID := gid.New(fx.scope.GetTenantID(), coredata.ThirdPartyEntityType)
|
||||||
|
orgThirdParty := coredata.ThirdParty{
|
||||||
|
ID: orgThirdPartyID,
|
||||||
|
OrganizationID: fx.organizationID,
|
||||||
|
Name: "Org Vendor",
|
||||||
|
Category: coredata.ThirdPartyCategoryAnalytics,
|
||||||
|
Certifications: []string{},
|
||||||
|
Countries: coredata.CountryCodes{},
|
||||||
|
Level: 1,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
||||||
|
return orgThirdParty.Insert(ctx, tx, fx.scope)
|
||||||
|
}))
|
||||||
|
|
||||||
|
newPattern := func(
|
||||||
|
pattern string,
|
||||||
|
source coredata.CookieSource,
|
||||||
|
commonID *gid.GID,
|
||||||
|
thirdPartyID *gid.GID,
|
||||||
|
) *coredata.TrackerPattern {
|
||||||
|
src := source
|
||||||
|
tp := &coredata.TrackerPattern{
|
||||||
|
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
|
||||||
|
OrganizationID: fx.organizationID,
|
||||||
|
CookieBannerID: fx.cookieBannerID,
|
||||||
|
CookieCategoryID: fx.cookieCategoryID,
|
||||||
|
CommonTrackerPatternID: commonID,
|
||||||
|
ThirdPartyID: thirdPartyID,
|
||||||
|
TrackerType: coredata.TrackerTypeCookie,
|
||||||
|
Pattern: pattern,
|
||||||
|
MatchType: coredata.TrackerPatternMatchTypeExact,
|
||||||
|
DisplayName: pattern,
|
||||||
|
MaxAgeSeconds: &maxAge,
|
||||||
|
Source: &src,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
||||||
|
return tp.Insert(ctx, tx, fx.scope)
|
||||||
|
}))
|
||||||
|
|
||||||
|
return tp
|
||||||
|
}
|
||||||
|
|
||||||
|
seedDetectedTracker := func(patternID gid.GID, identifier, domain string) {
|
||||||
|
d := domain
|
||||||
|
dt := &coredata.DetectedTracker{
|
||||||
|
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
|
||||||
|
CookieBannerID: fx.cookieBannerID,
|
||||||
|
TrackerPatternID: &patternID,
|
||||||
|
TrackerType: coredata.TrackerTypeCookie,
|
||||||
|
Identifier: identifier,
|
||||||
|
InitiatorDomain: &d,
|
||||||
|
LastDetectedAt: now,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
||||||
|
_, err := dt.Upsert(ctx, tx, fx.scope)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
unmapped := newPattern("unmapped", coredata.CookieSourceScript, nil, nil)
|
||||||
|
catalogLinked := newPattern("catalog_linked", coredata.CookieSourceScript, &linkedCommon.ID, nil)
|
||||||
|
thirdPartyLinked := newPattern("third_party_linked", coredata.CookieSourceScript, nil, &orgThirdPartyID)
|
||||||
|
extension := newPattern("extension", coredata.CookieSourceExtension, nil, nil)
|
||||||
|
otherDomainPattern := newPattern("other_domain", coredata.CookieSourceScript, nil, nil)
|
||||||
|
|
||||||
|
seedDetectedTracker(unmapped.ID, "unmapped_id", vendorDomain)
|
||||||
|
seedDetectedTracker(catalogLinked.ID, "catalog_linked_id", vendorDomain)
|
||||||
|
seedDetectedTracker(thirdPartyLinked.ID, "third_party_linked_id", vendorDomain)
|
||||||
|
seedDetectedTracker(extension.ID, "extension_id", vendorDomain)
|
||||||
|
seedDetectedTracker(otherDomainPattern.ID, "other_domain_id", otherDomain)
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
||||||
|
var patterns coredata.TrackerPatterns
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
count, err = patterns.RequestMappingForUnmappedByInitiatorDomains(ctx, tx, []string{vendorDomain})
|
||||||
|
|
||||||
|
return err
|
||||||
|
}))
|
||||||
|
|
||||||
|
assert.Equal(t, int64(1), count, "only the unmapped pattern sharing the domain must be re-armed")
|
||||||
|
|
||||||
|
load := func(id gid.GID) coredata.TrackerPattern {
|
||||||
|
var reloaded coredata.TrackerPattern
|
||||||
|
|
||||||
|
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
|
||||||
|
return reloaded.LoadByID(ctx, conn, fx.scope, id)
|
||||||
|
}))
|
||||||
|
|
||||||
|
return reloaded
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.NotNil(t, load(unmapped.ID).MappingRequestedAt, "unmapped pattern sharing the domain must be re-armed")
|
||||||
|
assert.Nil(t, load(catalogLinked.ID).MappingRequestedAt, "pattern linked to a vendor via the catalog must be left alone")
|
||||||
|
assert.Nil(t, load(thirdPartyLinked.ID).MappingRequestedAt, "pattern linked to an org third party must be left alone")
|
||||||
|
assert.Nil(t, load(extension.ID).MappingRequestedAt, "extension-sourced pattern must be skipped")
|
||||||
|
assert.Nil(t, load(otherDomainPattern.ID).MappingRequestedAt, "pattern seen on a different domain must not be re-armed")
|
||||||
|
|
||||||
|
// Empty domain set is a no-op.
|
||||||
|
var emptyCount int64
|
||||||
|
|
||||||
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
||||||
|
var patterns coredata.TrackerPatterns
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
emptyCount, err = patterns.RequestMappingForUnmappedByInitiatorDomains(ctx, tx, nil)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}))
|
||||||
|
|
||||||
|
assert.Equal(t, int64(0), emptyCount, "empty domain set must be a no-op")
|
||||||
|
}
|
||||||
|
|||||||
@@ -372,10 +372,37 @@ func (h *enrichmentHandler) persist(
|
|||||||
return fmt.Errorf("cannot persist common third party enrichment: %w", err)
|
return fmt.Errorf("cannot persist common third party enrichment: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
newDomains []string
|
||||||
|
remapped int64
|
||||||
|
)
|
||||||
|
|
||||||
for i := range domains {
|
for i := range domains {
|
||||||
if _, err := domains[i].Upsert(ctx, tx); err != nil {
|
inserted, err := domains[i].Upsert(ctx, tx)
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("cannot upsert common third party domain: %w", err)
|
return fmt.Errorf("cannot upsert common third party domain: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if inserted {
|
||||||
|
newDomains = append(newDomains, domains[i].Domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A newly-discovered owned domain can resolve org tracker
|
||||||
|
// patterns that were detected and left unmapped before the
|
||||||
|
// domain was known. Re-arm those still-unmapped patterns so
|
||||||
|
// the mapping worker re-resolves them via its domain-overlap
|
||||||
|
// signal. Only newly-inserted domains trigger this: a re-run
|
||||||
|
// that rediscovers existing domains cascaded them already.
|
||||||
|
if len(newDomains) > 0 {
|
||||||
|
var patterns coredata.TrackerPatterns
|
||||||
|
|
||||||
|
n, err := patterns.RequestMappingForUnmappedByInitiatorDomains(ctx, tx, newDomains)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("cannot re-arm mapping for unmapped tracker patterns: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
remapped = n
|
||||||
}
|
}
|
||||||
|
|
||||||
h.logger.InfoCtx(
|
h.logger.InfoCtx(
|
||||||
@@ -386,6 +413,8 @@ func (h *enrichmentHandler) persist(
|
|||||||
log.String("status", payload.Status),
|
log.String("status", payload.Status),
|
||||||
log.Bool("logo_stored", logoFile != nil),
|
log.Bool("logo_stored", logoFile != nil),
|
||||||
log.Int("domains_stored", len(domains)),
|
log.Int("domains_stored", len(domains)),
|
||||||
|
log.Int("domains_new", len(newDomains)),
|
||||||
|
log.Int64("tracker_patterns_remapped", remapped),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Reference in New Issue
Block a user