From eb7878c6182b1a1a958757f13186f4b453fa4331 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Wed, 10 Jun 2026 15:44:50 +0200 Subject: [PATCH] Exclude SDK bundle URL from tracker attribution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cookie/storage/resource detectors only skipped the API base-url origin and the page origin when computing a tracker's initiator. When the SDK is served from a CDN (e.g. jsDelivr) distinct from the API host, its own wrapper frames sit atop every setItem/document.cookie call stack and were never skipped, so getInitiatorURL returned the bundle URL. Third-party and browser-extension writes were therefore misattributed to cookie-banner.iife.js. Capture the SDK's own served script URL once at load (currentScript src, with an Error().stack fallback) and exclude it, at URL level, from both the initiator stack-walk and the resource detector. URL-level exclusion keeps other trackers served from the same shared CDN detectable. Signed-off-by: Émile Ré --- .../cookie-banner/src/detectors/initiator.ts | 6 ++ .../src/detectors/resource-detector.ts | 3 + .../src/detectors/self-origin.ts | 70 +++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 packages/cookie-banner/src/detectors/self-origin.ts diff --git a/packages/cookie-banner/src/detectors/initiator.ts b/packages/cookie-banner/src/detectors/initiator.ts index fe3eb21d3..dbc130976 100644 --- a/packages/cookie-banner/src/detectors/initiator.ts +++ b/packages/cookie-banner/src/detectors/initiator.ts @@ -12,6 +12,8 @@ // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR // PERFORMANCE OF THIS SOFTWARE. +import { getSelfResourceUrls } from "./self-origin"; + const EXTENSION_URL_RE = /(?:chrome|moz|safari-web)-extension:\/\//; const STACK_URL_RE = /(?:https?|(?:chrome|moz|safari-web)-extension):\/\/[^\s)'"`]+/g; @@ -43,6 +45,8 @@ export function getInitiatorURL(apiOrigin: string): InitiatorContext { let fromExtension = false; let url: string | null = null; + const selfUrls = getSelfResourceUrls(); + STACK_URL_RE.lastIndex = 0; let m: RegExpExecArray | null; while ((m = STACK_URL_RE.exec(stack)) !== null) { @@ -67,6 +71,8 @@ export function getInitiatorURL(apiOrigin: string): InitiatorContext { if (parsed.origin === location.origin) continue; const result = parsed.origin + parsed.pathname; + if (selfUrls.has(result)) continue; + url = result.length > MAX_INITIATOR_URL_LENGTH ? result.slice(0, MAX_INITIATOR_URL_LENGTH) : result; diff --git a/packages/cookie-banner/src/detectors/resource-detector.ts b/packages/cookie-banner/src/detectors/resource-detector.ts index 848a8719c..1bb4607a9 100644 --- a/packages/cookie-banner/src/detectors/resource-detector.ts +++ b/packages/cookie-banner/src/detectors/resource-detector.ts @@ -15,6 +15,7 @@ import type { Detector } from "./detector"; import { isExtensionContext } from "./extension-context"; import type { ReportQueue } from "./report-queue"; +import { getSelfResourceUrls } from "./self-origin"; import type { ResourceType } from "./types"; // Map browser-reported PerformanceResourceTiming.initiatorType to the @@ -223,6 +224,8 @@ export class ResourceDetector implements Detector { if (resourceType !== "service_worker" && parsed.origin === this.pageOrigin) return; const identifier = parsed.origin + parsed.pathname; + if (getSelfResourceUrls().has(identifier)) return; + this.queue.reportResource({ url: identifier, resource_type: resourceType }); } } diff --git a/packages/cookie-banner/src/detectors/self-origin.ts b/packages/cookie-banner/src/detectors/self-origin.ts new file mode 100644 index 000000000..c6d849de1 --- /dev/null +++ b/packages/cookie-banner/src/detectors/self-origin.ts @@ -0,0 +1,70 @@ +// Copyright (c) 2026 Probo Inc . +// +// 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. + +const HTTP_URL_RE = /https?:\/\/[^\s)'"`]+/; +const LINE_COL_SUFFIX_RE = /:\d+(?::\d+)?$/; + +function normalize(raw: string): string | null { + const cleaned = raw.replace(LINE_COL_SUFFIX_RE, ""); + try { + const parsed = new URL(cleaned); + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null; + return parsed.origin + parsed.pathname; + } catch { + return null; + } +} + +// selfResourceUrls is computed once at module load. It holds the +// normalized `origin + pathname` URL(s) of the SDK bundle itself so the +// detectors never attribute a tracker -- or report the bundle as a +// resource -- to our own served script. +// +// We capture `document.currentScript?.src` here because that reference +// only resolves while the loading script is still executing; by the time +// a detector runs it is typically null. We also derive the executing URL +// from the load-time stack as a fallback for `.mjs`/bundler builds where +// `currentScript` is null. Exclusion is URL-level (origin + pathname), +// not origin-level, because the bundle is commonly served from a shared +// CDN (e.g. cdn.jsdelivr.net) that also hosts unrelated trackers. +const selfResourceUrls: ReadonlySet = (() => { + const urls = new Set(); + + if ( + typeof document !== "undefined" + && document.currentScript instanceof HTMLScriptElement + && document.currentScript.src + ) { + const fromSrc = normalize(document.currentScript.src); + if (fromSrc) urls.add(fromSrc); + } + + const stack = new Error().stack; + if (stack) { + const match = stack.match(HTTP_URL_RE); + if (match) { + const fromStack = normalize(match[0]); + if (fromStack) urls.add(fromStack); + } + } + + return urls; +})(); + +// getSelfResourceUrls returns the normalized `origin + pathname` URL(s) +// of the SDK bundle itself. The set is empty when nothing is resolvable, +// in which case callers behave exactly as before. +export function getSelfResourceUrls(): ReadonlySet { + return selfResourceUrls; +}