diff --git a/pkg/probo/tracker_policy_document.go b/pkg/probo/tracker_policy_document.go index d0a45361f..7abb78b9a 100644 --- a/pkg/probo/tracker_policy_document.go +++ b/pkg/probo/tracker_policy_document.go @@ -269,28 +269,102 @@ func (s *GeneratedDocumentService) buildTrackerPolicyThirdParties( return nil, fmt.Errorf("cannot load distinct third party ids: %w", err) } - if len(thirdPartyIDs) == 0 { + var thirdParties coredata.ThirdParties + if len(thirdPartyIDs) > 0 { + if err := thirdParties.LoadByIDs(ctx, conn, scope, thirdPartyIDs); err != nil { + return nil, fmt.Errorf("cannot load third parties: %w", err) + } + } + + // Patterns not linked to an org ThirdParty still surface their catalog + // (CommonThirdParty) vendor, mirroring the banner's linkedThirdParties + // union, so the policy stays complete whether or not the vendor was + // imported into the org register. + commonPatternIDs, err := patterns.LoadDistinctCommonTrackerPatternIDsByCookieBannerID(ctx, conn, scope, cookieBannerID) + if err != nil { + return nil, fmt.Errorf("cannot load distinct common tracker pattern ids: %w", err) + } + + var commonParties coredata.CommonThirdParties + if len(commonPatternIDs) > 0 { + var commonPatterns coredata.CommonTrackerPatterns + if err := commonPatterns.LoadByIDs(ctx, conn, commonPatternIDs); err != nil { + return nil, fmt.Errorf("cannot load common tracker patterns: %w", err) + } + + seen := make(map[gid.GID]struct{}, len(commonPatterns)) + commonThirdPartyIDs := make([]gid.GID, 0, len(commonPatterns)) + + for _, cp := range commonPatterns { + if cp.CommonThirdPartyID == nil { + continue + } + + if _, ok := seen[*cp.CommonThirdPartyID]; ok { + continue + } + + seen[*cp.CommonThirdPartyID] = struct{}{} + commonThirdPartyIDs = append(commonThirdPartyIDs, *cp.CommonThirdPartyID) + } + + if len(commonThirdPartyIDs) > 0 { + if err := commonParties.LoadByIDs(ctx, conn, commonThirdPartyIDs); err != nil { + return nil, fmt.Errorf("cannot load common third parties: %w", err) + } + } + } + + if len(thirdParties) == 0 && len(commonParties) == 0 { return nil, nil } - var thirdParties coredata.ThirdParties - if err := thirdParties.LoadByIDs(ctx, conn, scope, thirdPartyIDs); err != nil { - return nil, fmt.Errorf("cannot load third parties: %w", err) + rows := make([]docgen.TrackerPolicyThirdParty, 0, len(thirdParties)+len(commonParties)) + + // Dedupe by name so a vendor present both as an org third party (one + // pattern) and as a catalog entry (an unlinked pattern) is listed + // once. Org third parties are appended first, so their richer + // (user-editable) data wins for a shared name. + seenName := make(map[string]struct{}, len(thirdParties)+len(commonParties)) + + addRow := func(name, description, privacyPolicyURL string) { + name = strings.TrimSpace(name) + + key := strings.ToLower(name) + if _, ok := seenName[key]; ok { + return + } + + seenName[key] = struct{}{} + + rows = append(rows, docgen.TrackerPolicyThirdParty{ + Name: name, + Description: collapseWhitespace(description), + PrivacyPolicyURL: strings.TrimSpace(privacyPolicyURL), + }) } - rows := make([]docgen.TrackerPolicyThirdParty, 0, len(thirdParties)) for _, tp := range thirdParties { - row := docgen.TrackerPolicyThirdParty{Name: strings.TrimSpace(tp.Name)} - + description := "" if tp.Description != nil { - row.Description = collapseWhitespace(*tp.Description) + description = *tp.Description } + privacyPolicyURL := "" if tp.PrivacyPolicyURL != nil { - row.PrivacyPolicyURL = strings.TrimSpace(*tp.PrivacyPolicyURL) + privacyPolicyURL = *tp.PrivacyPolicyURL } - rows = append(rows, row) + addRow(tp.Name, description, privacyPolicyURL) + } + + for _, cp := range commonParties { + privacyPolicyURL := "" + if cp.PrivacyPolicyURL != nil { + privacyPolicyURL = *cp.PrivacyPolicyURL + } + + addRow(cp.Name, "", privacyPolicyURL) } // LoadByIDs returns rows in an unspecified order (no ORDER BY), so sort