From c2582902e8b8c5332c2b105e444f07aaf0919274 Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 16 Apr 2026 14:14:05 +0200 Subject: [PATCH 1/3] fix: include-traits-in-cache-key --- sdk/index.ts | 8 +++++--- sdk/utils.ts | 23 +++++++++++++++++++++++ tests/sdk/flagsmith-cache.test.ts | 18 ++++++++++++++++-- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/sdk/index.ts b/sdk/index.ts index a8f2b83..72d5f9d 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -11,6 +11,7 @@ import { EnvironmentDataPollingManager } from './polling_manager.js'; import { Deferred, generateIdentitiesData, + generateIdentityCacheKey, getUserAgent, isTraitConfig, retryFetch @@ -226,7 +227,8 @@ export class Flagsmith { throw new Error('`identifier` argument is missing or invalid.'); } - const cachedItem = !!this.cache && (await this.cache.get(`flags-${identifier}`)); + const cacheKey = generateIdentityCacheKey(identifier, traits); + const cachedItem = !!this.cache && (await this.cache.get(cacheKey)); if (!!cachedItem) { return cachedItem; } @@ -497,7 +499,7 @@ export class Flagsmith { ); if (!!this.cache) { - await this.cache.set(`flags-${identifier}`, flags); + await this.cache.set(generateIdentityCacheKey(identifier, traits), flags); } return flags; @@ -535,7 +537,7 @@ export class Flagsmith { defaultFlagHandler: this.defaultFlagHandler }); if (!!this.cache) { - await this.cache.set(`flags-${identifier}`, flags); + await this.cache.set(generateIdentityCacheKey(identifier, traits), flags); } return flags; } diff --git a/sdk/utils.ts b/sdk/utils.ts index 7c4dd32..016e274 100644 --- a/sdk/utils.ts +++ b/sdk/utils.ts @@ -1,3 +1,4 @@ +import { createHash } from 'crypto'; import { Fetch, FlagsmithTraitValue, TraitConfig } from './types.js'; import { Dispatcher } from 'undici-types'; @@ -40,6 +41,28 @@ export function generateIdentitiesData(identifier: string, traits: Traits, trans }; } +export function generateIdentityCacheKey(identifier: string, traits?: Traits): string { + if (!traits || Object.keys(traits).length === 0) { + return `flags-${identifier}`; + } + + const normalized: [string, FlagsmithTraitValue][] = []; + for (const key of Object.keys(traits).sort()) { + const raw = traits[key]; + const value = isTraitConfig(raw) ? raw.value : raw; + if (value === undefined) continue; + normalized.push([key, value]); + } + + if (normalized.length === 0) { + return `flags-${identifier}`; + } + + const serialized = JSON.stringify(normalized); + const hash = createHash('sha256').update(serialized).digest('hex').substring(0, 16); + return `flags-${identifier}-${hash}`; +} + export const delay = (ms: number) => new Promise(resolve => setTimeout(() => resolve(undefined), ms)); diff --git a/tests/sdk/flagsmith-cache.test.ts b/tests/sdk/flagsmith-cache.test.ts index c4bc39c..803e29d 100644 --- a/tests/sdk/flagsmith-cache.test.ts +++ b/tests/sdk/flagsmith-cache.test.ts @@ -7,6 +7,7 @@ import { identitiesJSON, TestCache } from './utils.js'; +import { generateIdentityCacheKey } from '../../sdk/utils.js'; test('test_empty_cache_not_read_but_populated', async () => { const cache = new TestCache(); @@ -86,7 +87,7 @@ test('test_cache_used_for_identity_flags', async () => { const identityFlags = (await flg.getIdentityFlags(identifier, traits)).allFlags(); expect(set).toBeCalled(); - expect(await cache.get('flags-identifier')).toBeTruthy(); + expect(await cache.get(generateIdentityCacheKey(identifier, traits))).toBeTruthy(); expect(fetch).toBeCalledTimes(1); @@ -111,7 +112,7 @@ test('test_cache_used_for_identity_flags_local_evaluation', async () => { const identityFlags = (await flg.getIdentityFlags(identifier, traits)).allFlags(); expect(set).toBeCalled(); - expect(await cache.get('flags-identifier')).toBeTruthy(); + expect(await cache.get(generateIdentityCacheKey(identifier, traits))).toBeTruthy(); expect(fetch).toBeCalledTimes(1); @@ -119,3 +120,16 @@ test('test_cache_used_for_identity_flags_local_evaluation', async () => { expect(identityFlags[0].value).toBe('some-value'); expect(identityFlags[0].featureName).toBe('some_feature'); }); + +test('test_different_traits_produce_different_cache_entries', async () => { + const cache = new TestCache(); + const identifier = 'identifier'; + const flg = flagsmith({ cache }); + + await flg.getIdentityFlags(identifier, { some_trait: 'value_a' }); + await flg.getIdentityFlags(identifier, { some_trait: 'value_b' }); + + // Different traits should hit the API separately, not return stale cached results. + expect(fetch).toBeCalledTimes(2); + expect(Object.keys(cache.cache).length).toBe(2); +}); From 66f9b8a88363fc5a07b9797296c2189325dcd126 Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 16 Apr 2026 14:17:53 +0200 Subject: [PATCH 2/3] feat: added-pull-request-template --- .github/pull_request_template.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..33730f8 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,21 @@ +Thanks for submitting a PR! Please check the boxes below: + +- [ ] I have read the [Contributing Guide](/Flagsmith/flagsmith/blob/main/CONTRIBUTING.md). +- [ ] I have added information to `docs/` if required so people know about the feature. +- [ ] I have filled in the "Changes" section below. +- [ ] I have filled in the "How did you test this code" section below. + +## Changes + +Contributes to + + + +_Please describe._ + +## How did you test this code? + + + +_Please describe._ From 413200567fcf79fbca159e33d2aa48d95554f26b Mon Sep 17 00:00:00 2001 From: wadii Date: Thu, 16 Apr 2026 14:35:24 +0200 Subject: [PATCH 3/3] fix: removed-useless-comment --- tests/sdk/flagsmith-cache.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/sdk/flagsmith-cache.test.ts b/tests/sdk/flagsmith-cache.test.ts index 803e29d..da7fbb9 100644 --- a/tests/sdk/flagsmith-cache.test.ts +++ b/tests/sdk/flagsmith-cache.test.ts @@ -129,7 +129,6 @@ test('test_different_traits_produce_different_cache_entries', async () => { await flg.getIdentityFlags(identifier, { some_trait: 'value_a' }); await flg.getIdentityFlags(identifier, { some_trait: 'value_b' }); - // Different traits should hit the API separately, not return stale cached results. expect(fetch).toBeCalledTimes(2); expect(Object.keys(cache.cache).length).toBe(2); });