Skip shared infrastructure in domain matching
The deterministic tracker-mapping heuristics group patterns by shared initiator domain, but tag managers, customer-data platforms, and generic CDNs (Google Tag Manager, Segment, cloudfront.net, ...) initiate trackers for many unrelated vendors. Grouping on such a domain mis-attributes one vendor's tracker to another. Add uri.FilterSharedInfrastructureDomains backed by a curated eTLD+1 denylist and apply it once in resolveDeterministic, so sibling grouping, catalog domain matching, and the sibling re-enqueue cascade all ignore shared-infrastructure hosts. Vendor-specific domains such as google-analytics.com are intentionally kept as a same-vendor signal. The agent path is unchanged: it still sees observed domains, now with a prompt caveat about shared infrastructure. Update the two sibling tests that used googletagmanager.com as the initiator domain to a vendor domain, since that host is now stripped before grouping. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -280,10 +280,11 @@ func (h *trackerMappingHandler) Process(ctx context.Context, tp coredata.Tracker
|
|||||||
// deterministicResult carries the outcome of the pure-SQL catalog
|
// deterministicResult carries the outcome of the pure-SQL catalog
|
||||||
// signals (existing link, pattern, sibling origin, domain overlap) from
|
// signals (existing link, pattern, sibling origin, domain overlap) from
|
||||||
// the read phase to the agent and persist phases. domains holds the
|
// the read phase to the agent and persist phases. domains holds the
|
||||||
// unfiltered initiator domains observed for the pattern (used by the
|
// observed initiator domains for the pattern with shared-infrastructure
|
||||||
// sibling re-enqueue cascade); commonThirdPartyPreexisted records
|
// hosts removed (used by the sibling re-enqueue cascade);
|
||||||
// whether a catalog third party was already known before this run, so
|
// commonThirdPartyPreexisted records whether a catalog third party was
|
||||||
// the cascade only fires when this run is the one that resolved it.
|
// already known before this run, so the cascade only fires when this run
|
||||||
|
// is the one that resolved it.
|
||||||
type deterministicResult struct {
|
type deterministicResult struct {
|
||||||
origin string
|
origin string
|
||||||
commonPatternID *gid.GID
|
commonPatternID *gid.GID
|
||||||
@@ -312,9 +313,6 @@ func (h *trackerMappingHandler) resolveDeterministic(
|
|||||||
return res, fmt.Errorf("cannot load cookie banner for domain filtering: %w", err)
|
return res, fmt.Errorf("cannot load cookie banner for domain filtering: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DEBUG: don't commit
|
|
||||||
banner.Origin = "https://t.probo.com"
|
|
||||||
|
|
||||||
res.origin = banner.Origin
|
res.origin = banner.Origin
|
||||||
|
|
||||||
if tp.CommonTrackerPatternID != nil {
|
if tp.CommonTrackerPatternID != nil {
|
||||||
@@ -349,16 +347,22 @@ func (h *trackerMappingHandler) resolveDeterministic(
|
|||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
res.domains = loaded
|
// Shared tracker-delivery infrastructure (tag managers, CDPs, generic
|
||||||
|
// CDNs) initiates trackers for many unrelated vendors, so a shared
|
||||||
|
// initiator domain among them is not a same-vendor signal. Strip them
|
||||||
|
// once here so no downstream domain-overlap heuristic (sibling
|
||||||
|
// grouping, catalog domain match, or the sibling re-enqueue cascade)
|
||||||
|
// can group unrelated trackers on, say, a common googletagmanager.com.
|
||||||
|
res.domains = uri.FilterSharedInfrastructureDomains(loaded)
|
||||||
|
|
||||||
// Sibling matching is an org-local co-occurrence signal: two
|
// Sibling matching is an org-local co-occurrence signal: two
|
||||||
// patterns served from the same origin on the same banner are likely
|
// patterns served from the same origin on the same banner are likely
|
||||||
// the same vendor, even when that origin is the site's own
|
// the same vendor, even when that origin is the site's own
|
||||||
// (first-party) host — a tracker proxied through first-party still
|
// (first-party) host — a tracker proxied through first-party still
|
||||||
// co-occurs with its siblings. So it intentionally runs on the
|
// co-occurs with its siblings. So it intentionally keeps first-party
|
||||||
// unfiltered domains; the ambiguity guard in
|
// domains (shared infrastructure was already removed above); the
|
||||||
// resolveThirdPartyFromSiblings prevents grouping unrelated
|
// ambiguity guard in resolveThirdPartyFromSiblings prevents grouping
|
||||||
// first-party scripts.
|
// unrelated first-party scripts.
|
||||||
siblingMatch, err := h.matchBySiblingOrigin(ctx, tx, tp, res.domains)
|
siblingMatch, err := h.matchBySiblingOrigin(ctx, tx, tp, res.domains)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return res, fmt.Errorf("cannot match by sibling origin: %w", err)
|
return res, fmt.Errorf("cannot match by sibling origin: %w", err)
|
||||||
|
|||||||
@@ -512,8 +512,10 @@ func TestMatchBySiblingOrigin_SiblingWithThirdPartyID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Detected trackers store the eTLD+1 (uri.ExtractDomain), so the
|
// Detected trackers store the eTLD+1 (uri.ExtractDomain), so the
|
||||||
// sibling lookup matches on that exact value.
|
// sibling lookup matches on that exact value. Use a vendor-specific
|
||||||
initiatorDomain := "googletagmanager.com"
|
// domain rather than shared infrastructure (e.g. googletagmanager.com),
|
||||||
|
// which resolveDeterministic strips before sibling grouping.
|
||||||
|
initiatorDomain := "google-analytics.com"
|
||||||
siblingDetected := coredata.DetectedTracker{
|
siblingDetected := coredata.DetectedTracker{
|
||||||
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
|
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
|
||||||
CookieBannerID: fx.banner.ID,
|
CookieBannerID: fx.banner.ID,
|
||||||
@@ -569,7 +571,7 @@ func TestMatchBySiblingOrigin_SiblingWithThirdPartyID(t *testing.T) {
|
|||||||
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
got, err = h.matchBySiblingOrigin(ctx, tx, unmappedPattern, []string{"googletagmanager.com"})
|
got, err = h.matchBySiblingOrigin(ctx, tx, unmappedPattern, []string{"google-analytics.com"})
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}))
|
}))
|
||||||
@@ -1120,7 +1122,10 @@ func TestProcess_BackfillsCommonThirdPartyFromSibling(t *testing.T) {
|
|||||||
UpdatedAt: now,
|
UpdatedAt: now,
|
||||||
}
|
}
|
||||||
|
|
||||||
initiatorDomain := "googletagmanager.com"
|
// A vendor-specific initiator domain: resolveDeterministic strips
|
||||||
|
// shared infrastructure (e.g. googletagmanager.com) before sibling
|
||||||
|
// grouping, so the backfill must be driven by a real vendor domain.
|
||||||
|
initiatorDomain := "google-analytics.com"
|
||||||
|
|
||||||
siblingDetected := coredata.DetectedTracker{
|
siblingDetected := coredata.DetectedTracker{
|
||||||
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
|
ID: gid.New(fx.scope.GetTenantID(), coredata.DetectedTrackerEntityType),
|
||||||
|
|||||||
114
pkg/uri/uri.go
114
pkg/uri/uri.go
@@ -125,3 +125,117 @@ func FilterFirstPartyDomains(domains []string, siteOrigin string) []string {
|
|||||||
|
|
||||||
return filtered
|
return filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sharedInfrastructureDomains are eTLD+1 hosts that deliver trackers on
|
||||||
|
// behalf of many unrelated vendors: tag managers, customer-data
|
||||||
|
// platforms, and generic CDNs / static-asset / app-hosting domains. A
|
||||||
|
// tracker whose initiator domain is one of these tells us nothing about
|
||||||
|
// which vendor set it (e.g. a Meta pixel and a LinkedIn tag both loaded
|
||||||
|
// through Google Tag Manager would otherwise look like the same third
|
||||||
|
// party), so domain-overlap heuristics must ignore them.
|
||||||
|
//
|
||||||
|
// This set is the tuning surface for that exclusion. Two rules keep it
|
||||||
|
// safe to extend:
|
||||||
|
//
|
||||||
|
// - Only add domains that serve content for many *unrelated* vendors.
|
||||||
|
// Omit vendor-specific domains such as google-analytics.com: those
|
||||||
|
// are a legitimate same-vendor signal even though Google also runs
|
||||||
|
// Tag Manager and gstatic.
|
||||||
|
// - Entries are eTLD+1, so they must not collapse onto a vendor. We
|
||||||
|
// skip cloudflare.com for this reason (cdnjs.cloudflare.com shares
|
||||||
|
// its eTLD+1 with Cloudflare-the-vendor's own properties).
|
||||||
|
//
|
||||||
|
// For exhaustive, maintained coverage this would ideally be sourced from
|
||||||
|
// a community dataset (DuckDuckGo Tracker Radar's `cnames`/CDN entries or
|
||||||
|
// Disconnect's services list) vendored into the repo, rather than hand
|
||||||
|
// curated. This list covers the common offenders without that dependency;
|
||||||
|
// excluding a domain only ever makes grouping more conservative, so
|
||||||
|
// over-inclusion is the safe failure mode.
|
||||||
|
var sharedInfrastructureDomains = map[string]struct{}{
|
||||||
|
// Tag managers, customer-data platforms, and tag delivery.
|
||||||
|
"googletagmanager.com": {},
|
||||||
|
"segment.io": {},
|
||||||
|
"segment.com": {},
|
||||||
|
"tealium.com": {},
|
||||||
|
"tiqcdn.com": {},
|
||||||
|
"ensighten.com": {},
|
||||||
|
"adobedtm.com": {},
|
||||||
|
"mparticle.com": {},
|
||||||
|
"rudderlabs.com": {},
|
||||||
|
"rudderstack.com": {},
|
||||||
|
"tagcommander.com": {},
|
||||||
|
"commander1.com": {},
|
||||||
|
|
||||||
|
// Commercial CDNs and edge networks.
|
||||||
|
"cloudfront.net": {},
|
||||||
|
"akamai.net": {},
|
||||||
|
"akamaihd.net": {},
|
||||||
|
"akamaized.net": {},
|
||||||
|
"akamaiedge.net": {},
|
||||||
|
"edgekey.net": {},
|
||||||
|
"edgesuite.net": {},
|
||||||
|
"fastly.net": {},
|
||||||
|
"fastlylb.net": {},
|
||||||
|
"azureedge.net": {},
|
||||||
|
"azurefd.net": {},
|
||||||
|
"edgecastcdn.net": {},
|
||||||
|
"llnwd.net": {},
|
||||||
|
"hwcdn.net": {},
|
||||||
|
"cachefly.net": {},
|
||||||
|
"stackpathdns.com": {},
|
||||||
|
"stackpathcdn.com": {},
|
||||||
|
"netdna-cdn.com": {},
|
||||||
|
"netdna-ssl.com": {},
|
||||||
|
"kxcdn.com": {},
|
||||||
|
"b-cdn.net": {},
|
||||||
|
|
||||||
|
// Library, package, and static-asset CDNs.
|
||||||
|
"jsdelivr.net": {},
|
||||||
|
"unpkg.com": {},
|
||||||
|
"bootstrapcdn.com": {},
|
||||||
|
"maxcdn.com": {},
|
||||||
|
"cdnjs.com": {},
|
||||||
|
"jquery.com": {},
|
||||||
|
"aspnetcdn.com": {},
|
||||||
|
"skypack.dev": {},
|
||||||
|
"esm.sh": {},
|
||||||
|
"googleapis.com": {},
|
||||||
|
"gstatic.com": {},
|
||||||
|
|
||||||
|
// Font and media CDNs.
|
||||||
|
"typekit.net": {},
|
||||||
|
"fontawesome.com": {},
|
||||||
|
"cloudinary.com": {},
|
||||||
|
"imgix.net": {},
|
||||||
|
|
||||||
|
// Object storage and generic app / static hosting.
|
||||||
|
"amazonaws.com": {},
|
||||||
|
"github.io": {},
|
||||||
|
"githubusercontent.com": {},
|
||||||
|
"herokuapp.com": {},
|
||||||
|
"vercel.app": {},
|
||||||
|
"netlify.app": {},
|
||||||
|
"pages.dev": {},
|
||||||
|
"web.app": {},
|
||||||
|
"firebaseapp.com": {},
|
||||||
|
"wp.com": {},
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterSharedInfrastructureDomains removes eTLD+1 domains that belong to
|
||||||
|
// shared tracker-delivery infrastructure (tag managers, customer-data
|
||||||
|
// platforms, and generic CDNs). Such domains initiate trackers for many
|
||||||
|
// unrelated vendors, so a shared initiator domain among them is not a
|
||||||
|
// same-vendor signal and must not drive domain-overlap grouping. The
|
||||||
|
// input domains are expected to be eTLD+1 strings (as produced by
|
||||||
|
// ExtractDomain).
|
||||||
|
func FilterSharedInfrastructureDomains(domains []string) []string {
|
||||||
|
filtered := make([]string, 0, len(domains))
|
||||||
|
|
||||||
|
for _, d := range domains {
|
||||||
|
if _, shared := sharedInfrastructureDomains[strings.ToLower(d)]; !shared {
|
||||||
|
filtered = append(filtered, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|||||||
@@ -364,3 +364,56 @@ func TestFilterFirstPartyDomains(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFilterSharedInfrastructureDomains(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
domains []string
|
||||||
|
want []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "removes tag manager domain",
|
||||||
|
domains: []string{"googletagmanager.com", "posthog.com"},
|
||||||
|
want: []string{"posthog.com"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "removes generic cdn domain",
|
||||||
|
domains: []string{"cloudfront.net", "hotjar.com"},
|
||||||
|
want: []string{"hotjar.com"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "keeps vendor-specific domains",
|
||||||
|
domains: []string{"google-analytics.com", "stripe.com"},
|
||||||
|
want: []string{"google-analytics.com", "stripe.com"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "case insensitive match",
|
||||||
|
domains: []string{"GoogleTagManager.com", "Segment.IO"},
|
||||||
|
want: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mixed infra and vendor",
|
||||||
|
domains: []string{"gstatic.com", "doubleclick.net", "jsdelivr.net"},
|
||||||
|
want: []string{"doubleclick.net"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty domains list",
|
||||||
|
domains: []string{},
|
||||||
|
want: []string{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil domains list",
|
||||||
|
domains: nil,
|
||||||
|
want: []string{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
assert.Equal(t, tt.want, FilterSharedInfrastructureDomains(tt.domains))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user