Match sibling trackers on first-party origin

Sibling matching is an org-local co-occurrence signal: two patterns
served from the same origin on one banner are likely the same vendor,
even when that origin is the site's own (first-party) host. First-party
filtering only protects the global catalog (domain) match, where a
proxied tracker would otherwise hit the site owner's own entry. It now
runs solely before matchByDomain, so matchBySiblingOrigin sees the
unfiltered domains and promotion happens for patterns detected on the
banner's own origin.

Resolve the sibling's direct org third party and its catalog third party
as independent signals, so a single shared org third party no longer
short-circuits the common-pattern backfill.

Make the shared test fixtures unique per tenant: common_third_parties
and common_tracker_patterns are global with unique indexes, so parallel
tests previously collided on name, slug, and pattern. Also align the
sibling tests' stored initiator domains with production, which records
the eTLD+1.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-05-29 00:52:46 +02:00
parent 2a0523c5f2
commit c11bc57c36
3 changed files with 688 additions and 186 deletions

View File

@@ -90,99 +90,159 @@ func (h *trackerMappingHandler) Claim(ctx context.Context) (coredata.TrackerPatt
return tp, nil
}
// Process resolves the catalog mapping (when missing) and then promotes
// the pattern to an org ThirdParty (when eligible). Promotion is
// skipped for patterns still in the uncategorised category — the user
// must categorize a tracker before it creates or links an org
// ThirdParty. When a pattern is re-triggered by a manual move (it
// already carries a common_tracker_pattern_id), we MUST NOT re-resolve
// the catalog: the existing link is preserved and we jump straight to
// third-party promotion.
// catalogMatch is the result of a single catalog signal. commonPatternID
// is the catalog row the signal resolved (or backfilled); commonThirdPartyID
// is the catalog third party the signal discovered, when any; thirdPartyID
// is an existing org ThirdParty the signal knows directly (e.g. a sibling
// pattern already promoted in the same organization). A nil *catalogMatch
// means the signal produced nothing.
type catalogMatch struct {
commonPatternID *gid.GID
commonThirdPartyID *gid.GID
thirdPartyID *gid.GID
}
// Process resolves the catalog mapping for a tracker pattern and links it
// to an org ThirdParty. The primary goal is the org ThirdParty link; the
// catalog (common_tracker_patterns -> common_third_parties) is a fast,
// shared lookup layer that gets enriched along the way.
//
// Catalog resolution probes signals in order of confidence (existing
// catalog row, sibling origin, domain overlap, LLM agent) and keeps
// probing until it knows a common third party. Because every signal
// upserts the catalog row keyed by (tracker_type, pattern, max_age), a
// row that was previously unlinked is backfilled in place — this also
// applies on the re-trigger path, where the pattern already carries a
// common_tracker_pattern_id but its catalog row has no common third
// party yet.
//
// Org ThirdParty resolution links to an existing party freely (even for
// uncategorised or extension-sourced patterns); only the creation of a
// brand new org ThirdParty stays gated behind categorisation and a
// non-extension source.
func (h *trackerMappingHandler) Process(ctx context.Context, tp coredata.TrackerPattern) error {
return h.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
scope := coredata.NewScopeFromObjectID(tp.ID)
var banner coredata.CookieBanner
if err := banner.LoadByID(ctx, tx, scope, tp.CookieBannerID); err != nil {
return fmt.Errorf("cannot load cookie banner for domain filtering: %w", err)
}
banner.Origin = "https://t.probo.com"
var (
commonPatternID *gid.GID
err error
commonPatternID *gid.GID
commonThirdPartyID *gid.GID
directThirdPartyID *gid.GID
)
if tp.CommonTrackerPatternID != nil {
commonPatternID = tp.CommonTrackerPatternID
} else {
scope := coredata.NewScopeFromObjectID(tp.ID)
var banner coredata.CookieBanner
if err := banner.LoadByID(ctx, tx, scope, tp.CookieBannerID); err != nil {
return fmt.Errorf("cannot load cookie banner for domain filtering: %w", err)
var commonPattern coredata.CommonTrackerPattern
if err := commonPattern.LoadByID(ctx, tx, *commonPatternID); err != nil {
return fmt.Errorf("cannot load linked common tracker pattern: %w", err)
}
banner.Origin = "https://t.probo.com"
commonPatternID, err = h.matchByPattern(ctx, tx, tp)
commonThirdPartyID = commonPattern.CommonThirdPartyID
} else {
match, err := h.matchByPattern(ctx, tx, tp)
if err != nil {
return fmt.Errorf("cannot match by pattern: %w", err)
}
var domains []string
if match != nil {
commonPatternID = match.commonPatternID
commonThirdPartyID = match.commonThirdPartyID
}
}
if commonPatternID == nil {
var trackers coredata.DetectedTrackers
domains, err = trackers.LoadInitiatorDomainsByTrackerPatternID(ctx, tx, tp.ID, 10)
if err != nil {
return fmt.Errorf("cannot load initiator domains: %w", err)
}
domains = uri.FilterFirstPartyDomains(domains, banner.Origin)
commonPatternID, err = h.matchBySiblingOrigin(ctx, tx, tp, domains)
if err != nil {
return fmt.Errorf("cannot match by sibling origin: %w", err)
}
if commonThirdPartyID == nil {
domains, err := h.loadInitiatorDomains(ctx, tx, tp)
if err != nil {
return err
}
if commonPatternID == nil {
commonPatternID, err = h.matchByDomain(ctx, tx, tp, domains)
// Sibling matching is an org-local co-occurrence signal:
// two patterns served from the same origin on the same
// banner are likely the same vendor, even when that origin
// is the site's own (first-party) host — a tracker proxied
// through first-party still co-occurs with its siblings.
// So it intentionally runs on the unfiltered domains; the
// ambiguity guard in resolveThirdPartyFromSiblings prevents
// grouping unrelated first-party scripts.
match, err := h.matchBySiblingOrigin(ctx, tx, tp, domains)
if err != nil {
return fmt.Errorf("cannot match by sibling origin: %w", err)
}
if match != nil {
commonPatternID = firstNonNil(commonPatternID, match.commonPatternID)
commonThirdPartyID = match.commonThirdPartyID
directThirdPartyID = match.thirdPartyID
}
if commonThirdPartyID == nil {
// Domain matching hits the global catalog, so
// first-party domains must be stripped: a tracker
// proxied through the site's own host would otherwise
// match the site owner's own CommonThirdParty entry.
catalogDomains := uri.FilterFirstPartyDomains(domains, banner.Origin)
match, err := h.matchByDomain(ctx, tx, tp, catalogDomains)
if err != nil {
return fmt.Errorf("cannot match by domain: %w", err)
}
}
if commonPatternID == nil && h.mappingAgent != nil {
commonPatternID, err = h.identifyWithAgent(ctx, tx, tp, banner.Origin)
if err != nil {
return fmt.Errorf("cannot identify with agent: %w", err)
if match != nil {
commonPatternID = firstNonNil(commonPatternID, match.commonPatternID)
commonThirdPartyID = match.commonThirdPartyID
}
}
if commonPatternID == nil {
commonPatternID, err = h.createUnmatchedPattern(ctx, tx, tp)
if commonThirdPartyID == nil && h.mappingAgent != nil {
match, err := h.identifyWithAgent(ctx, tx, tp, banner.Origin)
if err != nil {
return fmt.Errorf("cannot create unmatched pattern: %w", err)
return fmt.Errorf("cannot identify with agent: %w", err)
}
if match != nil {
commonPatternID = firstNonNil(commonPatternID, match.commonPatternID)
commonThirdPartyID = match.commonThirdPartyID
}
}
}
thirdPartyID := tp.ThirdPartyID
if thirdPartyID == nil &&
commonPatternID != nil &&
(tp.Source == nil || *tp.Source != coredata.CookieSourceExtension) {
scope := coredata.NewScopeFromObjectID(tp.ID)
var category coredata.CookieCategory
if err := category.LoadByID(ctx, tx, scope, tp.CookieCategoryID); err != nil {
return fmt.Errorf("cannot load cookie category: %w", err)
if commonPatternID == nil {
id, err := h.createUnmatchedPattern(ctx, tx, tp)
if err != nil {
return fmt.Errorf("cannot create unmatched pattern: %w", err)
}
if category.Kind != coredata.CookieCategoryKindUncategorised {
promoted, err := h.promoteThirdParty(ctx, tx, tp, *commonPatternID)
commonPatternID = id
}
thirdPartyID := tp.ThirdPartyID
if thirdPartyID == nil {
switch {
case directThirdPartyID != nil:
thirdPartyID = directThirdPartyID
case commonThirdPartyID != nil:
allowCreate, err := h.creationAllowed(ctx, tx, scope, tp)
if err != nil {
return fmt.Errorf("cannot promote third party: %w", err)
return err
}
thirdPartyID = promoted
resolved, err := h.resolveOrgThirdParty(ctx, tx, tp, *commonThirdPartyID, allowCreate)
if err != nil {
return fmt.Errorf("cannot resolve org third party: %w", err)
}
thirdPartyID = resolved
}
}
@@ -198,7 +258,6 @@ func (h *trackerMappingHandler) Process(ctx context.Context, tp coredata.Tracker
}
}
scope := coredata.NewScopeFromObjectID(tp.ID)
if err := tp.Update(ctx, tx, scope); err != nil {
return fmt.Errorf("cannot update tracker pattern mapping: %w", err)
}
@@ -216,14 +275,69 @@ func (h *trackerMappingHandler) Process(ctx context.Context, tp coredata.Tracker
)
}
// matchByPattern looks for a catalog row with the same pattern. It now
// only returns the catalog ID; third-party resolution happens later in
// promoteThirdParty.
// firstNonNil returns a when it is set, otherwise b. It keeps the first
// catalog row id resolved by the pipeline stable: later signals upsert
// the same row (same key) and return the same id, but the explicit guard
// documents that the original match wins.
func firstNonNil(a, b *gid.GID) *gid.GID {
if a != nil {
return a
}
return b
}
// loadInitiatorDomains loads the distinct initiator domains observed for
// the pattern's detected trackers. The raw, unfiltered set is returned:
// callers matching against the global catalog must strip first-party
// domains themselves (uri.FilterFirstPartyDomains), but sibling matching
// deliberately keeps them, since co-occurrence on the site's own origin
// is still a valid same-vendor signal within a single banner.
func (h *trackerMappingHandler) loadInitiatorDomains(
ctx context.Context,
tx pg.Tx,
tp coredata.TrackerPattern,
) ([]string, error) {
var trackers coredata.DetectedTrackers
domains, err := trackers.LoadInitiatorDomainsByTrackerPatternID(ctx, tx, tp.ID, 10)
if err != nil {
return nil, fmt.Errorf("cannot load initiator domains: %w", err)
}
return domains, nil
}
// creationAllowed reports whether the pattern is eligible for creating a
// brand new org ThirdParty. Extension-sourced patterns are never allowed
// to create one, and a pattern must be categorized first.
func (h *trackerMappingHandler) creationAllowed(
ctx context.Context,
conn pg.Querier,
scope coredata.Scoper,
tp coredata.TrackerPattern,
) (bool, error) {
if tp.Source != nil && *tp.Source == coredata.CookieSourceExtension {
return false, nil
}
var category coredata.CookieCategory
if err := category.LoadByID(ctx, conn, scope, tp.CookieCategoryID); err != nil {
return false, fmt.Errorf("cannot load cookie category: %w", err)
}
return category.Kind != coredata.CookieCategoryKindUncategorised, nil
}
// matchByPattern looks for a catalog row with the same pattern and
// surfaces both the row id and the common third party it points at (when
// set), so the caller can short-circuit promotion or keep probing for a
// common third party to backfill an unlinked row.
func (h *trackerMappingHandler) matchByPattern(
ctx context.Context,
conn pg.Querier,
tp coredata.TrackerPattern,
) (*gid.GID, error) {
) (*catalogMatch, error) {
var commonPattern coredata.CommonTrackerPattern
if err := commonPattern.LoadByPattern(ctx, conn, tp.TrackerType, tp.Pattern, tp.MaxAgeSeconds); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
@@ -233,13 +347,17 @@ func (h *trackerMappingHandler) matchByPattern(
return nil, fmt.Errorf("cannot load common tracker pattern: %w", err)
}
return &commonPattern.ID, nil
return &catalogMatch{
commonPatternID: &commonPattern.ID,
commonThirdPartyID: commonPattern.CommonThirdPartyID,
}, nil
}
// matchByDomain finds a CommonThirdParty whose registered domains
// overlap the pattern's observed initiator domains, and upserts a
// CommonTrackerPattern linking the two. As with matchByPattern,
// third-party resolution is deferred to promoteThirdParty.
// CommonTrackerPattern linking the two. The upsert is keyed by
// (tracker_type, pattern, max_age), so it backfills a previously
// unlinked catalog row in place.
//
// The caller is responsible for loading and filtering the domains
// (removing first-party domains). Tracker scripts loaded through a
@@ -251,7 +369,7 @@ func (h *trackerMappingHandler) matchByDomain(
tx pg.Tx,
tp coredata.TrackerPattern,
domains []string,
) (*gid.GID, error) {
) (*catalogMatch, error) {
if len(domains) == 0 {
return nil, nil
}
@@ -287,7 +405,10 @@ func (h *trackerMappingHandler) matchByDomain(
return nil, fmt.Errorf("cannot upsert common tracker pattern from domain match: %w", err)
}
return &commonPattern.ID, nil
return &catalogMatch{
commonPatternID: &commonPattern.ID,
commonThirdPartyID: commonPattern.CommonThirdPartyID,
}, nil
}
func (h *trackerMappingHandler) identifyWithAgent(
@@ -295,7 +416,7 @@ func (h *trackerMappingHandler) identifyWithAgent(
tx pg.Tx,
tp coredata.TrackerPattern,
siteOrigin string,
) (*gid.GID, error) {
) (*catalogMatch, error) {
var trackers coredata.DetectedTrackers
domains, err := trackers.LoadInitiatorDomainsByTrackerPatternID(ctx, tx, tp.ID, 5)
@@ -388,7 +509,10 @@ func (h *trackerMappingHandler) identifyWithAgent(
log.Float64("confidence", identification.Confidence),
)
return &commonPattern.ID, nil
return &catalogMatch{
commonPatternID: &commonPattern.ID,
commonThirdPartyID: commonPattern.CommonThirdPartyID,
}, nil
}
func (h *trackerMappingHandler) resolveOrCreateCommonThirdParty(
@@ -453,15 +577,18 @@ func (h *trackerMappingHandler) resolveOrCreateCommonThirdParty(
}
// matchBySiblingOrigin finds other tracker patterns on the same banner
// that share initiator domains with the current pattern and are already
// mapped to a common third party. Sharing an origin across multiple
// detected patterns is a strong indicator of the same third party.
// that share initiator domains with the current pattern. Sharing an
// origin across multiple detected patterns is a strong indicator of the
// same third party. When the siblings resolve to a single existing org
// ThirdParty, that id is returned directly so promotion can link to it
// without re-running heuristics; otherwise the resolved common third
// party is upserted onto the catalog row.
func (h *trackerMappingHandler) matchBySiblingOrigin(
ctx context.Context,
tx pg.Tx,
tp coredata.TrackerPattern,
domains []string,
) (*gid.GID, error) {
) (*catalogMatch, error) {
if len(domains) == 0 {
return nil, nil
}
@@ -486,12 +613,19 @@ func (h *trackerMappingHandler) matchBySiblingOrigin(
scope := coredata.NewScopeFromObjectID(tp.ID)
commonThirdPartyID, err := h.resolveCommonThirdPartyFromSiblings(ctx, tx, scope, siblingIDs)
commonThirdPartyID, thirdPartyID, err := h.resolveThirdPartyFromSiblings(ctx, tx, scope, siblingIDs)
if err != nil {
return nil, fmt.Errorf("cannot resolve common third party from siblings: %w", err)
return nil, fmt.Errorf("cannot resolve third party from siblings: %w", err)
}
// No catalog third party to record: surface a directly-known org
// third party (if any) so promotion can still link to it, and leave
// catalog creation to a later signal or the unmatched fallback.
if commonThirdPartyID == nil {
if thirdPartyID != nil {
return &catalogMatch{thirdPartyID: thirdPartyID}, nil
}
return nil, nil
}
@@ -521,58 +655,81 @@ func (h *trackerMappingHandler) matchBySiblingOrigin(
log.String("common_third_party_id", commonThirdPartyID.String()),
)
return &commonPattern.ID, nil
return &catalogMatch{
commonPatternID: &commonPattern.ID,
commonThirdPartyID: commonPattern.CommonThirdPartyID,
thirdPartyID: thirdPartyID,
}, nil
}
// resolveCommonThirdPartyFromSiblings extracts a single unambiguous
// CommonThirdPartyID from sibling patterns. It first checks siblings
// with a third_party_id (fully promoted), then falls back to siblings
// with only a common_tracker_pattern_id.
func (h *trackerMappingHandler) resolveCommonThirdPartyFromSiblings(
// resolveThirdPartyFromSiblings inspects sibling patterns to resolve a
// third party. It returns two independent signals: a direct org
// ThirdParty (set only when the siblings share a single one — the
// strongest, same-org signal), and a single unambiguous catalog third
// party for backfill. The catalog third party is resolved first from the
// siblings' org ThirdParties, then, when those carry none, from siblings'
// common_tracker_pattern rows. Either signal may be nil; siblings that
// disagree on the catalog third party resolve it to nothing.
func (h *trackerMappingHandler) resolveThirdPartyFromSiblings(
ctx context.Context,
conn pg.Querier,
scope coredata.Scoper,
siblingIDs []gid.GID,
) (*gid.GID, error) {
) (commonThirdPartyID *gid.GID, thirdPartyID *gid.GID, err error) {
var patterns coredata.TrackerPatterns
thirdPartyIDs, err := patterns.LoadDistinctThirdPartyIDsByIDs(ctx, conn, scope, siblingIDs)
if err != nil {
return nil, fmt.Errorf("cannot load distinct third party ids from siblings: %w", err)
return nil, nil, fmt.Errorf("cannot load distinct third party ids from siblings: %w", err)
}
// A single org third party shared across the siblings is the
// strongest, same-org signal: link to it directly. This is resolved
// independently from the catalog third party used for backfill.
if len(thirdPartyIDs) == 1 {
directID := thirdPartyIDs[0]
thirdPartyID = &directID
}
if len(thirdPartyIDs) > 0 {
commonIDs := make(map[gid.GID]struct{})
for _, tpID := range thirdPartyIDs {
var tp coredata.ThirdParty
if err := tp.LoadByID(ctx, conn, scope, tpID); err != nil {
var t coredata.ThirdParty
if err := t.LoadByID(ctx, conn, scope, tpID); err != nil {
continue
}
if tp.CommonThirdPartyID != nil {
commonIDs[*tp.CommonThirdPartyID] = struct{}{}
if t.CommonThirdPartyID != nil {
commonIDs[*t.CommonThirdPartyID] = struct{}{}
}
}
if len(commonIDs) == 1 {
for id := range commonIDs {
return &id, nil
return &id, thirdPartyID, nil
}
}
// Siblings are promoted to several different catalog third
// parties: do not guess one. A single shared org third party (if
// any) is still a safe direct link.
if len(commonIDs) > 1 {
return nil, nil
return nil, thirdPartyID, nil
}
}
// Fall back to siblings carrying only a common_tracker_pattern_id, or
// whose org ThirdParty is not itself linked to the catalog. This is
// reached when the org-third-party scan above found no catalog third
// party, so it must not be short-circuited by a direct match.
commonPatternIDs, err := patterns.LoadDistinctCommonTrackerPatternIDsByIDs(ctx, conn, scope, siblingIDs)
if err != nil {
return nil, fmt.Errorf("cannot load distinct common tracker pattern ids from siblings: %w", err)
return nil, nil, fmt.Errorf("cannot load distinct common tracker pattern ids from siblings: %w", err)
}
if len(commonPatternIDs) == 0 {
return nil, nil
return nil, thirdPartyID, nil
}
commonIDs := make(map[gid.GID]struct{})
@@ -590,11 +747,11 @@ func (h *trackerMappingHandler) resolveCommonThirdPartyFromSiblings(
if len(commonIDs) == 1 {
for id := range commonIDs {
return &id, nil
return &id, thirdPartyID, nil
}
}
return nil, nil
return nil, thirdPartyID, nil
}
func (h *trackerMappingHandler) createUnmatchedPattern(
@@ -622,36 +779,29 @@ func (h *trackerMappingHandler) createUnmatchedPattern(
return &commonPattern.ID, nil
}
// promoteThirdParty resolves an org ThirdParty for the given pattern
// once the catalog mapping is known. The resolution order is:
// resolveOrgThirdParty resolves an org ThirdParty for the given pattern
// from a known catalog third party. The resolution order is:
//
// 1. Exact link by common_third_party_id (O(1)).
// 2. Heuristic match against the org's existing ThirdParty rows
// (lowercased name, suffix-stripped name, slug, website host,
// CommonThirdPartyDomain overlap).
// 3. Agent disambiguation when the heuristic is ambiguous.
// 4. Fallback create from CommonThirdParty.
// 4. Fallback create from CommonThirdParty — only when allowCreate.
//
// Linking to an existing org ThirdParty (steps 1-3) is always allowed.
// Creating a brand new org ThirdParty (step 4) is gated by allowCreate:
// when false, the function returns (nil, nil) rather than creating one.
// A confident heuristic/agent match is auto-tagged with
// common_third_party_id so subsequent promotions hit the exact-link
// path in O(1). Returns (nil, nil) when the catalog row has no
// CommonThirdPartyID — there is nothing to promote to.
func (h *trackerMappingHandler) promoteThirdParty(
// common_third_party_id so subsequent resolutions hit the exact-link
// path in O(1).
func (h *trackerMappingHandler) resolveOrgThirdParty(
ctx context.Context,
tx pg.Tx,
tp coredata.TrackerPattern,
commonPatternID gid.GID,
commonThirdPartyID gid.GID,
allowCreate bool,
) (*gid.GID, error) {
var commonPattern coredata.CommonTrackerPattern
if err := commonPattern.LoadByID(ctx, tx, commonPatternID); err != nil {
return nil, fmt.Errorf("cannot load common tracker pattern: %w", err)
}
if commonPattern.CommonThirdPartyID == nil {
return nil, nil
}
commonThirdPartyID := *commonPattern.CommonThirdPartyID
scope := coredata.NewScopeFromObjectID(tp.ID)
var existing coredata.ThirdParty
@@ -767,6 +917,10 @@ func (h *trackerMappingHandler) promoteThirdParty(
}
}
if !allowCreate {
return nil, nil
}
created, err := thirdparty.CreateFromCommon(ctx, tx, scope, tp.OrganizationID, commonParty)
if err != nil {
return nil, fmt.Errorf("cannot create third party from common: %w", err)

View File

@@ -30,7 +30,7 @@ import (
// promotionFixture extends workerFixture with a CommonThirdParty and a
// CommonTrackerPattern linking the catalog to the test pattern. It is
// the minimum scaffolding promoteThirdParty needs to run end-to-end.
// the minimum scaffolding resolveOrgThirdParty needs to run end-to-end.
type promotionFixture struct {
workerFixture
commonThirdParty coredata.CommonThirdParty
@@ -45,11 +45,20 @@ func seedPromotionFixture(t *testing.T, ctx context.Context, client *pg.Client)
fx := seedWorkerFixture(t, ctx, client)
now := time.Now().UTC().Truncate(time.Microsecond)
// common_third_parties (name/slug) and common_tracker_patterns
// (tracker_type, pattern, max_age_seconds) are global, NOT
// tenant-scoped, and both carry unique indexes. Tests run in
// parallel, so the catalog rows must be unique per fixture or
// concurrent runs collide. The tenant id is unique per fixture and
// makes a stable, collision-free suffix.
suffix := fx.scope.GetTenantID().String()
patternName := "_ga_" + suffix
commonThirdPartyID := gid.New(gid.NilTenant, coredata.CommonThirdPartyEntityType)
commonThirdParty := coredata.CommonThirdParty{
ID: commonThirdPartyID,
Name: "Google",
Slug: "google",
Name: "Google " + suffix,
Slug: "google-" + suffix,
Category: coredata.ThirdPartyCategoryAnalytics,
WebsiteURL: new("https://google.com"),
Certifications: []string{},
@@ -61,7 +70,7 @@ func seedPromotionFixture(t *testing.T, ctx context.Context, client *pg.Client)
ID: gid.New(gid.NilTenant, coredata.CommonTrackerPatternEntityType),
CommonThirdPartyID: &commonThirdPartyID,
TrackerType: coredata.TrackerTypeCookie,
Pattern: "_ga",
Pattern: patternName,
MatchType: coredata.TrackerPatternMatchTypeExact,
Description: "",
Confidence: 0.9,
@@ -76,9 +85,9 @@ func seedPromotionFixture(t *testing.T, ctx context.Context, client *pg.Client)
CookieCategoryID: fx.normalCategoryID,
CommonTrackerPatternID: &commonPattern.ID,
TrackerType: coredata.TrackerTypeCookie,
Pattern: "_ga",
Pattern: patternName,
MatchType: coredata.TrackerPatternMatchTypeExact,
DisplayName: "_ga",
DisplayName: patternName,
Description: "",
CreatedAt: now,
UpdatedAt: now,
@@ -134,7 +143,7 @@ func newMappingHandler(client *pg.Client) *trackerMappingHandler {
}
}
// promote runs promoteThirdParty inside its own transaction so each
// promote runs resolveOrgThirdParty inside its own transaction so each
// test case starts from a clean state.
func promote(
t *testing.T,
@@ -142,7 +151,8 @@ func promote(
h *trackerMappingHandler,
client *pg.Client,
tp coredata.TrackerPattern,
commonPatternID gid.GID,
commonThirdPartyID gid.GID,
allowCreate bool,
) *gid.GID {
t.Helper()
@@ -151,7 +161,7 @@ func promote(
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
var err error
got, err = h.promoteThirdParty(ctx, tx, tp, commonPatternID)
got, err = h.resolveOrgThirdParty(ctx, tx, tp, commonThirdPartyID, allowCreate)
return err
}))
@@ -183,7 +193,7 @@ func TestPromoteThirdParty_ExactCommonLink(t *testing.T) {
return existing.Insert(ctx, tx, fx.scope)
}))
got := promote(t, ctx, newMappingHandler(client), client, fx.trackerPattern, fx.commonPatternID)
got := promote(t, ctx, newMappingHandler(client), client, fx.trackerPattern, fx.commonThirdPartyID, true)
require.NotNil(t, got)
assert.Equal(t, existing.ID, *got, "should return the existing org ThirdParty linked by common id")
@@ -197,10 +207,13 @@ func TestPromoteThirdParty_HeuristicMatch(t *testing.T) {
fx := seedPromotionFixture(t, ctx, client)
now := time.Now().UTC().Truncate(time.Microsecond)
// Append a corporate suffix to the catalog name so the heuristic
// matches on the suffix-stripped name (score 0.9) rather than an
// exact link.
manualEntry := coredata.ThirdParty{
ID: gid.New(fx.scope.GetTenantID(), coredata.ThirdPartyEntityType),
OrganizationID: fx.organizationID,
Name: "Google LLC",
Name: fx.commonThirdParty.Name + " LLC",
Category: coredata.ThirdPartyCategoryAnalytics,
Certifications: []string{},
Countries: coredata.CountryCodes{},
@@ -212,7 +225,7 @@ func TestPromoteThirdParty_HeuristicMatch(t *testing.T) {
return manualEntry.Insert(ctx, tx, fx.scope)
}))
got := promote(t, ctx, newMappingHandler(client), client, fx.trackerPattern, fx.commonPatternID)
got := promote(t, ctx, newMappingHandler(client), client, fx.trackerPattern, fx.commonThirdPartyID, true)
require.NotNil(t, got)
assert.Equal(t, manualEntry.ID, *got, "heuristic match should return the manually-entered ThirdParty")
@@ -234,7 +247,7 @@ func TestPromoteThirdParty_FallbackCreate(t *testing.T) {
ctx := context.Background()
fx := seedPromotionFixture(t, ctx, client)
got := promote(t, ctx, newMappingHandler(client), client, fx.trackerPattern, fx.commonPatternID)
got := promote(t, ctx, newMappingHandler(client), client, fx.trackerPattern, fx.commonThirdPartyID, true)
require.NotNil(t, got, "fallback should create a new ThirdParty")
@@ -245,7 +258,7 @@ func TestPromoteThirdParty_FallbackCreate(t *testing.T) {
}))
assert.Equal(t, fx.organizationID, reloaded.OrganizationID)
assert.Equal(t, "Google", reloaded.Name)
assert.Equal(t, fx.commonThirdParty.Name, reloaded.Name)
require.NotNil(t, reloaded.CommonThirdPartyID)
assert.Equal(t, fx.commonThirdPartyID, *reloaded.CommonThirdPartyID)
assert.Equal(t, coredata.ThirdPartyCategoryAnalytics, reloaded.Category)
@@ -253,58 +266,21 @@ func TestPromoteThirdParty_FallbackCreate(t *testing.T) {
assert.False(t, reloaded.ShowOnTrustCenter)
}
func TestPromoteThirdParty_NoCommonThirdPartyOnPattern(t *testing.T) {
// TestResolveOrgThirdParty_CreationGated asserts that when no existing
// org ThirdParty matches the catalog third party, creating a new one is
// suppressed unless allowCreate is true.
func TestResolveOrgThirdParty_CreationGated(t *testing.T) {
t.Parallel()
client := newTestPgClient(t)
ctx := context.Background()
fx := seedWorkerFixture(t, ctx, client)
fx := seedPromotionFixture(t, ctx, client)
now := time.Now().UTC().Truncate(time.Microsecond)
commonPattern := coredata.CommonTrackerPattern{
ID: gid.New(gid.NilTenant, coredata.CommonTrackerPatternEntityType),
TrackerType: coredata.TrackerTypeCookie,
Pattern: "unknown_xyz",
MatchType: coredata.TrackerPatternMatchTypeExact,
Description: "",
Confidence: 0.5,
CreatedAt: now,
UpdatedAt: now,
}
gated := promote(t, ctx, newMappingHandler(client), client, fx.trackerPattern, fx.commonThirdPartyID, false)
assert.Nil(t, gated, "creation must be suppressed when allowCreate is false and nothing exists to link")
pattern := coredata.TrackerPattern{
ID: gid.New(fx.scope.GetTenantID(), coredata.TrackerPatternEntityType),
OrganizationID: fx.organizationID,
CookieBannerID: fx.banner.ID,
CookieCategoryID: fx.normalCategoryID,
CommonTrackerPatternID: &commonPattern.ID,
TrackerType: coredata.TrackerTypeCookie,
Pattern: "unknown_xyz",
MatchType: coredata.TrackerPatternMatchTypeExact,
DisplayName: "unknown_xyz",
CreatedAt: now,
UpdatedAt: now,
}
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
if _, err := commonPattern.Upsert(ctx, tx); err != nil {
return err
}
return pattern.Insert(ctx, tx, fx.scope)
}))
t.Cleanup(func() {
_ = client.WithTx(context.Background(), func(ctx context.Context, tx pg.Tx) error {
_, err := tx.Exec(ctx, `DELETE FROM common_tracker_patterns WHERE id = $1`, commonPattern.ID)
return err
})
})
got := promote(t, ctx, newMappingHandler(client), client, pattern, commonPattern.ID)
assert.Nil(t, got, "patterns whose catalog row has no CommonThirdPartyID should not be promoted")
allowed := promote(t, ctx, newMappingHandler(client), client, fx.trackerPattern, fx.commonThirdPartyID, true)
require.NotNil(t, allowed, "creation must proceed when allowCreate is true")
}
// TestProcess_PreservesCatalogMappingOnReTrigger asserts that when
@@ -539,7 +515,9 @@ func TestMatchBySiblingOrigin_SiblingWithThirdPartyID(t *testing.T) {
UpdatedAt: now,
}
initiatorDomain := "www.googletagmanager.com"
// Detected trackers store the eTLD+1 (uri.ExtractDomain), so the
// sibling lookup matches on that exact value.
initiatorDomain := "googletagmanager.com"
siblingDetected := coredata.DetectedTracker{
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
CookieBannerID: fx.banner.ID,
@@ -590,7 +568,7 @@ func TestMatchBySiblingOrigin_SiblingWithThirdPartyID(t *testing.T) {
h := newMappingHandler(client)
var got *gid.GID
var got *catalogMatch
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
var err error
@@ -600,12 +578,15 @@ func TestMatchBySiblingOrigin_SiblingWithThirdPartyID(t *testing.T) {
return err
}))
require.NotNil(t, got, "sibling origin match should return a common tracker pattern ID")
require.NotNil(t, got, "sibling origin match should return a catalog match")
require.NotNil(t, got.commonPatternID, "sibling origin match should return a common tracker pattern ID")
require.NotNil(t, got.thirdPartyID, "sibling origin match should surface the sibling's org third party directly")
assert.Equal(t, orgThirdParty.ID, *got.thirdPartyID)
var commonPattern coredata.CommonTrackerPattern
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
return commonPattern.LoadByID(ctx, conn, *got)
return commonPattern.LoadByID(ctx, conn, *got.commonPatternID)
}))
require.NotNil(t, commonPattern.CommonThirdPartyID)
@@ -622,11 +603,12 @@ func TestMatchBySiblingOrigin_AmbiguousThirdParties(t *testing.T) {
now := time.Now().UTC().Truncate(time.Microsecond)
otherSuffix := fx.scope.GetTenantID().String()
otherCommonThirdPartyID := gid.New(gid.NilTenant, coredata.CommonThirdPartyEntityType)
otherCommonThirdParty := coredata.CommonThirdParty{
ID: otherCommonThirdPartyID,
Name: "Facebook",
Slug: "facebook",
Name: "Facebook " + otherSuffix,
Slug: "facebook-" + otherSuffix,
Category: coredata.ThirdPartyCategoryMarketing,
Certifications: []string{},
CreatedAt: now,
@@ -698,7 +680,7 @@ func TestMatchBySiblingOrigin_AmbiguousThirdParties(t *testing.T) {
UpdatedAt: now,
}
sharedDomain := "cdn.shared-tracker.com"
sharedDomain := "shared-tracker.com"
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
if err := otherCommonThirdParty.Insert(ctx, tx); err != nil {
@@ -783,7 +765,7 @@ func TestMatchBySiblingOrigin_AmbiguousThirdParties(t *testing.T) {
h := newMappingHandler(client)
var got *gid.GID
var got *catalogMatch
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
var err error
@@ -824,7 +806,7 @@ func TestMatchBySiblingOrigin_NoSiblings(t *testing.T) {
h := newMappingHandler(client)
var got *gid.GID
var got *catalogMatch
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
var err error
@@ -865,7 +847,7 @@ func TestMatchBySiblingOrigin_EmptyDomains(t *testing.T) {
h := newMappingHandler(client)
var got *gid.GID
var got *catalogMatch
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
var err error
@@ -940,7 +922,7 @@ func TestMatchBySiblingOrigin_ConvergentSiblings(t *testing.T) {
UpdatedAt: now,
}
sharedDomain := "analytics.google.com"
sharedDomain := "google.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 {
@@ -1009,7 +991,7 @@ func TestMatchBySiblingOrigin_ConvergentSiblings(t *testing.T) {
h := newMappingHandler(client)
var got *gid.GID
var got *catalogMatch
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
var err error
@@ -1020,11 +1002,12 @@ func TestMatchBySiblingOrigin_ConvergentSiblings(t *testing.T) {
}))
require.NotNil(t, got, "multiple siblings converging to same third party should succeed")
require.NotNil(t, got.commonPatternID)
var commonPattern coredata.CommonTrackerPattern
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
return commonPattern.LoadByID(ctx, conn, *got)
return commonPattern.LoadByID(ctx, conn, *got.commonPatternID)
}))
require.NotNil(t, commonPattern.CommonThirdPartyID)
@@ -1071,8 +1054,374 @@ func TestPromoteThirdParty_ExactCommonLinkIgnoresSimilarUnlinked(t *testing.T) {
return linked.Insert(ctx, tx, fx.scope)
}))
got := promote(t, ctx, newMappingHandler(client), client, fx.trackerPattern, fx.commonPatternID)
got := promote(t, ctx, newMappingHandler(client), client, fx.trackerPattern, fx.commonThirdPartyID, true)
require.NotNil(t, got)
assert.Equal(t, linked.ID, *got, "exact-link path must short-circuit before the heuristic fires")
}
// TestProcess_BackfillsCommonThirdPartyFromSibling asserts that a pattern
// linked to an unlinked catalog row (no common_third_party_id) gets its
// catalog row backfilled from a sibling signal, and is promoted directly
// to the sibling's existing org ThirdParty.
func TestProcess_BackfillsCommonThirdPartyFromSibling(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,
}
siblingPattern := 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_backfill",
MatchType: coredata.TrackerPatternMatchTypeExact,
DisplayName: "_gid_backfill",
CreatedAt: now,
UpdatedAt: now,
}
unlinkedCommon := coredata.CommonTrackerPattern{
ID: gid.New(gid.NilTenant, coredata.CommonTrackerPatternEntityType),
TrackerType: coredata.TrackerTypeCookie,
Pattern: "_ga_backfill",
MatchType: coredata.TrackerPatternMatchTypeExact,
Confidence: 0.5,
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,
CommonTrackerPatternID: &unlinkedCommon.ID,
TrackerType: coredata.TrackerTypeCookie,
Pattern: "_ga_backfill",
MatchType: coredata.TrackerPatternMatchTypeExact,
DisplayName: "_ga_backfill",
CreatedAt: now,
UpdatedAt: now,
}
initiatorDomain := "googletagmanager.com"
siblingDetected := coredata.DetectedTracker{
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
CookieBannerID: fx.banner.ID,
TrackerPatternID: &siblingPattern.ID,
TrackerType: coredata.TrackerTypeCookie,
Identifier: "_gid_backfill",
InitiatorDomain: &initiatorDomain,
LastDetectedAt: now,
CreatedAt: now,
UpdatedAt: now,
}
targetDetected := coredata.DetectedTracker{
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
CookieBannerID: fx.banner.ID,
TrackerPatternID: &target.ID,
TrackerType: coredata.TrackerTypeCookie,
Identifier: "_ga_backfill",
InitiatorDomain: &initiatorDomain,
LastDetectedAt: now,
CreatedAt: now,
UpdatedAt: now,
}
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
if _, err := unlinkedCommon.Upsert(ctx, tx); err != nil {
return err
}
if err := orgThirdParty.Insert(ctx, tx, fx.scope); err != nil {
return err
}
if err := siblingPattern.Insert(ctx, tx, fx.scope); err != nil {
return err
}
if err := target.Insert(ctx, tx, fx.scope); err != nil {
return err
}
if _, err := siblingDetected.Upsert(ctx, tx, fx.scope); err != nil {
return err
}
if _, err := targetDetected.Upsert(ctx, tx, fx.scope); err != nil {
return err
}
return nil
}))
t.Cleanup(func() {
_ = client.WithTx(context.Background(), func(ctx context.Context, tx pg.Tx) error {
_, _ = tx.Exec(ctx, `DELETE FROM common_tracker_patterns WHERE id = $1`, unlinkedCommon.ID)
return nil
})
})
h := newMappingHandler(client)
require.NoError(t, h.Process(ctx, target))
var reloadedCommon coredata.CommonTrackerPattern
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
return reloadedCommon.LoadByID(ctx, conn, unlinkedCommon.ID)
}))
require.NotNil(t, reloadedCommon.CommonThirdPartyID, "the unlinked catalog row must be backfilled")
assert.Equal(t, fx.commonThirdPartyID, *reloadedCommon.CommonThirdPartyID)
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 be promoted to the sibling's org third party")
assert.Equal(t, orgThirdParty.ID, *reloadedTarget.ThirdPartyID)
require.NotNil(t, reloadedTarget.CommonTrackerPatternID)
assert.Equal(t, unlinkedCommon.ID, *reloadedTarget.CommonTrackerPatternID, "the existing catalog link must be preserved")
}
// TestProcess_UncategorisedLinksExistingThirdParty asserts that an
// uncategorised pattern is still linked to an already-existing matching
// org ThirdParty (linking to an existing party is ungated); only the
// creation of a new party stays gated, as covered by
// TestProcess_UncategorisedPatternIsNotPromoted.
func TestProcess_UncategorisedLinksExistingThirdParty(t *testing.T) {
t.Parallel()
client := newTestPgClient(t)
ctx := context.Background()
fx := seedPromotionFixture(t, ctx, client)
now := time.Now().UTC().Truncate(time.Microsecond)
existing := 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,
}
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
if err := existing.Insert(ctx, tx, fx.scope); err != nil {
return err
}
_, err := tx.Exec(
ctx,
`UPDATE tracker_patterns
SET cookie_category_id = $1,
mapping_requested_at = $2
WHERE id = $3`,
fx.uncategorisedID,
now,
fx.trackerPattern.ID,
)
return err
}))
var reloadedBefore coredata.TrackerPattern
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
return reloadedBefore.LoadByID(ctx, conn, fx.scope, fx.trackerPattern.ID)
}))
h := newMappingHandler(client)
require.NoError(t, h.Process(ctx, reloadedBefore))
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, fx.trackerPattern.ID)
}))
require.NotNil(t, reloaded.ThirdPartyID, "uncategorised pattern must still link to an existing org third party")
assert.Equal(t, existing.ID, *reloaded.ThirdPartyID)
}
// TestProcess_SiblingPromotionOnFirstPartyOrigin asserts that a pattern
// detected on the banner's own (first-party) origin is still grouped with
// its siblings sharing that origin. Sibling matching is an org-local
// co-occurrence signal and must not be defeated by the first-party domain
// filter that only protects the global catalog (domain) match.
func TestProcess_SiblingPromotionOnFirstPartyOrigin(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,
}
siblingPattern := 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: "_sibling_fp",
MatchType: coredata.TrackerPatternMatchTypeExact,
DisplayName: "_sibling_fp",
CreatedAt: now,
UpdatedAt: now,
}
unlinkedCommon := coredata.CommonTrackerPattern{
ID: gid.New(gid.NilTenant, coredata.CommonTrackerPatternEntityType),
TrackerType: coredata.TrackerTypeCookie,
Pattern: "__support__",
MatchType: coredata.TrackerPatternMatchTypeExact,
Confidence: 0.5,
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,
CommonTrackerPatternID: &unlinkedCommon.ID,
TrackerType: coredata.TrackerTypeCookie,
Pattern: "__support__",
MatchType: coredata.TrackerPatternMatchTypeExact,
DisplayName: "__support__",
CreatedAt: now,
UpdatedAt: now,
}
// The banner origin in seedWorkerFixture is an *.example.com host, so
// its eTLD+1 (the first-party domain) is "example.com". Detecting both
// patterns on that domain means uri.FilterFirstPartyDomains would strip
// it — the regression this test guards against.
firstPartyDomain := "example.com"
siblingDetected := coredata.DetectedTracker{
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
CookieBannerID: fx.banner.ID,
TrackerPatternID: &siblingPattern.ID,
TrackerType: coredata.TrackerTypeCookie,
Identifier: "_sibling_fp",
InitiatorDomain: &firstPartyDomain,
LastDetectedAt: now,
CreatedAt: now,
UpdatedAt: now,
}
targetDetected := coredata.DetectedTracker{
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
CookieBannerID: fx.banner.ID,
TrackerPatternID: &target.ID,
TrackerType: coredata.TrackerTypeCookie,
Identifier: "__support__",
InitiatorDomain: &firstPartyDomain,
LastDetectedAt: now,
CreatedAt: now,
UpdatedAt: now,
}
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
if _, err := unlinkedCommon.Upsert(ctx, tx); err != nil {
return err
}
if err := orgThirdParty.Insert(ctx, tx, fx.scope); err != nil {
return err
}
if err := siblingPattern.Insert(ctx, tx, fx.scope); err != nil {
return err
}
if err := target.Insert(ctx, tx, fx.scope); err != nil {
return err
}
if _, err := siblingDetected.Upsert(ctx, tx, fx.scope); err != nil {
return err
}
if _, err := targetDetected.Upsert(ctx, tx, fx.scope); err != nil {
return err
}
return nil
}))
t.Cleanup(func() {
_ = client.WithTx(context.Background(), func(ctx context.Context, tx pg.Tx) error {
_, _ = tx.Exec(ctx, `DELETE FROM common_tracker_patterns WHERE id = $1`, unlinkedCommon.ID)
return nil
})
})
h := newMappingHandler(client)
require.NoError(t, h.Process(ctx, target))
var reloadedCommon coredata.CommonTrackerPattern
require.NoError(t, client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {
return reloadedCommon.LoadByID(ctx, conn, unlinkedCommon.ID)
}))
require.NotNil(t, reloadedCommon.CommonThirdPartyID, "catalog row must be backfilled from the first-party sibling")
assert.Equal(t, fx.commonThirdPartyID, *reloadedCommon.CommonThirdPartyID)
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 sharing a first-party origin must be promoted via its sibling")
assert.Equal(t, orgThirdParty.ID, *reloadedTarget.ThirdPartyID)
}

View File

@@ -1418,7 +1418,6 @@ WHERE
%s
AND organization_id = @organization_id
AND common_third_party_id = @common_third_party_id
AND snapshot_id IS NULL
ORDER BY id ASC
LIMIT 1;
`