From 037aceae3a07e8746e515d5973b0c91c94b5ebe1 Mon Sep 17 00:00:00 2001 From: dylan Date: Thu, 20 Mar 2025 15:52:21 -0700 Subject: [PATCH 1/7] this seems reasonable? idk --- src/posthog-featureflags.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/posthog-featureflags.ts b/src/posthog-featureflags.ts index 43712e58d..4562c5b0f 100644 --- a/src/posthog-featureflags.ts +++ b/src/posthog-featureflags.ts @@ -393,7 +393,10 @@ export class PostHogFeatureFlags { this._requestInFlight = true this.instance._send_request({ method: 'POST', - url: this.instance.requestRouter.endpointFor('api', '/decide/?v=4'), + url: this.instance.requestRouter.endpointFor( + 'api', + token === 'sTMFPsFhdP1Ssg' ? '/flags/?v=2' : '/decide/?v=4' + ), data, compression: this.instance.config.disable_compression ? undefined : Compression.Base64, timeout: this.instance.config.feature_flag_request_timeout_ms, From 7005b5cedf3fb7e7f21a06ff0cfc6d74a2ea6de3 Mon Sep 17 00:00:00 2001 From: dylan Date: Thu, 20 Mar 2025 16:05:17 -0700 Subject: [PATCH 2/7] hmmmmmmm --- src/constants.ts | 2 ++ src/posthog-featureflags.ts | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/constants.ts b/src/constants.ts index 8621faab2..0cf9d7ce4 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -85,3 +85,5 @@ export const PERSISTENCE_RESERVED_PROPERTIES = [ ] export const SURVEYS_REQUEST_TIMEOUT_MS = 10000 + +export const DEFAULT_POSTHOG_APP_API_KEY = 'sTMFPsFhdP1Ssg' diff --git a/src/posthog-featureflags.ts b/src/posthog-featureflags.ts index 4562c5b0f..e6fd5acc1 100644 --- a/src/posthog-featureflags.ts +++ b/src/posthog-featureflags.ts @@ -22,6 +22,7 @@ import { STORED_GROUP_PROPERTIES_KEY, STORED_PERSON_PROPERTIES_KEY, FLAG_CALL_REPORTED, + DEFAULT_POSTHOG_APP_API_KEY, } from './constants' import { isArray, isUndefined } from './utils/type-utils' @@ -395,7 +396,7 @@ export class PostHogFeatureFlags { method: 'POST', url: this.instance.requestRouter.endpointFor( 'api', - token === 'sTMFPsFhdP1Ssg' ? '/flags/?v=2' : '/decide/?v=4' + token === DEFAULT_POSTHOG_APP_API_KEY ? '/flags/?v=2' : '/decide/?v=4' ), data, compression: this.instance.config.disable_compression ? undefined : Compression.Base64, @@ -416,6 +417,7 @@ export class PostHogFeatureFlags { this._requestInFlight = false if (!this._decideCalled) { + // NB: this will be true if remote config is enabled, which it is for the default posthog app api key this._decideCalled = true this.instance._onRemoteConfig(response.json ?? {}) } From bcc87ed85e706e57da8e4de49588f040955fc289 Mon Sep 17 00:00:00 2001 From: dylan Date: Thu, 20 Mar 2025 16:11:46 -0700 Subject: [PATCH 3/7] make it a bit more clear --- src/posthog-featureflags.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/posthog-featureflags.ts b/src/posthog-featureflags.ts index e6fd5acc1..e92607f3b 100644 --- a/src/posthog-featureflags.ts +++ b/src/posthog-featureflags.ts @@ -391,13 +391,12 @@ export class PostHogFeatureFlags { data.disable_flags = true } + const eligibleForFlagsV2 = token === DEFAULT_POSTHOG_APP_API_KEY && this.instance.config.__preview_remote_config + this._requestInFlight = true this.instance._send_request({ method: 'POST', - url: this.instance.requestRouter.endpointFor( - 'api', - token === DEFAULT_POSTHOG_APP_API_KEY ? '/flags/?v=2' : '/decide/?v=4' - ), + url: this.instance.requestRouter.endpointFor('api', eligibleForFlagsV2 ? '/flags/?v=2' : '/decide/?v=4'), data, compression: this.instance.config.disable_compression ? undefined : Compression.Base64, timeout: this.instance.config.feature_flag_request_timeout_ms, From 0ee7840298aaddfcec988912a24683e761c28539 Mon Sep 17 00:00:00 2001 From: dylan Date: Fri, 21 Mar 2025 13:32:14 -0700 Subject: [PATCH 4/7] don't make it a token, make it an experimental feature --- src/constants.ts | 2 -- src/posthog-featureflags.ts | 4 ++-- src/types.ts | 6 ++++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 0cf9d7ce4..8621faab2 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -85,5 +85,3 @@ export const PERSISTENCE_RESERVED_PROPERTIES = [ ] export const SURVEYS_REQUEST_TIMEOUT_MS = 10000 - -export const DEFAULT_POSTHOG_APP_API_KEY = 'sTMFPsFhdP1Ssg' diff --git a/src/posthog-featureflags.ts b/src/posthog-featureflags.ts index e92607f3b..d67bd8062 100644 --- a/src/posthog-featureflags.ts +++ b/src/posthog-featureflags.ts @@ -22,7 +22,6 @@ import { STORED_GROUP_PROPERTIES_KEY, STORED_PERSON_PROPERTIES_KEY, FLAG_CALL_REPORTED, - DEFAULT_POSTHOG_APP_API_KEY, } from './constants' import { isArray, isUndefined } from './utils/type-utils' @@ -391,7 +390,8 @@ export class PostHogFeatureFlags { data.disable_flags = true } - const eligibleForFlagsV2 = token === DEFAULT_POSTHOG_APP_API_KEY && this.instance.config.__preview_remote_config + const eligibleForFlagsV2 = + this.instance.config.__preview_flags_v2 && this.instance.config.__preview_remote_config this._requestInFlight = true this.instance._send_request({ diff --git a/src/types.ts b/src/types.ts index a100fe25a..744235183 100644 --- a/src/types.ts +++ b/src/types.ts @@ -891,6 +891,12 @@ export interface PostHogConfig { * */ __preview_experimental_cookieless_mode?: boolean + /** + * PREVIEW - MAY CHANGE WITHOUT WARNING - DO NOT USE IN PRODUCTION + * Whether to use the new /flags/ endpoint + * */ + __preview_flags_v2?: boolean + // ------- RETIRED CONFIGS - NO REPLACEMENT OR USAGE ------- /** @deprecated - NOT USED ANYMORE, kept here for backwards compatibility reasons */ From 675602b3e4dbd18d68cb2f5a4a962220d019a0d2 Mon Sep 17 00:00:00 2001 From: dylan Date: Fri, 21 Mar 2025 13:50:33 -0700 Subject: [PATCH 5/7] neat --- playground/nextjs/src/posthog.ts | 1 + src/__tests__/__snapshots__/config-snapshot.test.ts.snap | 5 +++++ src/posthog-featureflags.ts | 6 +++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/playground/nextjs/src/posthog.ts b/playground/nextjs/src/posthog.ts index f3a69e1d6..685794c3d 100644 --- a/playground/nextjs/src/posthog.ts +++ b/playground/nextjs/src/posthog.ts @@ -67,6 +67,7 @@ if (typeof window !== 'undefined') { opt_in_site_apps: true, __preview_remote_config: true, __preview_experimental_cookieless_mode: false, + __preview_flags_v2: true, ...configForConsent(), }) // Help with debugging diff --git a/src/__tests__/__snapshots__/config-snapshot.test.ts.snap b/src/__tests__/__snapshots__/config-snapshot.test.ts.snap index 4c34c32e3..906e0164b 100644 --- a/src/__tests__/__snapshots__/config-snapshot.test.ts.snap +++ b/src/__tests__/__snapshots__/config-snapshot.test.ts.snap @@ -502,6 +502,11 @@ exports[`config snapshot for PostHogConfig 1`] = ` \\"false\\", \\"true\\" ], + \\"__preview_flags_v2\\": [ + \\"undefined\\", + \\"false\\", + \\"true\\" + ], \\"api_method\\": [ \\"undefined\\", \\"string\\" diff --git a/src/posthog-featureflags.ts b/src/posthog-featureflags.ts index d67bd8062..71efc2386 100644 --- a/src/posthog-featureflags.ts +++ b/src/posthog-featureflags.ts @@ -390,6 +390,10 @@ export class PostHogFeatureFlags { data.disable_flags = true } + // NB: flags v2 requires remote config to be enabled as well, since the idea is that we will skip calling /decide altogether + // (which remote config does) and just use the /flags endpoint. May revisit this if we need to support flags v2 without remote config + // (e.g. we could call `/decide` with flags disabled for the data otherwise returned by remote config, and then still call + // `/flags/` to get the flag evaluation data). const eligibleForFlagsV2 = this.instance.config.__preview_flags_v2 && this.instance.config.__preview_remote_config @@ -415,8 +419,8 @@ export class PostHogFeatureFlags { this._requestInFlight = false + // NB: this block is only reached if this.instance.config.__preview_remote_config is false if (!this._decideCalled) { - // NB: this will be true if remote config is enabled, which it is for the default posthog app api key this._decideCalled = true this.instance._onRemoteConfig(response.json ?? {}) } From 9ecdd01a7ddd218be72d3b1bcb36528dd1f4f88a Mon Sep 17 00:00:00 2001 From: dylan Date: Fri, 21 Mar 2025 14:17:50 -0700 Subject: [PATCH 6/7] added tests --- functional_tests/feature-flags.test.ts | 60 ++++++++++++++++++++++++++ functional_tests/mock-server.ts | 10 ++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/functional_tests/feature-flags.test.ts b/functional_tests/feature-flags.test.ts index 00bee9c2e..32382482e 100644 --- a/functional_tests/feature-flags.test.ts +++ b/functional_tests/feature-flags.test.ts @@ -333,3 +333,63 @@ describe('FunctionalTests / Feature Flags', () => { }) }) }) + +describe('feature flags v2', () => { + let token: string + + beforeEach(() => { + token = uuidv7() + }) + + it('should call flags endpoint when eligible', async () => { + const posthog = await createPosthogInstance(token, { + __preview_flags_v2: true, + __preview_remote_config: true, + advanced_disable_decide: false, + }) + + await waitFor(() => { + expect(getRequests(token)['/flags/']).toEqual([ + expect.objectContaining({ + token, + distinct_id: posthog.get_distinct_id(), + }), + ]) + }) + }) + + it('should call decide endpoint when not eligible', async () => { + const posthog = await createPosthogInstance(token, { + __preview_flags_v2: false, + __preview_remote_config: true, + advanced_disable_decide: false, + }) + + await waitFor(() => { + expect(getRequests(token)['/decide/']).toEqual([ + expect.objectContaining({ + token, + distinct_id: posthog.get_distinct_id(), + }), + ]) + }) + }) + + // TODO: eventually I want to deprecate these behavior, but for now I want to make sure we don't break people + it('should call decide endpoint when preview flags is enabled but remote config is disabled', async () => { + const posthog = await createPosthogInstance(token, { + __preview_flags_v2: true, + __preview_remote_config: false, + advanced_disable_decide: false, + }) + + await waitFor(() => { + expect(getRequests(token)['/decide/']).toEqual([ + expect.objectContaining({ + token, + distinct_id: posthog.get_distinct_id(), + }), + ]) + }) + }) +}) diff --git a/functional_tests/mock-server.ts b/functional_tests/mock-server.ts index eea0ebbfe..9839ce8b1 100644 --- a/functional_tests/mock-server.ts +++ b/functional_tests/mock-server.ts @@ -7,10 +7,11 @@ import { RestRequest } from 'msw' import { decompressSync, strFromU8 } from 'fflate' // the request bodies in a store that we can inspect within tests. -const capturedRequests: { '/e/': any[]; '/engage/': any[]; '/decide/': any[] } = { +const capturedRequests: { '/e/': any[]; '/engage/': any[]; '/decide/': any[]; '/flags/': any[] } = { '/e/': [], '/engage/': [], '/decide/': [], + '/flags/': [], } const handleRequest = (group: string) => (req: RestRequest, res: ResponseComposition, ctx: RestContext) => { @@ -49,6 +50,9 @@ const server = setupServer( }), rest.post('http://localhost/decide/', (req, res, ctx) => { return handleRequest('/decide/')(req, res, ctx) + }), + rest.post('http://localhost/flags/', (req, res, ctx) => { + return handleRequest('/flags/')(req, res, ctx) }) ) @@ -66,6 +70,7 @@ export const getRequests = (token: string) => { '/e/': capturedRequests['/e/'].filter((request) => request.properties.token === token), '/engage/': capturedRequests['/engage/'].filter((request) => request.properties.token === token), '/decide/': capturedRequests['/decide/'].filter((request) => request.token === token), + '/flags/': capturedRequests['/flags/'].filter((request) => request.token === token), } } @@ -80,5 +85,8 @@ export const resetRequests = (token: string) => { '/decide/': (capturedRequests['/decide/'] = capturedRequests['/decide/'].filter( (request) => request.token !== token )), + '/flags/': (capturedRequests['/flags/'] = capturedRequests['/flags/'].filter( + (request) => request.token !== token + )), }) } From d0a37662d818ac65e8532b60da54f8562574657a Mon Sep 17 00:00:00 2001 From: dylan Date: Fri, 21 Mar 2025 14:38:08 -0700 Subject: [PATCH 7/7] logs for e2e tests --- src/posthog-core.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/posthog-core.ts b/src/posthog-core.ts index 1d6750eae..b04b282db 100644 --- a/src/posthog-core.ts +++ b/src/posthog-core.ts @@ -1794,11 +1794,18 @@ export class PostHog { } if (this.config.debug) { Config.DEBUG = true - logger.info('set_config', { - config, - oldConfig, - newConfig: { ...this.config }, - }) + logger.info( + 'set_config', + JSON.stringify( + { + config, + oldConfig, + newConfig: { ...this.config }, + }, + null, + 2 + ) + ) } this.sessionRecording?.startIfEnabledOrStop()