Add unified tracker detection backend

- TrackerType enum (cookie, local_storage, session_storage, indexed_db, script, iframe)
- TrackerPattern model with EXACT + PREFIX matching for all types
- DetectedTracker model with upsert on conflict
- ReportDetectedTrackers service method handling cookies, storage, and resources
- POST /detected-trackers endpoint on cookie-banner v1 API
- buildSnapshot() now reads from tracker_patterns (cookie type only)
- Entity types registered (89, 90)

Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
Émile Ré
2026-05-05 15:26:26 +04:00
parent e1e54ccd1f
commit f046c2967e
7 changed files with 1058 additions and 4 deletions

View File

@@ -53,6 +53,7 @@ func NewMux(
r.Get("/consents/{visitorID}", h.handleGetConsent)
r.Post("/consents", h.handlePostConsent)
r.Post("/detected-cookies", h.handleReportDetectedCookies)
r.Post("/detected-trackers", h.handleReportDetectedTrackers)
})
return r
@@ -268,3 +269,135 @@ func (h *Handler) handleReportDetectedCookies(w http.ResponseWriter, r *http.Req
w.WriteHeader(http.StatusNoContent)
}
type detectedStorageEntry struct {
Key string `json:"key"`
StorageType string `json:"storage_type"`
ValueSize *int `json:"value_size"`
}
type detectedResourceEntry struct {
Origin string `json:"origin"`
ResourceType string `json:"resource_type"`
}
type reportDetectedTrackersBody struct {
Cookies []detectedCookieEntry `json:"cookies"`
Storage []detectedStorageEntry `json:"storage"`
Resources []detectedResourceEntry `json:"resources"`
}
const maxDetectedTrackersPerRequest = 100
func (h *Handler) handleReportDetectedTrackers(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 reportDetectedTrackersBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
jsonutil.RenderBadRequest(w, fmt.Errorf("invalid request body"))
return
}
total := len(body.Cookies) + len(body.Storage) + len(body.Resources)
if total == 0 {
jsonutil.RenderBadRequest(w, fmt.Errorf("no items provided"))
return
}
if total > maxDetectedTrackersPerRequest {
jsonutil.RenderBadRequest(w, fmt.Errorf("too many items, maximum is %d", maxDetectedTrackersPerRequest))
return
}
var req cookiebanner.ReportDetectedTrackersRequest
for _, c := range body.Cookies {
name := strings.TrimSpace(c.Name)
if name == "" {
continue
}
var source coredata.CookieSource
switch strings.TrimSpace(c.Source) {
case "pre-existing":
source = coredata.CookieSourcePreExisting
default:
source = coredata.CookieSourceScript
}
req.Cookies = append(req.Cookies, cookiebanner.DetectedCookie{
Name: name,
MaxAgeSeconds: c.MaxAgeSeconds,
Source: source,
})
}
for _, s := range body.Storage {
key := strings.TrimSpace(s.Key)
if key == "" {
continue
}
var storageType coredata.TrackerType
switch strings.TrimSpace(s.StorageType) {
case "local_storage":
storageType = coredata.TrackerTypeLocalStorage
case "session_storage":
storageType = coredata.TrackerTypeSessionStorage
case "indexed_db":
storageType = coredata.TrackerTypeIndexedDB
default:
continue
}
req.Storage = append(req.Storage, cookiebanner.DetectedStorageItem{
Key: key,
StorageType: storageType,
ValueSize: s.ValueSize,
})
}
for _, res := range body.Resources {
origin := strings.TrimSpace(res.Origin)
if origin == "" {
continue
}
var resourceType coredata.TrackerType
switch strings.TrimSpace(res.ResourceType) {
case "script":
resourceType = coredata.TrackerTypeScript
case "iframe":
resourceType = coredata.TrackerTypeIframe
default:
continue
}
req.Resources = append(req.Resources, cookiebanner.DetectedResourceItem{
Origin: origin,
ResourceType: resourceType,
})
}
if len(req.Cookies)+len(req.Storage)+len(req.Resources) == 0 {
jsonutil.RenderBadRequest(w, fmt.Errorf("no valid items provided"))
return
}
if err := h.cookieBannerSvc.ReportDetectedTrackers(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 trackers", log.Error(err))
jsonutil.RenderInternalServerError(w)
return
}
w.WriteHeader(http.StatusNoContent)
}