Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
447 changes: 191 additions & 256 deletions crates/common/src/integrations/gpt.rs

Large diffs are not rendered by default.

14 changes: 8 additions & 6 deletions crates/js/lib/src/integrations/datadome/script_guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import { createScriptGuard } from '../../shared/script_guard';
* scripts inserted via appendChild, insertBefore, or any other dynamic DOM
* manipulation.
*
* Built on the shared script_guard factory with custom URL rewriting to preserve
* the original path from the DataDome URL (e.g., /tags.js, /js/check).
* Built on the shared script_guard factory, which registers with the shared
* DOM insertion dispatcher and preserves the original DataDome path
* (e.g., /tags.js, /js/check).
*/

/** Regex to match js.datadome.co as a domain in URLs */
Expand Down Expand Up @@ -63,16 +64,17 @@ function rewriteDataDomeUrl(originalUrl: string): string {
}

const guard = createScriptGuard({
name: 'DataDome',
displayName: 'DataDome',
id: 'datadome',
isTargetUrl: isDataDomeSdkUrl,
rewriteUrl: rewriteDataDomeUrl,
});

/**
* Install the DataDome guard to intercept dynamic script loading.
* Patches Element.prototype.appendChild and insertBefore to catch
* ANY dynamically inserted DataDome SDK script elements and rewrite their URLs
* before insertion. Works across all frameworks and vanilla JavaScript.
* Registers a handler with the shared DOM insertion dispatcher so dynamically
* inserted DataDome SDK script elements are rewritten before insertion.
* Works across all frameworks and vanilla JavaScript.
*/
export const installDataDomeGuard = guard.install;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { log } from '../../core/log';
import { createBeaconGuard } from '../../shared/beacon_guard';
import { createScriptGuard } from '../../shared/script_guard';

Expand Down Expand Up @@ -82,12 +83,15 @@ function extractGtmPath(url: string): string {
return parsed.pathname + parsed.search;
} catch (error) {
// Fallback: extract path after the domain using regex
console.warn('[GTM Guard] URL parsing failed for:', url, 'Error:', error);
log.warn('[GTM Guard] URL parsing failed; falling back to regex extraction', {
error,
url,
});
const match = url.match(
/(?:www\.(?:googletagmanager|google-analytics)\.com|analytics\.google\.com)(\/[^'"\s]*)/i
);
if (!match || !match[1]) {
console.warn('[GTM Guard] Fallback regex failed, using default path /gtm.js');
log.warn('[GTM Guard] Fallback regex failed; using default path /gtm.js', { url });
return '/gtm.js';
}
return match[1];
Expand All @@ -102,7 +106,8 @@ function rewriteGtmUrl(originalUrl: string): string {
}

const guard = createScriptGuard({
name: 'GTM',
displayName: 'GTM',
id: 'google_tag_manager',
isTargetUrl: isGtmUrl,
rewriteUrl: rewriteGtmUrl,
});
Expand Down
16 changes: 11 additions & 5 deletions crates/js/lib/src/integrations/gpt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,16 @@ export function installGptShim(): boolean {
// script can call it explicitly. The server emits:
// <script>window.__tsjs_gpt_enabled=true;
// window.__tsjs_installGptShim&&window.__tsjs_installGptShim();</script>
// Because that inline <script> runs *after* the unified bundle has evaluated,
// the function is guaranteed to be available by the time the inline script
// executes. This avoids a race where the module-scope auto-init would check
// `__tsjs_gpt_enabled` before the flag is set.
// The HTML pipeline currently injects that inline script before the unified
// bundle, so the explicit call is best-effort only. To make activation robust
// regardless of script order, the module also checks for a pre-set enable flag
// immediately after registering the function.
if (typeof window !== 'undefined') {
(window as Record<string, unknown>).__tsjs_installGptShim = installGptShim;
const win = window as Record<string, unknown>;

win.__tsjs_installGptShim = installGptShim;

if (win.__tsjs_gpt_enabled === true) {
installGptShim();
}
}
91 changes: 40 additions & 51 deletions crates/js/lib/src/integrations/gpt/script_guard.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { log } from '../../core/log';
import {
DEFAULT_DOM_INSERTION_HANDLER_PRIORITY,
type DomInsertionCandidate,
registerDomInsertionHandler,
} from '../../shared/dom_insertion_dispatcher';

/**
* GPT Script Interception Guard
Expand Down Expand Up @@ -29,8 +34,9 @@ import { log } from '../../core/log';
* 4. **`document.createElement` patch** — tags every newly created
* `<script>` element with a per-instance `src` descriptor as a
* fallback when the prototype descriptor cannot be installed.
* 5. **DOM insertion patches** on `appendChild` / `insertBefore` — catches
* scripts and `<link rel="preload">` elements at insertion time.
* 5. **Shared DOM insertion dispatcher** — catches scripts and
* `<link rel="preload">` elements at insertion time without stacking
* multiple prototype wrappers across integrations.
* 6. **`MutationObserver`** — catches elements added to the DOM via
* `innerHTML`, `.append()`, etc., or attribute mutations on existing
* elements.
Expand Down Expand Up @@ -110,9 +116,8 @@ let nativeSrcGet: ((this: HTMLScriptElement) => string) | undefined;
let nativeSrcDescriptor: PropertyDescriptor | undefined;
let nativeSetAttribute: typeof HTMLScriptElement.prototype.setAttribute | undefined;
let nativeCreateElement: typeof document.createElement | undefined;
let nativeAppendChild: typeof Element.prototype.appendChild | undefined;
let nativeInsertBefore: typeof Element.prototype.insertBefore | undefined;
let mutationObserver: MutationObserver | undefined;
let unregisterDomInsertionHandler: (() => void) | undefined;

// ---------------------------------------------------------------------------
// Tracking — prevent double-rewriting
Expand Down Expand Up @@ -157,33 +162,37 @@ function maybeRewrite(url: string): { url: string; didRewrite: boolean } {
/**
* Attempt to rewrite a script element's src if it points at a GPT domain.
*/
function rewriteScriptSrc(element: HTMLScriptElement, rawUrl: string): void {
function rewriteScriptSrc(element: HTMLScriptElement, rawUrl: string): boolean {
const { url: finalUrl, didRewrite } = maybeRewrite(rawUrl);
if (!didRewrite) return;
if (alreadyRewritten(element, finalUrl)) return;
if (!didRewrite || alreadyRewritten(element, finalUrl)) {
return false;
}

log.info(`${LOG_PREFIX}: rewriting script src`, { original: rawUrl, rewritten: finalUrl });
rewritten.set(element, finalUrl);
applySrc(element, finalUrl);
return true;
}

/**
* Attempt to rewrite a link element's href if it's a preload/prefetch for
* a GPT-domain script.
*/
function rewriteLinkHref(element: HTMLLinkElement): void {
const rel = element.getAttribute('rel');
if (rel !== 'preload' && rel !== 'prefetch') return;
if (element.getAttribute('as') !== 'script') return;
function rewriteLinkHref(
element: HTMLLinkElement,
href = element.href || element.getAttribute('href') || '',
rel = element.getAttribute('rel')
): boolean {
if (rel !== 'preload' && rel !== 'prefetch') return false;
if (element.getAttribute('as') !== 'script') return false;

const href = element.href || element.getAttribute('href') || '';
const { url: finalUrl, didRewrite } = maybeRewrite(href);
if (!didRewrite) return;
if (alreadyRewritten(element, finalUrl)) return;
if (!didRewrite || alreadyRewritten(element, finalUrl)) return false;

log.info(`${LOG_PREFIX}: rewriting ${rel} link`, { original: href, rewritten: finalUrl });
rewritten.set(element, finalUrl);
element.href = finalUrl;
return true;
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -408,45 +417,31 @@ function installCreateElementPatch(): void {
}

// ---------------------------------------------------------------------------
// Layer 5: DOM insertion patches (appendChild / insertBefore)
// Layer 5: shared DOM insertion dispatcher
// ---------------------------------------------------------------------------

/**
* Check a node at insertion time and rewrite if it's a GPT script or
* preload link.
*/
function checkNodeAtInsertion(node: Node): void {
if (!(node instanceof HTMLElement)) return;

if (node.tagName === 'SCRIPT') {
const src = (node as HTMLScriptElement).src || node.getAttribute('src') || '';
if (src) rewriteScriptSrc(node as HTMLScriptElement, src);
} else if (node.tagName === 'LINK') {
rewriteLinkHref(node as HTMLLinkElement);
function checkNodeAtInsertion(candidate: DomInsertionCandidate): boolean {
if (candidate.kind === 'script') {
return rewriteScriptSrc(candidate.element, candidate.url);
}

return rewriteLinkHref(candidate.element, candidate.url, candidate.rel);
}

function installDomInsertionPatches(): void {
function installDomInsertionDispatcher(): void {
if (typeof Element === 'undefined') return;

nativeAppendChild = Element.prototype.appendChild;
nativeInsertBefore = Element.prototype.insertBefore;

Element.prototype.appendChild = function <T extends Node>(this: Element, node: T): T {
checkNodeAtInsertion(node);
return nativeAppendChild!.call(this, node) as T;
};

Element.prototype.insertBefore = function <T extends Node>(
this: Element,
node: T,
reference: Node | null
): T {
checkNodeAtInsertion(node);
return nativeInsertBefore!.call(this, node, reference) as T;
};
unregisterDomInsertionHandler = registerDomInsertionHandler({
handle: checkNodeAtInsertion,
id: 'gpt',
priority: DEFAULT_DOM_INSERTION_HANDLER_PRIORITY,
});

log.info(`${LOG_PREFIX}: DOM insertion patches installed`);
log.info(`${LOG_PREFIX}: DOM insertion dispatcher registered`);
}

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -545,7 +540,7 @@ export function installGptGuard(): void {
installCreateElementPatch();

// Layer 5: intercept appendChild / insertBefore (scripts + link preloads)
installDomInsertionPatches();
installDomInsertionDispatcher();

// Layer 6: catch anything else via MutationObserver
installMutationObserver();
Expand Down Expand Up @@ -593,13 +588,9 @@ export function resetGuardState(): void {
}
}

if (typeof Element !== 'undefined') {
if (nativeAppendChild) {
Element.prototype.appendChild = nativeAppendChild;
}
if (nativeInsertBefore) {
Element.prototype.insertBefore = nativeInsertBefore;
}
if (unregisterDomInsertionHandler) {
unregisterDomInsertionHandler();
unregisterDomInsertionHandler = undefined;
}

nativeDocWrite = undefined;
Expand All @@ -609,8 +600,6 @@ export function resetGuardState(): void {
nativeSrcDescriptor = undefined;
nativeSetAttribute = undefined;
nativeCreateElement = undefined;
nativeAppendChild = undefined;
nativeInsertBefore = undefined;

rewritten = new WeakMap<HTMLScriptElement | HTMLLinkElement, string>();
instancePatched = new WeakSet<HTMLScriptElement>();
Expand Down
17 changes: 9 additions & 8 deletions crates/js/lib/src/integrations/lockr/nextjs_guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import { createScriptGuard } from '../../shared/script_guard';
* scripts inserted via appendChild, insertBefore, or any other dynamic DOM
* manipulation.
*
* Built on the shared script_guard factory which patches DOM methods to catch
* dynamic insertions and rewrite SDK URLs to use the first-party domain proxy
* endpoint, bypassing the need for server-side HTML rewriting in dynamic
* client-side scenarios.
* Built on the shared script_guard factory, which registers with the shared
* DOM insertion dispatcher to catch dynamic insertions and rewrite SDK URLs to
* the first-party proxy endpoint without relying on server-side HTML rewriting
* in client-side scenarios.
*/

/**
Expand Down Expand Up @@ -42,16 +42,17 @@ function isLockrSdkUrl(url: string): boolean {
}

const guard = createScriptGuard({
name: 'Lockr',
displayName: 'Lockr',
id: 'lockr',
isTargetUrl: isLockrSdkUrl,
proxyPath: '/integrations/lockr/sdk',
});

/**
* Install the Lockr guard to intercept dynamic script loading.
* Patches Element.prototype.appendChild and insertBefore to catch
* ANY dynamically inserted Lockr SDK script elements and rewrite their URLs
* before insertion. Works across all frameworks and vanilla JavaScript.
* Registers a handler with the shared DOM insertion dispatcher so dynamically
* inserted Lockr SDK script elements are rewritten before insertion.
* Works across all frameworks and vanilla JavaScript.
*/
export const installNextJsGuard = guard.install;

Expand Down
3 changes: 2 additions & 1 deletion crates/js/lib/src/integrations/permutive/script_guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ function isPermutiveSdkUrl(url: string): boolean {
}

const guard = createScriptGuard({
name: 'Permutive',
displayName: 'Permutive',
id: 'permutive',
isTargetUrl: isPermutiveSdkUrl,
proxyPath: '/integrations/permutive/sdk',
});
Expand Down
Loading