Re-enqueue unmapped siblings after mapping

The tracker-mapping worker processes one pattern at a time and
matchBySiblingOrigin only reads already-resolved siblings, so vendor
propagation across a banner was forward-only. A sibling processed
before its peer resolved a vendor (for example, one that failed the
agent and fell back to an unmatched catalog row) was never revisited,
even once a later sibling clearly identified the same third party.

When a Process run newly establishes a common third party, re-arm
mapping_requested_at on same-banner siblings that share an initiator
domain and are still unpromoted and non-extension-sourced. The worker
re-claims them and matchBySiblingOrigin now finds the freshly mapped
pattern. Guarding on third_party_id IS NULL, mapping_requested_at IS
NULL, and a not-pre-existing common third party keeps cascades finite.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-05-29 01:10:59 +02:00
parent c11bc57c36
commit 8c10997681
3 changed files with 471 additions and 1 deletions

View File

@@ -1425,3 +1425,348 @@ func TestProcess_SiblingPromotionOnFirstPartyOrigin(t *testing.T) {
require.NotNil(t, reloadedTarget.ThirdPartyID, "target sharing a first-party origin must be promoted via its sibling")
assert.Equal(t, orgThirdParty.ID, *reloadedTarget.ThirdPartyID)
}
// TestProcess_ReenqueuesUnmappedSiblingOnResolve asserts that when a
// pattern newly resolves a catalog third party, same-banner siblings
// that share an initiator domain but are still unpromoted get their
// mapping re-armed (backward propagation), while the already-promoted
// sibling that supplied the resolution is left untouched.
func TestProcess_ReenqueuesUnmappedSiblingOnResolve(t *testing.T) {
t.Parallel()
client := newTestPgClient(t)
ctx := context.Background()
fx := seedPromotionFixture(t, ctx, client)
now := time.Now().UTC().Truncate(time.Microsecond)
orgThirdParty := coredata.ThirdParty{
ID: gid.New(fx.scope.GetTenantID(), coredata.ThirdPartyEntityType),
OrganizationID: fx.organizationID,
CommonThirdPartyID: &fx.commonThirdPartyID,
Name: "Google LLC",
Category: coredata.ThirdPartyCategoryAnalytics,
Certifications: []string{},
Countries: coredata.CountryCodes{},
CreatedAt: now,
UpdatedAt: now,
}
mappedSibling := coredata.TrackerPattern{
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
OrganizationID: fx.organizationID,
CookieBannerID: fx.banner.ID,
CookieCategoryID: fx.normalCategoryID,
CommonTrackerPatternID: &fx.commonPatternID,
ThirdPartyID: &orgThirdParty.ID,
TrackerType: coredata.TrackerTypeCookie,
Pattern: "_gid_reenq",
MatchType: coredata.TrackerPatternMatchTypeExact,
DisplayName: "_gid_reenq",
CreatedAt: now,
UpdatedAt: now,
}
target := coredata.TrackerPattern{
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
OrganizationID: fx.organizationID,
CookieBannerID: fx.banner.ID,
CookieCategoryID: fx.normalCategoryID,
TrackerType: coredata.TrackerTypeCookie,
Pattern: "_ga_reenq_target",
MatchType: coredata.TrackerPatternMatchTypeExact,
DisplayName: "_ga_reenq_target",
CreatedAt: now,
UpdatedAt: now,
}
unmappedSibling := coredata.TrackerPattern{
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
OrganizationID: fx.organizationID,
CookieBannerID: fx.banner.ID,
CookieCategoryID: fx.normalCategoryID,
TrackerType: coredata.TrackerTypeCookie,
Pattern: "_unmapped_reenq",
MatchType: coredata.TrackerPatternMatchTypeExact,
DisplayName: "_unmapped_reenq",
CreatedAt: now,
UpdatedAt: now,
}
sharedDomain := "reenq-tracker.com"
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
if err := orgThirdParty.Insert(ctx, tx, fx.scope); err != nil {
return err
}
for _, p := range []coredata.TrackerPattern{mappedSibling, target, unmappedSibling} {
if err := p.Insert(ctx, tx, fx.scope); err != nil {
return err
}
}
for id, identifier := range map[gid.GID]string{
mappedSibling.ID: "_gid_reenq",
target.ID: "_ga_reenq_target",
unmappedSibling.ID: "_unmapped_reenq",
} {
patternID := id
det := coredata.DetectedTracker{
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
CookieBannerID: fx.banner.ID,
TrackerPatternID: &patternID,
TrackerType: coredata.TrackerTypeCookie,
Identifier: identifier,
InitiatorDomain: &sharedDomain,
LastDetectedAt: now,
CreatedAt: now,
UpdatedAt: now,
}
if _, err := det.Upsert(ctx, tx, fx.scope); err != nil {
return err
}
}
return nil
}))
h := newMappingHandler(client)
require.NoError(t, h.Process(ctx, target))
var reloadedTarget coredata.TrackerPattern
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
return reloadedTarget.LoadByID(ctx, conn, fx.scope, target.ID)
}))
require.NotNil(t, reloadedTarget.ThirdPartyID, "target must resolve via its promoted sibling")
var reloadedUnmapped coredata.TrackerPattern
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
return reloadedUnmapped.LoadByID(ctx, conn, fx.scope, unmappedSibling.ID)
}))
require.NotNil(t, reloadedUnmapped.MappingRequestedAt, "unmapped sibling sharing the origin must be re-enqueued")
var reloadedMapped coredata.TrackerPattern
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
return reloadedMapped.LoadByID(ctx, conn, fx.scope, mappedSibling.ID)
}))
assert.Nil(t, reloadedMapped.MappingRequestedAt, "already-promoted sibling must not be re-enqueued")
}
// TestProcess_DoesNotReenqueuePromotedOrExtensionSiblings asserts that
// the re-enqueue skips siblings that are already promoted or
// EXTENSION-sourced, while still re-arming a plain unmapped sibling.
func TestProcess_DoesNotReenqueuePromotedOrExtensionSiblings(t *testing.T) {
t.Parallel()
client := newTestPgClient(t)
ctx := context.Background()
fx := seedPromotionFixture(t, ctx, client)
now := time.Now().UTC().Truncate(time.Microsecond)
orgThirdParty := coredata.ThirdParty{
ID: gid.New(fx.scope.GetTenantID(), coredata.ThirdPartyEntityType),
OrganizationID: fx.organizationID,
CommonThirdPartyID: &fx.commonThirdPartyID,
Name: "Google LLC",
Category: coredata.ThirdPartyCategoryAnalytics,
Certifications: []string{},
Countries: coredata.CountryCodes{},
CreatedAt: now,
UpdatedAt: now,
}
mappedSibling := coredata.TrackerPattern{
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
OrganizationID: fx.organizationID,
CookieBannerID: fx.banner.ID,
CookieCategoryID: fx.normalCategoryID,
CommonTrackerPatternID: &fx.commonPatternID,
ThirdPartyID: &orgThirdParty.ID,
TrackerType: coredata.TrackerTypeCookie,
Pattern: "_gid_guard",
MatchType: coredata.TrackerPatternMatchTypeExact,
DisplayName: "_gid_guard",
CreatedAt: now,
UpdatedAt: now,
}
target := coredata.TrackerPattern{
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
OrganizationID: fx.organizationID,
CookieBannerID: fx.banner.ID,
CookieCategoryID: fx.normalCategoryID,
TrackerType: coredata.TrackerTypeCookie,
Pattern: "_ga_guard_target",
MatchType: coredata.TrackerPatternMatchTypeExact,
DisplayName: "_ga_guard_target",
CreatedAt: now,
UpdatedAt: now,
}
plainSibling := coredata.TrackerPattern{
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
OrganizationID: fx.organizationID,
CookieBannerID: fx.banner.ID,
CookieCategoryID: fx.normalCategoryID,
TrackerType: coredata.TrackerTypeCookie,
Pattern: "_plain_guard",
MatchType: coredata.TrackerPatternMatchTypeExact,
DisplayName: "_plain_guard",
CreatedAt: now,
UpdatedAt: now,
}
extensionSource := coredata.CookieSourceExtension
extensionSibling := coredata.TrackerPattern{
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
OrganizationID: fx.organizationID,
CookieBannerID: fx.banner.ID,
CookieCategoryID: fx.normalCategoryID,
TrackerType: coredata.TrackerTypeCookie,
Pattern: "_ext_guard",
MatchType: coredata.TrackerPatternMatchTypeExact,
DisplayName: "_ext_guard",
Source: &extensionSource,
CreatedAt: now,
UpdatedAt: now,
}
sharedDomain := "guard-tracker.com"
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
if err := orgThirdParty.Insert(ctx, tx, fx.scope); err != nil {
return err
}
patterns := map[gid.GID]string{
mappedSibling.ID: "_gid_guard",
target.ID: "_ga_guard_target",
plainSibling.ID: "_plain_guard",
extensionSibling.ID: "_ext_guard",
}
for _, p := range []coredata.TrackerPattern{mappedSibling, target, plainSibling, extensionSibling} {
if err := p.Insert(ctx, tx, fx.scope); err != nil {
return err
}
}
for id, identifier := range patterns {
patternID := id
det := coredata.DetectedTracker{
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
CookieBannerID: fx.banner.ID,
TrackerPatternID: &patternID,
TrackerType: coredata.TrackerTypeCookie,
Identifier: identifier,
InitiatorDomain: &sharedDomain,
LastDetectedAt: now,
CreatedAt: now,
UpdatedAt: now,
}
if _, err := det.Upsert(ctx, tx, fx.scope); err != nil {
return err
}
}
return nil
}))
h := newMappingHandler(client)
require.NoError(t, h.Process(ctx, target))
reload := func(id gid.GID) coredata.TrackerPattern {
var p coredata.TrackerPattern
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
return p.LoadByID(ctx, conn, fx.scope, id)
}))
return p
}
require.NotNil(t, reload(target.ID).ThirdPartyID, "target must resolve via its promoted sibling")
require.NotNil(t, reload(plainSibling.ID).MappingRequestedAt, "plain unmapped sibling must be re-enqueued")
assert.Nil(t, reload(mappedSibling.ID).MappingRequestedAt, "promoted sibling must not be re-enqueued")
assert.Nil(t, reload(extensionSibling.ID).MappingRequestedAt, "EXTENSION-sourced sibling must not be re-enqueued")
}
// TestProcess_NoReenqueueWhenCommonThirdPartyPreexisted asserts that the
// re-trigger path, where the pattern's linked catalog row already carries
// a common third party, adds no new signal and therefore leaves unmapped
// siblings untouched (the cascade terminator).
func TestProcess_NoReenqueueWhenCommonThirdPartyPreexisted(t *testing.T) {
t.Parallel()
client := newTestPgClient(t)
ctx := context.Background()
fx := seedPromotionFixture(t, ctx, client)
now := time.Now().UTC().Truncate(time.Microsecond)
unmappedSibling := coredata.TrackerPattern{
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
OrganizationID: fx.organizationID,
CookieBannerID: fx.banner.ID,
CookieCategoryID: fx.normalCategoryID,
TrackerType: coredata.TrackerTypeCookie,
Pattern: "_unmapped_preexist",
MatchType: coredata.TrackerPatternMatchTypeExact,
DisplayName: "_unmapped_preexist",
CreatedAt: now,
UpdatedAt: now,
}
sharedDomain := "preexist-tracker.com"
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
if err := unmappedSibling.Insert(ctx, tx, fx.scope); err != nil {
return err
}
for id, identifier := range map[gid.GID]string{
fx.trackerPattern.ID: fx.trackerPattern.Pattern,
unmappedSibling.ID: "_unmapped_preexist",
} {
patternID := id
det := coredata.DetectedTracker{
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
CookieBannerID: fx.banner.ID,
TrackerPatternID: &patternID,
TrackerType: coredata.TrackerTypeCookie,
Identifier: identifier,
InitiatorDomain: &sharedDomain,
LastDetectedAt: now,
CreatedAt: now,
UpdatedAt: now,
}
if _, err := det.Upsert(ctx, tx, fx.scope); err != nil {
return err
}
}
return fx.trackerPattern.SetMappingRequested(ctx, tx)
}))
h := newMappingHandler(client)
require.NoError(t, h.Process(ctx, fx.trackerPattern))
var reloadedUnmapped coredata.TrackerPattern
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
return reloadedUnmapped.LoadByID(ctx, conn, fx.scope, unmappedSibling.ID)
}))
assert.Nil(t, reloadedUnmapped.MappingRequestedAt, "re-trigger with a pre-existing common third party must not re-enqueue siblings")
}