From 8600f777d53e7618f6846c645c3f65cfd39bd704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Thu, 20 Mar 2025 14:03:33 -0300 Subject: [PATCH 01/27] WIP - use deferred promise to store and access environment --- .prettierrc.js => .prettierrc.cjs | 0 sdk/index.ts | 263 ++++++++++-------- sdk/polling_manager.ts | 11 +- sdk/types.ts | 2 +- sdk/utils.ts | 65 +++-- tests/sdk/flagsmith-cache.test.ts | 8 +- tests/sdk/flagsmith-environment-flags.test.ts | 14 - tests/sdk/flagsmith-identity-flags.test.ts | 32 ++- tests/sdk/flagsmith.test.ts | 121 ++++---- tests/sdk/utils.ts | 4 +- 10 files changed, 311 insertions(+), 209 deletions(-) rename .prettierrc.js => .prettierrc.cjs (100%) diff --git a/.prettierrc.js b/.prettierrc.cjs similarity index 100% rename from .prettierrc.js rename to .prettierrc.cjs diff --git a/sdk/index.ts b/sdk/index.ts index 60269d2..eb58ac3 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -1,5 +1,5 @@ import { Dispatcher } from 'undici-types'; -import { getEnvironmentFeatureStates, getIdentityFeatureStates } from '../flagsmith-engine/index.js'; +import { FeatureStateModel, getEnvironmentFeatureStates, getIdentityFeatureStates } from '../flagsmith-engine/index.js'; import { EnvironmentModel } from '../flagsmith-engine/index.js'; import { buildEnvironmentModel } from '../flagsmith-engine/environments/util.js'; import { IdentityModel } from '../flagsmith-engine/index.js'; @@ -11,7 +11,7 @@ import { FlagsmithAPIError, FlagsmithClientError } from './errors.js'; import { DefaultFlag, Flags } from './models.js'; import { EnvironmentDataPollingManager } from './polling_manager.js'; -import { generateIdentitiesData, retryFetch } from './utils.js'; +import { Deferred, generateIdentitiesData, retryFetch } from './utils.js'; import { SegmentModel } from '../flagsmith-engine/index.js'; import { getIdentitySegments } from '../flagsmith-engine/segments/evaluators.js'; import { Fetch, FlagsmithCache, FlagsmithConfig, FlagsmithTraitValue, ITraitConfig } from './types.js'; @@ -45,14 +45,14 @@ export class Flagsmith { environmentUrl?: string; environmentDataPollingManager?: EnvironmentDataPollingManager; - environment!: EnvironmentModel; + private environment?: EnvironmentModel; offlineMode: boolean = false; offlineHandler?: BaseOfflineHandler = undefined; identitiesWithOverridesByIdentifier?: Map; private cache?: FlagsmithCache; - private onEnvironmentChange?: (error: Error | null, result: EnvironmentModel) => void; + private onEnvironmentChange: (error: Error | null, result?: EnvironmentModel) => void; private analyticsProcessor?: AnalyticsProcessor; private logger: Logger; private customFetch: Fetch; @@ -101,7 +101,7 @@ export class Flagsmith { this.agent = data.agent; this.customFetch = data.fetch ?? fetch; this.environmentKey = data.environmentKey; - this.apiUrl = data.apiUrl || this.apiUrl; + this.apiUrl = data.apiUrl || DEFAULT_API_URL; this.customHeaders = data.customHeaders; this.requestTimeoutMs = 1000 * (data.requestTimeoutSeconds ?? DEFAULT_REQUEST_TIMEOUT_SECONDS); @@ -112,7 +112,7 @@ export class Flagsmith { this.enableAnalytics = data.enableAnalytics || false; this.defaultFlagHandler = data.defaultFlagHandler; - this.onEnvironmentChange = data.onEnvironmentChange; + this.onEnvironmentChange = (error, result) => data.onEnvironmentChange?.(error, result); this.logger = data.logger || pino(); this.offlineMode = data.offlineMode || false; this.offlineHandler = data.offlineHandler; @@ -124,10 +124,6 @@ export class Flagsmith { throw new Error('ValueError: Cannot use both defaultFlagHandler and offlineHandler.'); } - if (this.offlineHandler) { - this.environment = this.offlineHandler.getEnvironment(); - } - if (!!data.cache) { this.cache = data.cache; } @@ -150,12 +146,14 @@ export class Flagsmith { 'In order to use local evaluation, please generate a server key in the environment settings page.' ); } - this.environmentDataPollingManager = new EnvironmentDataPollingManager( - this, - this.environmentRefreshIntervalSeconds - ); - this.environmentDataPollingManager.start(); - this.updateEnvironment(); + if (this.environmentRefreshIntervalSeconds > 0){ + this.environmentDataPollingManager = new EnvironmentDataPollingManager( + this, + this.environmentRefreshIntervalSeconds, + this.logger, + ); + this.environmentDataPollingManager.start(); + } } if (data.enableAnalytics) { @@ -178,18 +176,19 @@ export class Flagsmith { if (!!cachedItem) { return cachedItem; } - if (this.enableLocalEvaluation && !this.offlineMode) { - return new Promise((resolve, reject) => - this.environmentPromise!.then(() => { - resolve(this.getEnvironmentFlagsFromDocument()); - }).catch(e => reject(e)) - ); - } - if (this.environment) { - return this.getEnvironmentFlagsFromDocument(); + try { + if (this.enableLocalEvaluation || this.offlineMode) { + const environment = await this.getEnvironment(); + return this.getEnvironmentFlagsFromDocument(environment); + } + return this.getEnvironmentFlagsFromApi(); + } catch (error) { + this.logger.error(error, 'getEnvironmentFlags failed'); + return new Flags({ + flags: {}, + defaultFlagHandler: this.defaultFlagHandler + }); } - - return this.getEnvironmentFlagsFromApi(); } /** @@ -217,18 +216,19 @@ export class Flagsmith { return cachedItem; } traits = traits || {}; - if (this.enableLocalEvaluation) { - return new Promise((resolve, reject) => - this.environmentPromise!.then(() => { - resolve(this.getIdentityFlagsFromDocument(identifier, traits || {})); - }).catch(e => reject(e)) - ); - } - if (this.offlineMode) { - return this.getIdentityFlagsFromDocument(identifier, traits || {}); + try { + if (this.enableLocalEvaluation || this.offlineMode) { + const environment = await this.getEnvironment(); + return this.getIdentityFlagsFromDocument(environment, identifier, traits || {}); + } + return await this.getIdentityFlagsFromApi(identifier, traits, transient); + } catch (error) { + this.logger.error(error, 'getIdentityFlags failed'); + return new Flags({ + flags: {}, + defaultFlagHandler: this.defaultFlagHandler + }); } - - return this.getIdentityFlagsFromApi(identifier, traits, transient); } /** @@ -242,66 +242,69 @@ export class Flagsmith { Flagsmith, e.g. {"num_orders": 10} * @returns Segments that the given identity belongs to. */ - getIdentitySegments( + async getIdentitySegments( identifier: string, traits?: { [key: string]: any } ): Promise { if (!identifier) { throw new Error('`identifier` argument is missing or invalid.'); } + if (!this.enableLocalEvaluation) { + console.error('This function is only permitted with local evaluation.'); + return Promise.resolve([]); + } traits = traits || {}; - if (this.enableLocalEvaluation) { - return new Promise((resolve, reject) => { - return this.environmentPromise!.then(() => { - const identityModel = this.getIdentityModel( - identifier, - Object.keys(traits || {}).map(key => ({ - key, - value: traits?.[key] - })) - ); + const environment = await this.getEnvironment(); + const identityModel = await this.getIdentityModel( + environment, + identifier, + Object.keys(traits || {}).map(key => ({ + key, + value: traits?.[key] + })) + ); - const segments = getIdentitySegments(this.environment, identityModel); - return resolve(segments); - }).catch(e => reject(e)); - }); - } - console.error('This function is only permitted with local evaluation.'); - return Promise.resolve([]); + return getIdentitySegments(environment, identityModel); } - /** - * Updates the environment state for local flag evaluation. - * Sets a local promise to prevent race conditions in getIdentityFlags / getIdentitySegments. - * You only need to call this if you wish to bypass environmentRefreshIntervalSeconds. - */ - async updateEnvironment() { + private async fetchEnvironment(): Promise { + const deferred = new Deferred(); + this.environmentPromise = deferred.promise; try { - const request = this.getEnvironmentFromApi(); - if (!this.environmentPromise) { - this.environmentPromise = request.then(res => { - this.environment = res; - }); - await this.environmentPromise; - } else { - this.environment = await request; - } - if (this.environment.identityOverrides?.length) { + const environment = await this.getEnvironmentFromApi(); + this.environment = environment; + if (environment.identityOverrides?.length) { this.identitiesWithOverridesByIdentifier = new Map( - this.environment.identityOverrides.map(identity => [ - identity.identifier, - identity - ]) + environment.identityOverrides.map(identity => [identity.identifier, identity]) ); } - if (this.onEnvironmentChange) { - this.onEnvironmentChange(null, this.environment); + deferred.resolve(environment); + return deferred.promise; + } catch (error) { + deferred.reject(error); + throw error; + } finally { + this.environmentPromise = undefined; + } + } + + /** + * Fetch the latest environment state from the Flagsmith API to use for local flag evaluation. + * + * If the environment is currently being fetched, calling this method will not cause additional fetches. + */ + async updateEnvironment(): Promise { + try { + if (this.environmentPromise) { + await this.environmentPromise + return } + const environment = await this.fetchEnvironment(); + this.onEnvironmentChange(null, environment); } catch (e) { - if (this.onEnvironmentChange) { - this.onEnvironmentChange(e as Error, this.environment); - } + this.logger.error(e, 'updateEnvironment failed'); + this.onEnvironmentChange(e as Error); } } @@ -350,7 +353,25 @@ export class Flagsmith { /** * This promise ensures that the environment is retrieved before attempting to locally evaluate. */ - private environmentPromise: Promise | undefined; + private environmentPromise?: Promise; + + /** + * Returns the current environment, fetching it from the API if needed. + * + * Calling this method concurrently while the environment is being fetched will not cause additional requests. + */ + async getEnvironment(): Promise { + if (this.offlineHandler) { + return this.offlineHandler.getEnvironment(); + } + if (this.environment) { + return this.environment; + } + if (!this.environmentPromise) { + this.environmentPromise = this.fetchEnvironment(); + } + return this.environmentPromise; + } private async getEnvironmentFromApi() { if (!this.environmentUrl) { @@ -360,9 +381,9 @@ export class Flagsmith { return buildEnvironmentModel(environment_data); } - private async getEnvironmentFlagsFromDocument(): Promise { + private async getEnvironmentFlagsFromDocument(environment: EnvironmentModel): Promise { const flags = Flags.fromFeatureStateModels({ - featureStates: getEnvironmentFeatureStates(this.environment), + featureStates: getEnvironmentFeatureStates(environment), analyticsProcessor: this.analyticsProcessor, defaultFlagHandler: this.defaultFlagHandler }); @@ -373,10 +394,12 @@ export class Flagsmith { } private async getIdentityFlagsFromDocument( + environment: EnvironmentModel, identifier: string, traits: { [key: string]: any } ): Promise { - const identityModel = this.getIdentityModel( + const identityModel = await this.getIdentityModel( + environment, identifier, Object.keys(traits).map(key => ({ key, @@ -384,7 +407,11 @@ export class Flagsmith { })) ); - const featureStates = getIdentityFeatureStates(this.environment, identityModel); + let featureStates: FeatureStateModel[] = []; + try { + const environment = await this.getEnvironment(); + featureStates = getIdentityFeatureStates(environment, identityModel); + } catch {} const flags = Flags.fromFeatureStateModels({ featureStates: featureStates, @@ -417,7 +444,8 @@ export class Flagsmith { return flags; } catch (e) { if (this.offlineHandler) { - return this.getEnvironmentFlagsFromDocument(); + const environment = this.offlineHandler.getEnvironment(); + return this.getEnvironmentFlagsFromDocument(environment); } if (this.defaultFlagHandler) { return new Flags({ @@ -438,41 +466,38 @@ export class Flagsmith { if (!this.identitiesUrl) { throw new Error('`apiUrl` argument is missing or invalid.'); } - try { - const data = generateIdentitiesData(identifier, traits, transient); - const jsonResponse = await this.getJSONResponse(this.identitiesUrl, 'POST', data); - const flags = Flags.fromAPIFlags({ - apiFlags: jsonResponse['flags'], - analyticsProcessor: this.analyticsProcessor, - defaultFlagHandler: this.defaultFlagHandler - }); - if (!!this.cache) { - await this.cache.set(`flags-${identifier}`, flags); - } - return flags; - } catch (e) { - if (this.offlineHandler) { - return this.getIdentityFlagsFromDocument(identifier, traits); - } - if (this.defaultFlagHandler) { - return new Flags({ - flags: {}, - defaultFlagHandler: this.defaultFlagHandler - }); - } - - throw e; + const data = generateIdentitiesData(identifier, traits, transient); + const jsonResponse = await this.getJSONResponse(this.identitiesUrl, 'POST', data); + const flags = Flags.fromAPIFlags({ + apiFlags: jsonResponse['flags'], + analyticsProcessor: this.analyticsProcessor, + defaultFlagHandler: this.defaultFlagHandler + }); + if (!!this.cache) { + await this.cache.set(`flags-${identifier}`, flags); } + return flags; } - private getIdentityModel(identifier: string, traits: { key: string; value: any }[]) { - const traitModels = traits.map(trait => new TraitModel(trait.key, trait.value)); - let identityWithOverrides = this.identitiesWithOverridesByIdentifier?.get(identifier); - if (identityWithOverrides) { - identityWithOverrides.updateTraits(traitModels); - return identityWithOverrides; - } - return new IdentityModel('0', traitModels, [], this.environment.apiKey, identifier); + private async getIdentityModel( + environment: EnvironmentModel, + identifier: string, + traits: { key: string; value: any }[] + ) { + return this.getEnvironment() + .then(environment => { + const traitModels = traits.map(trait => new TraitModel(trait.key, trait.value)); + let identityWithOverrides = + this.identitiesWithOverridesByIdentifier?.get(identifier); + if (identityWithOverrides) { + identityWithOverrides.updateTraits(traitModels); + return identityWithOverrides; + } + return new IdentityModel('0', traitModels, [], environment.apiKey, identifier); + }) + .catch(() => { + return new IdentityModel('0', [], [], '', identifier); + }); } } diff --git a/sdk/polling_manager.ts b/sdk/polling_manager.ts index 414a191..c53193c 100644 --- a/sdk/polling_manager.ts +++ b/sdk/polling_manager.ts @@ -1,20 +1,27 @@ import Flagsmith from './index.js'; +import { Logger } from 'pino'; export class EnvironmentDataPollingManager { private interval?: NodeJS.Timeout; private main: Flagsmith; private refreshIntervalSeconds: number; + private logger: Logger; - constructor(main: Flagsmith, refreshIntervalSeconds: number) { + constructor(main: Flagsmith, refreshIntervalSeconds: number, logger: Logger) { this.main = main; this.refreshIntervalSeconds = refreshIntervalSeconds; + this.logger = logger; } start() { const updateEnvironment = () => { if (this.interval) clearInterval(this.interval); this.interval = setInterval(async () => { - await this.main.updateEnvironment(); + try { + await this.main.updateEnvironment(); + } catch (error) { + this.logger.error('failed to poll environment', error); + } }, this.refreshIntervalSeconds * 1000); }; updateEnvironment(); diff --git a/sdk/types.ts b/sdk/types.ts index 1e99c4e..a175526 100644 --- a/sdk/types.ts +++ b/sdk/types.ts @@ -40,7 +40,7 @@ export interface FlagsmithConfig { enableAnalytics?: boolean; defaultFlagHandler?: (featureName: string) => DefaultFlag; cache?: FlagsmithCache; - onEnvironmentChange?: (error: Error | null, result: EnvironmentModel) => void; + onEnvironmentChange?: (error: Error | null, result?: EnvironmentModel) => void; logger?: Logger; offlineMode?: boolean; offlineHandler?: BaseOfflineHandler; diff --git a/sdk/utils.ts b/sdk/utils.ts index 5a4f262..779bf0c 100644 --- a/sdk/utils.ts +++ b/sdk/utils.ts @@ -48,23 +48,56 @@ export const retryFetch = ( timeoutMs: number = 10, // set an overall timeout for this function customFetch: Fetch, ): Promise => { - return new Promise((resolve, reject) => { - const retryWrapper = (n: number) => { - customFetch(url, { + const retryWrapper = async (n: number): Promise => { + try { + return await customFetch(url, { ...fetchOptions, signal: AbortSignal.timeout(timeoutMs) - }) - .then(res => resolve(res)) - .catch(async err => { - if (n > 0) { - await delay(1000); - retryWrapper(--n); - } else { - reject(err); - } }); - }; - - retryWrapper(retries); - }); + } catch (e) { + if (n > 0) { + await delay(1000); + return retryWrapper(--n); + } else { + throw e; + } + } + }; + return retryWrapper(retries); }; + +/** + * A deferred promise can be resolved or rejected outside its creation scope. + * + * @template T The type of the value that the deferred promise will resolve to. + * + * @example + * const deferred = new Deferred() + * + * // Pass the promise somewhere + * performAsyncOperation(deferred.promise) + * + * // Resolve it when ready from anywhere + * deferred.resolve("Operation completed") + * deferred.failed("Error") + */ +export class Deferred { + public readonly promise: Promise; + private resolvePromise!: (value: T | PromiseLike) => void; + private rejectPromise!: (reason?: unknown) => void; + + constructor(initial?: T) { + this.promise = new Promise((resolve, reject) => { + this.resolvePromise = resolve; + this.rejectPromise = reject; + }); + } + + public resolve(value: T | PromiseLike): void { + this.resolvePromise(value); + } + + public reject(reason?: unknown): void { + this.rejectPromise(reason); + } +} diff --git a/tests/sdk/flagsmith-cache.test.ts b/tests/sdk/flagsmith-cache.test.ts index f18df25..472b06b 100644 --- a/tests/sdk/flagsmith-cache.test.ts +++ b/tests/sdk/flagsmith-cache.test.ts @@ -61,14 +61,16 @@ test('test_get_environment_flags_uses_local_environment_when_available', async ( const cache = new TestCache(); const set = vi.spyOn(cache, 'set'); - const flg = flagsmith({ cache }); + const flg = flagsmith({ cache, enableLocalEvaluation: true }); const model = environmentModel(JSON.parse(environmentJSON)); - flg.environment = model; + const getEnvironment = vi.spyOn(flg, 'getEnvironment') + getEnvironment.mockResolvedValue(model) - const allFlags = await (await flg.getEnvironmentFlags()).allFlags(); + const allFlags = (await flg.getEnvironmentFlags()).allFlags(); expect(set).toBeCalled(); expect(fetch).toBeCalledTimes(0); + expect(getEnvironment).toBeCalledTimes(1); expect(allFlags[0].enabled).toBe(model.featureStates[0].enabled); expect(allFlags[0].value).toBe(model.featureStates[0].getValue()); expect(allFlags[0].featureName).toBe(model.featureStates[0].feature.name); diff --git a/tests/sdk/flagsmith-environment-flags.test.ts b/tests/sdk/flagsmith-environment-flags.test.ts index e6356f2..e3d4560 100644 --- a/tests/sdk/flagsmith-environment-flags.test.ts +++ b/tests/sdk/flagsmith-environment-flags.test.ts @@ -20,20 +20,6 @@ test('test_get_environment_flags_calls_api_when_no_local_environment', async () expect(allFlags[0].featureName).toBe('some_feature'); }); -test('test_get_environment_flags_uses_local_environment_when_available', async () => { - fetch.mockResolvedValue(new Response(flagsJSON)); - - const flg = flagsmith(); - const model = environmentModel(JSON.parse(environmentJSON)); - flg.environment = model; - - const allFlags = await (await flg.getEnvironmentFlags()).allFlags(); - expect(fetch).toBeCalledTimes(0); - expect(allFlags[0].enabled).toBe(model.featureStates[0].enabled); - expect(allFlags[0].value).toBe(model.featureStates[0].getValue()); - expect(allFlags[0].featureName).toBe(model.featureStates[0].feature.name); -}); - test('test_default_flag_is_used_when_no_environment_flags_returned', async () => { fetch.mockResolvedValue(new Response(JSON.stringify([]))); diff --git a/tests/sdk/flagsmith-identity-flags.test.ts b/tests/sdk/flagsmith-identity-flags.test.ts index 96e1999..4d4897b 100644 --- a/tests/sdk/flagsmith-identity-flags.test.ts +++ b/tests/sdk/flagsmith-identity-flags.test.ts @@ -125,6 +125,34 @@ test('test_default_flag_is_used_when_no_identity_flags_returned_and_no_custom_de expect(flag.enabled).toBe(false); }); +test("default flags are used when network is unavailable", async () => { + fetch.mockRejectedValue(new Error('API is unavailable')) + + const flg = flagsmith({ + environmentKey: 'key', + defaultFlagHandler: () => new DefaultFlag(undefined, true) + }); + + const flags = await flg.getIdentityFlags('identifier'); + const flag = flags.getFlag('some_feature'); + + expect(flag.isDefault).toBe(true); + expect(flag.enabled).toBe(true); +}); + +test("default flags are used when network is unavailable in local evaluation", async () => { + fetch.mockRejectedValue(new Error('API is unavailable')) + + const flg = flagsmith({ + environmentKey: 'ser.key', + enableLocalEvaluation: true, + defaultFlagHandler: () => new DefaultFlag(undefined, true) + }); + + const flags = await flg.getIdentityFlags('identifier'); + expect(flags.isFeatureEnabled('some_feature')).toBe(true); +}); + test('test_get_identity_flags_multivariate_value_with_local_evaluation_enabled', async () => { fetch.mockResolvedValue(new Response(environmentJSON)); @@ -170,10 +198,10 @@ test('test_transient_identity', async () => { test('test_identity_with_transient_traits', async () => { fetch.mockResolvedValue(new Response(identityWithTransientTraitsJSON)); const identifier = 'transient_trait_identifier'; - const traits = { + const traits = { some_trait: 'some_value', another_trait: {value: 'another_value', transient: true}, - explicitly_non_transient_trait: {value: 'non_transient_value', transient: false} + explicitly_non_transient_trait: {value: 'non_transient_value', transient: false} } const traitsInRequest = [ { diff --git a/tests/sdk/flagsmith.test.ts b/tests/sdk/flagsmith.test.ts index 4e87531..87e58f4 100644 --- a/tests/sdk/flagsmith.test.ts +++ b/tests/sdk/flagsmith.test.ts @@ -34,12 +34,8 @@ test('test_flagsmith_local_evaluation_key_required', () => { test('test_update_environment_sets_environment', async () => { fetch.mockResolvedValue(new Response(environmentJSON)); const flg = flagsmith(); - await flg.updateEnvironment(); - expect(flg.environment).toBeDefined(); - const model = environmentModel(JSON.parse(environmentJSON)); - - expect(flg.environment).toStrictEqual(model); + expect(await flg.getEnvironment()).toStrictEqual(model); }); test('test_set_agent_options', async () => { @@ -58,7 +54,6 @@ test('test_set_agent_options', async () => { }); await flg.updateEnvironment(); - expect(flg.environment).toBeDefined(); }); test('test_get_identity_segments', async () => { @@ -132,24 +127,27 @@ test('test_fetch_recovers_after_single_API_error', async () => { expect(flag.value).toBe('some-value'); }); -test('test_default_flag_used_after_multiple_API_errors', async () => { - fetch - .mockRejectedValue(new Error('Error during fetching the API response')); - const defaultFlag = new DefaultFlag('some-default-value', true); - - const defaultFlagHandler = (featureName: string) => defaultFlag; - - const flg = new Flagsmith({ - environmentKey: 'key', - defaultFlagHandler: defaultFlagHandler - }); - - const flags = await flg.getEnvironmentFlags(); - const flag = flags.getFlag('some_feature'); - expect(flag.isDefault).toBe(true); - expect(flag.enabled).toBe(defaultFlag.enabled); - expect(flag.value).toBe(defaultFlag.value); -}); +test.each([ + [false, 'key'], + [true, 'ser.key'] +])( + 'default flag handler is used when API is unavailable (local evaluation = %s)', + async (enableLocalEvaluation, environmentKey) => { + fetch.mockRejectedValue(new Error('API is unavailable')) + try { + const flg = flagsmith({ + enableLocalEvaluation, + environmentKey, + defaultFlagHandler: () => new DefaultFlag('some-default-value', true) + }); + const flags = await flg.getEnvironmentFlags(); + const flag = flags.getFlag('some_feature'); + expect(flag.isDefault).toBe(true); + expect(flag.enabled).toBe(true); + expect(flag.value).toBe('some-default-value'); + } catch {} + } +); test('default flag handler used when timeout occurs', async () => { fetch.mockImplementation(async (...args) => { @@ -196,34 +194,36 @@ test('test_throws_when_no_identityFlags_returned_due_to_error', async () => { }); test('test onEnvironmentChange is called when provided', async () => { - const callback = { - callback: (e: Error | null, result: EnvironmentModel) => { } - }; - const callbackSpy = vi.spyOn(callback, 'callback'); + const callback = vi.fn() const flg = new Flagsmith({ environmentKey: 'ser.key', enableLocalEvaluation: true, - onEnvironmentChange: callback.callback, + onEnvironmentChange: callback, }); - await delay(200); + fetch.mockRejectedValueOnce(new Error('API error')); + await flg.updateEnvironment().catch(() => { + // Expected rejection + }); - expect(callbackSpy).toBeCalled(); + expect(callback).toBeCalled(); }); test('test onEnvironmentChange is called after error', async () => { - const callback = vi.fn((e, result) => {}) - - const flg = new Flagsmith({ - environmentKey: 'ser.key', - enableLocalEvaluation: true, - onEnvironmentChange: callback, - }); - - await delay(200); - - expect(callback).toBeCalled(); + try { + fetch.mockRejectedValue(new Error('API error')) + const callback = vi.fn(); + const flg = new Flagsmith({ + environmentKey: 'ser.key', + enableLocalEvaluation: true, + onEnvironmentChange: callback, + }); + await flg.updateEnvironment(); + expect(callback).toHaveBeenCalled(); + } catch(e) { + console.log("????") + } }); test('getIdentityFlags throws error if identifier is empty string', async () => { @@ -234,15 +234,15 @@ test('getIdentityFlags throws error if identifier is empty string', async () => await expect(flg.getIdentityFlags('')).rejects.toThrow('`identifier` argument is missing or invalid.'); }) - -test('getIdentitySegments throws error if identifier is empty string', () => { +test('getIdentitySegments throws error if identifier is empty string', async () => { const flg = flagsmith({ environmentKey: 'key', }); - expect(() => { flg.getIdentitySegments(''); }).toThrow('`identifier` argument is missing or invalid.'); -}) - + await expect(flg.getIdentitySegments('')).rejects.toThrow( + '`identifier` argument is missing or invalid.' + ); +}); test('offline_mode', async () => { // Given @@ -286,6 +286,7 @@ test('test_flagsmith_uses_offline_handler_if_set_and_no_api_response', async () environmentKey: 'some-key', apiUrl: api_url, offlineHandler: mock_offline_handler, + offlineMode: true }); vi.spyOn(flg, 'getEnvironmentFlags'); @@ -305,10 +306,10 @@ test('test_flagsmith_uses_offline_handler_if_set_and_no_api_response', async () // When const environmentFlags: Flags = await flg.getEnvironmentFlags(); + expect(mock_offline_handler.getEnvironment).toHaveBeenCalledTimes(1); const identityFlags: Flags = await flg.getIdentityFlags('identity', {}); // Then - expect(mock_offline_handler.getEnvironment).toHaveBeenCalledTimes(1); expect(flg.getEnvironmentFlags).toHaveBeenCalled(); expect(flg.getIdentityFlags).toHaveBeenCalled(); @@ -347,11 +348,31 @@ function sleep(ms: number) { } test('test_localEvaluation_true__identity_overrides_evaluated', async () => { fetch.mockResolvedValue(new Response(environmentJSON)); + const flg = flagsmith({ + environmentKey: 'ser.key', + enableLocalEvaluation: true + }); + + const flags = await flg.getIdentityFlags('overridden-id'); + expect(flags.getFeatureValue('some_feature')).toEqual('some-overridden-value'); +}); + +test('getIdentityFlags succeeds if initial fetch failed then succeeded', async () => { + const defaultFlagHandler = vi.fn(() => new DefaultFlag('mock-default-value', false)); + + fetch.mockRejectedValueOnce(new Error('Initial API error')); const flg = flagsmith({ environmentKey: 'ser.key', enableLocalEvaluation: true, + defaultFlagHandler }); - const flags = await flg.getIdentityFlags("overridden-id"); - expect(flags.getFeatureValue("some_feature")).toEqual("some-overridden-value"); + const defaultFlags = await flg.getIdentityFlags('test-user'); + expect(defaultFlags.isFeatureEnabled('some_feature')).toBe(false); + expect(defaultFlagHandler).toHaveBeenCalled(); + + fetch.mockResolvedValue(new Response(environmentJSON)); + await flg.getEnvironment(); + const flags2 = await flg.getIdentityFlags('test-user'); + expect(flags2.isFeatureEnabled('some_feature')).toBe(true); }); diff --git a/tests/sdk/utils.ts b/tests/sdk/utils.ts index b02d38b..e3b139c 100644 --- a/tests/sdk/utils.ts +++ b/tests/sdk/utils.ts @@ -1,7 +1,7 @@ import { readFileSync } from 'fs'; import { buildEnvironmentModel } from '../../flagsmith-engine/environments/util.js'; import { AnalyticsProcessor } from '../../sdk/analytics.js'; -import Flagsmith from '../../sdk/index.js'; +import Flagsmith, {FlagsmithConfig} from '../../sdk/index.js'; import { FlagsmithCache } from '../../sdk/types.js'; import { Flags } from '../../sdk/models.js'; @@ -33,7 +33,7 @@ export function apiKey(): string { return 'sometestfakekey'; } -export function flagsmith(params = {}) { +export function flagsmith(params: FlagsmithConfig = {}) { return new Flagsmith({ environmentKey: apiKey(), fetch, From c596f07c8cd39c951145b7b246cb5deab1148fdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Thu, 20 Mar 2025 14:08:17 -0300 Subject: [PATCH 02/27] throw on getIdentityFlags if failed with no default flags --- sdk/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sdk/index.ts b/sdk/index.ts index eb58ac3..1f7a772 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -223,6 +223,9 @@ export class Flagsmith { } return await this.getIdentityFlagsFromApi(identifier, traits, transient); } catch (error) { + if (!this.defaultFlagHandler) { + throw new Error('getIdentityFlags failed and no default flag handler was provided') + } this.logger.error(error, 'getIdentityFlags failed'); return new Flags({ flags: {}, From 8bc1a3166f8dc51d2c49ac472c452a46c3df6607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Thu, 20 Mar 2025 17:28:31 -0300 Subject: [PATCH 03/27] enable restoreMocks for all tests --- vitest.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/vitest.config.ts b/vitest.config.ts index 3e68f84..bbf8292 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,6 +3,7 @@ import { defineConfig } from 'vitest/config' export default defineConfig({ test: { globals: true, + restoreMocks: true, coverage: { reporter: ['text'], exclude: [ From bc823c46a0fba9f915177b59315524ebdaf04f78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Thu, 20 Mar 2025 17:30:34 -0300 Subject: [PATCH 04/27] fix tests --- sdk/index.ts | 3 +- sdk/utils.ts | 2 +- tests/sdk/flagsmith.test.ts | 65 +++++++++++++++++++------------------ tests/sdk/utils.ts | 15 +++++++-- 4 files changed, 48 insertions(+), 37 deletions(-) diff --git a/sdk/index.ts b/sdk/index.ts index 1f7a772..eeb43de 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -286,7 +286,7 @@ export class Flagsmith { return deferred.promise; } catch (error) { deferred.reject(error); - throw error; + return deferred.promise; } finally { this.environmentPromise = undefined; } @@ -412,7 +412,6 @@ export class Flagsmith { let featureStates: FeatureStateModel[] = []; try { - const environment = await this.getEnvironment(); featureStates = getIdentityFeatureStates(environment, identityModel); } catch {} diff --git a/sdk/utils.ts b/sdk/utils.ts index 779bf0c..6022439 100644 --- a/sdk/utils.ts +++ b/sdk/utils.ts @@ -57,7 +57,7 @@ export const retryFetch = ( } catch (e) { if (n > 0) { await delay(1000); - return retryWrapper(--n); + return await retryWrapper(n - 1); } else { throw e; } diff --git a/tests/sdk/flagsmith.test.ts b/tests/sdk/flagsmith.test.ts index 87e58f4..2a09573 100644 --- a/tests/sdk/flagsmith.test.ts +++ b/tests/sdk/flagsmith.test.ts @@ -1,6 +1,14 @@ import Flagsmith from '../../sdk/index.js'; import { EnvironmentDataPollingManager } from '../../sdk/polling_manager.js'; -import { environmentJSON, environmentModel, flagsJSON, flagsmith, fetch, offlineEnvironmentJSON } from './utils.js'; +import { + environmentJSON, + environmentModel, + flagsJSON, + flagsmith, + fetch, + offlineEnvironmentJSON, + badFetch +} from './utils.js'; import { DefaultFlag, Flags } from '../../sdk/models.js'; import { delay } from '../../sdk/utils.js'; import { EnvironmentModel } from '../../flagsmith-engine/environments/models.js'; @@ -133,19 +141,17 @@ test.each([ ])( 'default flag handler is used when API is unavailable (local evaluation = %s)', async (enableLocalEvaluation, environmentKey) => { - fetch.mockRejectedValue(new Error('API is unavailable')) - try { - const flg = flagsmith({ - enableLocalEvaluation, - environmentKey, - defaultFlagHandler: () => new DefaultFlag('some-default-value', true) - }); - const flags = await flg.getEnvironmentFlags(); - const flag = flags.getFlag('some_feature'); - expect(flag.isDefault).toBe(true); - expect(flag.enabled).toBe(true); - expect(flag.value).toBe('some-default-value'); - } catch {} + const flg = flagsmith({ + enableLocalEvaluation, + environmentKey, + defaultFlagHandler: () => new DefaultFlag('some-default-value', true), + fetch: badFetch, + }); + const flags = await flg.getEnvironmentFlags(); + const flag = flags.getFlag('some_feature'); + expect(flag.isDefault).toBe(true); + expect(flag.enabled).toBe(true); + expect(flag.value).toBe('some-default-value'); } ); @@ -182,10 +188,9 @@ test('request timeout uses default if not provided', async () => { }) test('test_throws_when_no_identityFlags_returned_due_to_error', async () => { - fetch.mockResolvedValue(new Response('bad data')); - const flg = new Flagsmith({ environmentKey: 'key', + fetch: badFetch, }); await expect(async () => await flg.getIdentityFlags('identifier')) @@ -211,19 +216,15 @@ test('test onEnvironmentChange is called when provided', async () => { }); test('test onEnvironmentChange is called after error', async () => { - try { - fetch.mockRejectedValue(new Error('API error')) - const callback = vi.fn(); - const flg = new Flagsmith({ - environmentKey: 'ser.key', - enableLocalEvaluation: true, - onEnvironmentChange: callback, - }); - await flg.updateEnvironment(); - expect(callback).toHaveBeenCalled(); - } catch(e) { - console.log("????") - } + const callback = vi.fn(); + const flg = new Flagsmith({ + environmentKey: 'ser.key', + enableLocalEvaluation: true, + onEnvironmentChange: callback, + fetch: badFetch, + }); + await flg.updateEnvironment(); + expect(callback).toHaveBeenCalled(); }); test('getIdentityFlags throws error if identifier is empty string', async () => { @@ -347,18 +348,18 @@ function sleep(ms: number) { }); } test('test_localEvaluation_true__identity_overrides_evaluated', async () => { - fetch.mockResolvedValue(new Response(environmentJSON)); const flg = flagsmith({ environmentKey: 'ser.key', enableLocalEvaluation: true }); + await flg.updateEnvironment() const flags = await flg.getIdentityFlags('overridden-id'); expect(flags.getFeatureValue('some_feature')).toEqual('some-overridden-value'); }); test('getIdentityFlags succeeds if initial fetch failed then succeeded', async () => { - const defaultFlagHandler = vi.fn(() => new DefaultFlag('mock-default-value', false)); + const defaultFlagHandler = vi.fn(() => new DefaultFlag('mock-default-value', true)); fetch.mockRejectedValueOnce(new Error('Initial API error')); const flg = flagsmith({ @@ -368,7 +369,7 @@ test('getIdentityFlags succeeds if initial fetch failed then succeeded', async ( }); const defaultFlags = await flg.getIdentityFlags('test-user'); - expect(defaultFlags.isFeatureEnabled('some_feature')).toBe(false); + expect(defaultFlags.isFeatureEnabled('mock-default-value')).toBe(true); expect(defaultFlagHandler).toHaveBeenCalled(); fetch.mockResolvedValue(new Response(environmentJSON)); diff --git a/tests/sdk/utils.ts b/tests/sdk/utils.ts index e3b139c..d3f2f69 100644 --- a/tests/sdk/utils.ts +++ b/tests/sdk/utils.ts @@ -2,7 +2,7 @@ import { readFileSync } from 'fs'; import { buildEnvironmentModel } from '../../flagsmith-engine/environments/util.js'; import { AnalyticsProcessor } from '../../sdk/analytics.js'; import Flagsmith, {FlagsmithConfig} from '../../sdk/index.js'; -import { FlagsmithCache } from '../../sdk/types.js'; +import { Fetch, FlagsmithCache } from '../../sdk/types.js'; import { Flags } from '../../sdk/models.js'; const DATA_DIR = __dirname + '/data/'; @@ -19,7 +19,18 @@ export class TestCache implements FlagsmithCache { } } -export const fetch = vi.fn(global.fetch) +export const fetch = vi.fn((_, options) => { + const headers = options?.headers as Record; + if (!headers) throw new Error('missing request headers') + const env = headers['X-Environment-Key']; + if (!env) return Promise.resolve(new Response("missing x-environment-key header", { status: 404 })) + if (env.startsWith('ser.')) { + return Promise.resolve(new Response(environmentJSON, { status: 200 })) + } + return Promise.resolve(new Response(identitiesJSON, { status: 200 })) +}); + +export const badFetch: Fetch = () => { throw new Error('fetch failed')} export function analyticsProcessor() { return new AnalyticsProcessor({ From 612f7712bf44306089f05f8f8ae4fe3b9c417f01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Thu, 20 Mar 2025 17:37:48 -0300 Subject: [PATCH 05/27] fail if using local eval without a server-side key --- sdk/index.ts | 4 +--- tests/sdk/flagsmith-cache.test.ts | 2 +- tests/sdk/flagsmith.test.ts | 12 ++++++------ 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/sdk/index.ts b/sdk/index.ts index eeb43de..420806e 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -142,9 +142,7 @@ export class Flagsmith { if (this.enableLocalEvaluation) { if (!this.environmentKey.startsWith('ser.')) { - console.error( - 'In order to use local evaluation, please generate a server key in the environment settings page.' - ); + throw new Error('Using local evaluation requires a server-side environment key'); } if (this.environmentRefreshIntervalSeconds > 0){ this.environmentDataPollingManager = new EnvironmentDataPollingManager( diff --git a/tests/sdk/flagsmith-cache.test.ts b/tests/sdk/flagsmith-cache.test.ts index 472b06b..12c3f55 100644 --- a/tests/sdk/flagsmith-cache.test.ts +++ b/tests/sdk/flagsmith-cache.test.ts @@ -61,7 +61,7 @@ test('test_get_environment_flags_uses_local_environment_when_available', async ( const cache = new TestCache(); const set = vi.spyOn(cache, 'set'); - const flg = flagsmith({ cache, enableLocalEvaluation: true }); + const flg = flagsmith({ cache, environmentKey: 'ser.key', enableLocalEvaluation: true }); const model = environmentModel(JSON.parse(environmentJSON)); const getEnvironment = vi.spyOn(flg, 'getEnvironment') getEnvironment.mockResolvedValue(model) diff --git a/tests/sdk/flagsmith.test.ts b/tests/sdk/flagsmith.test.ts index 2a09573..bb96d32 100644 --- a/tests/sdk/flagsmith.test.ts +++ b/tests/sdk/flagsmith.test.ts @@ -31,12 +31,12 @@ test('test_flagsmith_starts_polling_manager_on_init_if_enabled', () => { test('test_flagsmith_local_evaluation_key_required', () => { fetch.mockResolvedValue(new Response(environmentJSON)); - console.error = vi.fn(); - new Flagsmith({ - environmentKey: 'bad.key', - enableLocalEvaluation: true - }); - expect(console.error).toBeCalled(); + expect(() => { + new Flagsmith({ + environmentKey: 'bad.key', + enableLocalEvaluation: true + }); + }).toThrow('Using local evaluation requires a server-side environment key') }); test('test_update_environment_sets_environment', async () => { From 5c08a0ab72fbb522d7dbf2ab4984e2092c21c38b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Thu, 20 Mar 2025 17:48:23 -0300 Subject: [PATCH 06/27] better --- tests/sdk/flagsmith.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sdk/flagsmith.test.ts b/tests/sdk/flagsmith.test.ts index bb96d32..da8eab0 100644 --- a/tests/sdk/flagsmith.test.ts +++ b/tests/sdk/flagsmith.test.ts @@ -361,7 +361,7 @@ test('test_localEvaluation_true__identity_overrides_evaluated', async () => { test('getIdentityFlags succeeds if initial fetch failed then succeeded', async () => { const defaultFlagHandler = vi.fn(() => new DefaultFlag('mock-default-value', true)); - fetch.mockRejectedValueOnce(new Error('Initial API error')); + fetch.mockRejectedValue(new Error('Initial API error')); const flg = flagsmith({ environmentKey: 'ser.key', enableLocalEvaluation: true, From e13b04b6c834ded7576f5ad13c273255dc0f6e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Thu, 20 Mar 2025 17:51:53 -0300 Subject: [PATCH 07/27] use logger --- sdk/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/index.ts b/sdk/index.ts index 420806e..821d15e 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -251,7 +251,7 @@ export class Flagsmith { throw new Error('`identifier` argument is missing or invalid.'); } if (!this.enableLocalEvaluation) { - console.error('This function is only permitted with local evaluation.'); + this.logger.error('This function is only permitted with local evaluation.'); return Promise.resolve([]); } From 8c2b4eea07965bf0c696b8fbbcfc556754e8c042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Thu, 20 Mar 2025 17:51:58 -0300 Subject: [PATCH 08/27] error handling --- sdk/index.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sdk/index.ts b/sdk/index.ts index 821d15e..df219d0 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -408,10 +408,7 @@ export class Flagsmith { })) ); - let featureStates: FeatureStateModel[] = []; - try { - featureStates = getIdentityFeatureStates(environment, identityModel); - } catch {} + const featureStates = getIdentityFeatureStates(environment, identityModel); const flags = Flags.fromFeatureStateModels({ featureStates: featureStates, From dde28b77167c8b4cd3decc518cbac3ae0b3fb1f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Thu, 20 Mar 2025 17:56:35 -0300 Subject: [PATCH 09/27] refactor --- sdk/index.ts | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/sdk/index.ts b/sdk/index.ts index df219d0..eb8eedd 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -257,7 +257,7 @@ export class Flagsmith { traits = traits || {}; const environment = await this.getEnvironment(); - const identityModel = await this.getIdentityModel( + const identityModel = this.getIdentityModel( environment, identifier, Object.keys(traits || {}).map(key => ({ @@ -399,7 +399,7 @@ export class Flagsmith { identifier: string, traits: { [key: string]: any } ): Promise { - const identityModel = await this.getIdentityModel( + const identityModel = this.getIdentityModel( environment, identifier, Object.keys(traits).map(key => ({ @@ -476,25 +476,19 @@ export class Flagsmith { return flags; } - private async getIdentityModel( + private getIdentityModel( environment: EnvironmentModel, identifier: string, traits: { key: string; value: any }[] ) { - return this.getEnvironment() - .then(environment => { - const traitModels = traits.map(trait => new TraitModel(trait.key, trait.value)); - let identityWithOverrides = - this.identitiesWithOverridesByIdentifier?.get(identifier); - if (identityWithOverrides) { - identityWithOverrides.updateTraits(traitModels); - return identityWithOverrides; - } - return new IdentityModel('0', traitModels, [], environment.apiKey, identifier); - }) - .catch(() => { - return new IdentityModel('0', [], [], '', identifier); - }); + const traitModels = traits.map(trait => new TraitModel(trait.key, trait.value)); + let identityWithOverrides = + this.identitiesWithOverridesByIdentifier?.get(identifier); + if (identityWithOverrides) { + identityWithOverrides.updateTraits(traitModels); + return identityWithOverrides; + } + return new IdentityModel('0', traitModels, [], environment.apiKey, identifier); } } From 5f277e06cf6ca6bfffb116a138bb4ae3054b2a2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Thu, 20 Mar 2025 17:56:55 -0300 Subject: [PATCH 10/27] remove duplicate test --- tests/sdk/flagsmith-identity-flags.test.ts | 29 ---------------------- 1 file changed, 29 deletions(-) diff --git a/tests/sdk/flagsmith-identity-flags.test.ts b/tests/sdk/flagsmith-identity-flags.test.ts index 4d4897b..36f95b0 100644 --- a/tests/sdk/flagsmith-identity-flags.test.ts +++ b/tests/sdk/flagsmith-identity-flags.test.ts @@ -125,35 +125,6 @@ test('test_default_flag_is_used_when_no_identity_flags_returned_and_no_custom_de expect(flag.enabled).toBe(false); }); -test("default flags are used when network is unavailable", async () => { - fetch.mockRejectedValue(new Error('API is unavailable')) - - const flg = flagsmith({ - environmentKey: 'key', - defaultFlagHandler: () => new DefaultFlag(undefined, true) - }); - - const flags = await flg.getIdentityFlags('identifier'); - const flag = flags.getFlag('some_feature'); - - expect(flag.isDefault).toBe(true); - expect(flag.enabled).toBe(true); -}); - -test("default flags are used when network is unavailable in local evaluation", async () => { - fetch.mockRejectedValue(new Error('API is unavailable')) - - const flg = flagsmith({ - environmentKey: 'ser.key', - enableLocalEvaluation: true, - defaultFlagHandler: () => new DefaultFlag(undefined, true) - }); - - const flags = await flg.getIdentityFlags('identifier'); - expect(flags.isFeatureEnabled('some_feature')).toBe(true); -}); - - test('test_get_identity_flags_multivariate_value_with_local_evaluation_enabled', async () => { fetch.mockResolvedValue(new Response(environmentJSON)); const identifier = 'identifier'; From 7c5467ec63298d192d5428e9db4a2da7e8615525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Thu, 20 Mar 2025 18:00:55 -0300 Subject: [PATCH 11/27] add getIdentityFlags failure test --- tests/sdk/flagsmith-identity-flags.test.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/sdk/flagsmith-identity-flags.test.ts b/tests/sdk/flagsmith-identity-flags.test.ts index 36f95b0..311bbfc 100644 --- a/tests/sdk/flagsmith-identity-flags.test.ts +++ b/tests/sdk/flagsmith-identity-flags.test.ts @@ -1,5 +1,13 @@ import Flagsmith from '../../sdk/index.js'; -import { fetch, environmentJSON, flagsmith, identitiesJSON, identityWithTransientTraitsJSON, transientIdentityJSON } from './utils.js'; +import { + fetch, + environmentJSON, + flagsmith, + identitiesJSON, + identityWithTransientTraitsJSON, + transientIdentityJSON, + badFetch +} from './utils.js'; import { DefaultFlag } from '../../sdk/models.js'; vi.mock('../../sdk/polling_manager'); @@ -205,3 +213,12 @@ test('test_identity_with_transient_traits', async () => { expect(identityFlags[0].value).toBe('some-identity-with-transient-trait-value'); expect(identityFlags[0].featureName).toBe('some_feature'); }); + +test('getIdentityFlags fails if API call failed and no default flag handler was provided', async () => { + const flg = flagsmith({ + fetch: badFetch, + }) + expect(flg.getIdentityFlags('user')) + .rejects + .toThrow('getIdentityFlags failed and no default flag handler was provided') +}) From 05b221f9e20396a3ce08da40487756c9fadbab55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Thu, 20 Mar 2025 19:06:05 -0300 Subject: [PATCH 12/27] log --- sdk/polling_manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/polling_manager.ts b/sdk/polling_manager.ts index c53193c..d01e7de 100644 --- a/sdk/polling_manager.ts +++ b/sdk/polling_manager.ts @@ -20,7 +20,7 @@ export class EnvironmentDataPollingManager { try { await this.main.updateEnvironment(); } catch (error) { - this.logger.error('failed to poll environment', error); + this.logger.error(error, 'failed to poll environment'); } }, this.refreshIntervalSeconds * 1000); }; From d69d9a62db279efb9e749b86771858e1a705ac32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Thu, 20 Mar 2025 19:49:02 -0300 Subject: [PATCH 13/27] fix warning --- tests/sdk/flagsmith-identity-flags.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sdk/flagsmith-identity-flags.test.ts b/tests/sdk/flagsmith-identity-flags.test.ts index 311bbfc..8f91ec0 100644 --- a/tests/sdk/flagsmith-identity-flags.test.ts +++ b/tests/sdk/flagsmith-identity-flags.test.ts @@ -218,7 +218,7 @@ test('getIdentityFlags fails if API call failed and no default flag handler was const flg = flagsmith({ fetch: badFetch, }) - expect(flg.getIdentityFlags('user')) + await expect(flg.getIdentityFlags('user')) .rejects .toThrow('getIdentityFlags failed and no default flag handler was provided') }) From 09c2ffa4d2e592cbd3bb338b9976e24ef996586d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Thu, 20 Mar 2025 20:42:28 -0300 Subject: [PATCH 14/27] Make tests fast + add retrydelay + TSdocs --- sdk/analytics.ts | 10 +++--- sdk/index.ts | 56 +++++++++++------------------ sdk/types.ts | 71 +++++++++++++++++++++++++++++++++++-- sdk/utils.ts | 3 +- tests/sdk/flagsmith.test.ts | 11 ++---- tests/sdk/utils.ts | 2 ++ 6 files changed, 101 insertions(+), 52 deletions(-) diff --git a/sdk/analytics.ts b/sdk/analytics.ts index 518bd2a..259c3e9 100644 --- a/sdk/analytics.ts +++ b/sdk/analytics.ts @@ -1,6 +1,6 @@ import { pino, Logger } from 'pino'; import { Fetch } from "./types.js"; -import { Flags } from "./models.js"; +import { FlagsmithConfig } from "./types.js"; export const ANALYTICS_ENDPOINT = './analytics/flags/'; @@ -21,17 +21,17 @@ export interface AnalyticsProcessorOptions { logger?: Logger; /** Custom {@link fetch} implementation to use for API requests. **/ fetch?: Fetch - + /** @deprecated Use {@link analyticsUrl} instead. **/ baseApiUrl?: string; } /** * Tracks how often individual features are evaluated whenever {@link trackFeature} is called. - * + * * Analytics data is posted after {@link trackFeature} is called and at least {@link ANALYTICS_TIMER} seconds have * passed since the previous analytics API request was made (if any), or by calling {@link flush}. - * + * * Data will stay in memory indefinitely until it can be successfully posted to the API. * @see https://docs.flagsmith.com/advanced-use/flag-analytics. */ @@ -89,7 +89,7 @@ export class AnalyticsProcessor { /** * Track a single evaluation event for a feature. * - * This method is called whenever {@link Flags.isFeatureEnabled}, {@link Flags.getFeatureValue} or {@link Flags.getFlag} are called. + * @see FlagsmithConfig.enableAnalytics */ trackFeature(featureName: string) { this.analyticsData[featureName] = (this.analyticsData[featureName] || 0) + 1; diff --git a/sdk/index.ts b/sdk/index.ts index eb8eedd..f0aa640 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -56,48 +56,30 @@ export class Flagsmith { private analyticsProcessor?: AnalyticsProcessor; private logger: Logger; private customFetch: Fetch; + private readonly requestRetryDelayMilliseconds: number; /** - * A Flagsmith client. + * A client for evaluating Flagsmith feature flags. * - * Provides an interface for interacting with the Flagsmith http API. - * Basic Usage:: + * Flags are evaluated remotely by the Flagsmith API over HTTP by default. + * To evaluate flags locally, use {@link enableLocalEvaluation}. * - * import flagsmith from Flagsmith - * const flagsmith = new Flagsmith({environmentKey: ''}); - * const environmentFlags = flagsmith.getEnvironmentFlags(); - * const featureEnabled = environmentFlags.isFeatureEnabled('foo'); - * const identityFlags = flagsmith.getIdentityFlags('identifier', {'foo': 'bar'}); - * const featureEnabledForIdentity = identityFlags.isFeatureEnabled("foo") + * @example + * import { Flagsmith, Flags, DefaultFlag } from 'flagsmith-nodejs' * - * @param {string} data.environmentKey: The environment key obtained from Flagsmith interface - * Required unless offlineMode is True. - @param {string} data.apiUrl: Override the URL of the Flagsmith API to communicate with - @param data.customHeaders: Additional headers to add to requests made to the - Flagsmith API - @param {number} data.requestTimeoutSeconds: Number of seconds to wait for a request to - complete before terminating the request - @param {boolean} data.enableLocalEvaluation: Enables local evaluation of flags - @param {number} data.environmentRefreshIntervalSeconds: If using local evaluation, - specify the interval period between refreshes of local environment data - @param {number} data.retries: a urllib3.Retry object to use on all http requests to the - Flagsmith API - @param {boolean} data.enableAnalytics: if enabled, sends additional requests to the Flagsmith - API to power flag analytics charts - @param data.defaultFlagHandler: callable which will be used in the case where - flags cannot be retrieved from the API or a non-existent feature is - requested - @param data.logger: an instance of the pino Logger class to use for logging - @param {boolean} data.offlineMode: sets the client into offline mode. Relies on offlineHandler for - evaluating flags. - @param {BaseOfflineHandler} data.offlineHandler: provide a handler for offline logic. Used to get environment - document from another source when in offlineMode. Works in place of - defaultFlagHandler if offlineMode is not set and using remote evaluation. + * const flagsmith = new Flagsmith({ + * environmentKey: '', + * defaultFlagHandler: (flagKey: string) => { new DefaultFlag(...) }, + * }); + * + * // Fetch the current environment flags + * const environmentFlags: Flags = flagsmith.getEnvironmentFlags() + * const isFooEnabled: boolean = environmentFlags.isFeatureEnabled('foo') + * + * // Evaluate flags for any identity + * const identityFlags: Flags = flagsmith.getIdentityFlags('my_user_123', {'vip': true}) + * const bannerVariation = identityFlags.getFeatureValue('banner_flag') */ constructor(data: FlagsmithConfig = {}) { - // if (!data.offlineMode && !data.environmentKey) { - // throw new Error('ValueError: environmentKey is required.'); - // } - this.agent = data.agent; this.customFetch = data.fetch ?? fetch; this.environmentKey = data.environmentKey; @@ -105,6 +87,7 @@ export class Flagsmith { this.customHeaders = data.customHeaders; this.requestTimeoutMs = 1000 * (data.requestTimeoutSeconds ?? DEFAULT_REQUEST_TIMEOUT_SECONDS); + this.requestRetryDelayMilliseconds = data.requestRetryDelayMilliseconds ?? 1000; this.enableLocalEvaluation = data.enableLocalEvaluation; this.environmentRefreshIntervalSeconds = data.environmentRefreshIntervalSeconds || this.environmentRefreshIntervalSeconds; @@ -339,6 +322,7 @@ export class Flagsmith { }, this.retries, this.requestTimeoutMs, + this.requestRetryDelayMilliseconds, this.customFetch, ); diff --git a/sdk/types.ts b/sdk/types.ts index a175526..0143a3c 100644 --- a/sdk/types.ts +++ b/sdk/types.ts @@ -28,21 +28,88 @@ export interface FlagsmithCache { export type Fetch = typeof fetch export interface FlagsmithConfig { + /** + * The environment's client-side or server-side key. + */ environmentKey?: string; + /** + * The Flagsmith API URL. Set this if you are not using Flagsmith's public SaaS service, i.e. https://app.flagsmith.com. + * + * @default https://edge.api.flagsmith.com/api/v1/ + */ apiUrl?: string; + /** + * A custom {@link Dispatcher} to use when making HTTP requests. + */ agent?: Dispatcher; + /** + * A custom {@link fetch} implementation to use when making HTTP requests. + */ fetch?: Fetch; - customHeaders?: { [key: string]: any }; + /** + * Custom headers to use in all HTTP requests. + */ + customHeaders?: HeadersInit + /** + * The network request timeout duration, in seconds. + * + * @default 10 + */ requestTimeoutSeconds?: number; + /** + * The amount of time, in milliseconds, to wait before retrying failed network requests. + */ + requestRetryDelayMilliseconds?: number; + /** + * If enabled, flags are evaluated locally using an environment definition cached in memory. + * + * The client will lazily fetch the environment from the Flagsmith API, and poll it every {@link environmentRefreshIntervalSeconds}. + */ enableLocalEvaluation?: boolean; + /** + * The time, in seconds, to wait before refreshing the cached environment definition. + * @default 60 + */ environmentRefreshIntervalSeconds?: number; + /** + * How many times to retry any failed network request before giving up. + * @default 3 + */ retries?: number; + /** + * If enabled, the client will keep track of any flags evaluated using {@link Flags.isFeatureEnabled}, + * {@link Flags.getFeatureValue} or {@link Flags.getFlag}, and periodically flush this data to the Flagsmith API. + */ enableAnalytics?: boolean; - defaultFlagHandler?: (featureName: string) => DefaultFlag; + /** + * Used to return fallback values for flags when evaluation fails for any reason. If not provided and flag + * evaluation fails, an error will be thrown intsead. + * + * @param flagKey The key of the flag that failed to evaluate. + * + * @example + * // All flags disabled and with no value by default + * const defaultHandler = () => new DefaultFlag(undefined, false) + * + * // Enable only VIP flags by default + * const vipDefaultHandler = (key: string) => new Default(undefined, key.startsWith('vip_')) + */ + defaultFlagHandler?: (flagKey: string) => DefaultFlag; cache?: FlagsmithCache; + /** + * A callback function to invoke whenever the cached environment is updated. + * @param error The error that occurred when the environment state failed to update, if any. + * @param result The updated environment state, if no error was thrown. + */ onEnvironmentChange?: (error: Error | null, result?: EnvironmentModel) => void; logger?: Logger; + /** + * If enabled, the client will work offline and not make any network requests. Requires {@link offlineHandler}. + */ offlineMode?: boolean; + /** + * If {@link offlineMode} is enabled, this handler is used to calculate the values of all flags. + */ offlineHandler?: BaseOfflineHandler; } diff --git a/sdk/utils.ts b/sdk/utils.ts index 6022439..612aef6 100644 --- a/sdk/utils.ts +++ b/sdk/utils.ts @@ -46,6 +46,7 @@ export const retryFetch = ( fetchOptions: RequestInit & { dispatcher?: Dispatcher }, retries: number = 3, timeoutMs: number = 10, // set an overall timeout for this function + retryDelayMs: number = 1000, customFetch: Fetch, ): Promise => { const retryWrapper = async (n: number): Promise => { @@ -56,7 +57,7 @@ export const retryFetch = ( }); } catch (e) { if (n > 0) { - await delay(1000); + await delay(retryDelayMs); return await retryWrapper(n - 1); } else { throw e; diff --git a/tests/sdk/flagsmith.test.ts b/tests/sdk/flagsmith.test.ts index da8eab0..5010c12 100644 --- a/tests/sdk/flagsmith.test.ts +++ b/tests/sdk/flagsmith.test.ts @@ -163,9 +163,9 @@ test('default flag handler used when timeout occurs', async () => { const defaultFlag = new DefaultFlag('some-default-value', true); - const defaultFlagHandler = (featureName: string) => defaultFlag; + const defaultFlagHandler = () => defaultFlag; - const flg = new Flagsmith({ + const flg = flagsmith({ environmentKey: 'key', defaultFlagHandler: defaultFlagHandler, requestTimeoutSeconds: 0.0001, @@ -188,7 +188,7 @@ test('request timeout uses default if not provided', async () => { }) test('test_throws_when_no_identityFlags_returned_due_to_error', async () => { - const flg = new Flagsmith({ + const flg = flagsmith({ environmentKey: 'key', fetch: badFetch, }); @@ -342,11 +342,6 @@ test('cannot create Flagsmith client in remote evaluation without API key', () = }); -function sleep(ms: number) { - return new Promise((resolve) => { - setTimeout(resolve, ms); - }); -} test('test_localEvaluation_true__identity_overrides_evaluated', async () => { const flg = flagsmith({ environmentKey: 'ser.key', diff --git a/tests/sdk/utils.ts b/tests/sdk/utils.ts index d3f2f69..b2727b8 100644 --- a/tests/sdk/utils.ts +++ b/tests/sdk/utils.ts @@ -47,6 +47,8 @@ export function apiKey(): string { export function flagsmith(params: FlagsmithConfig = {}) { return new Flagsmith({ environmentKey: apiKey(), + environmentRefreshIntervalSeconds: 0, + requestRetryDelayMilliseconds: 0, fetch, ...params, }); From e27cbcfd09cc8ee47f76d9d7a6bf3a1a188edbc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Thu, 20 Mar 2025 20:55:25 -0300 Subject: [PATCH 15/27] make config argument non-null + tsdocs --- sdk/index.ts | 52 ++++++++++++++++++++++--------------- tests/sdk/flagsmith.test.ts | 2 +- 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/sdk/index.ts b/sdk/index.ts index f0aa640..450bdd8 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -27,6 +27,30 @@ export { FlagsmithCache, FlagsmithConfig } from './types.js'; const DEFAULT_API_URL = 'https://edge.api.flagsmith.com/api/v1/'; const DEFAULT_REQUEST_TIMEOUT_SECONDS = 10; +/** + * A client for evaluating Flagsmith feature flags. + * + * Flags are evaluated remotely by the Flagsmith API over HTTP by default. + * To evaluate flags locally, create the client using {@link enableLocalEvaluation} and a server-side SDK key. + * + * @example + * import { Flagsmith, Flags, DefaultFlag } from 'flagsmith-nodejs' + * + * const flagsmith = new Flagsmith({ + * environmentKey: 'your_sdk_key', + * defaultFlagHandler: (flagKey: string) => { new DefaultFlag(...) }, + * }); + * + * // Fetch the current environment flags + * const environmentFlags: Flags = flagsmith.getEnvironmentFlags() + * const isFooEnabled: boolean = environmentFlags.isFeatureEnabled('foo') + * + * // Evaluate flags for any identity + * const identityFlags: Flags = flagsmith.getIdentityFlags('my_user_123', {'vip': true}) + * const bannerVariation = identityFlags.getFeatureValue('banner_flag') + * + * @see FlagsmithConfig +*/ export class Flagsmith { environmentKey?: string = undefined; apiUrl?: string = undefined; @@ -57,29 +81,15 @@ export class Flagsmith { private logger: Logger; private customFetch: Fetch; private readonly requestRetryDelayMilliseconds: number; + /** - * A client for evaluating Flagsmith feature flags. - * - * Flags are evaluated remotely by the Flagsmith API over HTTP by default. - * To evaluate flags locally, use {@link enableLocalEvaluation}. - * - * @example - * import { Flagsmith, Flags, DefaultFlag } from 'flagsmith-nodejs' + * Creates a new {@link Flagsmith} client. * - * const flagsmith = new Flagsmith({ - * environmentKey: '', - * defaultFlagHandler: (flagKey: string) => { new DefaultFlag(...) }, - * }); - * - * // Fetch the current environment flags - * const environmentFlags: Flags = flagsmith.getEnvironmentFlags() - * const isFooEnabled: boolean = environmentFlags.isFeatureEnabled('foo') - * - * // Evaluate flags for any identity - * const identityFlags: Flags = flagsmith.getIdentityFlags('my_user_123', {'vip': true}) - * const bannerVariation = identityFlags.getFeatureValue('banner_flag') - */ - constructor(data: FlagsmithConfig = {}) { + * If using local evaluation, the environment will be fetched lazily when needed by any method. Polling the + * environment for updates will start after {@link environmentRefreshIntervalSeconds} once the client is created. + * @param data The {@link FlagsmithConfig} options for this client. + */ + constructor(data: FlagsmithConfig) { this.agent = data.agent; this.customFetch = data.fetch ?? fetch; this.environmentKey = data.environmentKey; diff --git a/tests/sdk/flagsmith.test.ts b/tests/sdk/flagsmith.test.ts index 5010c12..1e4790e 100644 --- a/tests/sdk/flagsmith.test.ts +++ b/tests/sdk/flagsmith.test.ts @@ -338,7 +338,7 @@ test('cannot use both default handler and offline handler', () => { test('cannot create Flagsmith client in remote evaluation without API key', () => { // When and Then - expect(() => new Flagsmith()).toThrowError('ValueError: environmentKey is required.'); + expect(() => new Flagsmith({ environmentKey: '' })).toThrowError('ValueError: environmentKey is required.'); }); From 4049a571e76d4aa38ef975a8a035a9c378080b24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Thu, 20 Mar 2025 20:59:03 -0300 Subject: [PATCH 16/27] s/environment definition/environment state --- sdk/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/types.ts b/sdk/types.ts index 0143a3c..deadc40 100644 --- a/sdk/types.ts +++ b/sdk/types.ts @@ -61,13 +61,13 @@ export interface FlagsmithConfig { */ requestRetryDelayMilliseconds?: number; /** - * If enabled, flags are evaluated locally using an environment definition cached in memory. + * If enabled, flags are evaluated locally using the environment state cached in memory. * * The client will lazily fetch the environment from the Flagsmith API, and poll it every {@link environmentRefreshIntervalSeconds}. */ enableLocalEvaluation?: boolean; /** - * The time, in seconds, to wait before refreshing the cached environment definition. + * The time, in seconds, to wait before refreshing the cached environment state. * @default 60 */ environmentRefreshIntervalSeconds?: number; From e71aca5c03ebcc34407136e68bcddc35a7c66edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Thu, 20 Mar 2025 21:59:14 -0300 Subject: [PATCH 17/27] docs --- sdk/types.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sdk/types.ts b/sdk/types.ts index deadc40..e7f7584 100644 --- a/sdk/types.ts +++ b/sdk/types.ts @@ -3,6 +3,7 @@ import { EnvironmentModel } from '../flagsmith-engine/index.js'; import { Dispatcher } from 'undici-types'; import { Logger } from 'pino'; import { BaseOfflineHandler } from './offline_handlers.js'; +import { Flagsmith } from './index.js' export type IFlagsmithValue = T; @@ -27,6 +28,9 @@ export interface FlagsmithCache { export type Fetch = typeof fetch +/** + * The configuration options for a {@link Flagsmith} client. + */ export interface FlagsmithConfig { /** * The environment's client-side or server-side key. From d5446e45415f9d78f4715ec90ed73744e1192de9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Fri, 21 Mar 2025 00:05:27 -0300 Subject: [PATCH 18/27] words --- sdk/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/types.ts b/sdk/types.ts index e7f7584..6762d64 100644 --- a/sdk/types.ts +++ b/sdk/types.ts @@ -37,7 +37,7 @@ export interface FlagsmithConfig { */ environmentKey?: string; /** - * The Flagsmith API URL. Set this if you are not using Flagsmith's public SaaS service, i.e. https://app.flagsmith.com. + * The Flagsmith API URL. Set this if you are not using Flagsmith's public service, i.e. https://app.flagsmith.com. * * @default https://edge.api.flagsmith.com/api/v1/ */ From 0e8fa1aa8de85c032067f51f7b97b770666f465e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Fri, 21 Mar 2025 09:31:48 -0300 Subject: [PATCH 19/27] unused import --- sdk/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sdk/index.ts b/sdk/index.ts index 450bdd8..5ecd632 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -1,5 +1,5 @@ import { Dispatcher } from 'undici-types'; -import { FeatureStateModel, getEnvironmentFeatureStates, getIdentityFeatureStates } from '../flagsmith-engine/index.js'; +import { getEnvironmentFeatureStates, getIdentityFeatureStates } from '../flagsmith-engine/index.js'; import { EnvironmentModel } from '../flagsmith-engine/index.js'; import { buildEnvironmentModel } from '../flagsmith-engine/environments/util.js'; import { IdentityModel } from '../flagsmith-engine/index.js'; @@ -7,7 +7,7 @@ import { TraitModel } from '../flagsmith-engine/index.js'; import {ANALYTICS_ENDPOINT, AnalyticsProcessor} from './analytics.js'; import { BaseOfflineHandler } from './offline_handlers.js'; -import { FlagsmithAPIError, FlagsmithClientError } from './errors.js'; +import { FlagsmithAPIError } from './errors.js'; import { DefaultFlag, Flags } from './models.js'; import { EnvironmentDataPollingManager } from './polling_manager.js'; From 44f62db6af94b12ba0c4d6436d6fa2b8e7037202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Fri, 21 Mar 2025 09:33:21 -0300 Subject: [PATCH 20/27] Propagate getIdentityFlags error Co-authored-by: Kim Gustyr --- sdk/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/index.ts b/sdk/index.ts index 5ecd632..6539466 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -215,7 +215,7 @@ export class Flagsmith { return await this.getIdentityFlagsFromApi(identifier, traits, transient); } catch (error) { if (!this.defaultFlagHandler) { - throw new Error('getIdentityFlags failed and no default flag handler was provided') + throw new Error('getIdentityFlags failed and no default flag handler was provided', { cause: error }) } this.logger.error(error, 'getIdentityFlags failed'); return new Flags({ From 88b9176f72bfb1a2f03a6f1c4e5c3c99c75380d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Fri, 21 Mar 2025 10:22:23 -0300 Subject: [PATCH 21/27] remove mocking for happy paths --- tests/sdk/flagsmith-cache.test.ts | 10 -------- tests/sdk/flagsmith-environment-flags.test.ts | 10 -------- tests/sdk/flagsmith-identity-flags.test.ts | 6 ----- tests/sdk/flagsmith.test.ts | 24 +++++++------------ tests/sdk/utils.ts | 21 +++++++++++----- 5 files changed, 23 insertions(+), 48 deletions(-) diff --git a/tests/sdk/flagsmith-cache.test.ts b/tests/sdk/flagsmith-cache.test.ts index 12c3f55..e80ae3a 100644 --- a/tests/sdk/flagsmith-cache.test.ts +++ b/tests/sdk/flagsmith-cache.test.ts @@ -5,8 +5,6 @@ beforeEach(() => { }); test('test_empty_cache_not_read_but_populated', async () => { - fetch.mockResolvedValue(new Response(flagsJSON)); - const cache = new TestCache(); const set = vi.spyOn(cache, 'set'); @@ -23,8 +21,6 @@ test('test_empty_cache_not_read_but_populated', async () => { }); test('test_api_not_called_when_cache_present', async () => { - fetch.mockResolvedValue(new Response(flagsJSON)); - const cache = new TestCache(); const set = vi.spyOn(cache, 'set'); @@ -56,8 +52,6 @@ test('test_api_called_twice_when_no_cache', async () => { }); test('test_get_environment_flags_uses_local_environment_when_available', async () => { - fetch.mockResolvedValue(new Response(flagsJSON)); - const cache = new TestCache(); const set = vi.spyOn(cache, 'set'); @@ -77,8 +71,6 @@ test('test_get_environment_flags_uses_local_environment_when_available', async ( }); test('test_cache_used_for_identity_flags', async () => { - fetch.mockResolvedValue(new Response(identitiesJSON)); - const cache = new TestCache(); const set = vi.spyOn(cache, 'set'); @@ -100,8 +92,6 @@ test('test_cache_used_for_identity_flags', async () => { }); test('test_cache_used_for_identity_flags_local_evaluation', async () => { - fetch.mockResolvedValue(new Response(environmentJSON)); - const cache = new TestCache(); const set = vi.spyOn(cache, 'set'); diff --git a/tests/sdk/flagsmith-environment-flags.test.ts b/tests/sdk/flagsmith-environment-flags.test.ts index e3d4560..e34d2f5 100644 --- a/tests/sdk/flagsmith-environment-flags.test.ts +++ b/tests/sdk/flagsmith-environment-flags.test.ts @@ -9,8 +9,6 @@ beforeEach(() => { }); test('test_get_environment_flags_calls_api_when_no_local_environment', async () => { - fetch.mockResolvedValue(new Response(flagsJSON)); - const flg = flagsmith(); const allFlags = await (await flg.getEnvironmentFlags()).allFlags(); @@ -43,8 +41,6 @@ test('test_default_flag_is_used_when_no_environment_flags_returned', async () => }); test('test_analytics_processor_tracks_flags', async () => { - fetch.mockResolvedValue(new Response(flagsJSON)); - const defaultFlag = new DefaultFlag('some-default-value', true); const defaultFlagHandler = (featureName: string) => defaultFlag; @@ -64,8 +60,6 @@ test('test_analytics_processor_tracks_flags', async () => { }); test('test_getFeatureValue', async () => { - fetch.mockResolvedValue(new Response(flagsJSON)); - const defaultFlag = new DefaultFlag('some-default-value', true); const defaultFlagHandler = (featureName: string) => defaultFlag; @@ -108,8 +102,6 @@ test('test_non_200_response_raises_flagsmith_api_error', async () => { await expect(flg.getEnvironmentFlags()).rejects.toThrow(); }); test('test_default_flag_is_not_used_when_environment_flags_returned', async () => { - fetch.mockResolvedValue(new Response(flagsJSON)); - const defaultFlag = new DefaultFlag('some-default-value', true); const defaultFlagHandler = (featureName: string) => defaultFlag; @@ -147,8 +139,6 @@ test('test_default_flag_is_used_when_bad_api_response_happens', async () => { }); test('test_local_evaluation', async () => { - fetch.mockResolvedValue(new Response(environmentJSON)); - const defaultFlag = new DefaultFlag('some-default-value', true); const defaultFlagHandler = (featureName: string) => defaultFlag; diff --git a/tests/sdk/flagsmith-identity-flags.test.ts b/tests/sdk/flagsmith-identity-flags.test.ts index 8f91ec0..8dfa474 100644 --- a/tests/sdk/flagsmith-identity-flags.test.ts +++ b/tests/sdk/flagsmith-identity-flags.test.ts @@ -18,7 +18,6 @@ beforeEach(() => { test('test_get_identity_flags_calls_api_when_no_local_environment_no_traits', async () => { - fetch.mockResolvedValue(new Response(identitiesJSON)); const identifier = 'identifier'; const flg = flagsmith(); @@ -31,7 +30,6 @@ test('test_get_identity_flags_calls_api_when_no_local_environment_no_traits', as }); test('test_get_identity_flags_uses_environment_when_local_environment_no_traits', async () => { - fetch.mockResolvedValue(new Response(environmentJSON)) const identifier = 'identifier'; const flg = flagsmith({ @@ -48,7 +46,6 @@ test('test_get_identity_flags_uses_environment_when_local_environment_no_traits' }); test('test_get_identity_flags_calls_api_when_no_local_environment_with_traits', async () => { - fetch.mockResolvedValue(new Response(identitiesJSON)) const identifier = 'identifier'; const traits = { some_trait: 'some_value' }; const flg = flagsmith(); @@ -61,8 +58,6 @@ test('test_get_identity_flags_calls_api_when_no_local_environment_with_traits', }); test('test_default_flag_is_not_used_when_identity_flags_returned', async () => { - fetch.mockResolvedValue(new Response(identitiesJSON)) - const defaultFlag = new DefaultFlag('some-default-value', true); const defaultFlagHandler = (featureName: string) => defaultFlag; @@ -140,7 +135,6 @@ test('test_get_identity_flags_multivariate_value_with_local_evaluation_enabled', const flg = flagsmith({ environmentKey: 'ser.key', enableLocalEvaluation: true, - }); const identityFlags = (await flg.getIdentityFlags(identifier)) diff --git a/tests/sdk/flagsmith.test.ts b/tests/sdk/flagsmith.test.ts index 1e4790e..2405a06 100644 --- a/tests/sdk/flagsmith.test.ts +++ b/tests/sdk/flagsmith.test.ts @@ -3,7 +3,6 @@ import { EnvironmentDataPollingManager } from '../../sdk/polling_manager.js'; import { environmentJSON, environmentModel, - flagsJSON, flagsmith, fetch, offlineEnvironmentJSON, @@ -21,7 +20,6 @@ beforeEach(() => { }); test('test_flagsmith_starts_polling_manager_on_init_if_enabled', () => { - fetch.mockResolvedValue(new Response(environmentJSON)); new Flagsmith({ environmentKey: 'ser.key', enableLocalEvaluation: true @@ -30,7 +28,6 @@ test('test_flagsmith_starts_polling_manager_on_init_if_enabled', () => { }); test('test_flagsmith_local_evaluation_key_required', () => { - fetch.mockResolvedValue(new Response(environmentJSON)); expect(() => { new Flagsmith({ environmentKey: 'bad.key', @@ -40,8 +37,9 @@ test('test_flagsmith_local_evaluation_key_required', () => { }); test('test_update_environment_sets_environment', async () => { - fetch.mockResolvedValue(new Response(environmentJSON)); - const flg = flagsmith(); + const flg = flagsmith({ + environmentKey: 'ser.key', + }); const model = environmentModel(JSON.parse(environmentJSON)); expect(await flg.getEnvironment()).toStrictEqual(model); }); @@ -49,7 +47,7 @@ test('test_update_environment_sets_environment', async () => { test('test_set_agent_options', async () => { const agent = new Agent({}) - fetch.mockImplementation((url, options) => { + fetch.mockImplementationOnce((url, options) => { //@ts-ignore I give up if (options.dispatcher !== agent) { throw new Error("Agent has not been set on retry fetch") @@ -65,7 +63,6 @@ test('test_set_agent_options', async () => { }); test('test_get_identity_segments', async () => { - fetch.mockResolvedValue(new Response(environmentJSON)); const flg = flagsmith({ environmentKey: 'ser.key', enableLocalEvaluation: true @@ -78,7 +75,6 @@ test('test_get_identity_segments', async () => { test('test_get_identity_segments_empty_without_local_eval', async () => { - fetch.mockResolvedValue(new Response(environmentJSON)); const flg = new Flagsmith({ environmentKey: 'ser.key', enableLocalEvaluation: false @@ -88,8 +84,6 @@ test('test_get_identity_segments_empty_without_local_eval', async () => { }); test('test_update_environment_uses_req_when_inited', async () => { - fetch.mockResolvedValue(new Response(environmentJSON)); - const flg = flagsmith({ environmentKey: 'ser.key', enableLocalEvaluation: true, @@ -103,7 +97,6 @@ test('test_update_environment_uses_req_when_inited', async () => { }); test('test_isFeatureEnabled_environment', async () => { - fetch.mockResolvedValue(new Response(environmentJSON)); const defaultFlag = new DefaultFlag('some-default-value', true); const defaultFlagHandler = (featureName: string) => defaultFlag; @@ -121,9 +114,7 @@ test('test_isFeatureEnabled_environment', async () => { }); test('test_fetch_recovers_after_single_API_error', async () => { - fetch - .mockRejectedValue('Error during fetching the API response') - .mockResolvedValue(new Response(flagsJSON)); + fetch.mockRejectedValueOnce('Error during fetching the API response') const flg = flagsmith({ environmentKey: 'key', }); @@ -157,8 +148,9 @@ test.each([ test('default flag handler used when timeout occurs', async () => { fetch.mockImplementation(async (...args) => { - await sleep(10000) - return fetch(...args) + const forever = new Promise(() => {}) + await forever + throw new Error('waited forever') }); const defaultFlag = new DefaultFlag('some-default-value', true); diff --git a/tests/sdk/utils.ts b/tests/sdk/utils.ts index b2727b8..6a1a744 100644 --- a/tests/sdk/utils.ts +++ b/tests/sdk/utils.ts @@ -19,15 +19,24 @@ export class TestCache implements FlagsmithCache { } } -export const fetch = vi.fn((_, options) => { +export const fetch = vi.fn((url: string, options?: RequestInit) => { const headers = options?.headers as Record; if (!headers) throw new Error('missing request headers') const env = headers['X-Environment-Key']; - if (!env) return Promise.resolve(new Response("missing x-environment-key header", { status: 404 })) + if (!env) return Promise.resolve(new Response('missing x-environment-key header', { status: 404 })); if (env.startsWith('ser.')) { - return Promise.resolve(new Response(environmentJSON, { status: 200 })) + if (url.includes('/environment-document')) { + return Promise.resolve(new Response(environmentJSON, { status: 200 })) + } + return Promise.resolve(new Response('environment-document called without a server-side key', { status: 401 })) } - return Promise.resolve(new Response(identitiesJSON, { status: 200 })) + if (url.includes("/flags")) { + return Promise.resolve(new Response(flagsJSON, { status: 200 })) + } + if (url.includes("/identities")) { + return Promise.resolve(new Response(identitiesJSON, { status: 200 })) + } + return Promise.resolve(new Response('unknown url ' + url, { status: 404 })) }); export const badFetch: Fetch = () => { throw new Error('fetch failed')} @@ -36,7 +45,7 @@ export function analyticsProcessor() { return new AnalyticsProcessor({ environmentKey: 'test-key', analyticsUrl: 'http://testUrl/analytics/flags/', - fetch, + fetch: (url, options) => fetch(url.toString(), options), }); } @@ -49,7 +58,7 @@ export function flagsmith(params: FlagsmithConfig = {}) { environmentKey: apiKey(), environmentRefreshIntervalSeconds: 0, requestRetryDelayMilliseconds: 0, - fetch, + fetch: (url, options) => fetch(url.toString(), options), ...params, }); } From ed889392f26ea267a03e477aa5b8873a5e315203 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Fri, 21 Mar 2025 10:34:33 -0300 Subject: [PATCH 22/27] remove redundant mock clear/reset --- tests/sdk/analytics.test.ts | 4 ---- tests/sdk/flagsmith-cache.test.ts | 4 ---- tests/sdk/flagsmith-environment-flags.test.ts | 4 ---- tests/sdk/flagsmith-identity-flags.test.ts | 5 ----- tests/sdk/flagsmith.test.ts | 4 ---- tests/sdk/polling.test.ts | 4 ---- 6 files changed, 25 deletions(-) diff --git a/tests/sdk/analytics.test.ts b/tests/sdk/analytics.test.ts index fa687dc..da9ea4d 100644 --- a/tests/sdk/analytics.test.ts +++ b/tests/sdk/analytics.test.ts @@ -1,9 +1,5 @@ import {analyticsProcessor, fetch} from './utils.js'; -afterEach(() => { - vi.resetAllMocks(); -}); - test('test_analytics_processor_track_feature_updates_analytics_data', () => { const aP = analyticsProcessor(); aP.trackFeature("myFeature"); diff --git a/tests/sdk/flagsmith-cache.test.ts b/tests/sdk/flagsmith-cache.test.ts index e80ae3a..77fd92e 100644 --- a/tests/sdk/flagsmith-cache.test.ts +++ b/tests/sdk/flagsmith-cache.test.ts @@ -1,9 +1,5 @@ import { fetch, environmentJSON, environmentModel, flagsJSON, flagsmith, identitiesJSON, TestCache } from './utils.js'; -beforeEach(() => { - vi.clearAllMocks(); -}); - test('test_empty_cache_not_read_but_populated', async () => { const cache = new TestCache(); const set = vi.spyOn(cache, 'set'); diff --git a/tests/sdk/flagsmith-environment-flags.test.ts b/tests/sdk/flagsmith-environment-flags.test.ts index e34d2f5..701bf4e 100644 --- a/tests/sdk/flagsmith-environment-flags.test.ts +++ b/tests/sdk/flagsmith-environment-flags.test.ts @@ -4,10 +4,6 @@ import { DefaultFlag } from '../../sdk/models.js'; vi.mock('../../sdk/polling_manager'); -beforeEach(() => { - vi.clearAllMocks(); -}); - test('test_get_environment_flags_calls_api_when_no_local_environment', async () => { const flg = flagsmith(); const allFlags = await (await flg.getEnvironmentFlags()).allFlags(); diff --git a/tests/sdk/flagsmith-identity-flags.test.ts b/tests/sdk/flagsmith-identity-flags.test.ts index 8dfa474..c616fde 100644 --- a/tests/sdk/flagsmith-identity-flags.test.ts +++ b/tests/sdk/flagsmith-identity-flags.test.ts @@ -12,11 +12,6 @@ import { DefaultFlag } from '../../sdk/models.js'; vi.mock('../../sdk/polling_manager'); -beforeEach(() => { - vi.clearAllMocks(); -}); - - test('test_get_identity_flags_calls_api_when_no_local_environment_no_traits', async () => { const identifier = 'identifier'; diff --git a/tests/sdk/flagsmith.test.ts b/tests/sdk/flagsmith.test.ts index 2405a06..0d8661b 100644 --- a/tests/sdk/flagsmith.test.ts +++ b/tests/sdk/flagsmith.test.ts @@ -15,10 +15,6 @@ import { BaseOfflineHandler } from '../../sdk/offline_handlers.js'; import { Agent } from 'undici'; vi.mock('../../sdk/polling_manager'); -beforeEach(() => { - vi.clearAllMocks(); -}); - test('test_flagsmith_starts_polling_manager_on_init_if_enabled', () => { new Flagsmith({ environmentKey: 'ser.key', diff --git a/tests/sdk/polling.test.ts b/tests/sdk/polling.test.ts index d69afb6..65b1cb1 100644 --- a/tests/sdk/polling.test.ts +++ b/tests/sdk/polling.test.ts @@ -3,10 +3,6 @@ import { EnvironmentDataPollingManager } from '../../sdk/polling_manager.js'; import { delay } from '../../sdk/utils.js'; vi.mock('../../sdk'); -beforeEach(() => { - vi.clearAllMocks() -}); - test('test_polling_manager_correctly_stops_if_never_started', async () => { const flagsmith = new Flagsmith({ environmentKey: 'key' From 47e0418bb0bd75922a4d02bb2a44f5bd01350f4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Fri, 21 Mar 2025 11:03:14 -0300 Subject: [PATCH 23/27] refactor + remove duplicate logic --- sdk/index.ts | 49 +++++++------------ tests/sdk/flagsmith-environment-flags.test.ts | 2 +- 2 files changed, 19 insertions(+), 32 deletions(-) diff --git a/sdk/index.ts b/sdk/index.ts index 6539466..9a6b01e 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -169,11 +169,13 @@ export class Flagsmith { } try { if (this.enableLocalEvaluation || this.offlineMode) { - const environment = await this.getEnvironment(); - return this.getEnvironmentFlagsFromDocument(environment); + return await this.getEnvironmentFlagsFromDocument(); } - return this.getEnvironmentFlagsFromApi(); + return await this.getEnvironmentFlagsFromApi(); } catch (error) { + if (!this.defaultFlagHandler) { + throw new Error('getEnvironmentFlags failed and no default flag handler was provided', { cause: error }); + } this.logger.error(error, 'getEnvironmentFlags failed'); return new Flags({ flags: {}, @@ -209,8 +211,7 @@ export class Flagsmith { traits = traits || {}; try { if (this.enableLocalEvaluation || this.offlineMode) { - const environment = await this.getEnvironment(); - return this.getIdentityFlagsFromDocument(environment, identifier, traits || {}); + return await this.getIdentityFlagsFromDocument(identifier, traits || {}); } return await this.getIdentityFlagsFromApi(identifier, traits, transient); } catch (error) { @@ -376,7 +377,8 @@ export class Flagsmith { return buildEnvironmentModel(environment_data); } - private async getEnvironmentFlagsFromDocument(environment: EnvironmentModel): Promise { + private async getEnvironmentFlagsFromDocument(): Promise { + const environment = await this.getEnvironment(); const flags = Flags.fromFeatureStateModels({ featureStates: getEnvironmentFeatureStates(environment), analyticsProcessor: this.analyticsProcessor, @@ -389,10 +391,10 @@ export class Flagsmith { } private async getIdentityFlagsFromDocument( - environment: EnvironmentModel, identifier: string, traits: { [key: string]: any } ): Promise { + const environment = await this.getEnvironment(); const identityModel = this.getIdentityModel( environment, identifier, @@ -422,31 +424,16 @@ export class Flagsmith { if (!this.environmentFlagsUrl) { throw new Error('`apiUrl` argument is missing or invalid.'); } - try { - const apiFlags = await this.getJSONResponse(this.environmentFlagsUrl, 'GET'); - const flags = Flags.fromAPIFlags({ - apiFlags: apiFlags, - analyticsProcessor: this.analyticsProcessor, - defaultFlagHandler: this.defaultFlagHandler - }); - if (!!this.cache) { - await this.cache.set('flags', flags); - } - return flags; - } catch (e) { - if (this.offlineHandler) { - const environment = this.offlineHandler.getEnvironment(); - return this.getEnvironmentFlagsFromDocument(environment); - } - if (this.defaultFlagHandler) { - return new Flags({ - flags: {}, - defaultFlagHandler: this.defaultFlagHandler - }); - } - - throw e; + const apiFlags = await this.getJSONResponse(this.environmentFlagsUrl, 'GET'); + const flags = Flags.fromAPIFlags({ + apiFlags: apiFlags, + analyticsProcessor: this.analyticsProcessor, + defaultFlagHandler: this.defaultFlagHandler + }); + if (!!this.cache) { + await this.cache.set('flags', flags); } + return flags; } private async getIdentityFlagsFromApi( diff --git a/tests/sdk/flagsmith-environment-flags.test.ts b/tests/sdk/flagsmith-environment-flags.test.ts index 701bf4e..b08ad61 100644 --- a/tests/sdk/flagsmith-environment-flags.test.ts +++ b/tests/sdk/flagsmith-environment-flags.test.ts @@ -82,7 +82,7 @@ test('test_throws_when_no_default_flag_handler_after_multiple_API_errors', async await expect(async () => { const flags = await flg.getEnvironmentFlags(); const flag = flags.getFlag('some_feature'); - }).rejects.toThrow('Error during fetching the API response'); + }).rejects.toThrow('getEnvironmentFlags failed and no default flag handler was provided'); }); test('test_non_200_response_raises_flagsmith_api_error', async () => { From e042105c74ded1b81a62eeec46fea41ef35bdebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Fri, 21 Mar 2025 11:44:50 -0300 Subject: [PATCH 24/27] fix doc link --- sdk/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/index.ts b/sdk/index.ts index 9a6b01e..3038523 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -31,7 +31,7 @@ const DEFAULT_REQUEST_TIMEOUT_SECONDS = 10; * A client for evaluating Flagsmith feature flags. * * Flags are evaluated remotely by the Flagsmith API over HTTP by default. - * To evaluate flags locally, create the client using {@link enableLocalEvaluation} and a server-side SDK key. + * To evaluate flags locally, create the client using {@link FlagsmithConfig.enableLocalEvaluation} and a server-side SDK key. * * @example * import { Flagsmith, Flags, DefaultFlag } from 'flagsmith-nodejs' From 56764cc54c3b5fa6fb851d7489507361da191aba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Fri, 21 Mar 2025 11:50:15 -0300 Subject: [PATCH 25/27] moar docs --- sdk/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/index.ts b/sdk/index.ts index 3038523..f8691c9 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -47,7 +47,7 @@ const DEFAULT_REQUEST_TIMEOUT_SECONDS = 10; * * // Evaluate flags for any identity * const identityFlags: Flags = flagsmith.getIdentityFlags('my_user_123', {'vip': true}) - * const bannerVariation = identityFlags.getFeatureValue('banner_flag') + * const bannerVariation: string = identityFlags.getFeatureValue('banner_flag') * * @see FlagsmithConfig */ From 93ada18e13752414712b82d68c4ac5c0fe95e9fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Mon, 24 Mar 2025 13:21:43 -0300 Subject: [PATCH 26/27] Update tests/sdk/utils.ts Co-authored-by: Matthew Elwell --- tests/sdk/utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/sdk/utils.ts b/tests/sdk/utils.ts index 6a1a744..a725ca1 100644 --- a/tests/sdk/utils.ts +++ b/tests/sdk/utils.ts @@ -24,8 +24,8 @@ export const fetch = vi.fn((url: string, options?: RequestInit) => { if (!headers) throw new Error('missing request headers') const env = headers['X-Environment-Key']; if (!env) return Promise.resolve(new Response('missing x-environment-key header', { status: 404 })); - if (env.startsWith('ser.')) { - if (url.includes('/environment-document')) { + if (url.includes('/environment-document')) { + if (env.startsWith('ser.')) { return Promise.resolve(new Response(environmentJSON, { status: 200 })) } return Promise.resolve(new Response('environment-document called without a server-side key', { status: 401 })) From b20470dd24fcde1f8040e1f43014fb5358db1214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Mon, 24 Mar 2025 15:59:18 -0300 Subject: [PATCH 27/27] 6.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8629f28..dff0eaf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "flagsmith-nodejs", - "version": "5.1.1", + "version": "6.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "flagsmith-nodejs", - "version": "5.1.1", + "version": "6.0.0", "license": "MIT", "dependencies": { "pino": "^8.8.0", diff --git a/package.json b/package.json index 54f8b77..4989659 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flagsmith-nodejs", - "version": "5.1.1", + "version": "6.0.0", "description": "Flagsmith lets you manage features flags and remote config across web, mobile and server side applications. Deliver true Continuous Integration. Get builds out faster. Control who has access to new features.", "main": "./build/cjs/index.js", "type": "module",