Skip to content

Commit 2b5df9f

Browse files
authored
feat: Support transient identities and traits (#158)
feat: Support transient identities and traits
1 parent a2a3c1c commit 2b5df9f

File tree

9 files changed

+241
-51
lines changed

9 files changed

+241
-51
lines changed

flagsmith-engine/identities/models.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export class IdentityModel {
2020
environmentApiKey: string,
2121
identifier: string,
2222
identityUuid?: string,
23-
djangoID?: number
23+
djangoID?: number,
2424
) {
2525
this.identityUuid = identityUuid || uuidv4();
2626
this.createdDate = Date.parse(created_date) || Date.now();

flagsmith-engine/identities/traits/models.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
export class TraitModel {
22
traitKey: string;
33
traitValue: any;
4-
54
constructor(key: string, value: any) {
65
this.traitKey = key;
76
this.traitValue = value;

sdk/index.ts

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { EnvironmentDataPollingManager } from './polling_manager';
1414
import { generateIdentitiesData, retryFetch } from './utils';
1515
import { SegmentModel } from '../flagsmith-engine/segments/models';
1616
import { getIdentitySegments } from '../flagsmith-engine/segments/evaluators';
17-
import { FlagsmithCache, FlagsmithConfig } from './types';
17+
import { FlagsmithCache, FlagsmithConfig, FlagsmithTraitValue, ITraitConfig } from './types';
1818
import pino, { Logger } from 'pino';
1919

2020
export { AnalyticsProcessor } from './analytics';
@@ -39,7 +39,6 @@ export class Flagsmith {
3939
enableAnalytics: boolean = false;
4040
defaultFlagHandler?: (featureName: string) => DefaultFlag;
4141

42-
4342
environmentFlagsUrl?: string;
4443
identitiesUrl?: string;
4544
environmentUrl?: string;
@@ -168,11 +167,11 @@ export class Flagsmith {
168167

169168
this.analyticsProcessor = data.enableAnalytics
170169
? new AnalyticsProcessor({
171-
environmentKey: this.environmentKey,
172-
baseApiUrl: this.apiUrl,
173-
requestTimeoutMs: this.requestTimeoutMs,
174-
logger: this.logger
175-
})
170+
environmentKey: this.environmentKey,
171+
baseApiUrl: this.apiUrl,
172+
requestTimeoutMs: this.requestTimeoutMs,
173+
logger: this.logger
174+
})
176175
: undefined;
177176
}
178177
}
@@ -207,11 +206,15 @@ export class Flagsmith {
207206
*
208207
* @param {string} identifier a unique identifier for the identity in the current
209208
environment, e.g. email address, username, uuid
210-
* @param {{[key:string]:any}} traits? a dictionary of traits to add / update on the identity in
211-
Flagsmith, e.g. {"num_orders": 10}
209+
* @param {{[key:string]:any | ITraitConfig}} traits? a dictionary of traits to add / update on the identity in
210+
Flagsmith, e.g. {"num_orders": 10} or {age: {value: 30, transient: true}}
212211
* @returns Flags object holding all the flags for the given identity.
213212
*/
214-
async getIdentityFlags(identifier: string, traits?: { [key: string]: any }): Promise<Flags> {
213+
async getIdentityFlags(
214+
identifier: string,
215+
traits?: { [key: string]: FlagsmithTraitValue | ITraitConfig },
216+
transient: boolean = false
217+
): Promise<Flags> {
215218
if (!identifier) {
216219
throw new Error('`identifier` argument is missing or invalid.');
217220
}
@@ -232,7 +235,7 @@ export class Flagsmith {
232235
return this.getIdentityFlagsFromDocument(identifier, traits || {});
233236
}
234237

235-
return this.getIdentityFlagsFromApi(identifier, traits);
238+
return this.getIdentityFlagsFromApi(identifier, traits, transient);
236239
}
237240

238241
/**
@@ -293,8 +296,11 @@ export class Flagsmith {
293296
}
294297
if (this.environment.identityOverrides?.length) {
295298
this.identitiesWithOverridesByIdentifier = new Map<string, IdentityModel>(
296-
this.environment.identityOverrides.map(identity => [identity.identifier, identity]
297-
));
299+
this.environment.identityOverrides.map(identity => [
300+
identity.identifier,
301+
identity
302+
])
303+
);
298304
}
299305
if (this.onEnvironmentChange) {
300306
this.onEnvironmentChange(null, this.environment);
@@ -433,12 +439,16 @@ export class Flagsmith {
433439
}
434440
}
435441

436-
private async getIdentityFlagsFromApi(identifier: string, traits: { [key: string]: any }) {
442+
private async getIdentityFlagsFromApi(
443+
identifier: string,
444+
traits: { [key: string]: FlagsmithTraitValue | ITraitConfig },
445+
transient: boolean = false
446+
) {
437447
if (!this.identitiesUrl) {
438448
throw new Error('`apiUrl` argument is missing or invalid.');
439449
}
440450
try {
441-
const data = generateIdentitiesData(identifier, traits);
451+
const data = generateIdentitiesData(identifier, traits, transient);
442452
const jsonResponse = await this.getJSONResponse(this.identitiesUrl, 'POST', data);
443453
const flags = Flags.fromAPIFlags({
444454
apiFlags: jsonResponse['flags'],

sdk/types.ts

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,38 @@
1-
import { DefaultFlag, Flags } from "./models";
2-
import { EnvironmentModel } from "../flagsmith-engine";
3-
import { RequestInit } from "node-fetch";
4-
import { Logger } from "pino";
5-
import { BaseOfflineHandler } from "./offline_handlers";
1+
import { DefaultFlag, Flags } from './models';
2+
import { EnvironmentModel } from '../flagsmith-engine';
3+
import { RequestInit } from 'node-fetch';
4+
import { Logger } from 'pino';
5+
import { BaseOfflineHandler } from './offline_handlers';
66

7+
export type IFlagsmithValue<T = string | number | boolean | null> = T;
78
export interface FlagsmithCache {
8-
get(key: string): Promise<Flags|undefined> | undefined;
9-
set(key: string, value: Flags, ttl: string | number): boolean | Promise<boolean>;
10-
has(key: string): boolean | Promise<boolean>;
11-
[key: string]: any;
9+
get(key: string): Promise<Flags | undefined> | undefined;
10+
set(key: string, value: Flags, ttl: string | number): boolean | Promise<boolean>;
11+
has(key: string): boolean | Promise<boolean>;
12+
[key: string]: any;
1213
}
1314

1415
export interface FlagsmithConfig {
15-
environmentKey?: string;
16-
apiUrl?: string;
17-
agent?:RequestInit['agent'];
18-
customHeaders?: { [key: string]: any };
19-
requestTimeoutSeconds?: number;
20-
enableLocalEvaluation?: boolean;
21-
environmentRefreshIntervalSeconds?: number;
22-
retries?: number;
23-
enableAnalytics?: boolean;
24-
defaultFlagHandler?: (featureName: string) => DefaultFlag;
25-
cache?: FlagsmithCache,
26-
onEnvironmentChange?: (error: Error | null, result: EnvironmentModel) => void,
27-
logger?: Logger
28-
offlineMode?: boolean;
29-
offlineHandler?: BaseOfflineHandler;
16+
environmentKey?: string;
17+
apiUrl?: string;
18+
agent?: RequestInit['agent'];
19+
customHeaders?: { [key: string]: any };
20+
requestTimeoutSeconds?: number;
21+
enableLocalEvaluation?: boolean;
22+
environmentRefreshIntervalSeconds?: number;
23+
retries?: number;
24+
enableAnalytics?: boolean;
25+
defaultFlagHandler?: (featureName: string) => DefaultFlag;
26+
cache?: FlagsmithCache;
27+
onEnvironmentChange?: (error: Error | null, result: EnvironmentModel) => void;
28+
logger?: Logger;
29+
offlineMode?: boolean;
30+
offlineHandler?: BaseOfflineHandler;
3031
}
32+
33+
export interface ITraitConfig {
34+
value: FlagsmithTraitValue;
35+
transient?: boolean;
36+
}
37+
38+
export declare type FlagsmithTraitValue = IFlagsmithValue;

sdk/utils.ts

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,38 @@
11
import fetch, { RequestInit, Response } from 'node-fetch';
2+
import { FlagsmithTraitValue, ITraitConfig } from './types';
23
// @ts-ignore
34
if (typeof fetch.default !== 'undefined') fetch = fetch.default;
45

5-
export function generateIdentitiesData(identifier: string, traits: { [key: string]: any }) {
6-
const traitsGenerated = Object.entries(traits).map(trait => ({
7-
trait_key: trait[0],
8-
trait_value: trait[1]
9-
}));
6+
type Traits = { [key: string]: ITraitConfig | FlagsmithTraitValue };
7+
8+
export function isTraitConfig(
9+
traitValue: ITraitConfig | FlagsmithTraitValue
10+
): traitValue is ITraitConfig {
11+
return !!traitValue && typeof traitValue == 'object' && traitValue.value !== undefined;
12+
}
13+
14+
export function generateIdentitiesData(identifier: string, traits: Traits, transient: boolean) {
15+
const traitsGenerated = Object.entries(traits).map(([key, value]) => {
16+
if (isTraitConfig(value)) {
17+
return {
18+
trait_key: key,
19+
trait_value: value?.value,
20+
transient: value?.transient,
21+
};
22+
} else {
23+
return {
24+
trait_key: key,
25+
trait_value: value,
26+
};
27+
}
28+
});
29+
if (transient) {
30+
return {
31+
identifier: identifier,
32+
traits: traitsGenerated,
33+
transient: true
34+
};
35+
}
1036
return {
1137
identifier: identifier,
1238
traits: traitsGenerated
@@ -20,7 +46,7 @@ export const retryFetch = (
2046
url: string,
2147
fetchOptions: RequestInit,
2248
retries: number = 3,
23-
timeout: number = 10// set an overall timeout for this function
49+
timeout: number = 10 // set an overall timeout for this function
2450
): Promise<Response> => {
2551
return new Promise((resolve, reject) => {
2652
const retryWrapper = (n: number) => {
@@ -49,9 +75,9 @@ export const retryFetch = (
4975
if (timeoutId) {
5076
clearTimeout(timeoutId);
5177
}
52-
})
53-
})
54-
}
78+
});
79+
});
80+
};
5581

5682
retryWrapper(retries);
5783
});
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
{
2+
"traits": [
3+
{
4+
"id": 1,
5+
"trait_key": "some_trait",
6+
"trait_value": "some_value"
7+
},
8+
{
9+
"id": 2,
10+
"trait_key": "transient_key",
11+
"trait_value": "transient_value",
12+
"transient": true
13+
},
14+
{
15+
"id": 3,
16+
"trait_key": "explicitly_non_transient_trait",
17+
"trait_value": "non_transient_value",
18+
"transient": false
19+
}
20+
],
21+
"flags": [
22+
{
23+
"id": 1,
24+
"feature": {
25+
"id": 1,
26+
"name": "some_feature",
27+
"created_date": "2019-08-27T14:53:45.698555Z",
28+
"initial_value": null,
29+
"description": null,
30+
"default_enabled": false,
31+
"type": "STANDARD",
32+
"project": 1
33+
},
34+
"feature_state_value": "some-identity-with-transient-trait-value",
35+
"enabled": true,
36+
"environment": 1,
37+
"identity": null,
38+
"feature_segment": null
39+
}
40+
]
41+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"traits": [
3+
{
4+
"id": 1,
5+
"trait_key": "some_trait",
6+
"trait_value": "some_value"
7+
}
8+
],
9+
"flags": [
10+
{
11+
"id": 1,
12+
"feature": {
13+
"id": 1,
14+
"name": "some_feature",
15+
"created_date": "2019-08-27T14:53:45.698555Z",
16+
"initial_value": null,
17+
"description": null,
18+
"default_enabled": false,
19+
"type": "STANDARD",
20+
"project": 1
21+
},
22+
"feature_state_value": "some-transient-identity-value",
23+
"enabled": false,
24+
"environment": 1,
25+
"identity": null,
26+
"feature_segment": null
27+
}
28+
]
29+
}

tests/sdk/flagsmith-identity-flags.test.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import Flagsmith from '../../sdk';
22
import fetch from 'node-fetch';
3-
import { environmentJSON, flagsmith, identitiesJSON } from './utils';
3+
import { environmentJSON, flagsmith, identitiesJSON, identityWithTransientTraitsJSON, transientIdentityJSON } from './utils';
44
import { DefaultFlag } from '../../sdk/models';
55

66
jest.mock('node-fetch');
@@ -155,3 +155,72 @@ test('test_get_identity_flags_multivariate_value_with_local_evaluation_enabled',
155155
expect(identityFlags.getFeatureValue('mv_feature')).toBe('bar');
156156
expect(identityFlags.isFeatureEnabled('mv_feature')).toBe(false);
157157
});
158+
159+
160+
test('test_transient_identity', async () => {
161+
// @ts-ignore
162+
fetch.mockReturnValue(Promise.resolve(new Response(transientIdentityJSON())));
163+
const identifier = 'transient_identifier';
164+
const traits = { some_trait: 'some_value' };
165+
const traitsInRequest = [{trait_key:Object.keys(traits)[0],trait_value:traits.some_trait}]
166+
const transient = true;
167+
const flg = flagsmith();
168+
const identityFlags = (await flg.getIdentityFlags(identifier, traits, transient)).allFlags();
169+
170+
expect(fetch).toHaveBeenCalledWith(
171+
`https://edge.api.flagsmith.com/api/v1/identities/`,
172+
{
173+
method: 'POST',
174+
agent: undefined,
175+
headers: { 'Content-Type': 'application/json', 'X-Environment-Key': 'sometestfakekey' },
176+
body: JSON.stringify({identifier, traits: traitsInRequest, transient })
177+
}
178+
);
179+
180+
expect(identityFlags[0].enabled).toBe(false);
181+
expect(identityFlags[0].value).toBe('some-transient-identity-value');
182+
expect(identityFlags[0].featureName).toBe('some_feature');
183+
});
184+
185+
186+
test('test_identity_with_transient_traits', async () => {
187+
// @ts-ignore
188+
fetch.mockReturnValue(Promise.resolve(new Response(identityWithTransientTraitsJSON())));
189+
const identifier = 'transient_trait_identifier';
190+
const traits = {
191+
some_trait: 'some_value',
192+
another_trait: {value: 'another_value', transient: true},
193+
explicitly_non_transient_trait: {value: 'non_transient_value', transient: false}
194+
}
195+
const traitsInRequest = [
196+
{
197+
trait_key:Object.keys(traits)[0],
198+
trait_value:traits.some_trait,
199+
},
200+
{
201+
trait_key:Object.keys(traits)[1],
202+
trait_value:traits.another_trait.value,
203+
transient: true,
204+
},
205+
{
206+
trait_key:Object.keys(traits)[2],
207+
trait_value:traits.explicitly_non_transient_trait.value,
208+
transient: false,
209+
},
210+
]
211+
const flg = flagsmith();
212+
213+
const identityFlags = (await flg.getIdentityFlags(identifier, traits)).allFlags();
214+
expect(fetch).toHaveBeenCalledWith(
215+
`https://edge.api.flagsmith.com/api/v1/identities/`,
216+
{
217+
method: 'POST',
218+
agent: undefined,
219+
headers: { 'Content-Type': 'application/json', 'X-Environment-Key': 'sometestfakekey' },
220+
body: JSON.stringify({identifier, traits: traitsInRequest})
221+
}
222+
);
223+
expect(identityFlags[0].enabled).toBe(true);
224+
expect(identityFlags[0].value).toBe('some-identity-with-transient-trait-value');
225+
expect(identityFlags[0].featureName).toBe('some_feature');
226+
});

0 commit comments

Comments
 (0)