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
4 changes: 0 additions & 4 deletions flagsmith-engine/environments/integrations/models.ts

This file was deleted.

7 changes: 2 additions & 5 deletions flagsmith-engine/environments/models.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { FeatureStateModel } from '../features/models';
import { IdentityModel } from '../identities/models';
import { ProjectModel } from '../projects/models';
import { IntegrationModel } from './integrations/models';

export class EnvironmentAPIKeyModel {
id: number;
Expand Down Expand Up @@ -37,10 +37,7 @@ export class EnvironmentModel {
apiKey: string;
project: ProjectModel;
featureStates: FeatureStateModel[] = [];
amplitude_config?: IntegrationModel;
segment_config?: IntegrationModel;
mixpanel_config?: IntegrationModel;
heap_config?: IntegrationModel;
identityOverrides: IdentityModel[] = [];

constructor(id: number, apiKey: string, project: ProjectModel) {
this.id = id;
Expand Down
6 changes: 6 additions & 0 deletions flagsmith-engine/environments/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { buildFeatureStateModel } from '../features/util';
import { buildIdentityModel } from '../identities/util';
import { buildProjectModel } from '../projects/util';
import { EnvironmentAPIKeyModel, EnvironmentModel } from './models';

Expand All @@ -13,6 +14,11 @@ export function buildEnvironmentModel(environmentJSON: any) {
project
);
environmentModel.featureStates = featureStates;
if (!!environmentJSON.identity_overrides) {
environmentModel.identityOverrides = environmentJSON.identity_overrides.map((identityData: any) =>
buildIdentityModel(identityData)
);
}
return environmentModel;
}

Expand Down
27 changes: 14 additions & 13 deletions flagsmith-engine/features/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,26 @@ export function buildFeatureStateModel(featuresStateModelJSON: any): FeatureStat
featuresStateModelJSON.enabled,
featuresStateModelJSON.django_id,
featuresStateModelJSON.feature_state_value,
featuresStateModelJSON.uuid
featuresStateModelJSON.featurestate_uuid
);

featureStateModel.featureSegment = featuresStateModelJSON.feature_segment ?
buildFeatureSegment(featuresStateModelJSON.feature_segment) :
featureStateModel.featureSegment = featuresStateModelJSON.feature_segment ?
buildFeatureSegment(featuresStateModelJSON.feature_segment) :
undefined;

const multivariateFeatureStateValues = featuresStateModelJSON.multivariate_feature_state_values
? featuresStateModelJSON.multivariate_feature_state_values.map((fsv: any) => {
const featureOption = new MultivariateFeatureOptionModel(
fsv.multivariate_feature_option.value,
fsv.multivariate_feature_option.id
);
return new MultivariateFeatureStateValueModel(
featureOption,
fsv.percentage_allocation,
fsv.id
);
})
const featureOption = new MultivariateFeatureOptionModel(
fsv.multivariate_feature_option.value,
fsv.multivariate_feature_option.id
);
return new MultivariateFeatureStateValueModel(
featureOption,
fsv.percentage_allocation,
fsv.id,
fsv.mv_fs_value_uuid
);
})
: [];

featureStateModel.multivariateFeatureStateValues = multivariateFeatureStateValues;
Expand Down
1 change: 0 additions & 1 deletion flagsmith-engine/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { SegmentModel } from './segments/models';
import { FeatureStateNotFound } from './utils/errors';

export { EnvironmentModel } from './environments/models';
export { IntegrationModel } from './environments/integrations/models';
export { FeatureStateModel } from './features/models';
export { IdentityModel } from './identities/models';
export { TraitModel } from './identities/traits/models';
Expand Down
1 change: 0 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ export {

export {
EnvironmentModel,
IntegrationModel,
FeatureStateModel,
IdentityModel,
TraitModel,
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 22 additions & 10 deletions sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export class Flagsmith {
offlineMode: boolean = false;
offlineHandler?: BaseOfflineHandler = undefined;

identitiesWithOverridesByIdentifier?: Map<string, IdentityModel>;

private cache?: FlagsmithCache;
private onEnvironmentChange?: (error: Error | null, result: EnvironmentModel) => void;
private analyticsProcessor?: AnalyticsProcessor;
Expand Down Expand Up @@ -143,13 +145,13 @@ export class Flagsmith {
if (!this.environmentKey) {
throw new Error('ValueError: environmentKey is required.');
}

const apiUrl = data.apiUrl || DEFAULT_API_URL;
this.apiUrl = apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`;
this.environmentFlagsUrl = `${this.apiUrl}flags/`;
this.identitiesUrl = `${this.apiUrl}identities/`;
this.environmentUrl = `${this.apiUrl}environment-document/`;

if (this.enableLocalEvaluation) {
if (!this.environmentKey.startsWith('ser.')) {
console.error(
Expand All @@ -166,11 +168,11 @@ export class Flagsmith {

this.analyticsProcessor = data.enableAnalytics
? new AnalyticsProcessor({
environmentKey: this.environmentKey,
baseApiUrl: this.apiUrl,
requestTimeoutMs: this.requestTimeoutMs,
logger: this.logger
})
environmentKey: this.environmentKey,
baseApiUrl: this.apiUrl,
requestTimeoutMs: this.requestTimeoutMs,
logger: this.logger
})
: undefined;
}
}
Expand Down Expand Up @@ -256,7 +258,7 @@ export class Flagsmith {
if (this.enableLocalEvaluation) {
return new Promise((resolve, reject) => {
return this.environmentPromise!.then(() => {
const identityModel = this.buildIdentityModel(
const identityModel = this.getIdentityModel(
identifier,
Object.keys(traits || {}).map(key => ({
key,
Expand Down Expand Up @@ -289,6 +291,11 @@ export class Flagsmith {
} else {
this.environment = await request;
}
if (this.environment.identityOverrides?.length) {
this.identitiesWithOverridesByIdentifier = new Map<string, IdentityModel>(
this.environment.identityOverrides.map(identity => [identity.identifier, identity]
));
}
if (this.onEnvironmentChange) {
this.onEnvironmentChange(null, this.environment);
}
Expand Down Expand Up @@ -370,7 +377,7 @@ export class Flagsmith {
identifier: string,
traits: { [key: string]: any }
): Promise<Flags> {
const identityModel = this.buildIdentityModel(
const identityModel = this.getIdentityModel(
identifier,
Object.keys(traits).map(key => ({
key,
Expand Down Expand Up @@ -458,8 +465,13 @@ export class Flagsmith {
}
}

private buildIdentityModel(identifier: string, traits: { key: string; value: any }[]) {
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);
}
}
Expand Down
28 changes: 27 additions & 1 deletion tests/sdk/data/environment.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"feature_states": [
{
"feature_state_value": "segment_override",
"featurestate_uuid": "dd77a1ab-08cf-4743-8a3b-19e730444a14",
"multivariate_feature_state_values": [],
"django_id": 81027,
"feature": {
Expand Down Expand Up @@ -88,5 +89,30 @@
"featurestate_uuid": "96fc3503-09d7-48f1-a83b-2dc903d5c08a",
"enabled": false
}
],
"identity_overrides": [
{
"identifier": "overridden-id",
"identity_uuid": "0f21cde8-63c5-4e50-baca-87897fa6cd01",
"created_date": "2019-08-27T14:53:45.698555Z",
"updated_at": "2023-07-14 16:12:00.000000",
"environment_api_key": "B62qaMZNwfiqT76p38ggrQ",
"identity_features": [
{
"id": 1,
"feature": {
"id": 1,
"name": "some_feature",
"type": "STANDARD"
},
"featurestate_uuid": "1bddb9a5-7e59-42c6-9be9-625fa369749f",
"feature_state_value": "some-overridden-value",
"enabled": false,
"environment": 1,
"identity": null,
"feature_segment": null
}
]
}
]
}
}
73 changes: 31 additions & 42 deletions tests/sdk/flagsmith.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import Flagsmith from '../../sdk';
import { EnvironmentDataPollingManager } from '../../sdk/polling_manager';
import fetch, {RequestInit} from 'node-fetch';
import fetch, { RequestInit } from 'node-fetch';
import { environmentJSON, environmentModel, flagsJSON, flagsmith, identitiesJSON } from './utils';
import { DefaultFlag, Flags } from '../../sdk/models';
import {delay, retryFetch} from '../../sdk/utils';
import * as utils from '../../sdk/utils';
import { delay } from '../../sdk/utils';
import { EnvironmentModel } from '../../flagsmith-engine/environments/models';
import https from 'https'
import { BaseOfflineHandler } from '../../sdk/offline_handlers';
Expand Down Expand Up @@ -48,18 +47,15 @@ test('test_update_environment_sets_environment', async () => {

const model = environmentModel(JSON.parse(environmentJSON()));

wipeFeatureStateUUIDs(flg.environment)
wipeFeatureStateUUIDs(model)

expect(flg.environment).toStrictEqual(model);
});

test('test_set_agent_options', async () => {
const agent = new https.Agent({})

// @ts-ignore
fetch.mockImplementation((url:string, options:RequestInit)=>{
if(options.agent!==agent) {
fetch.mockImplementation((url: string, options: RequestInit) => {
if (options.agent !== agent) {
throw new Error("Agent has not been set on retry fetch")
}
return Promise.resolve(new Response(environmentJSON()))
Expand Down Expand Up @@ -276,7 +272,7 @@ test('getIdentitySegments throws error if identifier is empty string', () => {
})


test('offline_mode', async() => {
test('offline_mode', async () => {
// Given
const environment: EnvironmentModel = environmentModel(JSON.parse(environmentJSON('offline-environment.json')));

Expand Down Expand Up @@ -311,19 +307,19 @@ test('test_flagsmith_uses_offline_handler_if_set_and_no_api_response', async ()
const environment: EnvironmentModel = environmentModel(JSON.parse(environmentJSON('offline-environment.json')));
const api_url = 'http://some.flagsmith.com/api/v1/';
const mock_offline_handler = new BaseOfflineHandler() as jest.Mocked<BaseOfflineHandler>;

jest.spyOn(mock_offline_handler, 'getEnvironment').mockReturnValue(environment);

const flagsmith = new Flagsmith({
environmentKey: 'some-key',
apiUrl: api_url,
offlineHandler: mock_offline_handler,
environmentKey: 'some-key',
apiUrl: api_url,
offlineHandler: mock_offline_handler,
});

jest.spyOn(flagsmith, 'getEnvironmentFlags');
jest.spyOn(flagsmith, 'getIdentityFlags');


flagsmith.environmentFlagsUrl = 'http://some.flagsmith.com/api/v1/environment-flags';
flagsmith.identitiesUrl = 'http://some.flagsmith.com/api/v1/identities';

Expand All @@ -337,64 +333,57 @@ test('test_flagsmith_uses_offline_handler_if_set_and_no_api_response', async ()
fetch.mockReturnValue(Promise.resolve(errorResponse));

// When
const environmentFlags:Flags = await flagsmith.getEnvironmentFlags();
const identityFlags:Flags = await flagsmith.getIdentityFlags('identity', {});
const environmentFlags: Flags = await flagsmith.getEnvironmentFlags();
const identityFlags: Flags = await flagsmith.getIdentityFlags('identity', {});

// Then
expect(mock_offline_handler.getEnvironment).toHaveBeenCalledTimes(1);
expect(flagsmith.getEnvironmentFlags).toHaveBeenCalled();
expect(flagsmith.getIdentityFlags).toHaveBeenCalled();

expect(environmentFlags.isFeatureEnabled('some_feature')).toBe(true);
expect(environmentFlags.getFeatureValue('some_feature')).toBe('offline-value');

expect(identityFlags.isFeatureEnabled('some_feature')).toBe(true);
expect(identityFlags.getFeatureValue('some_feature')).toBe('offline-value');
});

test('cannot use offline mode without offline handler', () => {
// When and Then
expect(() => new Flagsmith({ offlineMode: true, offlineHandler: undefined })).toThrowError(
'ValueError: offlineHandler must be provided to use offline mode.'
'ValueError: offlineHandler must be provided to use offline mode.'
);
});

test('cannot use both default handler and offline handler', () => {
// When and Then
expect(() => new Flagsmith({
offlineHandler: new BaseOfflineHandler(),
defaultFlagHandler: (flagName) => new DefaultFlag('foo', true)
offlineHandler: new BaseOfflineHandler(),
defaultFlagHandler: (flagName) => new DefaultFlag('foo', true)
})).toThrowError('ValueError: Cannot use both defaultFlagHandler and offlineHandler.');
});

test('cannot create Flagsmith client in remote evaluation without API key', () => {
// When and Then
// @ts-ignore
expect(() => new Flagsmith()).toThrowError('ValueError: environmentKey is required.');
});


async function wipeFeatureStateUUIDs (environmentModel: EnvironmentModel) {
// TODO: this has been pulled out of tests above as a helper function.
// I'm not entirely sure why it's necessary, however, we should look to remove.
environmentModel.featureStates.forEach(fs => {
// @ts-ignore
fs.featurestateUUID = undefined;
fs.multivariateFeatureStateValues.forEach(mvfsv => {
// @ts-ignore
mvfsv.mvFsValueUuid = undefined;
})
});
environmentModel.project.segments.forEach(s => {
s.featureStates.forEach(fs => {
// @ts-ignore
fs.featurestateUUID = undefined;
})
})
}

function sleep(ms: number) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
test('test_localEvaluation_true__identity_overrides_evaluated', async () => {
// @ts-ignore
fetch.mockReturnValue(Promise.resolve(new Response(environmentJSON())));

const flg = new Flagsmith({
environmentKey: 'ser.key',
enableLocalEvaluation: true,
});

const flags = await flg.getIdentityFlags("overridden-id");
expect(flags.getFeatureValue("some_feature")).toEqual("some-overridden-value");
});