Exclude SDK bundle URL from tracker attribution
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é <emile@probo.com>
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
70
packages/cookie-banner/src/detectors/self-origin.ts
Normal file
70
packages/cookie-banner/src/detectors/self-origin.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.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.
|
||||
|
||||
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<string> = (() => {
|
||||
const urls = new Set<string>();
|
||||
|
||||
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<string> {
|
||||
return selfResourceUrls;
|
||||
}
|
||||
Reference in New Issue
Block a user