cookiebanner: capture script initiator URL on detected trackers
When third-party JS sets a cookie or writes to local/sessionStorage inside a customer page, the SDK now walks the synchronous call stack to find the first non-extension, non-Probo, non-first-party http(s) URL. That origin+path is sent as initiator_url on the report payload, persisted in a new nullable column on detected_trackers, and preserved across upserts via COALESCE. This unlocks per-vendor attribution for cookies and storage writes without needing pattern name matching, so future categorisation logic can simply look up the initiator URL in the existing tracker_resources table and inherit that vendor's category. GraphQL/MCP exposure is intentionally deferred -- the column is captured now, surfaced later. Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -113,6 +113,7 @@ type (
|
||||
Name string
|
||||
MaxAgeSeconds *int
|
||||
Source coredata.CookieSource
|
||||
InitiatorURL *string
|
||||
}
|
||||
|
||||
ReportDetectedCookiesRequest struct {
|
||||
@@ -120,9 +121,10 @@ type (
|
||||
}
|
||||
|
||||
DetectedStorageItem struct {
|
||||
Key string
|
||||
StorageType coredata.TrackerType
|
||||
ValueSize *int
|
||||
Key string
|
||||
StorageType coredata.TrackerType
|
||||
ValueSize *int
|
||||
InitiatorURL *string
|
||||
}
|
||||
|
||||
DetectedResourceItem struct {
|
||||
@@ -2035,6 +2037,7 @@ func (s *Service) ReportDetectedTrackers(
|
||||
Identifier: dc.Name,
|
||||
MaxAgeSeconds: dc.MaxAgeSeconds,
|
||||
Source: &dc.Source,
|
||||
InitiatorURL: dc.InitiatorURL,
|
||||
},
|
||||
&inserted,
|
||||
); err != nil {
|
||||
@@ -2051,9 +2054,10 @@ func (s *Service) ReportDetectedTrackers(
|
||||
uncategorised.ID,
|
||||
now,
|
||||
detectedTrackerInfo{
|
||||
TrackerType: ds.StorageType,
|
||||
Identifier: ds.Key,
|
||||
ValueSize: ds.ValueSize,
|
||||
TrackerType: ds.StorageType,
|
||||
Identifier: ds.Key,
|
||||
ValueSize: ds.ValueSize,
|
||||
InitiatorURL: ds.InitiatorURL,
|
||||
},
|
||||
&inserted,
|
||||
); err != nil {
|
||||
@@ -2096,6 +2100,7 @@ type detectedTrackerInfo struct {
|
||||
MaxAgeSeconds *int
|
||||
Source *coredata.CookieSource
|
||||
ValueSize *int
|
||||
InitiatorURL *string
|
||||
}
|
||||
|
||||
func (s *Service) reportDetectedTracker(
|
||||
@@ -2168,6 +2173,7 @@ func (s *Service) reportDetectedTracker(
|
||||
MaxAgeSeconds: info.MaxAgeSeconds,
|
||||
Source: info.Source,
|
||||
ValueSize: info.ValueSize,
|
||||
InitiatorURL: info.InitiatorURL,
|
||||
LastDetectedAt: now,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
|
||||
@@ -35,6 +35,7 @@ type (
|
||||
MaxAgeSeconds *int `db:"max_age_seconds"`
|
||||
Source *CookieSource `db:"source"`
|
||||
ValueSize *int `db:"value_size"`
|
||||
InitiatorURL *string `db:"initiator_url"`
|
||||
LastDetectedAt time.Time `db:"last_detected_at"`
|
||||
CreatedAt time.Time `db:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at"`
|
||||
@@ -59,6 +60,7 @@ INSERT INTO detected_trackers (
|
||||
max_age_seconds,
|
||||
source,
|
||||
value_size,
|
||||
initiator_url,
|
||||
last_detected_at,
|
||||
created_at,
|
||||
updated_at
|
||||
@@ -72,6 +74,7 @@ INSERT INTO detected_trackers (
|
||||
@max_age_seconds,
|
||||
@source,
|
||||
@value_size,
|
||||
@initiator_url,
|
||||
@last_detected_at,
|
||||
@created_at,
|
||||
@updated_at
|
||||
@@ -83,6 +86,7 @@ ON CONFLICT (cookie_banner_id, tracker_type, identifier) DO UPDATE
|
||||
) THEN EXCLUDED.source
|
||||
ELSE detected_trackers.source
|
||||
END,
|
||||
initiator_url = COALESCE(EXCLUDED.initiator_url, detected_trackers.initiator_url),
|
||||
updated_at = EXCLUDED.updated_at
|
||||
`
|
||||
|
||||
@@ -97,6 +101,7 @@ ON CONFLICT (cookie_banner_id, tracker_type, identifier) DO UPDATE
|
||||
"source": dt.Source,
|
||||
"source_script": CookieSourceScript,
|
||||
"value_size": dt.ValueSize,
|
||||
"initiator_url": dt.InitiatorURL,
|
||||
"last_detected_at": dt.LastDetectedAt,
|
||||
"created_at": dt.CreatedAt,
|
||||
"updated_at": dt.UpdatedAt,
|
||||
|
||||
19
pkg/coredata/migrations/20260511T063353Z.sql
Normal file
19
pkg/coredata/migrations/20260511T063353Z.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- 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.
|
||||
|
||||
-- Capture the third-party script URL (origin+path) that triggered a
|
||||
-- detected cookie or storage write, so the auto-categorisation worker
|
||||
-- can attribute the artifact to a known vendor via the existing
|
||||
-- tracker_resources table.
|
||||
ALTER TABLE detected_trackers ADD COLUMN initiator_url TEXT;
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -221,16 +222,45 @@ func (h *Handler) handlePostConsent(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
type detectedCookieEntry struct {
|
||||
Name string `json:"name"`
|
||||
MaxAgeSeconds *int `json:"max_age_seconds"`
|
||||
Source string `json:"source"`
|
||||
Name string `json:"name"`
|
||||
MaxAgeSeconds *int `json:"max_age_seconds"`
|
||||
Source string `json:"source"`
|
||||
InitiatorURL *string `json:"initiator_url,omitempty"`
|
||||
}
|
||||
|
||||
type reportDetectedCookiesBody struct {
|
||||
Cookies []detectedCookieEntry `json:"cookies"`
|
||||
}
|
||||
|
||||
const maxDetectedCookiesPerRequest = 100
|
||||
const (
|
||||
maxDetectedCookiesPerRequest = 100
|
||||
maxInitiatorURLLength = 1024
|
||||
)
|
||||
|
||||
// sanitizeInitiatorURL validates and normalises a script URL captured
|
||||
// in the customer page's stack trace. It drops the value when it does
|
||||
// not parse as an http(s) URL with a host, or when it exceeds the
|
||||
// length cap. Returns nil for missing or invalid input.
|
||||
func sanitizeInitiatorURL(raw *string) *string {
|
||||
if raw == nil {
|
||||
return nil
|
||||
}
|
||||
s := strings.TrimSpace(*raw)
|
||||
if s == "" || len(s) > maxInitiatorURLLength {
|
||||
return nil
|
||||
}
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return nil
|
||||
}
|
||||
if u.Host == "" {
|
||||
return nil
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
func (h *Handler) handleReportDetectedCookies(w http.ResponseWriter, r *http.Request) {
|
||||
bannerID, err := gid.ParseGID(chi.URLParam(r, "bannerID"))
|
||||
@@ -278,6 +308,7 @@ func (h *Handler) handleReportDetectedCookies(w http.ResponseWriter, r *http.Req
|
||||
Name: name,
|
||||
MaxAgeSeconds: c.MaxAgeSeconds,
|
||||
Source: source,
|
||||
InitiatorURL: sanitizeInitiatorURL(c.InitiatorURL),
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -306,9 +337,10 @@ func (h *Handler) handleReportDetectedCookies(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
type detectedStorageEntry struct {
|
||||
Key string `json:"key"`
|
||||
StorageType string `json:"storage_type"`
|
||||
ValueSize *int `json:"value_size"`
|
||||
Key string `json:"key"`
|
||||
StorageType string `json:"storage_type"`
|
||||
ValueSize *int `json:"value_size"`
|
||||
InitiatorURL *string `json:"initiator_url,omitempty"`
|
||||
}
|
||||
|
||||
type detectedResourceEntry struct {
|
||||
@@ -372,6 +404,7 @@ func (h *Handler) handleReportDetectedTrackers(w http.ResponseWriter, r *http.Re
|
||||
Name: name,
|
||||
MaxAgeSeconds: c.MaxAgeSeconds,
|
||||
Source: source,
|
||||
InitiatorURL: sanitizeInitiatorURL(c.InitiatorURL),
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -397,9 +430,10 @@ func (h *Handler) handleReportDetectedTrackers(w http.ResponseWriter, r *http.Re
|
||||
req.Storage = append(
|
||||
req.Storage,
|
||||
cookiebanner.DetectedStorageItem{
|
||||
Key: key,
|
||||
StorageType: storageType,
|
||||
ValueSize: s.ValueSize,
|
||||
Key: key,
|
||||
StorageType: storageType,
|
||||
ValueSize: s.ValueSize,
|
||||
InitiatorURL: sanitizeInitiatorURL(s.InitiatorURL),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user