Add detected-cookies public REST endpoint for cookie auto-discovery

The JS SDK will override document.cookie to detect unknown cookies set
by the website and report them to a new POST /{bannerID}/detected-cookies
endpoint. Reported cookies are inserted into the "Uncategorised" category
and a draft version is created so the admin can review them.

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-04-22 16:29:06 +04:00
parent 990dfa8438
commit e52b7cc19e
2 changed files with 138 additions and 0 deletions

View File

@@ -127,6 +127,15 @@ type (
Action coredata.CookieConsentAction
}
DetectedCookie struct {
Name string
Duration string
}
ReportDetectedCookiesRequest struct {
Cookies []DetectedCookie
}
BannerConfig struct {
BannerID gid.GID `json:"banner_id"`
Version int `json:"version"`
@@ -1717,3 +1726,62 @@ func (s *Service) RecordConsent(
return record, nil
}
func (s *Service) ReportDetectedCookies(
ctx context.Context,
bannerID gid.GID,
req ReportDetectedCookiesRequest,
) error {
return s.pg.WithTx(
ctx,
func(ctx context.Context, tx pg.Tx) error {
var banner coredata.CookieBanner
if err := banner.LoadActiveByID(ctx, tx, bannerID); err != nil {
if errors.Is(err, coredata.ErrResourceNotFound) {
return ErrBannerNotFound
}
return fmt.Errorf("cannot load active cookie banner: %w", err)
}
scope := coredata.NewScopeFromObjectID(banner.ID)
var uncategorised coredata.CookieCategory
if err := uncategorised.LoadUncategorisedByCookieBannerID(ctx, tx, scope, banner.ID); err != nil {
return fmt.Errorf("cannot load uncategorised category: %w", err)
}
inserted := 0
now := time.Now()
for _, dc := range req.Cookies {
cookie := &coredata.Cookie{
ID: gid.New(scope.GetTenantID(), coredata.CookieEntityType),
OrganizationID: banner.OrganizationID,
CookieBannerID: banner.ID,
CookieCategoryID: uncategorised.ID,
Name: dc.Name,
Duration: dc.Duration,
Description: "",
CreatedAt: now,
UpdatedAt: now,
}
if err := cookie.Insert(ctx, tx, scope); err != nil {
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
continue
}
return fmt.Errorf("cannot insert detected cookie: %w", err)
}
inserted++
}
if inserted > 0 {
if _, err := s.ensureDraftVersionForBanner(ctx, tx, scope, banner.ID); err != nil {
return fmt.Errorf("cannot ensure draft version: %w", err)
}
}
return nil
},
)
}

View File

@@ -19,6 +19,7 @@ import (
"errors"
"fmt"
"net/http"
"strings"
"time"
"github.com/go-chi/chi/v5"
@@ -51,6 +52,7 @@ func NewMux(
r.Get("/config", h.handleGetConfig)
r.Get("/consents/{visitorID}", h.handleGetConsent)
r.Post("/consents", h.handlePostConsent)
r.Post("/detected-cookies", h.handleReportDetectedCookies)
})
return r
@@ -179,3 +181,71 @@ func (h *Handler) handlePostConsent(w http.ResponseWriter, r *http.Request) {
},
)
}
type detectedCookieEntry struct {
Name string `json:"name"`
Duration string `json:"duration"`
}
type reportDetectedCookiesBody struct {
Cookies []detectedCookieEntry `json:"cookies"`
}
const maxDetectedCookiesPerRequest = 100
func (h *Handler) handleReportDetectedCookies(w http.ResponseWriter, r *http.Request) {
bannerID, err := gid.ParseGID(chi.URLParam(r, "bannerID"))
if err != nil {
jsonutil.RenderBadRequest(w, fmt.Errorf("invalid banner id"))
return
}
var body reportDetectedCookiesBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
jsonutil.RenderBadRequest(w, fmt.Errorf("invalid request body"))
return
}
if len(body.Cookies) == 0 {
jsonutil.RenderBadRequest(w, fmt.Errorf("cookies list is empty"))
return
}
if len(body.Cookies) > maxDetectedCookiesPerRequest {
jsonutil.RenderBadRequest(w, fmt.Errorf("too many cookies, maximum is %d", maxDetectedCookiesPerRequest))
return
}
detected := make([]cookiebanner.DetectedCookie, 0, len(body.Cookies))
for _, c := range body.Cookies {
name := strings.TrimSpace(c.Name)
if name == "" {
continue
}
detected = append(detected, cookiebanner.DetectedCookie{
Name: name,
Duration: strings.TrimSpace(c.Duration),
})
}
if len(detected) == 0 {
jsonutil.RenderBadRequest(w, fmt.Errorf("no valid cookie names provided"))
return
}
req := cookiebanner.ReportDetectedCookiesRequest{
Cookies: detected,
}
if err := h.cookieBannerSvc.ReportDetectedCookies(r.Context(), bannerID, req); err != nil {
if errors.Is(err, cookiebanner.ErrBannerNotFound) {
jsonutil.RenderNotFound(w, fmt.Errorf("banner not found"))
return
}
h.logger.ErrorCtx(r.Context(), "cannot report detected cookies", log.Error(err))
jsonutil.RenderInternalServerError(w)
return
}
w.WriteHeader(http.StatusNoContent)
}