From 5cd7b5d52cc5ae825f08f0a7b89888ed032259e6 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:13:47 -0700 Subject: [PATCH] fix(telemetry): send PostHog events through the main HTTP client so they honor the proxy (#35972) * fix(telemetry): send PostHog events through the main HTTP client so they honor the proxy * fix(telemetry): omit credentials on PostHog sends --- ...posthog-transport-main-http-client.test.ts | 39 +++++++++++++++++++ src/main/telemetry/posthog-transport.ts | 6 ++- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/main/telemetry/posthog-transport-main-http-client.test.ts diff --git a/src/main/telemetry/posthog-transport-main-http-client.test.ts b/src/main/telemetry/posthog-transport-main-http-client.test.ts new file mode 100644 index 00000000000..1e1d2aa81ba --- /dev/null +++ b/src/main/telemetry/posthog-transport-main-http-client.test.ts @@ -0,0 +1,39 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { setMainHttpClient } from '../network/http-client' +import { createPostHogClient } from './posthog-transport' + +describe('PostHog transport', () => { + const mainFetch = vi.fn(async () => new Response('{}', { status: 200 })) + const globalFetch = vi.fn(async () => new Response('{}', { status: 200 })) + + afterEach(() => { + setMainHttpClient(null) + vi.unstubAllGlobals() + mainFetch.mockClear() + globalFetch.mockClear() + }) + + const capture = async (consent: Parameters[1]) => { + vi.stubGlobal('fetch', globalFetch) + setMainHttpClient({ fetch: mainFetch, proxySession: () => null }) + const client = createPostHogClient('test-key', consent) + client.capture({ distinctId: 'install-1', event: 'app_opened' }) + await client.shutdown() + } + + // The desktop client sends through the session that carries the user's proxy. + it('sends through the main HTTP client, not global fetch', async () => { + await capture({ effective: 'enabled' }) + expect(mainFetch).toHaveBeenCalledWith( + expect.stringContaining('/batch'), + expect.objectContaining({ method: 'POST', credentials: 'omit' }) + ) + expect(globalFetch).not.toHaveBeenCalled() + }) + + it('sends nothing when telemetry is disabled', async () => { + await capture({ effective: 'disabled', reason: 'user_opt_out' }) + expect(mainFetch).not.toHaveBeenCalled() + expect(globalFetch).not.toHaveBeenCalled() + }) +}) diff --git a/src/main/telemetry/posthog-transport.ts b/src/main/telemetry/posthog-transport.ts index 8367fd8e88d..1fc61f4570e 100644 --- a/src/main/telemetry/posthog-transport.ts +++ b/src/main/telemetry/posthog-transport.ts @@ -1,6 +1,7 @@ import { arch as osArch, platform as osPlatform, release as osRelease } from 'node:os' import type * as PostHogSdk from 'posthog-node' import { getAppEnvironment } from '../../shared/app-environment' +import { getMainHttpClient } from '../network/http-client' import type { CommonProps, EventName } from '../../shared/telemetry-events' import type { ConsentState } from './consent' @@ -84,7 +85,10 @@ export function createPostHogClient(writeKey: string, consent: ConsentState): Po // Disable SDK GeoIP/IP enrichment to keep the wire inside the declared allow-list. disableGeoip: true, // Increase offline buffer from 1000 to 5000; overflow drops oldest events. - maxQueueSize: 5000 + maxQueueSize: 5000, + // The SDK's default global fetch skips the desktop session's proxy; resolved per send. + // Omit credentials so the desktop session never attaches its cookies to telemetry. + fetch: (url, options) => getMainHttpClient().fetch(url, { ...options, credentials: 'omit' }) }) if (shouldOptOutSdkAtInit(consent)) {