Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -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 <!-- insert issue # or URL -->

<!-- Change "Contributes to" to "Closes" if this PR, when merged, completely resolves the referenced issue.
Leave "Contributes to" if the changes need to be released first. -->

_Please describe._

## How did you test this code?

<!-- If the answer is manually, please include a quick step-by-step on how to test this PR. -->

_Please describe._
8 changes: 5 additions & 3 deletions sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { EnvironmentDataPollingManager } from './polling_manager.js';
import {
Deferred,
generateIdentitiesData,
generateIdentityCacheKey,
getUserAgent,
isTraitConfig,
retryFetch
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
23 changes: 23 additions & 0 deletions sdk/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createHash } from 'crypto';
import { Fetch, FlagsmithTraitValue, TraitConfig } from './types.js';
import { Dispatcher } from 'undici-types';

Expand Down Expand Up @@ -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));

Expand Down
17 changes: 15 additions & 2 deletions tests/sdk/flagsmith-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);

Expand All @@ -111,11 +112,23 @@ 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);

expect(identityFlags[0].enabled).toBe(true);
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' });

expect(fetch).toBeCalledTimes(2);
expect(Object.keys(cache.cache).length).toBe(2);
});
Loading