diff --git a/packages/cookie-banner/src/detectors/cookie-detector.ts b/packages/cookie-banner/src/detectors/cookie-detector.ts index 89238acca..1aa99d0cd 100644 --- a/packages/cookie-banner/src/detectors/cookie-detector.ts +++ b/packages/cookie-banner/src/detectors/cookie-detector.ts @@ -14,17 +14,11 @@ import { isDeletion, parseCookieName, parseMaxAgeSeconds } from "../cookie-utils"; import type { Detector } from "./detector"; +import { isExtensionCaller, isExtensionContext } from "./extension-context"; import { getInitiatorURL } from "./initiator"; import type { ReportQueue } from "./report-queue"; import type { DetectedCookieEntry } from "./types"; -const EXTENSION_URL_RE = /(?:chrome|moz|safari-web)-extension:\/\//; - -function isExtensionCaller(): boolean { - const stack = new Error().stack ?? ""; - return EXTENSION_URL_RE.test(stack); -} - export class CookieDetector implements Detector { private readonly queue: ReportQueue; private readonly apiOrigin: string; @@ -41,6 +35,8 @@ export class CookieDetector implements Detector { start(): void { this.queue.onNotFound(() => this.stop()); + if (isExtensionContext()) return; + const desc = Object.getOwnPropertyDescriptor(Document.prototype, "cookie") ?? Object.getOwnPropertyDescriptor(HTMLDocument.prototype, "cookie"); diff --git a/packages/cookie-banner/src/detectors/extension-context.ts b/packages/cookie-banner/src/detectors/extension-context.ts new file mode 100644 index 000000000..84513f2f2 --- /dev/null +++ b/packages/cookie-banner/src/detectors/extension-context.ts @@ -0,0 +1,58 @@ +// 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. + +export const EXTENSION_URL_RE = /(?:chrome|moz|safari-web)-extension:\/\//; + +const EXTENSION_PROTOCOLS = new Set([ + "chrome-extension:", + "moz-extension:", + "safari-web-extension:", +]); + +// isExtensionCaller inspects the synchronous JS call stack for a frame +// whose URL is a browser extension origin. It is a best-effort check: +// some hooks (PerformanceObserver, browser internals firing events from +// their own threads) will not carry the original injector frame, so a +// `false` result does not guarantee the caller is first-party. +export function isExtensionCaller(): boolean { + const stack = new Error().stack ?? ""; + return EXTENSION_URL_RE.test(stack); +} + +// extensionContext is evaluated once at module load. We capture +// `document.currentScript?.src` here because that reference only +// resolves while the loading script is still executing -- by the time +// a detector's `start()` is called, currentScript will typically be +// null. Computing this lazily would defeat the check. +const extensionContext: boolean = (() => { + if (typeof location !== "undefined" && EXTENSION_PROTOCOLS.has(location.protocol)) { + return true; + } + if (typeof document !== "undefined") { + const src = document.currentScript instanceof HTMLScriptElement + ? document.currentScript.src + : null; + if (src && EXTENSION_URL_RE.test(src)) return true; + } + return false; +})(); + +// isExtensionContext reports whether the SDK itself is being executed +// from inside a browser extension (either an extension page, or a +// script loaded via an extension URL). Detectors use this to skip +// pre-existing-state scans that have no caller stack to inspect and +// would otherwise attribute the extension's own data to the host page. +export function isExtensionContext(): boolean { + return extensionContext; +} diff --git a/packages/cookie-banner/src/detectors/resource-detector.ts b/packages/cookie-banner/src/detectors/resource-detector.ts index c2af4d79e..498a7dbed 100644 --- a/packages/cookie-banner/src/detectors/resource-detector.ts +++ b/packages/cookie-banner/src/detectors/resource-detector.ts @@ -13,11 +13,10 @@ // PERFORMANCE OF THIS SOFTWARE. import type { Detector } from "./detector"; +import { isExtensionCaller, isExtensionContext } from "./extension-context"; import type { ReportQueue } from "./report-queue"; import type { ResourceType } from "./types"; -const EXTENSION_URL_RE = /(?:chrome|moz|safari-web)-extension:\/\//; - // Map browser-reported PerformanceResourceTiming.initiatorType to the // server-side tracker_resource_type. Anything we cannot classify is // dropped rather than reported as "other" to keep the table tidy. @@ -71,10 +70,13 @@ export class ResourceDetector implements Detector { start(): void { this.queue.onNotFound(() => this.stop()); - this.scanExisting(); this.observeMutations(); this.observePerformance(); this.wrapServiceWorker(); + + if (isExtensionContext()) return; + + this.scanExisting(); this.scanServiceWorkers(); } @@ -205,7 +207,7 @@ export class ResourceDetector implements Detector { } private processResource(src: string, resourceType: ResourceType): void { - if (EXTENSION_URL_RE.test(src)) return; + if (isExtensionCaller()) return; let parsed: URL; try { diff --git a/packages/cookie-banner/src/detectors/storage-detector.ts b/packages/cookie-banner/src/detectors/storage-detector.ts index fbd7a775f..961937760 100644 --- a/packages/cookie-banner/src/detectors/storage-detector.ts +++ b/packages/cookie-banner/src/detectors/storage-detector.ts @@ -13,17 +13,12 @@ // PERFORMANCE OF THIS SOFTWARE. import type { Detector } from "./detector"; +import { isExtensionCaller, isExtensionContext } from "./extension-context"; import { getInitiatorURL } from "./initiator"; import type { ReportQueue } from "./report-queue"; import type { DetectedStorageEntry, StorageSource } from "./types"; const OWN_KEY_PREFIX = "probo_consent:"; -const EXTENSION_URL_RE = /(?:chrome|moz|safari-web)-extension:\/\//; - -function isExtensionCaller(): boolean { - const stack = new Error().stack ?? ""; - return EXTENSION_URL_RE.test(stack); -} export class StorageDetector implements Detector { private readonly queue: ReportQueue; @@ -43,6 +38,9 @@ export class StorageDetector implements Detector { this.wrapStorage(); this.wrapIndexedDB(); this.wrapCacheStorage(); + + if (isExtensionContext()) return; + this.scanExisting(); this.scanCacheStorage(); } @@ -90,6 +88,7 @@ export class StorageDetector implements Detector { IDBFactory.prototype.open = function (name: string, version?: number) { const request = originalOpen.call(this, name, version); + if (isExtensionCaller()) return request; self.onIndexedDBOpen(name); return request; }; @@ -104,7 +103,9 @@ export class StorageDetector implements Detector { const self = this; caches.open = function (name: string): Promise { - self.onCacheStorageOpen(name, "script"); + if (!isExtensionCaller()) { + self.onCacheStorageOpen(name, "script"); + } return originalOpen(name); }; }