+
{pattern.displayName}
+ {commonTrackerPatternId
+ ? (
+
+ {commonTrackerPatternId}
+
+ )
+ : {__("Manual")}}
{pattern.description && (
{pattern.description}
diff --git a/e2e/console/tracker_pattern_test.go b/e2e/console/tracker_pattern_test.go
index be65a8322..f053639e4 100644
--- a/e2e/console/tracker_pattern_test.go
+++ b/e2e/console/tracker_pattern_test.go
@@ -15,12 +15,16 @@
package console_test
import (
+ "context"
"testing"
+ "time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.probo.inc/probo/e2e/internal/factory"
"go.probo.inc/probo/e2e/internal/testutil"
+ "go.probo.inc/probo/pkg/coredata"
+ "go.probo.inc/probo/pkg/gid"
)
func TestTrackerPattern_Create(t *testing.T) {
@@ -45,6 +49,7 @@ func TestTrackerPattern_Create(t *testing.T) {
displayName
maxAgeSeconds
description
+ commonTrackerPatternId
createdAt
updatedAt
}
@@ -60,15 +65,16 @@ func TestTrackerPattern_Create(t *testing.T) {
CreateTrackerPattern struct {
TrackerPatternEdge struct {
Node struct {
- ID string `json:"id"`
- Pattern string `json:"pattern"`
- MatchType string `json:"matchType"`
- TrackerType string `json:"trackerType"`
- DisplayName string `json:"displayName"`
- MaxAgeSeconds *int `json:"maxAgeSeconds"`
- Description string `json:"description"`
- CreatedAt string `json:"createdAt"`
- UpdatedAt string `json:"updatedAt"`
+ ID string `json:"id"`
+ Pattern string `json:"pattern"`
+ MatchType string `json:"matchType"`
+ TrackerType string `json:"trackerType"`
+ DisplayName string `json:"displayName"`
+ MaxAgeSeconds *int `json:"maxAgeSeconds"`
+ Description string `json:"description"`
+ CommonTrackerPatternID *string `json:"commonTrackerPatternId"`
+ CreatedAt string `json:"createdAt"`
+ UpdatedAt string `json:"updatedAt"`
} `json:"node"`
} `json:"trackerPatternEdge"`
CookieBanner struct {
@@ -101,6 +107,7 @@ func TestTrackerPattern_Create(t *testing.T) {
require.NotNil(t, node.MaxAgeSeconds)
assert.Equal(t, maxAge, *node.MaxAgeSeconds)
assert.Equal(t, "Google Analytics tracking cookie", node.Description)
+ assert.Nil(t, node.CommonTrackerPatternID, "a manually created pattern is not linked to the common catalog")
assert.Equal(t, bannerID, result.CreateTrackerPattern.CookieBanner.ID)
})
@@ -607,6 +614,93 @@ func TestTrackerPattern_List(t *testing.T) {
})
}
+func TestTrackerPattern_CommonTrackerPatternID(t *testing.T) {
+ t.Parallel()
+
+ t.Run("reflects the common catalog link", func(t *testing.T) {
+ t.Parallel()
+ owner := testutil.NewClient(t, testutil.RoleOwner)
+
+ bannerID := factory.CreateCookieBanner(owner)
+ categoryID := factory.CreateCookieCategory(owner, bannerID)
+ patternID := factory.CreateTrackerPattern(owner, categoryID)
+
+ const query = `
+ query($id: ID!) {
+ node(id: $id) {
+ ... on TrackerPattern {
+ id
+ commonTrackerPatternId
+ }
+ }
+ }
+ `
+
+ var result struct {
+ Node struct {
+ ID string `json:"id"`
+ CommonTrackerPatternID *string `json:"commonTrackerPatternId"`
+ } `json:"node"`
+ }
+
+ require.NoError(t, owner.Execute(query, map[string]any{"id": patternID}, &result))
+ assert.Nil(t, result.Node.CommonTrackerPatternID, "a freshly created pattern has no catalog link")
+
+ commonID := seedCommonTrackerPattern(t)
+ linkTrackerPatternToCommon(t, patternID, commonID)
+
+ require.NoError(t, owner.Execute(query, map[string]any{"id": patternID}, &result))
+ require.NotNil(t, result.Node.CommonTrackerPatternID, "the catalog link must surface once set")
+ assert.Equal(t, commonID.String(), *result.Node.CommonTrackerPatternID)
+ })
+}
+
+func seedCommonTrackerPattern(t *testing.T) gid.GID {
+ t.Helper()
+
+ ctx := context.Background()
+ conn := dialTestPg(t, ctx)
+ t.Cleanup(func() { _ = conn.Close(ctx) })
+
+ id := gid.New(gid.NilTenant, coredata.CommonTrackerPatternEntityType)
+ now := time.Now().UTC()
+
+ _, err := conn.Exec(ctx, `
+ INSERT INTO common_tracker_patterns (
+ id, tracker_type, pattern, match_type, description, confidence, created_at, updated_at
+ ) VALUES (
+ $1, $2, $3, $4, $5, $6, $7, $8
+ )
+ `, id, "COOKIE", "e2e_common_"+id.String(), "EXACT", "Seeded catalog description", 1.0, now, now)
+ require.NoError(t, err)
+
+ t.Cleanup(func() {
+ cleanupCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ cleanupConn := dialTestPg(t, cleanupCtx)
+ defer func() { _ = cleanupConn.Close(cleanupCtx) }()
+
+ _, err := cleanupConn.Exec(cleanupCtx, `DELETE FROM common_tracker_patterns WHERE id = $1`, id)
+ assert.NoError(t, err, "cleanup: cannot delete seeded common tracker pattern %s", id)
+ })
+
+ return id
+}
+
+func linkTrackerPatternToCommon(t *testing.T, patternID string, commonID gid.GID) {
+ t.Helper()
+
+ ctx := context.Background()
+ conn := dialTestPg(t, ctx)
+ t.Cleanup(func() { _ = conn.Close(ctx) })
+
+ _, err := conn.Exec(ctx, `
+ UPDATE tracker_patterns SET common_tracker_pattern_id = $1 WHERE id = $2
+ `, commonID, patternID)
+ require.NoError(t, err)
+}
+
func TestTrackerPattern_RBAC(t *testing.T) {
t.Parallel()
diff --git a/packages/n8n-node/CHANGELOG.md b/packages/n8n-node/CHANGELOG.md
index 12632b4a2..034bd84da 100644
--- a/packages/n8n-node/CHANGELOG.md
+++ b/packages/n8n-node/CHANGELOG.md
@@ -4,6 +4,10 @@ All notable changes to the `@probo/n8n-nodes-probo` package will be documented i
## Unreleased
+### Added
+
+- Expose `commonTrackerPatternId` on tracker pattern `get`/`getAll` operations to indicate whether a pattern is linked to the common tracker catalog
+
## [0.191.0] - 2026-06-02
### Added
diff --git a/packages/n8n-node/nodes/Probo/actions/trackerPattern/get.operation.ts b/packages/n8n-node/nodes/Probo/actions/trackerPattern/get.operation.ts
index 7141481fa..cc533a729 100644
--- a/packages/n8n-node/nodes/Probo/actions/trackerPattern/get.operation.ts
+++ b/packages/n8n-node/nodes/Probo/actions/trackerPattern/get.operation.ts
@@ -51,6 +51,7 @@ export async function execute(
source
excluded
lastMatchedAt
+ commonTrackerPatternId
createdAt
updatedAt
}
diff --git a/packages/n8n-node/nodes/Probo/actions/trackerPattern/getAll.operation.ts b/packages/n8n-node/nodes/Probo/actions/trackerPattern/getAll.operation.ts
index 01c2680e0..e07149f94 100644
--- a/packages/n8n-node/nodes/Probo/actions/trackerPattern/getAll.operation.ts
+++ b/packages/n8n-node/nodes/Probo/actions/trackerPattern/getAll.operation.ts
@@ -86,6 +86,7 @@ export async function execute(
source
excluded
lastMatchedAt
+ commonTrackerPatternId
createdAt
updatedAt
}
diff --git a/pkg/cmd/tracker-pattern/list/list.go b/pkg/cmd/tracker-pattern/list/list.go
index c2eb1e562..c4b34e126 100644
--- a/pkg/cmd/tracker-pattern/list/list.go
+++ b/pkg/cmd/tracker-pattern/list/list.go
@@ -40,6 +40,7 @@ query($id: ID!, $first: Int, $after: CursorKey) {
source
excluded
lastMatchedAt
+ commonTrackerPatternId
}
}
pageInfo {
@@ -53,14 +54,15 @@ query($id: ID!, $first: Int, $after: CursorKey) {
`
type trackerPattern struct {
- ID string `json:"id"`
- Pattern string `json:"pattern"`
- MatchType string `json:"matchType"`
- TrackerType string `json:"trackerType"`
- DisplayName string `json:"displayName"`
- Source *string `json:"source"`
- Excluded bool `json:"excluded"`
- LastMatchedAt *string `json:"lastMatchedAt"`
+ ID string `json:"id"`
+ Pattern string `json:"pattern"`
+ MatchType string `json:"matchType"`
+ TrackerType string `json:"trackerType"`
+ DisplayName string `json:"displayName"`
+ Source *string `json:"source"`
+ Excluded bool `json:"excluded"`
+ LastMatchedAt *string `json:"lastMatchedAt"`
+ CommonTrackerPatternID *string `json:"commonTrackerPatternId"`
}
func NewCmdList(f *cmdutil.Factory) *cobra.Command {
@@ -157,10 +159,15 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
lastMatched = cmdutil.FormatTime(*p.LastMatchedAt)
}
- rows = append(rows, []string{p.ID, p.Pattern, p.MatchType, p.TrackerType, p.DisplayName, source, excluded, lastMatched})
+ commonPatternID := ""
+ if p.CommonTrackerPatternID != nil {
+ commonPatternID = *p.CommonTrackerPatternID
+ }
+
+ rows = append(rows, []string{p.ID, p.Pattern, p.MatchType, p.TrackerType, p.DisplayName, source, excluded, lastMatched, commonPatternID})
}
- t := cmdutil.NewTable("ID", "PATTERN", "MATCH TYPE", "TRACKER TYPE", "DISPLAY NAME", "SOURCE", "EXCLUDED", "LAST MATCHED").Rows(rows...)
+ t := cmdutil.NewTable("ID", "PATTERN", "MATCH TYPE", "TRACKER TYPE", "DISPLAY NAME", "SOURCE", "EXCLUDED", "LAST MATCHED", "COMMON PATTERN ID").Rows(rows...)
_, _ = fmt.Fprintln(f.IOStreams.Out, t)
if totalCount > len(patterns) {
diff --git a/pkg/cmd/tracker-pattern/view/view.go b/pkg/cmd/tracker-pattern/view/view.go
index f43e089ff..91309bf5f 100644
--- a/pkg/cmd/tracker-pattern/view/view.go
+++ b/pkg/cmd/tracker-pattern/view/view.go
@@ -39,6 +39,7 @@ query($id: ID!) {
source
excluded
lastMatchedAt
+ commonTrackerPatternId
createdAt
updatedAt
}
@@ -48,19 +49,20 @@ query($id: ID!) {
type viewResponse struct {
Node *struct {
- Typename string `json:"__typename"`
- ID string `json:"id"`
- Pattern string `json:"pattern"`
- MatchType string `json:"matchType"`
- TrackerType string `json:"trackerType"`
- DisplayName string `json:"displayName"`
- MaxAgeSeconds *int `json:"maxAgeSeconds"`
- Description *string `json:"description"`
- Source string `json:"source"`
- Excluded bool `json:"excluded"`
- LastMatchedAt *string `json:"lastMatchedAt"`
- CreatedAt string `json:"createdAt"`
- UpdatedAt string `json:"updatedAt"`
+ Typename string `json:"__typename"`
+ ID string `json:"id"`
+ Pattern string `json:"pattern"`
+ MatchType string `json:"matchType"`
+ TrackerType string `json:"trackerType"`
+ DisplayName string `json:"displayName"`
+ MaxAgeSeconds *int `json:"maxAgeSeconds"`
+ Description *string `json:"description"`
+ Source string `json:"source"`
+ Excluded bool `json:"excluded"`
+ LastMatchedAt *string `json:"lastMatchedAt"`
+ CommonTrackerPatternID *string `json:"commonTrackerPatternId"`
+ CreatedAt string `json:"createdAt"`
+ UpdatedAt string `json:"updatedAt"`
} `json:"node"`
}
@@ -138,6 +140,12 @@ func NewCmdView(f *cmdutil.Factory) *cobra.Command {
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Last Matched:"), cmdutil.FormatTime(*v.LastMatchedAt))
}
+ if v.CommonTrackerPatternID != nil && *v.CommonTrackerPatternID != "" {
+ _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Common Pattern:"), *v.CommonTrackerPatternID)
+ } else {
+ _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Origin:"), "Manual (no catalog link)")
+ }
+
_, _ = fmt.Fprintln(out)
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(v.CreatedAt))
_, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(v.UpdatedAt))
diff --git a/pkg/server/api/console/v1/graphql/cookie_banner.graphql b/pkg/server/api/console/v1/graphql/cookie_banner.graphql
index 1bc17728d..2bca80a12 100644
--- a/pkg/server/api/console/v1/graphql/cookie_banner.graphql
+++ b/pkg/server/api/console/v1/graphql/cookie_banner.graphql
@@ -313,6 +313,16 @@ type TrackerPattern implements Node
"""
commonThirdParty: CommonThirdParty @goField(forceResolver: true)
+ """
+ The common tracker-pattern catalog entry this pattern is linked to,
+ if any. Non-null means the pattern was matched to the global catalog
+ (so its description may originate from the Open Cookie Database seed
+ or the mapping/enrichment agents); null means no catalog link
+ (description was added manually or inherited). Exposed primarily for
+ debugging agent-generated descriptions.
+ """
+ commonTrackerPatternId: ID
+
detectedTrackers(
first: Int
after: CursorKey
diff --git a/pkg/server/api/console/v1/types/tracker_pattern.go b/pkg/server/api/console/v1/types/tracker_pattern.go
index 6d746d65d..e816ec3cc 100644
--- a/pkg/server/api/console/v1/types/tracker_pattern.go
+++ b/pkg/server/api/console/v1/types/tracker_pattern.go
@@ -31,10 +31,14 @@ type (
// (cookieCategory, detectedTrackers, thirdParty, commonThirdParty,
// detectedCount, permission) are populated by the resolver.
//
- // ThirdPartyID and CommonTrackerPatternID are not exposed in
- // GraphQL — they are foreign-key handles the resolver uses to load
- // the linked third party (org-scoped or via the common catalog)
- // without re-querying coredata.
+ // ThirdPartyID is not exposed in GraphQL — it is a foreign-key
+ // handle the resolver uses to load the linked org-scoped third
+ // party without re-querying coredata.
+ //
+ // CommonTrackerPatternID is exposed directly as the
+ // commonTrackerPatternId field: a non-null value indicates the
+ // pattern is linked to the common tracker-pattern catalog, which is
+ // used to debug the provenance of agent-generated descriptions.
TrackerPattern struct {
ID gid.GID `json:"id"`
TrackerType coredata.TrackerType `json:"trackerType"`
@@ -55,7 +59,7 @@ type (
Permission bool `json:"permission"`
ThirdPartyID *gid.GID `json:"-"`
- CommonTrackerPatternID *gid.GID `json:"-"`
+ CommonTrackerPatternID *gid.GID `json:"commonTrackerPatternId,omitempty"`
}
TrackerPatternConnection struct {
diff --git a/pkg/server/api/mcp/v1/specification.yaml b/pkg/server/api/mcp/v1/specification.yaml
index c5867c0ce..3abc9491b 100644
--- a/pkg/server/api/mcp/v1/specification.yaml
+++ b/pkg/server/api/mcp/v1/specification.yaml
@@ -9560,6 +9560,11 @@ components:
- "null"
format: date-time
description: Timestamp when a cookie last matched this pattern
+ common_tracker_pattern_id:
+ anyOf:
+ - $ref: "#/components/schemas/GID"
+ - type: "null"
+ description: Linked common tracker-pattern catalog ID, if any. Non-null indicates the pattern was matched to the global catalog (description may originate from the seed or agents); null means no catalog link.
created_at:
type: string
format: date-time
diff --git a/pkg/server/api/mcp/v1/types/tracker_pattern.go b/pkg/server/api/mcp/v1/types/tracker_pattern.go
index b5786d555..b9960aae8 100644
--- a/pkg/server/api/mcp/v1/types/tracker_pattern.go
+++ b/pkg/server/api/mcp/v1/types/tracker_pattern.go
@@ -26,21 +26,22 @@ func NewTrackerPattern(p *coredata.TrackerPattern) *TrackerPattern {
}
return &TrackerPattern{
- ID: p.ID,
- OrganizationID: p.OrganizationID,
- CookieBannerID: p.CookieBannerID,
- CookieCategoryID: p.CookieCategoryID,
- TrackerType: TrackerPatternTrackerType(p.TrackerType),
- Pattern: p.Pattern,
- MatchType: TrackerPatternMatchType(p.MatchType),
- DisplayName: p.DisplayName,
- MaxAgeSeconds: p.MaxAgeSeconds,
- Description: p.Description,
- Source: source,
- Excluded: p.Excluded,
- LastMatchedAt: p.LastMatchedAt,
- CreatedAt: p.CreatedAt,
- UpdatedAt: p.UpdatedAt,
+ ID: p.ID,
+ OrganizationID: p.OrganizationID,
+ CookieBannerID: p.CookieBannerID,
+ CookieCategoryID: p.CookieCategoryID,
+ TrackerType: TrackerPatternTrackerType(p.TrackerType),
+ Pattern: p.Pattern,
+ MatchType: TrackerPatternMatchType(p.MatchType),
+ DisplayName: p.DisplayName,
+ MaxAgeSeconds: p.MaxAgeSeconds,
+ Description: p.Description,
+ Source: source,
+ Excluded: p.Excluded,
+ LastMatchedAt: p.LastMatchedAt,
+ CommonTrackerPatternID: p.CommonTrackerPatternID,
+ CreatedAt: p.CreatedAt,
+ UpdatedAt: p.UpdatedAt,
}
}