Filter consent records by version number

Replace the opaque cookieBannerVersionId filter with an
integer version filter. The SQL filter now resolves the
version number via a subquery against cookie_banner_versions.

Also fix the CookieBannerVersion resolver on consent records
to load the full version from the database instead of
returning a stub with only the ID set (which caused the
version to always display as 0).

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-27 15:09:21 +04:00
parent 4147239fbc
commit 84632fe795
6 changed files with 75 additions and 38 deletions

View File

@@ -64,7 +64,7 @@ const consentRecordsFragment = graphql`
last: { type: "Int", defaultValue: null } last: { type: "Int", defaultValue: null }
action: { type: "CookieConsentAction", defaultValue: null } action: { type: "CookieConsentAction", defaultValue: null }
visitorId: { type: "String", defaultValue: null } visitorId: { type: "String", defaultValue: null }
cookieBannerVersionId: { type: "ID", defaultValue: null } version: { type: "Int", defaultValue: null }
) { ) {
consentRecords( consentRecords(
first: $first first: $first
@@ -75,7 +75,7 @@ const consentRecordsFragment = graphql`
filter: { filter: {
action: $action action: $action
visitorId: $visitorId visitorId: $visitorId
cookieBannerVersionId: $cookieBannerVersionId version: $version
} }
) )
@connection( @connection(
@@ -109,7 +109,8 @@ export default function CookieBannerConsentRecordsPage({
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [actionFilter, setActionFilter] = useState<CookieConsentAction | null>(null); const [actionFilter, setActionFilter] = useState<CookieConsentAction | null>(null);
const [visitorIdFilter, setVisitorIdFilter] = useState<string>(""); const [visitorIdFilter, setVisitorIdFilter] = useState<string>("");
const [versionIdFilter, setVersionIdFilter] = useState<string>(""); const [versionFilter, setVersionFilter] = useState<string>("");
const [versionError, setVersionError] = useState(false);
const { data: fragmentData, ...pagination } = usePaginationFragment< const { data: fragmentData, ...pagination } = usePaginationFragment<
CookieBannerConsentRecordsPageRefetchQuery, CookieBannerConsentRecordsPageRefetchQuery,
@@ -124,7 +125,7 @@ export default function CookieBannerConsentRecordsPage({
{ {
action: actionFilter, action: actionFilter,
visitorId: visitorIdFilter || null, visitorId: visitorIdFilter || null,
cookieBannerVersionId: versionIdFilter || null, version: versionFilter ? parseInt(versionFilter, 10) : null,
...overrides, ...overrides,
}, },
{ fetchPolicy: "network-only" }, { fetchPolicy: "network-only" },
@@ -142,8 +143,13 @@ export default function CookieBannerConsentRecordsPage({
refetchFilters({ visitorId: visitorIdFilter || null }); refetchFilters({ visitorId: visitorIdFilter || null });
}; };
const handleVersionIdSubmit = () => { const handleVersionSubmit = () => {
refetchFilters({ cookieBannerVersionId: versionIdFilter || null }); if (versionFilter && !/^\d+$/.test(versionFilter)) {
setVersionError(true);
return;
}
setVersionError(false);
refetchFilters({ version: versionFilter ? parseInt(versionFilter, 10) : null });
}; };
const refetchWithFilters: ComponentProps<typeof SortableTable>["refetch"] = ({ order }) => { const refetchWithFilters: ComponentProps<typeof SortableTable>["refetch"] = ({ order }) => {
@@ -151,7 +157,7 @@ export default function CookieBannerConsentRecordsPage({
order: { direction: order.direction, field: order.field as CookieConsentRecordOrderField }, order: { direction: order.direction, field: order.field as CookieConsentRecordOrderField },
action: actionFilter, action: actionFilter,
visitorId: visitorIdFilter || null, visitorId: visitorIdFilter || null,
cookieBannerVersionId: versionIdFilter || null, version: versionFilter ? parseInt(versionFilter, 10) : null,
}); });
}; };
@@ -177,11 +183,15 @@ export default function CookieBannerConsentRecordsPage({
className="w-48" className="w-48"
/> />
<Input <Input
placeholder={__("Version ID")} placeholder={__("Banner version")}
value={versionIdFilter} value={versionFilter}
onChange={e => setVersionIdFilter(e.target.value)} invalid={versionError}
onKeyDown={e => e.key === "Enter" && handleVersionIdSubmit()} onChange={(e) => {
onBlur={handleVersionIdSubmit} setVersionFilter(e.target.value);
setVersionError(false);
}}
onKeyDown={e => e.key === "Enter" && handleVersionSubmit()}
onBlur={handleVersionSubmit}
className="w-48" className="w-48"
/> />
</div> </div>
@@ -198,7 +208,7 @@ export default function CookieBannerConsentRecordsPage({
<Tr> <Tr>
<Th>{__("Visitor ID")}</Th> <Th>{__("Visitor ID")}</Th>
<Th>{__("Action")}</Th> <Th>{__("Action")}</Th>
<Th>{__("Version")}</Th> <Th>{__("Banner Version")}</Th>
<Th>{__("IP Address")}</Th> <Th>{__("IP Address")}</Th>
<Th>{__("SDK Version")}</Th> <Th>{__("SDK Version")}</Th>
<Th>{__("Consent Data")}</Th> <Th>{__("Consent Data")}</Th>

View File

@@ -86,7 +86,6 @@ export function ConsentRecordRow({ recordKey }: ConsentRecordRowProps) {
{record.cookieBannerVersion {record.cookieBannerVersion
? ( ? (
<span className="font-mono text-sm"> <span className="font-mono text-sm">
v
{record.cookieBannerVersion.version} {record.cookieBannerVersion.version}
</span> </span>
) )

View File

@@ -16,25 +16,23 @@ package coredata
import ( import (
"github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5"
"go.probo.inc/probo/pkg/gid"
) )
type CookieConsentRecordFilter struct { type CookieConsentRecordFilter struct {
action *CookieConsentAction action *CookieConsentAction
visitorID *string visitorID *string
cookieBannerVersionID *gid.GID version *int
} }
func NewCookieConsentRecordFilter( func NewCookieConsentRecordFilter(
action *CookieConsentAction, action *CookieConsentAction,
visitorID *string, visitorID *string,
cookieBannerVersionID *gid.GID, version *int,
) *CookieConsentRecordFilter { ) *CookieConsentRecordFilter {
return &CookieConsentRecordFilter{ return &CookieConsentRecordFilter{
action: action, action: action,
visitorID: visitorID, visitorID: visitorID,
cookieBannerVersionID: cookieBannerVersionID, version: version,
} }
} }
@@ -58,8 +56,12 @@ AND
AND AND
( (
CASE CASE
WHEN @filter_cookie_banner_version_id::text IS NOT NULL THEN WHEN @filter_version::int IS NOT NULL THEN
cookie_banner_version_id = @filter_cookie_banner_version_id cookie_banner_version_id = (
SELECT id FROM cookie_banner_versions
WHERE cookie_banner_id = cookie_consent_records.cookie_banner_id
AND version = @filter_version
)
ELSE TRUE ELSE TRUE
END END
)` )`
@@ -67,9 +69,9 @@ AND
func (f *CookieConsentRecordFilter) SQLArguments() pgx.StrictNamedArgs { func (f *CookieConsentRecordFilter) SQLArguments() pgx.StrictNamedArgs {
args := pgx.StrictNamedArgs{ args := pgx.StrictNamedArgs{
"filter_action": nil, "filter_action": nil,
"filter_visitor_id": nil, "filter_visitor_id": nil,
"filter_cookie_banner_version_id": nil, "filter_version": nil,
} }
if f.action != nil { if f.action != nil {
@@ -80,8 +82,8 @@ func (f *CookieConsentRecordFilter) SQLArguments() pgx.StrictNamedArgs {
args["filter_visitor_id"] = *f.visitorID args["filter_visitor_id"] = *f.visitorID
} }
if f.cookieBannerVersionID != nil { if f.version != nil {
args["filter_cookie_banner_version_id"] = f.cookieBannerVersionID.String() args["filter_version"] = *f.version
} }
return args return args

View File

@@ -13,7 +13,6 @@ import (
"go.gearno.de/kit/log" "go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/cookiebanner" "go.probo.inc/probo/pkg/cookiebanner"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/gid"
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/probo" "go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/server/api/console/v1/schema" "go.probo.inc/probo/pkg/server/api/console/v1/schema"
@@ -144,17 +143,17 @@ func (r *cookieBannerResolver) ConsentRecords(ctx context.Context, obj *types.Co
scope := coredata.NewScopeFromObjectID(obj.ID) scope := coredata.NewScopeFromObjectID(obj.ID)
var ( var (
action *coredata.CookieConsentAction action *coredata.CookieConsentAction
visitorID *string visitorID *string
cookieBannerVersionID *gid.GID version *int
) )
if filter != nil { if filter != nil {
action = filter.Action action = filter.Action
visitorID = filter.VisitorID visitorID = filter.VisitorID
cookieBannerVersionID = filter.CookieBannerVersionID version = filter.Version
} }
coredataFilter := coredata.NewCookieConsentRecordFilter(action, visitorID, cookieBannerVersionID) coredataFilter := coredata.NewCookieConsentRecordFilter(action, visitorID, version)
records, err := r.cookieBanner.ListCookieConsentRecordsForBanner(ctx, scope, obj.ID, cursor, coredataFilter) records, err := r.cookieBanner.ListCookieConsentRecordsForBanner(ctx, scope, obj.ID, cursor, coredataFilter)
if err != nil { if err != nil {

View File

@@ -7,8 +7,10 @@ package console_v1
import ( import (
"context" "context"
"errors"
"go.gearno.de/kit/log" "go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/cookiebanner"
"go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/probo" "go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/server/api/console/v1/schema" "go.probo.inc/probo/pkg/server/api/console/v1/schema"
@@ -23,7 +25,32 @@ func (r *cookieConsentRecordResolver) CookieBanner(ctx context.Context, obj *typ
// CookieBannerVersion is the resolver for the cookieBannerVersion field. // CookieBannerVersion is the resolver for the cookieBannerVersion field.
func (r *cookieConsentRecordResolver) CookieBannerVersion(ctx context.Context, obj *types.CookieConsentRecord) (*types.CookieBannerVersion, error) { func (r *cookieConsentRecordResolver) CookieBannerVersion(ctx context.Context, obj *types.CookieConsentRecord) (*types.CookieBannerVersion, error) {
return obj.CookieBannerVersion, nil if obj.CookieBannerVersion == nil {
return nil, nil
}
if err := r.authorize(ctx, obj.CookieBannerVersion.ID, probo.ActionCookieBannerVersionGet); err != nil {
return nil, err
}
scope := coredata.NewScopeFromObjectID(obj.CookieBannerVersion.ID)
version, err := r.cookieBanner.GetCookieBannerVersion(ctx, scope, obj.CookieBannerVersion.ID)
if err != nil {
if errors.Is(err, cookiebanner.ErrVersionNotFound) {
return nil, nil
}
r.logger.ErrorCtx(ctx, "cannot get cookie banner version", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.CookieBannerVersion{
ID: version.ID,
Version: version.Version,
State: string(version.State),
CreatedAt: version.CreatedAt,
UpdatedAt: version.UpdatedAt,
}, nil
} }
// TotalCount is the resolver for the totalCount field. // TotalCount is the resolver for the totalCount field.

View File

@@ -53,7 +53,7 @@ input CookieConsentRecordOrder
input CookieConsentRecordFilter { input CookieConsentRecordFilter {
action: CookieConsentAction action: CookieConsentAction
visitorId: String visitorId: String
cookieBannerVersionId: ID version: Int
} }
type CookieConsentRecord implements Node { type CookieConsentRecord implements Node {