diff --git a/.changeset/posthog-no-implicit-local-evaluation.md b/.changeset/posthog-no-implicit-local-evaluation.md new file mode 100644 index 00000000..9264537d --- /dev/null +++ b/.changeset/posthog-no-implicit-local-evaluation.md @@ -0,0 +1,9 @@ +--- +"@flags-sdk/posthog": patch +--- + +Stop the default `postHogAdapter` from enabling local evaluation when `POSTHOG_PERSONAL_API_KEY` is set. + +`POSTHOG_PERSONAL_API_KEY` is only needed for provider data discovery (`getProviderData`), which reads it independently. Passing it into the runtime `posthog-node` client also enabled local evaluation, which starts a per-process feature-flag poller (`featureFlagsPollingInterval: 10_000`). On serverless deployments each warm process ran its own poller, generating large numbers of PostHog feature flag requests unrelated to actual traffic. + +The default adapter no longer forwards the personal API key or the polling interval to the runtime client. To use local evaluation, create the adapter explicitly with `createPostHogAdapter` and pass `personalApiKey` and `featureFlagsPollingInterval` in `postHogOptions`. diff --git a/packages/adapter-posthog/src/index.test.ts b/packages/adapter-posthog/src/index.test.ts index b2364a9f..af2a828e 100644 --- a/packages/adapter-posthog/src/index.test.ts +++ b/packages/adapter-posthog/src/index.test.ts @@ -1,4 +1,5 @@ import type { ReadonlyHeaders, ReadonlyRequestCookies } from 'flags'; +import { PostHog } from 'posthog-node'; import { beforeAll, describe, expect, it, vi } from 'vitest'; import { type PostHogEntities, postHogAdapter } from '.'; @@ -30,6 +31,9 @@ describe('postHogAdapter', () => { beforeAll(() => { process.env.NEXT_PUBLIC_POSTHOG_KEY = 'test-posthog-key'; process.env.NEXT_PUBLIC_POSTHOG_HOST = 'https://us.i.posthog.com'; + // Present because provider data discovery needs it, but it must not leak + // into the runtime client. See issue #393. + process.env.POSTHOG_PERSONAL_API_KEY = 'phx_test_personal_api_key'; }); describe('isFeatureEnabled', () => { @@ -90,5 +94,17 @@ describe('postHogAdapter', () => { expect(postHogClientMock.getFeatureFlagPayload).toHaveBeenCalled(); }); }); + + // Regression test for issue #393: the default adapter must not forward + // `POSTHOG_PERSONAL_API_KEY` into the runtime `posthog-node` client, because + // that enables local evaluation and starts a per-process feature-flag poller. + describe('default runtime client options', () => { + it('should not enable local evaluation from POSTHOG_PERSONAL_API_KEY', () => { + expect(PostHog).toHaveBeenCalled(); + const options = vi.mocked(PostHog).mock.calls[0]?.[1] ?? {}; + expect(options).not.toHaveProperty('personalApiKey'); + expect(options).not.toHaveProperty('featureFlagsPollingInterval'); + }); + }); }); }); diff --git a/packages/adapter-posthog/src/index.ts b/packages/adapter-posthog/src/index.ts index 4cd28242..b030d287 100644 --- a/packages/adapter-posthog/src/index.ts +++ b/packages/adapter-posthog/src/index.ts @@ -116,8 +116,12 @@ function getOrCreateDefaultAdapter() { postHogKey: assertEnv('NEXT_PUBLIC_POSTHOG_KEY'), postHogOptions: { host: assertEnv('NEXT_PUBLIC_POSTHOG_HOST'), - personalApiKey: process.env.POSTHOG_PERSONAL_API_KEY, - featureFlagsPollingInterval: 10_000, + // `POSTHOG_PERSONAL_API_KEY` is only needed for provider data discovery + // (see `getProviderData`), which reads it independently of this client. + // Passing it here would enable local evaluation in `posthog-node`, which + // starts a per-process feature-flag poller. To use local evaluation, + // create the adapter explicitly with `createPostHogAdapter` and pass + // `personalApiKey`/`featureFlagsPollingInterval` in `postHogOptions`. // Presumption: Server IP is likely not a good proxy for user location disableGeoip: true, },