Add tracker pattern detail page with properties and detected trackers sections

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-05-22 11:54:44 +02:00
parent f8debf5406
commit b46f2656f5
20 changed files with 1077 additions and 3 deletions

View File

@@ -465,6 +465,18 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
return types.NewCookieConsentRecord(record), nil
}
case coredata.TrackerPatternEntityType:
action = probo.ActionTrackerPatternGet
loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) {
scope := coredata.NewScopeFromObjectID(id)
pattern, err := r.cookieBanner.GetTrackerPattern(ctx, scope, id)
if err != nil {
return nil, err
}
return types.NewTrackerPatternNode(pattern), nil
}
case coredata.CookieBannerVersionEntityType:
action = probo.ActionCookieBannerVersionGet
loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) {

View File

@@ -434,6 +434,19 @@ func (r *cookieCategoryConnectionResolver) TotalCount(ctx context.Context, obj *
return count, nil
}
// TotalCount is the resolver for the totalCount field.
func (r *detectedTrackerConnectionResolver) TotalCount(ctx context.Context, obj *types.DetectedTrackerConnection) (int, error) {
scope := coredata.NewScopeFromObjectID(obj.ParentID)
count, err := r.cookieBanner.CountDetectedTrackersByPatternID(ctx, scope, obj.ParentID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count detected trackers", log.Error(err))
return 0, gqlutils.Internal(ctx)
}
return count, nil
}
// CreateCookieBanner is the resolver for the createCookieBanner field.
func (r *mutationResolver) CreateCookieBanner(ctx context.Context, input types.CreateCookieBannerInput) (*types.CreateCookieBannerPayload, error) {
if err := r.authorize(ctx, input.OrganizationID, probo.ActionCookieBannerCreate); err != nil {
@@ -1278,6 +1291,37 @@ func (r *trackerPatternResolver) DetectedCount(ctx context.Context, obj *types.T
return count, nil
}
// DetectedTrackers is the resolver for the detectedTrackers field.
func (r *trackerPatternResolver) DetectedTrackers(ctx context.Context, obj *types.TrackerPattern, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DetectedTrackerOrderBy) (*types.DetectedTrackerConnection, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionTrackerPatternGet); err != nil {
return nil, err
}
pageOrderBy := page.OrderBy[coredata.DetectedTrackerOrderField]{
Field: coredata.DetectedTrackerOrderFieldLastDetectedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.DetectedTrackerOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
scope := coredata.NewScopeFromObjectID(obj.ID)
trackers, err := r.cookieBanner.ListDetectedTrackersForPattern(ctx, scope, obj.ID, cursor)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list detected trackers", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
p := page.NewPage(trackers, cursor)
return types.NewDetectedTrackerConnection(p, r, obj.ID), nil
}
// Permission is the resolver for the permission field.
func (r *trackerPatternResolver) Permission(ctx context.Context, obj *types.TrackerPattern, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action)
@@ -1390,6 +1434,11 @@ func (r *Resolver) CookieCategoryConnection() schema.CookieCategoryConnectionRes
return &cookieCategoryConnectionResolver{r}
}
// DetectedTrackerConnection returns schema.DetectedTrackerConnectionResolver implementation.
func (r *Resolver) DetectedTrackerConnection() schema.DetectedTrackerConnectionResolver {
return &detectedTrackerConnectionResolver{r}
}
// TrackerPattern returns schema.TrackerPatternResolver implementation.
func (r *Resolver) TrackerPattern() schema.TrackerPatternResolver { return &trackerPatternResolver{r} }
@@ -1413,6 +1462,7 @@ type cookieBannerConnectionResolver struct{ *Resolver }
type cookieBannerVersionResolver struct{ *Resolver }
type cookieCategoryResolver struct{ *Resolver }
type cookieCategoryConnectionResolver struct{ *Resolver }
type detectedTrackerConnectionResolver struct{ *Resolver }
type trackerPatternResolver struct{ *Resolver }
type trackerPatternConnectionResolver struct{ *Resolver }
type trackerResourceResolver struct{ *Resolver }

View File

@@ -275,6 +275,14 @@ type TrackerPattern implements Node {
createdAt: Datetime!
updatedAt: Datetime!
detectedTrackers(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: DetectedTrackerOrder
): DetectedTrackerConnection @goField(forceResolver: true)
permission(action: String!): Boolean! @goField(forceResolver: true)
}
@@ -310,6 +318,52 @@ input TrackerPatternFilter
cookieCategoryId: ID
}
type DetectedTracker implements Node {
id: ID!
identifier: String!
initiatorUrl: String
maxAgeSeconds: Int
source: CookieSource
lastDetectedAt: Datetime!
createdAt: Datetime!
}
enum DetectedTrackerOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.DetectedTrackerOrderField"
) {
INITIATOR_URL
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DetectedTrackerOrderFieldInitiatorURL"
)
LAST_DETECTED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DetectedTrackerOrderFieldLastDetectedAt"
)
}
input DetectedTrackerOrder
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.DetectedTrackerOrderBy"
) {
direction: OrderDirection!
field: DetectedTrackerOrderField!
}
type DetectedTrackerConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.DetectedTrackerConnection"
) {
totalCount: Int! @goField(forceResolver: true)
edges: [DetectedTrackerEdge!]!
pageInfo: PageInfo!
}
type DetectedTrackerEdge {
cursor: CursorKey!
node: DetectedTracker!
}
enum TrackerResourceType
@goModel(model: "go.probo.inc/probo/pkg/coredata.TrackerResourceType") {
SCRIPT

View File

@@ -0,0 +1,76 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package types
import (
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page"
)
type (
DetectedTrackerOrderBy OrderBy[coredata.DetectedTrackerOrderField]
DetectedTrackerConnection struct {
TotalCount int
Edges []*DetectedTrackerEdge
PageInfo PageInfo
Resolver any
ParentID gid.GID
}
)
func NewDetectedTrackerConnection(
p *page.Page[*coredata.DetectedTracker, coredata.DetectedTrackerOrderField],
parentType any,
parentID gid.GID,
) *DetectedTrackerConnection {
edges := make([]*DetectedTrackerEdge, len(p.Data))
for i := range edges {
edges[i] = NewDetectedTrackerEdge(p.Data[i], p.Cursor.OrderBy.Field)
}
return &DetectedTrackerConnection{
Edges: edges,
PageInfo: *NewPageInfo(p),
Resolver: parentType,
ParentID: parentID,
}
}
func NewDetectedTrackerEdge(
dt *coredata.DetectedTracker,
orderBy coredata.DetectedTrackerOrderField,
) *DetectedTrackerEdge {
return &DetectedTrackerEdge{
Cursor: dt.CursorKey(orderBy),
Node: NewDetectedTrackerNode(dt),
}
}
func NewDetectedTrackerNode(dt *coredata.DetectedTracker) *DetectedTracker {
return &DetectedTracker{
ID: dt.ID,
Identifier: dt.Identifier,
InitiatorURL: dt.InitiatorURL,
MaxAgeSeconds: dt.MaxAgeSeconds,
Source: dt.Source,
LastDetectedAt: dt.LastDetectedAt,
CreatedAt: dt.CreatedAt,
}
}