Skip to content
Merged
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
10 changes: 10 additions & 0 deletions docs/content/scripts/fathom-analytics.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ links:
::script-docs
::

## Proxying is not supported

Unlike most analytics integrations in Nuxt Scripts, Fathom **cannot** be proxied (`proxy: true`).

Fathom's bot detection uses the connecting source IP address. When beacons are proxied, they reach Fathom from your server's IP (typically a datacenter), and Fathom's bot detection ignores `X-Forwarded-For` from arbitrary servers, so every visitor gets flagged as a bot.

Fathom previously offered an official Custom Domain feature (CNAME to their infrastructure) for first-party hosting, but they [deprecated it in May 2023](https://usefathom.com/changelog/mar2023-firewall-settings) and there is no replacement.

Bundling (`bundle: true`) **is** supported: the script is served from your origin, but beacons still go directly to `cdn.usefathom.com` from the browser so real client IPs reach Fathom's bot detection correctly.

## Defaults

- **Trigger**: Script will load when Nuxt is hydrated.
Expand Down
6 changes: 4 additions & 2 deletions packages/script/src/plugins/rewrite-ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,9 @@ export function rewriteScriptUrlsAST(content: string, filename: string, rewrites

// SDK patch: neutralize-domain-check
// Matches `.indexOf("...domain...") < 0` and rewrites `< 0` to `< -1`
if (sdkPatches?.some(p => p.type === 'neutralize-domain-check')
const neutralizePatches = sdkPatches?.filter((p): p is { type: 'neutralize-domain-check', domain: string } =>
p.type === 'neutralize-domain-check')
if (neutralizePatches?.length
&& node.type === 'BinaryExpression'
&& (node as any).operator === '<') {
const left = (node as any).left
Expand All @@ -372,7 +374,7 @@ export function rewriteScriptUrlsAST(content: string, filename: string, rewrites
if (prop === 'indexOf' && left.arguments?.length === 1) {
const arg = left.arguments[0]
if (arg?.type === 'Literal' && typeof arg.value === 'string'
&& rewrites.some(r => arg.value.includes(r.from))) {
&& neutralizePatches.some(p => arg.value.includes(p.domain))) {
s.overwrite(right.start, right.end, '-1')
}
}
Expand Down
33 changes: 23 additions & 10 deletions packages/script/src/plugins/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,12 @@ async function downloadScript(opts: {
let size = 0
let fetched = false

// Use storage to cache the font data between builds
// Include proxy in cache key to differentiate proxied vs non-proxied versions
// Also include a hash of proxyRewrites content to handle different proxyPrefix values
const proxyRewritesHash = proxyRewrites?.length ? `-${ohash(proxyRewrites)}` : ''
const cacheKey = proxyRewrites?.length ? `bundle-proxy:${filename.replace('.js', `${proxyRewritesHash}.js`)}` : `bundle:${filename}`
// Cache patched bundles under a separate prefix so they don't collide with
// raw bundles. Hash the rewrite/patch inputs so changes to either (different
// proxyPrefix, new sdkPatches domain, etc.) invalidate the cache.
const hasRewrites = !!(proxyRewrites?.length || sdkPatches?.length)
const rewriteHash = hasRewrites ? `-${ohash({ proxyRewrites, sdkPatches })}` : ''
const cacheKey = hasRewrites ? `bundle-patched:${filename.replace('.js', `${rewriteHash}.js`)}` : `bundle:${filename}`
const shouldUseCache = !forceDownload && await storage.hasItem(cacheKey) && !(await isCacheExpired(storage, filename, cacheMaxAge))

if (shouldUseCache) {
Expand All @@ -160,12 +161,14 @@ async function downloadScript(opts: {
fetched = true

await storage.setItemRaw(`bundle:${filename}`, res)
// Apply URL rewrites for proxy mode (AST-based at build time)
if (proxyRewrites?.length && res) {
// Apply AST rewrites at build time. Runs when either proxy rewrites are
// present (proxy mode) or bundle-only sdkPatches are configured (e.g.
// Fathom's neutralize-domain-check).
if (hasRewrites && res) {
const content = res.toString('utf-8')
const rewritten = rewriteScriptUrlsAST(content, filename, proxyRewrites, sdkPatches, { skipApiRewrites, neutralizeCanvas })
const rewritten = rewriteScriptUrlsAST(content, filename, proxyRewrites ?? [], sdkPatches, { skipApiRewrites, neutralizeCanvas })
res = Buffer.from(rewritten, 'utf-8')
logger.debug(`Rewrote ${proxyRewrites.length} URL patterns in ${filename}`)
logger.debug(`Rewrote ${proxyRewrites?.length ?? 0} URL patterns + ${sdkPatches?.length ?? 0} sdk patches in ${filename}`)
}

await storage.setItemRaw(cacheKey, res)
Expand Down Expand Up @@ -466,8 +469,18 @@ export function NuxtScriptBundleTransformer(options: AssetBundlerTransformerOpti
from: domain,
to: `${options.proxyPrefix}/${domain}`,
}))
const sdkPatches = proxyConfig?.sdkPatches
// Bundle-only SDK patches (independent of proxy). Used when bundling
// a script that needs neutralize-domain-check etc. but should keep
// sending requests directly to its origin (e.g. Fathom).
// When both are defined, proxyConfig.sdkPatches wins β€” proxy patches
// are typically tuned for the rewritten URL set and should take precedence.
const bundleConfig = typeof script?.bundle === 'object' ? script.bundle : undefined
const sdkPatches = proxyConfig?.sdkPatches ?? bundleConfig?.sdkPatches
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// Skip API rewrites (sendBeacon/fetch/XHR/Image β†’ __nuxtScripts.*) when:
// 1. Partytown is active (uses resolveUrl instead), OR
// 2. No proxy is active (no intercept plugin loaded β€” calls would crash)
const skipApiRewrites = !!(registryKey && options.partytownScripts?.has(registryKey))
|| !proxyConfig
// Gate canvas fingerprinting neutralization on the script's hardware privacy flag
const neutralizeCanvas = proxyConfig?.privacy !== undefined
&& typeof proxyConfig.privacy === 'object'
Expand Down
19 changes: 12 additions & 7 deletions packages/script/src/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,12 @@ export const registryMeta: RegistryScriptMeta[] = [
m('plausibleAnalytics', 'Plausible Analytics', 'analytics', 'useScriptPlausibleAnalytics', { bundle: true, proxy: true, partytown: true }, PRIVACY_IP_ONLY),
m('cloudflareWebAnalytics', 'Cloudflare Web Analytics', 'analytics', 'useScriptCloudflareWebAnalytics', { bundle: true, proxy: true, partytown: true }, PRIVACY_IP_ONLY),
m('posthog', 'PostHog', 'analytics', 'useScriptPostHog', { proxy: true }, PRIVACY_IP_ONLY),
m('fathomAnalytics', 'Fathom Analytics', 'analytics', 'useScriptFathomAnalytics', { bundle: true, proxy: true, partytown: true }, PRIVACY_IP_ONLY),
// proxy intentionally off: proxied beacons reach Fathom from the server's IP
// (datacenter) and Fathom's bot detection ignores X-Forwarded-For, flagging
// every visitor as a bot. Bundle is supported via neutralize-domain-check β€”
// the script is served from the user's origin but beacons still go directly
// to cdn.usefathom.com so Fathom sees real client IPs. See nuxt/scripts#720.
m('fathomAnalytics', 'Fathom Analytics', 'analytics', 'useScriptFathomAnalytics', { bundle: true }, null),
m('matomoAnalytics', 'Matomo Analytics', 'analytics', 'useScriptMatomoAnalytics', { proxy: true, partytown: true }, PRIVACY_IP_ONLY),
m('rybbitAnalytics', 'Rybbit Analytics', 'analytics', 'useScriptRybbitAnalytics', { bundle: true, proxy: true }, PRIVACY_IP_ONLY),
m('databuddyAnalytics', 'Databuddy Analytics', 'analytics', 'useScriptDatabuddyAnalytics', { bundle: true, proxy: true }, PRIVACY_IP_ONLY),
Expand Down Expand Up @@ -337,13 +342,13 @@ export async function registry(resolve?: (path: string) => Promise<string>): Pro
src: 'https://cdn.usefathom.com/script.js',
category: 'analytics',
envDefaults: { site: '' },
bundle: true,
proxy: {
domains: ['cdn.usefathom.com', 'usefathom.com'],
privacy: PRIVACY_IP_ONLY,
sdkPatches: [{ type: 'neutralize-domain-check' }],
// Bundle without proxy: serve the script from the user's origin (faster
// load, ad-blocker resistant for domain-based blocking) but keep beacons
// pointed at cdn.usefathom.com via the neutralize-domain-check patch so
// Fathom sees real client IPs. Proxying is unsupported (see #720).
bundle: {
sdkPatches: [{ type: 'neutralize-domain-check', domain: 'cdn.usefathom.com' }],
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
partytown: { forwards: ['fathom', 'fathom.trackEvent', 'fathom.trackPageview'] },
}),
def('matomoAnalytics', {
schema: MatomoAnalyticsOptions,
Expand Down
13 changes: 10 additions & 3 deletions packages/script/src/runtime/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,13 @@ export interface ScriptDomain {
export interface BundleCapability {
/** Custom URL resolution. If omitted, the script's `src` is used. */
resolve?: (options?: any) => string | false
/**
* AST-level SDK patches applied during bundling, independent of proxy.
* Use for scripts that need self-hosted detection neutralization but should
* still send beacons directly to the origin (e.g. Fathom, where proxying
* triggers bot detection but bundling is otherwise safe).
*/
sdkPatches?: SdkPatch[]
}

/**
Expand Down Expand Up @@ -408,11 +415,11 @@ export interface ProxyCapability {
export type SdkPatch
/**
* Neutralize self-hosted detection checks like `.indexOf("cdn.example.com") < 0`.
* When a script is proxied, its src no longer contains the original CDN domain,
* causing these checks to incorrectly detect "self-hosted" mode.
* When a script is bundled or proxied, its src no longer contains the original
* CDN domain, causing these checks to incorrectly detect "self-hosted" mode.
* This patch makes such comparisons always evaluate to false.
*/
= | { type: 'neutralize-domain-check' }
= | { type: 'neutralize-domain-check', domain: string }
/**
* Replace `<expr>.split("<separator>")[0]` patterns used by SDKs that derive
* their API host from `document.currentScript.src`. When bundled, the script src
Expand Down
11 changes: 2 additions & 9 deletions test/e2e-dev/first-party.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,8 +647,8 @@ describe('first-party privacy stripping', () => {
'redditPixel', // rdt('track', ...) triggers pixel fires
'plausibleAnalytics', // plausible() triggers fetch POST
'umamiAnalytics', // umami.track() triggers fetch POST
'fathomAnalytics', // fathom.trackGoal() triggers beacon
// cloudflareWebAnalytics β€” auto-engagement only, no CTA buttons
// fathomAnalytics β€” bundle/proxy disabled (Fathom bot-detection flags self-hosted/proxied traffic, see #720)
])

/**
Expand All @@ -673,7 +673,6 @@ describe('first-party privacy stripping', () => {
'googleAnalytics', // scope-resolved AST rewrite for sendBeacon/fetch/XHR/Image
'snapchatPixel', // scope-resolved AST rewrite for sendBeacon/XHR
// googleTagManager β€” uses createElement('script') injection, not interceptable via XHR/fetch/sendBeacon
'fathomAnalytics', // bundled + self-hosted detection neutralized, sendBeacon/Image interception
'plausibleAnalytics', // bundled + auto-inject endpoint, sendBeacon interception (needs extension: 'local' + __plausible flag for headless)
'tiktokPixel', // AST rewrite for analytics.tiktok.com, sendBeacon/fetch interception
// databuddyAnalytics β€” SDK doesn't fire events with demo clientId in test window
Expand Down Expand Up @@ -910,13 +909,7 @@ describe('first-party privacy stripping', () => {
}, { pre: preClickProxyCount, post: postClickProxyCount })
}, 30000)

it('fathomAnalytics', async () => {
const { captures, rawCaptures, proxyRequests, externalRequests, preClickProxyCount, postClickProxyCount } = await testProvider('fathomAnalytics', '/fathom')
await assertCaptures('fathomAnalytics', captures, rawCaptures, proxyRequests, externalRequests, {
proxyPrefix: '/_scripts/p/fathom',
domains: ['usefathom.com'],
}, { pre: preClickProxyCount, post: postClickProxyCount })
}, 30000)
// fathomAnalytics β€” bundle/proxy disabled in registry (see #720), script loads directly from CDN

it('intercom', async () => {
const { captures, rawCaptures, proxyRequests, externalRequests, preClickProxyCount, postClickProxyCount } = await testProvider('intercom', '/intercom-test')
Expand Down
118 changes: 118 additions & 0 deletions test/unit/bundle-sdk-patches.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Integration guard for bundle-only sdkPatches (nuxt/scripts#720 / Fathom).
// Exercises the full NuxtScriptBundleTransformer β†’ downloadScript β†’
// rewriteScriptUrlsAST pipeline to prove the neutralize-domain-check patch is
// actually applied to the stored bundle. A prior regression gated the rewrite
// on proxyRewrites.length, so bundle-only patches were silently dropped while
// direct unit tests of rewriteScriptUrlsAST still passed.
import type { AssetBundlerTransformerOptions } from '../../packages/script/src/plugins/transform'
import { hash } from 'ohash'
import { hasProtocol } from 'ufo'
import { describe, expect, it, vi } from 'vitest'
import { NuxtScriptBundleTransformer } from '../../packages/script/src/plugins/transform'

vi.mock('ohash', async (og) => {
const mod = await og<typeof import('ohash')>()
return { ...mod, hash: vi.fn(mod.hash) }
})
vi.mock('ufo', async (og) => {
const mod = await og<typeof import('ufo')>()
return { ...mod, hasProtocol: vi.fn(mod.hasProtocol) }
})

const mockBundleStorage: any = {
getItem: vi.fn(),
setItem: vi.fn(),
getItemRaw: vi.fn(),
setItemRaw: vi.fn(),
hasItem: vi.fn().mockResolvedValue(false),
}
vi.mock('../../packages/script/src/assets', () => ({
bundleStorage: vi.fn(() => mockBundleStorage),
}))

const fetchMock = vi.fn()
vi.stubGlobal('fetch', fetchMock)
Comment on lines +22 to +34
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect vi.mock factories in this test and nearby unit tests for top-level captures.
rg -n -C4 "vi\.mock\(" --type=ts

Repository: nuxt/scripts

Length of output: 13278


🏁 Script executed:

#!/bin/bash
# Check if the test files with similar patterns exist and are part of the test suite
ls -lh test/unit/transform.test.ts test/unit/bundle-two-deploy-repro.test.ts test/unit/bundle-sdk-patches.test.ts 2>/dev/null
echo "---"
# Look for vi.hoisted usage patterns in the codebase
rg "vi\.hoisted\(" --type=ts -n

Repository: nuxt/scripts

Length of output: 397


🌐 Web query:

Vitest vi.mock hoisting behavior temporal dead zone TDZ factory function

πŸ’‘ Result:

Vitest's vi.mock calls are hoisted to the top of the test file and executed before any imports or top-level code, similar to JavaScript hoisting but enforced by Vitest's static analysis and transformation. This hoisting ensures mocks are in place before modules are loaded. The factory function in vi.mock(path, factory) runs in this hoisted position, creating a Temporal Dead Zone (TDZ)-like restriction: it cannot access let/const variables declared later in the file, as they are uninitialized (ReferenceError). Top-level var variables might access as undefined, but this is unreliable. Vitest throws an explicit error if top-level variables appear in the factory: "There was an error when mocking a module. If you are using 'vi.mock' factory, make sure there are no top level variables inside, since this call is hoisted to top of the file." Workarounds: - Use vi.hoisted( => {...}) to define shared mocks/variables before vi.mock, accessible in the factory. - Use dynamic imports inside the factory (e.g., await import('./other-module')). - For non-hoisted mocking, use vi.doMock, but it only affects subsequent imports. - Avoid top-level variables in factories; define everything inside or use vi.hoisted. Example causing TDZ-like error: const myVar = 'value'; // let/const - TDZ when factory runs vi.mock('./module', => ({ fn: vi.fn( => myVar) })); // Error: myVar not initialized Correct with vi.hoisted: const mocks = vi.hoisted( => ({ myFn: vi.fn })); vi.mock('./module', => ({ fn: mocks.myFn })); Official docs confirm: "you cannot use any variables inside the factory that are defined outside the factory" (vitest.dev/api/vi.html#vi-mock).

Citations:


Move mock state into vi.hoisted() before using it in a vi.mock() factory.

Vitest forbids top-level variables in vi.mock() factories. The factory runs during module hoisting before mockBundleStorage and fetchMock are initialized, causing a runtime error. Use vi.hoisted() to define shared mock state that is accessible to the factory.

πŸ§ͺ Proposed fix
-const mockBundleStorage: any = {
-  getItem: vi.fn(),
-  setItem: vi.fn(),
-  getItemRaw: vi.fn(),
-  setItemRaw: vi.fn(),
-  hasItem: vi.fn().mockResolvedValue(false),
-}
+const { mockBundleStorage, fetchMock } = vi.hoisted(() => ({
+  mockBundleStorage: {
+    getItem: vi.fn(),
+    setItem: vi.fn(),
+    getItemRaw: vi.fn(),
+    setItemRaw: vi.fn(),
+    hasItem: vi.fn().mockResolvedValue(false),
+  },
+  fetchMock: vi.fn(),
+}))
+
 vi.mock('../../packages/script/src/assets', () => ({
   bundleStorage: vi.fn(() => mockBundleStorage),
 }))
 
-const fetchMock = vi.fn()
 vi.stubGlobal('fetch', fetchMock)

Note: test/unit/transform.test.ts and test/unit/bundle-two-deploy-repro.test.ts have the same issue and should also be fixed.

πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const mockBundleStorage: any = {
getItem: vi.fn(),
setItem: vi.fn(),
getItemRaw: vi.fn(),
setItemRaw: vi.fn(),
hasItem: vi.fn().mockResolvedValue(false),
}
vi.mock('../../packages/script/src/assets', () => ({
bundleStorage: vi.fn(() => mockBundleStorage),
}))
const fetchMock = vi.fn()
vi.stubGlobal('fetch', fetchMock)
const { mockBundleStorage, fetchMock } = vi.hoisted(() => ({
mockBundleStorage: {
getItem: vi.fn(),
setItem: vi.fn(),
getItemRaw: vi.fn(),
setItemRaw: vi.fn(),
hasItem: vi.fn().mockResolvedValue(false),
},
fetchMock: vi.fn(),
}))
vi.mock('../../packages/script/src/assets', () => ({
bundleStorage: vi.fn(() => mockBundleStorage),
}))
vi.stubGlobal('fetch', fetchMock)
πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/unit/bundle-sdk-patches.test.ts` around lines 22 - 34, The tests declare
mockBundleStorage and fetchMock at top-level and then reference them inside a
vi.mock factory, which runs during hoisting; fix by moving shared mock state
into vi.hoisted() so the factory can access initialized mocks: create hoisted
variables for mockBundleStorage and fetchMock via vi.hoisted(), initialize their
mock methods there, then change the vi.mock('../../packages/script/src/assets',
...) factory to return the hoisted mockBundleStorage and use
vi.stubGlobal('fetch', fetchMock) after hoisting; apply the same change to
test/unit/transform.test.ts and test/unit/bundle-two-deploy-repro.test.ts.


vi.mock('@nuxt/kit', async (og) => {
const mod = await og<typeof import('@nuxt/kit')>()
const nuxt = {
options: { buildDir: '.nuxt', app: { baseURL: '/' }, runtimeConfig: { app: {} } },
hooks: { hook: vi.fn() },
}
return { ...mod, useNuxt: () => nuxt, tryUseNuxt: () => nuxt }
})

vi.mocked(hasProtocol).mockImplementation(() => true)
vi.mocked(hash).mockImplementation(() => 'fathom-script')

function mockUpstream(bytes: Buffer) {
fetchMock.mockResolvedValueOnce({
ok: true,
arrayBuffer: () => Promise.resolve(bytes),
headers: { get: () => null },
_data: bytes,
} as any)
}

async function runTransform(code: string, options: AssetBundlerTransformerOptions) {
const plugin = NuxtScriptBundleTransformer(options).vite() as any
await plugin.transform.handler.call({}, code, 'file.js')
}

describe('bundle-only sdkPatches integration', () => {
const fathomLike = Buffer.from(
`(function(){var e=document.currentScript;if(e.src.indexOf("cdn.usefathom.com")<0){t="custom"}})();`,
)

it('applies neutralize-domain-check to bundle-only scripts (no proxy)', async () => {
mockUpstream(fathomLike)
const renderedScript = new Map()

await runTransform(
`const instance = useScriptFathomAnalytics({ site: '123' }, { bundle: true })`,
{
renderedScript,
scripts: [
{
bundle: {
resolve: () => 'https://cdn.usefathom.com/script.js',
sdkPatches: [{ type: 'neutralize-domain-check', domain: 'cdn.usefathom.com' }],
},
import: { name: 'useScriptFathomAnalytics', from: '' },
},
],
},
)

const stored = [...renderedScript.values()][0]
expect(stored, 'bundle was not stored').toBeDefined()
const content = (stored.content as Buffer).toString('utf-8')
// Patch rewrites `< 0` to `< -1` on the fathom domain indexOf comparison,
// preserving the original whitespace (minified `<0` stays minified).
expect(content).toMatch(/indexOf\("cdn\.usefathom\.com"\)\s*<\s*-1/)
expect(content).not.toMatch(/indexOf\("cdn\.usefathom\.com"\)\s*<\s*0\b/)
})

it('leaves bundles untouched when no patches are configured', async () => {
mockUpstream(fathomLike)
const renderedScript = new Map()

await runTransform(
`const instance = useScript('https://cdn.usefathom.com/script.js', { bundle: true })`,
{
renderedScript,
scripts: [
{
bundle: { resolve: () => 'https://cdn.usefathom.com/script.js' },
import: { name: 'useScript', from: '' },
},
],
},
)

const stored = [...renderedScript.values()][0]
expect(stored).toBeDefined()
const content = (stored.content as Buffer).toString('utf-8')
expect(content).toBe(fathomLike.toString('utf-8'))
})
})
9 changes: 4 additions & 5 deletions test/unit/proxy-configs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,10 +373,9 @@ describe('proxy configs', () => {
expect(config?.domains).toContain('basket.databuddy.cc')
})

it('returns proxy config for fathomAnalytics', async () => {
it('does not return proxy config for fathomAnalytics (removed: see #720, bot-detection flags proxied traffic)', async () => {
const config = (await getProxyConfigs()).fathomAnalytics
expect(config).toBeDefined()
expect(config?.domains).toContain('cdn.usefathom.com')
expect(config).toBeUndefined()
})

it('returns proxy config for intercom', async () => {
Expand Down Expand Up @@ -407,7 +406,7 @@ describe('proxy configs', () => {
})

describe('getProxyConfigs', () => {
it('returns all proxy configs (excluding removed: GTM, Segment, Crisp)', async () => {
it('returns all proxy configs (excluding removed: GTM, Segment, Crisp, Fathom)', async () => {
const configs = await getProxyConfigs()
expect(configs).toHaveProperty('googleAnalytics')
expect(configs).not.toHaveProperty('googleTagManager')
Expand All @@ -425,7 +424,7 @@ describe('proxy configs', () => {
expect(configs).toHaveProperty('rybbitAnalytics')
expect(configs).toHaveProperty('umamiAnalytics')
expect(configs).toHaveProperty('databuddyAnalytics')
expect(configs).toHaveProperty('fathomAnalytics')
expect(configs).not.toHaveProperty('fathomAnalytics')
expect(configs).toHaveProperty('intercom')
expect(configs).not.toHaveProperty('crisp')
expect(configs).toHaveProperty('vercelAnalytics')
Expand Down
Loading
Loading