From 80e6e0007abea4694382dfc2a2f8c9d3ccce956a Mon Sep 17 00:00:00 2001 From: Ben White Date: Thu, 12 Dec 2024 09:43:44 +0100 Subject: [PATCH] Fixes --- src/__tests__/decide.test.ts | 221 ++++++++++++ src/__tests__/decide.ts | 329 ------------------ .../{featureflags.ts => featureflags.test.ts} | 18 +- src/decide.ts | 5 +- src/posthog-core.ts | 7 +- src/posthog-featureflags.ts | 14 +- 6 files changed, 245 insertions(+), 349 deletions(-) create mode 100644 src/__tests__/decide.test.ts delete mode 100644 src/__tests__/decide.ts rename src/__tests__/{featureflags.ts => featureflags.test.ts} (98%) diff --git a/src/__tests__/decide.test.ts b/src/__tests__/decide.test.ts new file mode 100644 index 000000000..1e9c95b7e --- /dev/null +++ b/src/__tests__/decide.test.ts @@ -0,0 +1,221 @@ +import { Decide } from '../decide' +import { PostHogPersistence } from '../posthog-persistence' +import { RequestRouter } from '../utils/request-router' +import { PostHog } from '../posthog-core' +import { PostHogConfig, Properties, RemoteConfig } from '../types' +import '../entrypoints/external-scripts-loader' +import { assignableWindow } from '../utils/globals' + +describe('Decide', () => { + let posthog: PostHog + + beforeEach(() => { + // clean the JSDOM to prevent interdependencies between tests + + const defaultConfig: Partial = { + token: 'testtoken', + api_host: 'https://test.com', + persistence: 'memory', + } + + document.body.innerHTML = '' + document.head.innerHTML = '' + jest.spyOn(window.console, 'error').mockImplementation() + + posthog = { + config: { ...defaultConfig }, + persistence: new PostHogPersistence(defaultConfig as PostHogConfig), + register: (props: Properties) => posthog.persistence!.register(props), + unregister: (key: string) => posthog.persistence!.unregister(key), + get_property: (key: string) => posthog.persistence!.props[key], + capture: jest.fn(), + _addCaptureHook: jest.fn(), + _onRemoteConfig: jest.fn(), + get_distinct_id: jest.fn().mockImplementation(() => 'distinctid'), + _send_request: jest.fn().mockImplementation(({ callback }) => callback?.({ config: {} })), + featureFlags: { + resetRequestQueue: jest.fn(), + reloadFeatureFlags: jest.fn(), + receivedFeatureFlags: jest.fn(), + setReloadingPaused: jest.fn(), + _callDecideEndpoint: jest.fn(), + }, + requestRouter: new RequestRouter({ config: defaultConfig } as unknown as PostHog), + _hasBootstrappedFeatureFlags: jest.fn(), + getGroups: () => ({ organization: '5' }), + } as unknown as PostHog + }) + + describe('constructor', () => { + it('should call _callDecideEndpoint on constructor', () => { + new Decide(posthog).call() + + expect(posthog.featureFlags._callDecideEndpoint).toHaveBeenCalledTimes(1) + expect(posthog.featureFlags._callDecideEndpoint).toHaveBeenLastCalledWith({ + data: { + disable_flags: undefined, + }, + callback: expect.any(Function), + }) + }) + + it('should not call _callDecideEndpoint on constructor if advanced_disable_decide', () => { + posthog.config.advanced_disable_decide = true + new Decide(posthog).call() + + expect(posthog.featureFlags._callDecideEndpoint).toHaveBeenCalledTimes(0) + }) + + it('should call _callDecideEndpoint with disable_flags true if advanced_disable_feature_flags is set', () => { + console.log('posthog.config', posthog.config) + posthog.config.advanced_disable_feature_flags = true + posthog.config.advanced_disable_feature_flags_on_first_load = false + + new Decide(posthog).call() + expect(posthog.featureFlags._callDecideEndpoint).toHaveBeenCalledTimes(1) + expect(posthog.featureFlags._callDecideEndpoint).toHaveBeenLastCalledWith({ + data: { + disable_flags: true, + }, + callback: expect.any(Function), + }) + }) + + it('should call _callDecideEndpoint with disable_flags true if advanced_disable_feature_flags_on_first_load is set', () => { + posthog.config.advanced_disable_feature_flags = false + posthog.config.advanced_disable_feature_flags_on_first_load = true + + new Decide(posthog).call() + expect(posthog.featureFlags._callDecideEndpoint).toHaveBeenCalledTimes(1) + expect(posthog.featureFlags._callDecideEndpoint).toHaveBeenLastCalledWith({ + data: { + disable_flags: true, + }, + callback: expect.any(Function), + }) + }) + }) + + // describe('parseDecideResponse', () => { + + // it('Make sure receivedFeatureFlags is called with errors if the decide response fails', () => { + // ;(window as any).POSTHOG_DEBUG = true + + // subject(undefined as unknown as DecideResponse) + + // expect(posthog.featureFlags.receivedFeatureFlags).toHaveBeenCalledWith({}, true) + // expect(console.error).toHaveBeenCalledWith( + // '[PostHog.js] [Decide]', + // 'Failed to fetch feature flags from PostHog.' + // ) + // }) + + // it('Make sure receivedFeatureFlags is not called if advanced_disable_feature_flags_on_first_load is set', () => { + // posthog.config = { + // api_host: 'https://test.com', + // token: 'testtoken', + // persistence: 'memory', + // advanced_disable_feature_flags_on_first_load: true, + // } as PostHogConfig + + // const decideResponse = { + // featureFlags: { 'test-flag': true }, + // } as unknown as DecideResponse + // subject(decideResponse) + + // expect(posthog._onRemoteConfig).toHaveBeenCalledWith(decideResponse) + // expect(posthog.featureFlags.receivedFeatureFlags).not.toHaveBeenCalled() + // }) + + // it('Make sure receivedFeatureFlags is not called if advanced_disable_feature_flags is set', () => { + // posthog.config = { + // api_host: 'https://test.com', + // token: 'testtoken', + // persistence: 'memory', + // advanced_disable_feature_flags: true, + // } as PostHogConfig + + // const decideResponse = { + // featureFlags: { 'test-flag': true }, + // } as unknown as DecideResponse + // subject(decideResponse) + + // expect(posthog._onRemoteConfig).toHaveBeenCalledWith(decideResponse) + // expect(posthog.featureFlags.receivedFeatureFlags).not.toHaveBeenCalled() + // }) + // }) + + describe('remote config', () => { + const config = { surveys: true } as RemoteConfig + + beforeEach(() => { + posthog.config.__preview_remote_config = true + assignableWindow._POSTHOG_CONFIG = undefined + assignableWindow.POSTHOG_DEBUG = true + + assignableWindow.__PosthogExtensions__.loadExternalDependency = jest.fn( + (_ph: PostHog, _name: string, cb: (err?: any) => void) => { + assignableWindow._POSTHOG_CONFIG = config as RemoteConfig + cb() + } + ) + + posthog._send_request = jest.fn().mockImplementation(({ callback }) => callback?.({ json: config })) + }) + + it('properly pulls from the window and uses it if set', () => { + assignableWindow._POSTHOG_CONFIG = config as RemoteConfig + new Decide(posthog).call() + + expect(assignableWindow.__PosthogExtensions__.loadExternalDependency).not.toHaveBeenCalled() + expect(posthog._send_request).not.toHaveBeenCalled() + + expect(posthog._onRemoteConfig).toHaveBeenCalledWith(config) + }) + + it('loads the script if window config not set', () => { + new Decide(posthog).call() + + expect(assignableWindow.__PosthogExtensions__.loadExternalDependency).toHaveBeenCalledWith( + posthog, + 'remote-config', + expect.any(Function) + ) + expect(posthog._send_request).not.toHaveBeenCalled() + expect(posthog._onRemoteConfig).toHaveBeenCalledWith(config) + }) + + it('loads the json if window config not set and js failed', () => { + assignableWindow.__PosthogExtensions__.loadExternalDependency = jest.fn( + (_ph: PostHog, _name: string, cb: (err?: any) => void) => { + cb() + } + ) + + new Decide(posthog).call() + + expect(assignableWindow.__PosthogExtensions__.loadExternalDependency).toHaveBeenCalled() + expect(posthog._send_request).toHaveBeenCalledWith({ + method: 'GET', + url: 'https://test.com/array/testtoken/config', + callback: expect.any(Function), + }) + expect(posthog._onRemoteConfig).toHaveBeenCalledWith(config) + }) + + it.each([ + [true, true], + [false, false], + [undefined, true], + ])('conditionally reloads feature flags - hasFlags: %s, shouldReload: %s', (hasFeatureFlags, shouldReload) => { + assignableWindow._POSTHOG_CONFIG = { hasFeatureFlags } as RemoteConfig + new Decide(posthog).call() + + if (shouldReload) { + expect(posthog.featureFlags.reloadFeatureFlags).toHaveBeenCalled() + } else { + expect(posthog.featureFlags.reloadFeatureFlags).not.toHaveBeenCalled() + } + }) + }) +}) diff --git a/src/__tests__/decide.ts b/src/__tests__/decide.ts deleted file mode 100644 index a7c8ec266..000000000 --- a/src/__tests__/decide.ts +++ /dev/null @@ -1,329 +0,0 @@ -import { Decide } from '../decide' -import { PostHogPersistence } from '../posthog-persistence' -import { RequestRouter } from '../utils/request-router' -import { PostHog } from '../posthog-core' -import { DecideResponse, PostHogConfig, Properties, RemoteConfig } from '../types' -import '../entrypoints/external-scripts-loader' -import { assignableWindow } from '../utils/globals' - -const expectDecodedSendRequest = ( - send_request: PostHog['_send_request'], - data: Record, - noCompression: boolean, - posthog: PostHog -) => { - const lastCall = jest.mocked(send_request).mock.calls[jest.mocked(send_request).mock.calls.length - 1] - - const decoded = lastCall[0].data - // Helper to give us more accurate error messages - expect(decoded).toEqual(data) - - expect(posthog._send_request).toHaveBeenCalledWith({ - url: 'https://test.com/decide/?v=3', - data, - method: 'POST', - callback: expect.any(Function), - compression: noCompression ? undefined : 'base64', - timeout: undefined, - }) -} - -describe('Decide', () => { - let posthog: PostHog - - const decide = () => new Decide(posthog) - - const defaultConfig: Partial = { - token: 'testtoken', - api_host: 'https://test.com', - persistence: 'memory', - } - - beforeEach(() => { - // clean the JSDOM to prevent interdependencies between tests - document.body.innerHTML = '' - document.head.innerHTML = '' - jest.spyOn(window.console, 'error').mockImplementation() - - posthog = { - config: defaultConfig, - persistence: new PostHogPersistence(defaultConfig as PostHogConfig), - register: (props: Properties) => posthog.persistence!.register(props), - unregister: (key: string) => posthog.persistence!.unregister(key), - get_property: (key: string) => posthog.persistence!.props[key], - capture: jest.fn(), - _addCaptureHook: jest.fn(), - _onRemoteConfig: jest.fn(), - get_distinct_id: jest.fn().mockImplementation(() => 'distinctid'), - _send_request: jest.fn().mockImplementation(({ callback }) => callback?.({ config: {} })), - featureFlags: { - resetRequestQueue: jest.fn(), - reloadFeatureFlags: jest.fn(), - receivedFeatureFlags: jest.fn(), - setReloadingPaused: jest.fn(), - _startReloadTimer: jest.fn(), - }, - requestRouter: new RequestRouter({ config: defaultConfig } as unknown as PostHog), - _hasBootstrappedFeatureFlags: jest.fn(), - getGroups: () => ({ organization: '5' }), - } as unknown as PostHog - }) - - describe('constructor', () => { - it('should call instance._send_request on constructor', () => { - decide().call() - - expectDecodedSendRequest( - posthog._send_request, - { - token: 'testtoken', - distinct_id: 'distinctid', - groups: { organization: '5' }, - }, - false, - posthog - ) - }) - - it('should send all stored properties with decide request', () => { - posthog.register({ - $stored_person_properties: { key: 'value' }, - $stored_group_properties: { organization: { orgName: 'orgValue' } }, - }) - - decide().call() - - expectDecodedSendRequest( - posthog._send_request, - { - token: 'testtoken', - distinct_id: 'distinctid', - groups: { organization: '5' }, - person_properties: { key: 'value' }, - group_properties: { organization: { orgName: 'orgValue' } }, - }, - false, - posthog - ) - }) - - it('should send disable flags with decide request when config is set', () => { - posthog.config = { - api_host: 'https://test.com', - token: 'testtoken', - persistence: 'memory', - advanced_disable_feature_flags: true, - } as PostHogConfig - - posthog.register({ - $stored_person_properties: { key: 'value' }, - $stored_group_properties: { organization: { orgName: 'orgValue' } }, - }) - decide().call() - - expectDecodedSendRequest( - posthog._send_request, - { - token: 'testtoken', - distinct_id: 'distinctid', - groups: { organization: '5' }, - person_properties: { key: 'value' }, - group_properties: { organization: { orgName: 'orgValue' } }, - disable_flags: true, - }, - false, - posthog - ) - }) - - it('should disable compression when config is set', () => { - posthog.config = { - api_host: 'https://test.com', - token: 'testtoken', - persistence: 'memory', - disable_compression: true, - } as PostHogConfig - - posthog.register({ - $stored_person_properties: {}, - $stored_group_properties: {}, - }) - decide().call() - - // noCompression is true - expectDecodedSendRequest( - posthog._send_request, - { - token: 'testtoken', - distinct_id: 'distinctid', - groups: { organization: '5' }, - person_properties: {}, - group_properties: {}, - }, - true, - posthog - ) - }) - - it('should send disable flags with decide request when config for advanced_disable_feature_flags_on_first_load is set', () => { - posthog.config = { - api_host: 'https://test.com', - token: 'testtoken', - persistence: 'memory', - advanced_disable_feature_flags_on_first_load: true, - } as PostHogConfig - - posthog.register({ - $stored_person_properties: { key: 'value' }, - $stored_group_properties: { organization: { orgName: 'orgValue' } }, - }) - - decide().call() - - expectDecodedSendRequest( - posthog._send_request, - { - token: 'testtoken', - distinct_id: 'distinctid', - groups: { organization: '5' }, - person_properties: { key: 'value' }, - group_properties: { organization: { orgName: 'orgValue' } }, - disable_flags: true, - }, - false, - posthog - ) - }) - }) - - describe('parseDecideResponse', () => { - const subject = (decideResponse: DecideResponse) => decide().parseDecideResponse(decideResponse) - - it('properly parses decide response', () => { - subject({} as DecideResponse) - - expect(posthog.featureFlags.receivedFeatureFlags).toHaveBeenCalledWith({}, false) - expect(posthog._onRemoteConfig).toHaveBeenCalledWith({}) - }) - - it('Make sure receivedFeatureFlags is called with errors if the decide response fails', () => { - ;(window as any).POSTHOG_DEBUG = true - - subject(undefined as unknown as DecideResponse) - - expect(posthog.featureFlags.receivedFeatureFlags).toHaveBeenCalledWith({}, true) - expect(console.error).toHaveBeenCalledWith( - '[PostHog.js] [Decide]', - 'Failed to fetch feature flags from PostHog.' - ) - }) - - it('Make sure receivedFeatureFlags is not called if advanced_disable_feature_flags_on_first_load is set', () => { - posthog.config = { - api_host: 'https://test.com', - token: 'testtoken', - persistence: 'memory', - advanced_disable_feature_flags_on_first_load: true, - } as PostHogConfig - - const decideResponse = { - featureFlags: { 'test-flag': true }, - } as unknown as DecideResponse - subject(decideResponse) - - expect(posthog._onRemoteConfig).toHaveBeenCalledWith(decideResponse) - expect(posthog.featureFlags.receivedFeatureFlags).not.toHaveBeenCalled() - }) - - it('Make sure receivedFeatureFlags is not called if advanced_disable_feature_flags is set', () => { - posthog.config = { - api_host: 'https://test.com', - token: 'testtoken', - persistence: 'memory', - advanced_disable_feature_flags: true, - } as PostHogConfig - - const decideResponse = { - featureFlags: { 'test-flag': true }, - } as unknown as DecideResponse - subject(decideResponse) - - expect(posthog._onRemoteConfig).toHaveBeenCalledWith(decideResponse) - expect(posthog.featureFlags.receivedFeatureFlags).not.toHaveBeenCalled() - }) - }) - - describe('remote config', () => { - const config = { surveys: true } as RemoteConfig - - beforeEach(() => { - posthog.config.__preview_remote_config = true - assignableWindow._POSTHOG_CONFIG = undefined - assignableWindow.POSTHOG_DEBUG = true - - assignableWindow.__PosthogExtensions__.loadExternalDependency = jest.fn( - (_ph: PostHog, _name: string, cb: (err?: any) => void) => { - assignableWindow._POSTHOG_CONFIG = config as RemoteConfig - cb() - } - ) - - posthog._send_request = jest.fn().mockImplementation(({ callback }) => callback?.({ json: config })) - }) - - it('properly pulls from the window and uses it if set', () => { - assignableWindow._POSTHOG_CONFIG = config as RemoteConfig - decide().call() - - expect(assignableWindow.__PosthogExtensions__.loadExternalDependency).not.toHaveBeenCalled() - expect(posthog._send_request).not.toHaveBeenCalled() - - expect(posthog._onRemoteConfig).toHaveBeenCalledWith(config) - }) - - it('loads the script if window config not set', () => { - decide().call() - - expect(assignableWindow.__PosthogExtensions__.loadExternalDependency).toHaveBeenCalledWith( - posthog, - 'remote-config', - expect.any(Function) - ) - expect(posthog._send_request).not.toHaveBeenCalled() - expect(posthog._onRemoteConfig).toHaveBeenCalledWith(config) - }) - - it('loads the json if window config not set and js failed', () => { - assignableWindow.__PosthogExtensions__.loadExternalDependency = jest.fn( - (_ph: PostHog, _name: string, cb: (err?: any) => void) => { - cb() - } - ) - - decide().call() - - expect(assignableWindow.__PosthogExtensions__.loadExternalDependency).toHaveBeenCalled() - expect(posthog._send_request).toHaveBeenCalledWith({ - method: 'GET', - url: 'https://test.com/array/testtoken/config', - callback: expect.any(Function), - }) - expect(posthog._onRemoteConfig).toHaveBeenCalledWith(config) - }) - - it.each([ - [true, true], - [false, false], - [undefined, true], - ])('conditionally reloads feature flags - hasFlags: %s, shouldReload: %s', (hasFeatureFlags, shouldReload) => { - assignableWindow._POSTHOG_CONFIG = { hasFeatureFlags } as RemoteConfig - decide().call() - - if (shouldReload) { - expect(posthog.featureFlags.reloadFeatureFlags).toHaveBeenCalled() - } else { - expect(posthog.featureFlags.reloadFeatureFlags).not.toHaveBeenCalled() - } - }) - }) -}) diff --git a/src/__tests__/featureflags.ts b/src/__tests__/featureflags.test.ts similarity index 98% rename from src/__tests__/featureflags.ts rename to src/__tests__/featureflags.test.ts index 8dd585b94..ccb3ed071 100644 --- a/src/__tests__/featureflags.ts +++ b/src/__tests__/featureflags.test.ts @@ -67,7 +67,7 @@ describe('featureflags', () => { }) it('should return flags from persistence even if decide endpoint was not hit', () => { - featureFlags.instance.decideEndpointWasHit = false + featureFlags._hasLoadedFlags = false expect(featureFlags.getFlags()).toEqual([ 'beta-feature', @@ -80,7 +80,7 @@ describe('featureflags', () => { it('should warn if decide endpoint was not hit and no flags exist', () => { ;(window as any).POSTHOG_DEBUG = true - featureFlags.instance.decideEndpointWasHit = false + featureFlags._hasLoadedFlags = false instance.persistence.unregister('$enabled_feature_flags') instance.persistence.unregister('$active_feature_flags') @@ -101,7 +101,7 @@ describe('featureflags', () => { }) it('should return the right feature flag and call capture', () => { - featureFlags.instance.decideEndpointWasHit = false + featureFlags._hasLoadedFlags = false expect(featureFlags.getFlags()).toEqual([ 'beta-feature', @@ -132,7 +132,7 @@ describe('featureflags', () => { }) it('should call capture for every different flag response', () => { - featureFlags.instance.decideEndpointWasHit = true + featureFlags._hasLoadedFlags = true instance.persistence.register({ $enabled_feature_flags: { @@ -156,13 +156,13 @@ describe('featureflags', () => { instance.persistence.register({ $enabled_feature_flags: {}, }) - featureFlags.instance.decideEndpointWasHit = false + featureFlags._hasLoadedFlags = false expect(featureFlags.getFlagVariants()).toEqual({}) expect(featureFlags.isFeatureEnabled('beta-feature')).toEqual(undefined) // no extra capture call because flags haven't loaded yet. expect(instance.capture).toHaveBeenCalledTimes(1) - featureFlags.instance.decideEndpointWasHit = true + featureFlags._hasLoadedFlags = true instance.persistence.register({ $enabled_feature_flags: { x: 'y' }, }) @@ -185,7 +185,7 @@ describe('featureflags', () => { }) it('should return the right feature flag and not call capture', () => { - featureFlags.instance.decideEndpointWasHit = true + featureFlags._hasLoadedFlags = true expect(featureFlags.isFeatureEnabled('beta-feature', { send_event: false })).toEqual(true) expect(instance.capture).not.toHaveBeenCalled() @@ -315,7 +315,7 @@ describe('featureflags', () => { }) it('onFeatureFlags callback should be called immediately if feature flags were loaded', () => { - featureFlags.instance.decideEndpointWasHit = true + featureFlags._hasLoadedFlags = true let called = false featureFlags.onFeatureFlags(() => (called = true)) expect(called).toEqual(true) @@ -324,7 +324,7 @@ describe('featureflags', () => { }) it('onFeatureFlags should not return flags that are off', () => { - featureFlags.instance.decideEndpointWasHit = true + featureFlags._hasLoadedFlags = true let _flags = [] let _variants = {} featureFlags.onFeatureFlags((flags, variants) => { diff --git a/src/decide.ts b/src/decide.ts index dfac7e789..a8212c031 100644 --- a/src/decide.ts +++ b/src/decide.ts @@ -7,10 +7,7 @@ import { assignableWindow, document } from './utils/globals' const logger = createLogger('[Decide]') export class Decide { - constructor(private readonly instance: PostHog) { - // don't need to wait for `decide` to return if flags were provided on initialisation - this.instance.decideEndpointWasHit = this.instance._hasBootstrappedFeatureFlags() - } + constructor(private readonly instance: PostHog) {} private _loadRemoteConfigJs(cb: (config?: RemoteConfig) => void): void { if (assignableWindow.__PosthogExtensions__?.loadExternalDependency) { diff --git a/src/posthog-core.ts b/src/posthog-core.ts index da63cf58c..31b9b13ce 100644 --- a/src/posthog-core.ts +++ b/src/posthog-core.ts @@ -275,7 +275,6 @@ export class PostHog { _triggered_notifs: any compression?: Compression __request_queue: QueuedRequestOptions[] - decideEndpointWasHit: boolean analyticsDefaultEndpoint: string version = Config.LIB_VERSION _initialPersonProfilesConfig: 'always' | 'never' | 'identified_only' | null @@ -285,6 +284,11 @@ export class PostHog { private _internalEventEmitter = new SimpleEventEmitter() + // Legacy property to support existing usage + public get decideEndpointWasHit(): boolean { + return this.featureFlags._decideEndpointWasHit + } + /** DEPRECATED: We keep this to support existing usage but now one should just call .setPersonProperties */ people: { set: (prop: string | Properties, to?: string, callback?: RequestCallback) => void @@ -294,7 +298,6 @@ export class PostHog { constructor() { this.config = defaultConfig() - this.decideEndpointWasHit = false this.SentryIntegration = SentryIntegration this.sentryIntegration = (options?: SentryIntegrationOptions) => sentryIntegration(this, options) this.__request_queue = [] diff --git a/src/posthog-featureflags.ts b/src/posthog-featureflags.ts index ad0f1d54b..8a9e48f76 100644 --- a/src/posthog-featureflags.ts +++ b/src/posthog-featureflags.ts @@ -85,10 +85,12 @@ export class PostHogFeatureFlags { _override_warning: boolean = false featureFlagEventHandlers: FeatureFlagsCallback[] $anon_distinct_id: string | undefined + _decideEndpointWasHit: boolean = false private _requestInFlight: boolean = false private _reloadingDisabled: boolean = false private _additionalReloadRequested: boolean = false private _reloadDebouncer?: any + private _hasLoadedFlags: boolean = false constructor(private instance: PostHog) { this.featureFlagEventHandlers = [] @@ -135,8 +137,9 @@ export class PostHogFeatureFlags { * 2. Delay a few milliseconds after each reloadFeatureFlags call to batch subsequent changes together */ reloadFeatureFlags(): void { - if (this._reloadingDisabled) { + if (this._reloadingDisabled || this.instance.config.advanced_disable_feature_flags) { // If reloading has been explicitly disabled then we don't want to do anything + // Or if feature flags are disabled return } @@ -229,7 +232,7 @@ export class PostHogFeatureFlags { * @param {Object|String} options (optional) If {send_event: false}, we won't send an $feature_flag_call event to PostHog. */ getFeatureFlag(key: string, options: { send_event?: boolean } = {}): boolean | string | undefined { - if (!this.instance.decideEndpointWasHit && !(this.getFlags() && this.getFlags().length > 0)) { + if (!this._hasLoadedFlags && !(this.getFlags() && this.getFlags().length > 0)) { logger.warn('getFeatureFlag for key "' + key + '" failed. Feature flags didn\'t load in time.') return undefined } @@ -268,7 +271,7 @@ export class PostHogFeatureFlags { * @param {Object|String} options (optional) If {send_event: false}, we won't send an $feature_flag_call event to PostHog. */ isFeatureEnabled(key: string, options: { send_event?: boolean } = {}): boolean | undefined { - if (!this.instance.decideEndpointWasHit && !(this.getFlags() && this.getFlags().length > 0)) { + if (!this._hasLoadedFlags && !(this.getFlags() && this.getFlags().length > 0)) { logger.warn('isFeatureEnabled for key "' + key + '" failed. Feature flags didn\'t load in time.') return undefined } @@ -287,7 +290,8 @@ export class PostHogFeatureFlags { if (!this.instance.persistence) { return } - this.instance.decideEndpointWasHit = true + this._hasLoadedFlags = true + const currentFlags = this.getFlagVariants() const currentFlagPayloads = this.getFlagPayloads() parseFeatureFlagDecideResponse(response, this.instance.persistence, currentFlags, currentFlagPayloads) @@ -341,7 +345,7 @@ export class PostHogFeatureFlags { */ onFeatureFlags(callback: FeatureFlagsCallback): () => void { this.addFeatureFlagsHandler(callback) - if (this.instance.decideEndpointWasHit) { + if (this._hasLoadedFlags) { const { flags, flagVariants } = this._prepareFeatureFlagsForCallbacks() callback(flags, flagVariants) }