From e48d5ba0df543385c5927b8cab4d6f26d7d08d9a Mon Sep 17 00:00:00 2001 From: Julian Jandl Date: Sat, 2 Mar 2024 10:08:37 +0100 Subject: [PATCH] feat: add sendBeacon request parameters hook (#555) * 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 --- src/lib/types.ts | 20 ++++++++++++ src/lib/web-worker/init-web-worker.ts | 2 +- src/lib/web-worker/worker-exec.ts | 36 +++++++++++++++++----- src/lib/web-worker/worker-navigator.ts | 5 +-- tests/platform/navigator/index.html | 26 ++++++++++++++++ tests/platform/navigator/navigator.spec.ts | 20 +++++++++++- 6 files changed, 97 insertions(+), 12 deletions(-) diff --git a/src/lib/types.ts b/src/lib/types.ts index b9c792c7..240fd45f 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -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 * @@ -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. * diff --git a/src/lib/web-worker/init-web-worker.ts b/src/lib/web-worker/init-web-worker.ts index 4003c789..782ce316 100644 --- a/src/lib/web-worker/init-web-worker.ts +++ b/src/lib/web-worker/init-web-worker.ts @@ -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])(); diff --git a/src/lib/web-worker/worker-exec.ts b/src/lib/web-worker/worker-exec.ts index ee4640d6..9caee6de 100644 --- a/src/lib/web-worker/worker-exec.ts +++ b/src/lib/web-worker/worker-exec.ts @@ -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$]; @@ -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) { @@ -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 = () => ``; diff --git a/src/lib/web-worker/worker-navigator.ts b/src/lib/web-worker/worker-navigator.ts index 95e0e252..1daec1da 100644 --- a/src/lib/web-worker/worker-navigator.ts +++ b/src/lib/web-worker/worker-navigator.ts @@ -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'; @@ -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); @@ -25,6 +25,7 @@ export const createNavigator = (env: WebWorkerEnvironment) => { body, mode: 'no-cors', keepalive: true, + ...resolveSendBeaconRequestParameters(env, url), }); return true; } catch (e) { diff --git a/tests/platform/navigator/index.html b/tests/platform/navigator/index.html index d69d362a..35408807 100644 --- a/tests/platform/navigator/index.html +++ b/tests/platform/navigator/index.html @@ -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' + }) + }; + } + } }; @@ -83,6 +95,20 @@

Navigator

})(); +
  • + sendBeacon() resolveParams + + +
  • Assign value 5 to navigator.a diff --git a/tests/platform/navigator/navigator.spec.ts b/tests/platform/navigator/navigator.spec.ts index 87c6e856..d312543f 100644 --- a/tests/platform/navigator/navigator.spec.ts +++ b/tests/platform/navigator/navigator.spec.ts @@ -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((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'); @@ -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');