Skip to content

Commit

Permalink
feat: add sendBeacon request parameters hook (#555)
Browse files Browse the repository at this point in the history
* feat: add beacon request parameters hook

This change introduces a new hook called `resolveSendBeaconRequestParameters`. This enables consumers to modify a subset of the RequestInit parameters being used by the fetch request that polyfills the navigator.sendBeacon API in the worker context, e.g. setting keepalive: false

* chore: run prettier

* fix: properly map resolveSendBeaconRequestParameters

* feat: add resolved beacon parameters to logging

* test: implement tests for resolveSendBeaconRequestParameters
  • Loading branch information
hoebbelsB authored Mar 2, 2024
1 parent b2f9b0b commit e48d5ba
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 12 deletions.
20 changes: 20 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,14 @@ export type SerializedInstance =
*/
export type ResolveUrlType = 'fetch' | 'xhr' | 'script' | 'iframe' | 'image';

/**
* @public
*/
export type SendBeaconParameters = Pick<
RequestInit,
'keepalive' | 'mode' | 'headers' | 'signal' | 'cache'
>;

/**
* https://partytown.builder.io/configuration
*
Expand All @@ -398,6 +406,18 @@ export interface PartytownConfig {
* @returns The returned value must be a URL interface, otherwise the default resolved URL is used.
*/
resolveUrl?(url: URL, location: Location, type: ResolveUrlType): URL | undefined | null;
/**
* The `resolveSendBeaconRequestParameters()` hook can be used to modify the RequestInit parameters
* being used by the fetch request that polyfills the navigator.sendBeacon API in the worker context.
*
* @param url - The URL to be resolved. This is a URL https://developer.mozilla.org/en-US/docs/Web/API/URL, not a string.
* @param location - The current window location.
* @returns The returned value must be a SendBeaconParameters interface, otherwise the default parameters are used.
*/
resolveSendBeaconRequestParameters?(
url: URL,
location: Location
): SendBeaconParameters | undefined | null;
/**
* When set to `true`, Partytown scripts are not inlined and not minified.
*
Expand Down
2 changes: 1 addition & 1 deletion src/lib/web-worker/init-web-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const initWebWorker = (initWebWorkerData: InitWebWorkerData) => {
delete (self as any).postMessage;
delete (self as any).WorkerGlobalScope;

(commaSplit('resolveUrl,get,set,apply') as any).map(
(commaSplit('resolveUrl,resolveSendBeaconRequestParameters,get,set,apply') as any).map(
(configName: keyof PartytownInternalConfig) => {
if (config[configName]) {
config[configName] = new Function('return ' + config[configName])();
Expand Down
36 changes: 28 additions & 8 deletions src/lib/web-worker/worker-exec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,14 +213,7 @@ export const insertIframe = (winId: WinId, iframe: WorkerInstance) => {
callback();
};

export const resolveToUrl = (
env: WebWorkerEnvironment,
url: string,
type: ResolveUrlType | null,
baseLocation?: Location,
resolvedUrl?: URL,
configResolvedUrl?: any
) => {
const resolveBaseLocation = (env: WebWorkerEnvironment, baseLocation?: Location) => {
baseLocation = env.$location$;
while (!baseLocation.host) {
env = environments[env.$parentWinId$];
Expand All @@ -229,6 +222,18 @@ export const resolveToUrl = (
break;
}
}
return baseLocation;
};

export const resolveToUrl = (
env: WebWorkerEnvironment,
url: string,
type: ResolveUrlType | null,
baseLocation?: Location,
resolvedUrl?: URL,
configResolvedUrl?: any
) => {
baseLocation = resolveBaseLocation(env, baseLocation);

resolvedUrl = new URL(url || '', baseLocation as any);
if (type && webWorkerCtx.$config$.resolveUrl) {
Expand All @@ -243,5 +248,20 @@ export const resolveToUrl = (
export const resolveUrl = (env: WebWorkerEnvironment, url: string, type: ResolveUrlType | null) =>
resolveToUrl(env, url, type) + '';

export const resolveSendBeaconRequestParameters = (env: WebWorkerEnvironment, url: string) => {
const baseLocation = resolveBaseLocation(env);
const resolvedUrl = new URL(url || '', baseLocation as any);
if (webWorkerCtx.$config$.resolveSendBeaconRequestParameters) {
const configResolvedParams = webWorkerCtx.$config$.resolveSendBeaconRequestParameters!(
resolvedUrl,
baseLocation
);
if (configResolvedParams) {
return configResolvedParams;
}
}
return {};
};

export const getPartytownScript = () =>
`<script src="${partytownLibUrl('partytown.js?v=' + VERSION)}"></script>`;
5 changes: 3 additions & 2 deletions src/lib/web-worker/worker-navigator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { WebWorkerEnvironment } from '../types';
import { debug } from '../utils';
import { logWorker } from '../log';
import { resolveUrl } from './worker-exec';
import { resolveSendBeaconRequestParameters, resolveUrl } from './worker-exec';
import { webWorkerCtx } from './worker-constants';
import { getter } from './worker-proxy';

Expand All @@ -13,7 +13,7 @@ export const createNavigator = (env: WebWorkerEnvironment) => {
logWorker(
`sendBeacon: ${resolveUrl(env, url, null)}${
body ? ', data: ' + JSON.stringify(body) : ''
}`
}, resolvedParams: ${JSON.stringify(resolveSendBeaconRequestParameters(env, url))}`
);
} catch (e) {
console.error(e);
Expand All @@ -25,6 +25,7 @@ export const createNavigator = (env: WebWorkerEnvironment) => {
body,
mode: 'no-cors',
keepalive: true,
...resolveSendBeaconRequestParameters(env, url),
});
return true;
} catch (e) {
Expand Down
26 changes: 26 additions & 0 deletions tests/platform/navigator/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@
logSetters: true,
logStackTraces: false,
logScriptExecution: true,
resolveSendBeaconRequestParameters: (url, location) => {
if (url.searchParams.has('withParams')) {
return {
keepalive: false,
mode: 'same-origin',
cache: 'no-cache',
headers: new Headers({
'custom-header': 'custom-value'
})
};
}
}
};
</script>
<script src="/~partytown/debug/partytown.js"></script>
Expand Down Expand Up @@ -83,6 +95,20 @@ <h1>Navigator</h1>
})();
</script>
</li>
<li>
<strong>sendBeacon() resolveParams</strong>
<code id="testSendBeaconResolveParams"></code>
<script type="text/partytown">
(function () {
const elm = document.getElementById('testSendBeaconResolveParams');
const formData = new FormData();
formData.append('name', 'value');
const success = navigator.sendBeacon('api.js?withParams=1', formData);
elm.textContent = success;
elm.className = 'testSendBeaconResolveParams';
})();
</script>
</li>
<li>
<strong>Assign value 5 to navigator.a</strong>
<code id="testNavKey"></code>
Expand Down
20 changes: 19 additions & 1 deletion tests/platform/navigator/navigator.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { test, expect } from '@playwright/test';
import { test, expect, Request } from '@playwright/test';

test('navigator', async ({ page }) => {
const sendBeaconWithParamsRequest = new Promise<Request>((resolve) => {
page.on('request', (request) => {
if (request.url().includes('api.js?withParams=1')) {
resolve(request);
}
});
});

await page.goto('/tests/platform/navigator/');

const testUserAgent = await page.waitForSelector('.testUserAgent');
Expand All @@ -11,6 +19,16 @@ test('navigator', async ({ page }) => {
const testSendBeacon = page.locator('#testSendBeacon');
await expect(testSendBeacon).toContainText('true');

await page.waitForSelector('.testSendBeaconResolveParams');
const testSendBeaconResolveParams = page.locator('#testSendBeaconResolveParams');
await expect(testSendBeaconResolveParams).toContainText('true');
const beaconWithParamsRequest = await sendBeaconWithParamsRequest;
const headers = await beaconWithParamsRequest.allHeaders();
expect(headers['cache-control']).toBe('max-age=0');
expect(headers['custom-header']).toBe('custom-value');
expect(headers['sec-fetch-mode']).toBe('same-origin');
expect(headers['sec-fetch-site']).toBe('same-origin');

await page.waitForSelector('.testNavKey');
const testNavKey = page.locator('#testNavKey');
await expect(testNavKey).toContainText('5');
Expand Down

0 comments on commit e48d5ba

Please sign in to comment.