Skip to content
Open

Fix 2 #690

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
1ae32de
Add support for `loadIframesOnMainThread` configuration
thegauravthakur Jan 13, 2026
3c36ace
Add support for `loadIframesOnMainThread` configuration
thegauravthakur Jan 13, 2026
5961b99
Add support for `loadIframesOnMainThread` configuration
thegauravthakur Jan 13, 2026
5b48053
Add support for `loadIframesOnMainThread` configuration
thegauravthakur Jan 13, 2026
5a71278
Add support for `loadIframesOnMainThread` configuration
thegauravthakur Jan 14, 2026
994144d
Add support for `loadIframesOnMainThread` configuration
thegauravthakur Jan 14, 2026
4b10cfb
Add enhanced tracking and debugging features for GA4/GTM events and c…
thegauravthakur Jan 15, 2026
b1bec39
Add enhanced tracking and debugging features for GA4/GTM events and c…
thegauravthakur Jan 15, 2026
d4264ae
Add enhanced tracking and debugging features for GA4/GTM events and c…
thegauravthakur Jan 15, 2026
25e69b7
Remove GA4/GTM-specific tracking and debugging logic
thegauravthakur Jan 15, 2026
4f5a8d7
Add advanced handling and fallback mechanisms for GA4 `page_view` tra…
thegauravthakur Jan 15, 2026
4fd7136
Add advanced handling and fallback mechanisms for GA4 `page_view` tra…
thegauravthakur Jan 15, 2026
cf1cbeb
Add advanced handling and fallback mechanisms for GA4 `page_view` tra…
thegauravthakur Jan 15, 2026
07ef4e5
Add advanced handling and fallback mechanisms for GA4 `page_view` tra…
thegauravthakur Jan 15, 2026
f4b308b
Add GA4 Measurement Protocol library for event tracking, ecommerce su…
thegauravthakur Jan 16, 2026
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
34 changes: 34 additions & 0 deletions docs/src/routes/configuration/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Partytown does not require a config for it to work, however a config can be set
| `forward` | An array of strings representing function calls on the main thread to forward to the web worker. See [Forwarding Events and Triggers](/forwarding-events) for more info. |
| `lib` | Path where the Partytown library can be found your server. Note that the path must both start and end with a `/` character, and the files must be hosted from the same origin as the webpage. Default is `/~partytown/` |
| `loadScriptsOnMainThread` | An array of strings or regular expressions (RegExp) used to filter out which script are executed via Partytown and the main thread. An example is as follows: `loadScriptsOnMainThread: ["https://test.com/analytics.js", "inline-script-id", /regex-matched-script\.js/]`. |
| `loadIframesOnMainThread` | An array of strings or regular expressions (RegExp) used to filter which iframes are loaded directly on the main thread instead of being handled by Partytown's worker. This is essential for iframes that require service worker registration, cross-origin cookie access, or other main-thread-only capabilities. See [Google Tag Manager Compatibility](#google-tag-manager-sw_iframe-compatibility) for more info. |
| `resolveUrl` | Hook that is called to resolve URLs which can be used to modify URLs. The hook uses the API: `resolveUrl(url: URL, location: URL, method: string)`. See the [Proxying Requests](/proxying-requests) for more information. |
| `nonce` | The nonce property may be set on script elements created by Partytown. This should be set only when dealing with content security policies and when the use of `unsafe-inline` is disabled (using `nonce-*` instead). |
| `fallbackTimeout` | A timeout in ms until Partytown initialization is considered as failed & fallbacks to the regular execution in main thread. Default is 9999 |
Expand Down Expand Up @@ -40,3 +41,36 @@ What we mean by "vanilla config", is that the Partytown config can be set withou
```

Please see the [integration guides](/integrations) for more information.

## Google Tag Manager sw_iframe Compatibility

Google Tag Manager (GTM) uses a service worker iframe (`sw_iframe.html`) for certain features like:

- Enhanced Conversions for Google Ads
- Cross-origin cookie access for retargeting
- Remarketing audience building

By default, Partytown intercepts iframe creation and attempts to load the content via XHR, which fails for `sw_iframe.html` due to CORS restrictions. Additionally, service worker registration requires the main thread and cannot be done from within a web worker.

To resolve this, use the `loadIframesOnMainThread` configuration to allow these iframes to load directly on the main thread:

```html
<script>
partytown = {
forward: ['dataLayer.push'],
loadIframesOnMainThread: [
// Allow GTM service worker iframes to load on main thread
/googletagmanager\.com.*sw_iframe/,
// Or use a string pattern
'https://www.googletagmanager.com/static/service_worker',
],
};
</script>
```

This configuration ensures that:

1. The iframe is created directly on the main thread
2. The browser loads the iframe content natively (no XHR interception)
3. Service workers can be registered normally
4. Cross-origin cookie access works as expected for Google Ads
2 changes: 2 additions & 0 deletions src/integration/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface PartytownConfig {
get?: GetHook;
globalFns?: string[];
lib?: string;
loadIframesOnMainThread?: (string | RegExp)[];
loadScriptsOnMainThread?: (string | RegExp)[];
logCalls?: boolean;
logGetters?: boolean;
Expand All @@ -37,6 +38,7 @@ export interface PartytownConfig {
logStackTraces?: boolean;
// (undocumented)
mainWindowAccessors?: string[];
noCorsUrls?: (string | RegExp)[];
nonce?: string;
// Warning: (ae-forgotten-export) The symbol "SendBeaconParameters" needs to be exported by the entry point index.d.ts
resolveSendBeaconRequestParameters?(url: URL, location: Location): SendBeaconParameters | undefined | null;
Expand Down
4 changes: 3 additions & 1 deletion src/lib/atomics/sync-create-messenger-atomics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import { onMessageFromWebWorker } from '../sandbox/on-messenge-from-worker';
import { readMainInterfaces, readMainPlatform } from '../sandbox/read-main-platform';

const createMessengerAtomics: Messenger = async (receiveMessage) => {
const size = 1024 * 1024 * 1024;
// Use a reasonable size for the shared buffer (64MB should be plenty for most responses)
// A 1GB buffer was causing issues in some browsers
const size = 64 * 1024 * 1024;
const sharedDataBuffer = new SharedArrayBuffer(size);
const sharedData = new Int32Array(sharedDataBuffer);

Expand Down
31 changes: 30 additions & 1 deletion src/lib/sandbox/main-access-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { deserializeFromWorker, serializeForWorker } from './main-serialization'
import { getInstance, setInstanceId } from './main-instances';
import { normalizedWinId } from '../log';
import { winCtxs } from './main-constants';
import { mainWindow, shouldLoadIframeOnMainThread } from './main-globals';

export const mainAccessHandler = async (
worker: PartytownWebWorker,
Expand Down Expand Up @@ -169,7 +170,35 @@ const applyToInstance = (
// previous is the setter name
// current is the setter value
// next tells us this was a setter
instance[previous] = deserializeFromWorker(worker, current);
const value = deserializeFromWorker(worker, current);

// Check if this is an iframe src being set that should load on main thread
if (
previous === 'src' &&
instance?.nodeName === 'IFRAME' &&
typeof value === 'string' &&
shouldLoadIframeOnMainThread(value)
) {
// Create iframe directly in the main document (parent of sandbox)
// so it can register service workers and access cookies properly
const mainThreadIframe = mainWindow.document.createElement('iframe');
mainThreadIframe.src = value;
mainThreadIframe.style.display = 'none';
mainThreadIframe.setAttribute('data-partytown-main-thread', 'true');

// Append to body or sandboxParent in main document
const sandboxParent =
mainWindow.document.querySelector(
mainWindow.partytown?.sandboxParent || 'body'
) || mainWindow.document.body;
sandboxParent.appendChild(mainThreadIframe);

// Don't set src on the sandbox iframe to avoid duplicate loading
// The worker state will still reference this iframe but it won't load
return;
}

instance[previous] = value;

// setters never return a value
return;
Expand Down
3 changes: 2 additions & 1 deletion src/lib/sandbox/main-forward-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const mainForwardTrigger = (worker: PartytownWebWorker, $winId$: WinId, w
let i: number;
let mainForwardFn: typeof win;

let forwardCall = ($forward$: string[], args: any) =>
let forwardCall = ($forward$: string[], args: any) => {
worker.postMessage([
WorkerMessageType.ForwardMainTrigger,
{
Expand All @@ -22,6 +22,7 @@ export const mainForwardTrigger = (worker: PartytownWebWorker, $winId$: WinId, w
$args$: serializeForWorker($winId$, Array.from(args)),
},
]);
};

win._ptf = undefined;

Expand Down
30 changes: 30 additions & 0 deletions src/lib/sandbox/main-globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,33 @@ export const docImpl = document.implementation.createHTMLDocument();

export const config: PartytownConfig = mainWindow.partytown || {};
export const libPath = (config.lib || '/~partytown/') + (debug ? 'debug/' : '');

/**
* Check if an iframe URL should be loaded on the main thread instead of inside the sandbox.
* Handles both original format (string | RegExp)[] and serialized format ['regexp'|'string', pattern][].
*/
export const shouldLoadIframeOnMainThread = (url: string): boolean => {
const patterns = config.loadIframesOnMainThread;
if (!patterns) return false;

return patterns.some((pattern: any) => {
// Handle serialized format: ['regexp', 'pattern'] or ['string', 'pattern']
if (Array.isArray(pattern)) {
const [type, value] = pattern;
if (type === 'regexp') {
return new RegExp(value).test(url);
} else if (type === 'string') {
return url.includes(value);
}
return false;
}
// Handle original format: string or RegExp
if (typeof pattern === 'string') {
return url.includes(pattern);
}
if (pattern instanceof RegExp) {
return pattern.test(url);
}
return false;
});
};
38 changes: 37 additions & 1 deletion src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,37 @@ export interface PartytownConfig {
* // Loads the `https://test.com/analytics.js` script on the main thread
*/
loadScriptsOnMainThread?: (string | RegExp)[];
/**
* This array can be used to filter which iframes are loaded via
* Partytown's worker and which should load directly on the main thread.
* This is particularly useful for iframes that require service worker
* registration, cross-origin cookie access, or other main-thread-only
* capabilities (e.g., Google Tag Manager's sw_iframe.html).
*
* When an iframe's `src` matches a pattern in this list, Partytown will:
* - Create the iframe on the main thread
* - Allow it to load naturally without worker interception
* - Enable full browser capabilities (service workers, cookies, etc.)
*
* @example loadIframesOnMainThread:['https://www.googletagmanager.com/static/service_worker', /googletagmanager\.com/]
* // Allows GTM service worker iframes to load on the main thread
*/
loadIframesOnMainThread?: (string | RegExp)[];
/**
* An array of URL patterns for which fetch() requests should use
* `mode: 'no-cors'`. This is useful for third-party tracking pixels
* and conversion tracking URLs that don't need response data but
* fail due to CORS when running in the worker context.
*
* Note: With `no-cors` mode, the response body cannot be read, but the
* request is still sent to the server (fire-and-forget). This is suitable
* for tracking/analytics requests where you just need the request to reach
* the server.
*
* @example noCorsUrls: [/googleads\.g\.doubleclick\.net/, /google-analytics\.com/]
* // Makes fetch requests to Google Ads and Analytics use no-cors mode
*/
noCorsUrls?: (string | RegExp)[];
get?: GetHook;
set?: SetHook;
apply?: ApplyHook;
Expand Down Expand Up @@ -552,8 +583,13 @@ export interface PartytownConfig {
nonce?: string;
}

export type PartytownInternalConfig = Omit<PartytownConfig, 'loadScriptsOnMainThread'> & {
export type PartytownInternalConfig = Omit<
PartytownConfig,
'loadScriptsOnMainThread' | 'loadIframesOnMainThread' | 'noCorsUrls'
> & {
loadScriptsOnMainThread?: ['regexp' | 'string', string][];
loadIframesOnMainThread?: ['regexp' | 'string', string][];
noCorsUrls?: ['regexp' | 'string', string][];
};

/**
Expand Down
52 changes: 49 additions & 3 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,34 @@ function escapeRegExp(input: string) {

export function testIfMustLoadScriptOnMainThread(
config: PartytownInternalConfig,
value: string
url: string
): boolean {
return (
config.loadScriptsOnMainThread
?.map(([type, value]) => new RegExp(type === 'string' ? escapeRegExp(value) : value))
.some((regexp) => regexp.test(value)) ?? false
?.map(([type, pattern]) => new RegExp(type === 'string' ? escapeRegExp(pattern) : pattern))
.some((regexp) => regexp.test(url)) ?? false
);
}

export function testIfMustLoadIframeOnMainThread(
config: PartytownInternalConfig,
url: string
): boolean {
return (
config.loadIframesOnMainThread
?.map(([type, pattern]) => new RegExp(type === 'string' ? escapeRegExp(pattern) : pattern))
.some((regexp) => regexp.test(url)) ?? false
);
}

export function testIfShouldUseNoCors(
config: PartytownInternalConfig,
url: string
): boolean {
return (
config.noCorsUrls
?.map(([type, pattern]) => new RegExp(type === 'string' ? escapeRegExp(pattern) : pattern))
.some((regexp) => regexp.test(url)) ?? false
);
}

Expand All @@ -243,6 +265,30 @@ export function serializeConfig(config: PartytownConfig) {
]
) satisfies Required<PartytownInternalConfig>['loadScriptsOnMainThread'];
}
if (key === 'loadIframesOnMainThread') {
value = (
value as Required<PartytownConfig | PartytownInternalConfig>['loadIframesOnMainThread']
).map((iframeUrl) =>
Array.isArray(iframeUrl)
? iframeUrl
: [
typeof iframeUrl === 'string' ? 'string' : 'regexp',
typeof iframeUrl === 'string' ? iframeUrl : iframeUrl.source,
]
) satisfies Required<PartytownInternalConfig>['loadIframesOnMainThread'];
}
if (key === 'noCorsUrls') {
value = (
value as Required<PartytownConfig | PartytownInternalConfig>['noCorsUrls']
).map((url) =>
Array.isArray(url)
? url
: [
typeof url === 'string' ? 'string' : 'regexp',
typeof url === 'string' ? url : url.source,
]
) satisfies Required<PartytownInternalConfig>['noCorsUrls'];
}
return value;
});
}
12 changes: 12 additions & 0 deletions src/lib/web-worker/init-web-worker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { commaSplit, webWorkerCtx } from './worker-constants';
import type { InitWebWorkerData, PartytownInternalConfig } from '../types';
import { testIfShouldUseNoCors } from '../utils';

export const initWebWorker = (initWebWorkerData: InitWebWorkerData) => {
const config: PartytownInternalConfig = (webWorkerCtx.$config$ = JSON.parse(
Expand All @@ -18,6 +19,17 @@ export const initWebWorker = (initWebWorkerData: InitWebWorkerData) => {
delete (self as any).postMessage;
delete (self as any).WorkerGlobalScope;

// Patch self.fetch to support noCorsUrls config
// This is needed because minified code might call self.fetch() directly
const originalFetch = (self as any).fetch;
(self as any).fetch = (input: RequestInfo | URL, init?: RequestInit) => {
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
if (testIfShouldUseNoCors(webWorkerCtx.$config$, url)) {
init = { ...init, mode: 'no-cors', credentials: 'include' };
}
return originalFetch(input, init);
};

(commaSplit('resolveUrl,resolveSendBeaconRequestParameters,get,set,apply') as any).map(
(configName: keyof PartytownInternalConfig) => {
if (config[configName]) {
Expand Down
4 changes: 2 additions & 2 deletions src/lib/web-worker/worker-document.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { callMethod, getter, setter } from './worker-proxy';
import { blockingSetter, callMethod, getter, setter } from './worker-proxy';
import {
CallType,
NodeName,
Expand Down Expand Up @@ -43,7 +43,7 @@ export const patchDocument = (
},
set(value) {
if (env.$isSameOrigin$) {
setter(this, ['cookie'], value);
blockingSetter(this, ['cookie'], value);
} else if (debug) {
warnCrossOrigin('set', 'cookie', env);
}
Expand Down
Loading
Loading