When visitorId is null (no localStorage entry), the client now
skips both the cookie check and the /consents/:id fetch that
was always returning 404 for first-time visitors. The visitor ID
is created lazily on first consent action instead.
Signed-off-by: Émile Ré <emile@getprobo.com>
Replace per-row UPDATE inside the detected-tracker loop with a single
bulk UPDATE ... WHERE id = ANY(...) after the loop. The old approach
locked pattern rows in request-dependent order, causing deadlocks
under concurrent ReportDetectedTrackers calls.
Signed-off-by: Émile Ré <emile@getprobo.com>
Map each resource type and tracker type to a specific badge
variant instead of only distinguishing scripts from everything
else. Drop the manual BadgeVariant type alias in favor of
as-const inference.
Signed-off-by: Émile Ré <emile@getprobo.com>
When a cookie name has no separator and looks variable (e.g. a long
hex string), heuristicTemplate returned "*" which created a catch-all
glob pattern matching every tracker of the same type and duration
bucket. Skip these separator-less names entirely so they remain as
individual exact-match patterns for human triage.
Signed-off-by: Émile Ré <emile@getprobo.com>
- relay: key uploadables map by actual variable name instead of iteration
index so order-mismatch between Object.keys passes can't desync the
multipart map from form field names
- mcp/v1: drop dead commented middleware line
- DurationPicker: tighten parse regex to require PT prefix for M/H and P
for D/W, and reject NaN in stringify so cleared inputs don't produce
invalid duration strings
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
The HTTP transport was replaced after WithSSRFProtection() was applied,
discarding the protected dialer. Wrap the existing transport with
userAgentTransport instead of replacing it.
Signed-off-by: Émile Ré <emile@getprobo.com>
- Preserve SSRF protection by wrapping the existing transport
instead of replacing it with a bare http.Transport
- Strip DSN from url.Parse error to avoid leaking credentials
- Gate CommonThirdPartyCombobox on search length to prevent
showing stale results when input is shortened
- Handle multi-value and uppercase sizes attributes in
parseSizeAttr for correct icon-size ranking
- Match rel tokens containing "icon" (e.g. "shortcut icon")
instead of requiring an exact match
- Limit HTML response body to 10 MiB before parsing
- Reject sslmode=prefer explicitly in both import tools
Signed-off-by: Émile Ré <emile@getprobo.com>
Replace the Google Favicons API with HTML head tag parsing
to find higher-quality logos (SVG, apple-touch-icon, large
PNG icons, msapplication-TileImage). The new pkg/webinspect
package parses a website's DOM tree and is extensible for
future resource extraction (footer links, etc.).
Signed-off-by: Émile Ré <emile@getprobo.com>
The description field was never surfaced in the UI and added no value.
Drop it from the database, Go structs, GraphQL schema, import tool,
frontend fragment, and vendor seed data.
Signed-off-by: Émile Ré <emile@getprobo.com>
Fetch favicons at import time instead of calling Google's favicon
service per page load. Logos are stored as public files in S3 and
served through the existing /api/files/v1/{id} endpoint.
Signed-off-by: Émile Ré <emile@getprobo.com>
The CreateVendorDialog previously loaded the entire @probo/vendors
JSON bundle client-side and used MiniSearch for fuzzy search. This
replaces it with a GraphQL query against the common_third_parties
database table, searched server-side via ILIKE filtering.
Backend: adds CommonThirdParty GraphQL type, a pkg/thirdparty
service, and a commonThirdParties(name) root query. Frontend:
splits into CommonThirdPartyCombobox (display) and an @inline
fragment read on selection via readInlineData.
Signed-off-by: Émile Ré <emile@getprobo.com>
Introduce a globally-shared, non-tenant-scoped common_third_parties
table that mirrors the public subset of vendor metadata, plus a
one-shot cmd/common-third-parties-import CLI that seeds it from
packages/vendors/data.json. The catalog will back future flows (e.g.
vendor autocomplete) so each tenant no longer needs to duplicate the
same baseline data.
The importer is idempotent via ON CONFLICT (lower(name)) DO UPDATE
and prints inserted/updated counts. GIDs use gid.NilTenant since the
table is not tenant-scoped; uniqueness still comes from the entity
type plus 14 bytes of timestamp/random suffix.
Signed-off-by: Émile Ré <emile@getprobo.com>
Fixes GHSA-6m6c-36f7-fhxh (Gantt infinite loop DoS), GHSA-xcj9-5m2h-648r
and GHSA-87f9-hvmw-gh4p (CSS injection via classDefs/configuration), and
GHSA-ghcm-xqfw-q4vr (HTML injection via classDef in state diagrams).
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
tracker_patterns rows were created with entity type 88 (removed
CookiePatternEntityType) instead of 89 (TrackerPatternEntityType), and
detected_trackers rows migrated from the cookies table carried entity
type 85 (removed CookieEntityType) instead of 90 (DetectedTrackerEntityType).
Signed-off-by: Émile Ré <emile@getprobo.com>
When IP geolocation returned no matching CIDR block, LookupCountryByIP
returned an empty string with nil error. The handler took the address of
that empty string, producing a non-nil pointer to "", which was inserted
into the database. Guard against this by returning nil when the resolved
country code is empty, and backfill existing rows with a migration.
Signed-off-by: Émile Ré <emile@getprobo.com>
fmt.Sprintf interprets the literal % characters in the LIKE escape
clause as format verbs, corrupting the query and causing a 500 on
the /report endpoint. Reorder tracker type / source filters in the
trackers page.
Signed-off-by: Émile Ré <emile@getprobo.com>
The pattern analysis worker now recognises UUID-like, hash-like,
and long numeric tokens as variable parts and replaces them with
wildcards heuristically, even from a single observation. This
prevents site-specific identifiers from being treated as static
text while meaningful suffixes (window_id, posthog, …) get
incorrectly wildcarded.
Also upgrades globMatch and the FindMatchingPattern SQL query
to support multiple wildcards in a single pattern.
Signed-off-by: Émile Ré <emile@getprobo.com>
The trackers page now lets users filter by tracker type
(Cookie, localStorage, sessionStorage, IndexedDB, Cache
Storage) in addition to the existing source filter. Each
tracker type and cookie source badge uses a distinct color
for quick visual scanning.
Signed-off-by: Émile Ré <emile@getprobo.com>
Add a rule to contrib/claude/ui.md requiring the Icon-suffixed exports
(e.g. EyeIcon) instead of aliased bare names (Eye as IconEye), and fix
TrackerResourceRow to follow it.
Signed-off-by: Émile Ré <emile@getprobo.com>
The early return from flushSync() when this.flushing was true skipped
the synchronous sendBeacon path, dropping pending reports during page
unload. Move the flushing guard after the sendBeacon attempt so the
unload-safe transport is always tried; only the keepalive-fetch
fallback is gated on the mutex.
Signed-off-by: Émile Ré <emile@getprobo.com>
The previous test seeded tracker resources directly via the
GraphQL `CreateTrackerResource` mutation, which doesn't go
through the public `/report` handler. Switching the seeding to
`ReportDetectedResources` covers the end-to-end path -- payload
decoding, validation, and the new upsert -- so the listing assert
is now grounded in the same write path that real clients use.
Signed-off-by: Émile Ré <emile@getprobo.com>
flushSync() previously deleted pending entries unconditionally
once a transport accepted the payload. That dropped data when
the page was restored from bfcache after visibilitychange:hidden,
and it allowed concurrent flushes to take overlapping batches.
Now items are only removed from `pending` once the transport
confirms delivery: synchronously for sendBeacon, in the keepalive
fetch's `.then` on an `ok` response. Failed fetches leave entries
queued so the next flush can retry, and a `flushing` guard prevents
overlapping in-flight batches.
Signed-off-by: Émile Ré <emile@getprobo.com>
The method uses ON CONFLICT ... DO UPDATE, so the name now
matches the actual behaviour. TrackerPattern.InsertIfNotExists
keeps its name because it uses ON CONFLICT ... DO NOTHING.
Signed-off-by: Émile Ré <emile@getprobo.com>
The detector emits tracker_resource entries (scripts, iframes, images,
beacons, fonts, fetches, media, service workers); the "third-party"
qualifier was misleading because the same code path also fires for
service workers, which are always same-origin per browser security
rules. Rename the class and file to match what they actually produce.
Signed-off-by: Émile Ré <emile@getprobo.com>
The three detectors (cookies, storage, resources) each duplicated the
same debounce, batch, retry, and NotFoundError plumbing and each fired
its own POST /report despite the server already accepting a unified
{cookies, storage, resources} payload. Collapse the three sender paths
into a single ReportQueue so a 2 s debounce window produces one request
instead of up to three, dedup is centralised behind type-namespaced
keys (c:/s:/r:) that cannot collide across detectors, and a tab-close
drain via sendBeacon (with keepalive fetch fallback) saves the last
debounce window of detections that previously vanished on unload.
Signed-off-by: Émile Ré <emile@getprobo.com>