Skip to content

Commit 767324d

Browse files
Add IConfig DTO and Configs SDK client wrapper
1 parent 008756a commit 767324d

15 files changed

Lines changed: 305 additions & 29 deletions

File tree

src/dtos/types.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,37 @@ export interface IRBSegment {
215215
} | null
216216
}
217217

218+
// Similar to ISplit
219+
// - with optional fields related to targeting information and
220+
// - an optional link fields that binds configurations to other entities
221+
export interface IConfig {
222+
name: string,
223+
changeNumber: number,
224+
status?: 'ACTIVE' | 'ARCHIVED',
225+
conditions?: ISplitCondition[] | null,
226+
prerequisites?: null | {
227+
n: string,
228+
ts: string[]
229+
}[]
230+
killed?: boolean,
231+
defaultTreatment: string,
232+
trafficTypeName?: string,
233+
seed?: number,
234+
trafficAllocation?: number,
235+
trafficAllocationSeed?: number
236+
configurations: {
237+
[variantName: string]: string | object | null
238+
},
239+
sets?: string[],
240+
impressionsDisabled?: boolean,
241+
// a map of entities (e.g., pipeline, feature-flag, etc) to configuration variants
242+
links?: {
243+
[entityType: string]: {
244+
[entityName: string]: string
245+
}
246+
}
247+
}
248+
218249
export interface ISplit {
219250
name: string,
220251
changeNumber: number,
@@ -231,7 +262,7 @@ export interface ISplit {
231262
trafficAllocation?: number,
232263
trafficAllocationSeed?: number
233264
configurations?: {
234-
[treatmentName: string]: string
265+
[treatmentName: string]: string | object | null
235266
},
236267
sets?: string[],
237268
impressionsDisabled?: boolean
@@ -254,6 +285,20 @@ export interface ISplitChangesResponse {
254285
}
255286
}
256287

288+
/** Interface of the parsed JSON response of `/configs` */
289+
export interface IConfigsResponse {
290+
configs?: {
291+
t: number,
292+
s?: number,
293+
d: IConfig[]
294+
},
295+
rbs?: {
296+
t: number,
297+
s?: number,
298+
d: IRBSegment[]
299+
}
300+
}
301+
257302
/** Interface of the parsed JSON response of `/segmentChanges/{segmentName}` */
258303
export interface ISegmentChangesResponse {
259304
name: string,

src/evaluator/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export interface IEvaluation {
2222
treatment?: string,
2323
label: string,
2424
changeNumber?: number,
25-
config?: string | null
25+
config?: string | object | null
2626
}
2727

2828
export type IEvaluationResult = IEvaluation & { treatment: string; impressionsDisabled?: boolean }

src/sdkClient/client.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { validateSplitExistence } from '../utils/inputValidation/splitExistence'
55
import { validateTrafficTypeExistence } from '../utils/inputValidation/trafficTypeExistence';
66
import { SDK_NOT_READY } from '../utils/labels';
77
import { CONTROL, TREATMENT, TREATMENTS, TREATMENT_WITH_CONFIG, TREATMENTS_WITH_CONFIG, TRACK, TREATMENTS_WITH_CONFIG_BY_FLAGSETS, TREATMENTS_BY_FLAGSETS, TREATMENTS_BY_FLAGSET, TREATMENTS_WITH_CONFIG_BY_FLAGSET, GET_TREATMENTS_WITH_CONFIG, GET_TREATMENTS_BY_FLAG_SETS, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, GET_TREATMENTS_BY_FLAG_SET, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET, GET_TREATMENT_WITH_CONFIG, GET_TREATMENT, GET_TREATMENTS, TRACK_FN_LABEL } from '../utils/constants';
8-
import { IEvaluationResult } from '../evaluator/types';
8+
import { IEvaluation, IEvaluationResult } from '../evaluator/types';
99
import SplitIO from '../../types/splitio';
1010
import { IMPRESSION, IMPRESSION_QUEUEING } from '../logger/constants';
1111
import { ISdkFactoryContext } from '../sdkFactory/types';
@@ -72,7 +72,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
7272
const treatments: SplitIO.Treatments | SplitIO.TreatmentsWithConfig = {};
7373
const properties = stringify(options);
7474
Object.keys(evaluationResults).forEach(featureFlagName => {
75-
treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue);
75+
treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue) as SplitIO.Treatment | SplitIO.TreatmentWithConfig;
7676
});
7777
impressionsTracker.track(queue, attributes);
7878

@@ -101,7 +101,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
101101
const treatments: SplitIO.Treatments | SplitIO.TreatmentsWithConfig = {};
102102
const properties = stringify(options);
103103
Object.keys(evaluationResults).forEach(featureFlagName => {
104-
treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue);
104+
treatments[featureFlagName] = processEvaluation(evaluationResults[featureFlagName], featureFlagName, key, properties, withConfig, methodName, queue) as SplitIO.Treatment | SplitIO.TreatmentWithConfig;
105105
});
106106
impressionsTracker.track(queue, attributes);
107107

@@ -139,7 +139,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
139139
withConfig: boolean,
140140
invokingMethodName: string,
141141
queue: ImpressionDecorated[]
142-
): SplitIO.Treatment | SplitIO.TreatmentWithConfig {
142+
): SplitIO.Treatment | Pick<IEvaluation, 'treatment' | 'config'> {
143143
const matchingKey = getMatching(key);
144144
const bucketingKey = getBucketing(key);
145145

src/sdkConfig/configObject.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import SplitIO from '../../types/splitio';
2+
import { isString } from '../utils/lang';
3+
4+
function createConfigObject(value: any): SplitIO.Config {
5+
return {
6+
value,
7+
getString(propertyName: string, propertyDefaultValue?: string): string {
8+
const val = value != null ? value[propertyName] : undefined;
9+
if (typeof val === 'string') return val;
10+
return propertyDefaultValue !== undefined ? propertyDefaultValue : '';
11+
},
12+
getNumber(propertyName: string, propertyDefaultValue?: number): number {
13+
const val = value != null ? value[propertyName] : undefined;
14+
if (typeof val === 'number') return val;
15+
return propertyDefaultValue !== undefined ? propertyDefaultValue : 0;
16+
},
17+
getBoolean(propertyName: string, propertyDefaultValue?: boolean): boolean {
18+
const val = value != null ? value[propertyName] : undefined;
19+
if (typeof val === 'boolean') return val;
20+
return propertyDefaultValue !== undefined ? propertyDefaultValue : false;
21+
},
22+
getArray(propertyName: string): SplitIO.ConfigArray {
23+
const val = value != null ? value[propertyName] : undefined;
24+
return createConfigArrayObject(Array.isArray(val) ? val : []);
25+
},
26+
getObject(propertyName: string): SplitIO.Config {
27+
const val = value != null ? value[propertyName] : undefined;
28+
return createConfigObject(val != null && typeof val === 'object' && !Array.isArray(val) ? val : null);
29+
}
30+
};
31+
}
32+
33+
function createConfigArrayObject(arr: any[]): SplitIO.ConfigArray {
34+
return {
35+
value: arr,
36+
getString(index: number, propertyDefaultValue?: string): string {
37+
const val = arr[index];
38+
if (typeof val === 'string') return val;
39+
return propertyDefaultValue !== undefined ? propertyDefaultValue : '';
40+
},
41+
getNumber(index: number, propertyDefaultValue?: number): number {
42+
const val = arr[index];
43+
if (typeof val === 'number') return val;
44+
return propertyDefaultValue !== undefined ? propertyDefaultValue : 0;
45+
},
46+
getBoolean(index: number, propertyDefaultValue?: boolean): boolean {
47+
const val = arr[index];
48+
if (typeof val === 'boolean') return val;
49+
return propertyDefaultValue !== undefined ? propertyDefaultValue : false;
50+
},
51+
getArray(index: number): SplitIO.ConfigArray {
52+
const val = arr[index];
53+
return createConfigArrayObject(Array.isArray(val) ? val : []);
54+
},
55+
getObject(index: number): SplitIO.Config {
56+
const val = arr[index];
57+
return createConfigObject(val != null && typeof val === 'object' && !Array.isArray(val) ? val : null);
58+
}
59+
};
60+
}
61+
62+
export function parseConfig(config: string | object | null): SplitIO.Config {
63+
try {
64+
// @ts-ignore
65+
return createConfigObject(isString(config) ? JSON.parse(config) : config);
66+
} catch {
67+
return createConfigObject(null);
68+
}
69+
}

src/sdkConfig/index-ff-wrapper.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { ISdkFactoryParams } from '../sdkFactory/types';
2+
import { sdkFactory } from '../sdkFactory/index';
3+
import SplitIO from '../../types/splitio';
4+
import { objectAssign } from '../utils/lang/objectAssign';
5+
import { parseConfig } from './configObject';
6+
import { validateTarget } from '../utils/inputValidation/target';
7+
import { GET_CONFIG } from '../utils/constants';
8+
import { ISettings } from '../types';
9+
10+
/**
11+
* Configs SDK Client factory implemented as a wrapper over the FF SDK.
12+
* Exposes getConfig and track at the root level instead of requiring a client() call.
13+
* getConfig delegates to getTreatmentWithConfig and wraps the parsed JSON config in a Config object.
14+
*/
15+
export function configsClientFactory(params: ISdkFactoryParams): SplitIO.ConfigsClient {
16+
const ffSdk = sdkFactory({ ...params, lazyInit: true }) as (SplitIO.ISDK | SplitIO.IAsyncSDK) & { init(): void };
17+
const ffClient = ffSdk.client() as SplitIO.IClient & { init(): void; flush(): Promise<void> };
18+
const ffManager = ffSdk.manager();
19+
const log = (ffSdk.settings as ISettings).log;
20+
21+
return objectAssign(
22+
// Inherit status interface (EventEmitter, Event, getStatus, ready, whenReady, whenReadyFromCache) from ffClient
23+
Object.create(ffClient) as SplitIO.IStatusInterface,
24+
{
25+
settings: ffSdk.settings,
26+
Logger: ffSdk.Logger,
27+
28+
init() {
29+
ffSdk.init();
30+
},
31+
32+
flush(): Promise<void> {
33+
return ffClient.flush();
34+
},
35+
36+
destroy(): Promise<void> {
37+
return ffSdk.destroy();
38+
},
39+
40+
getConfig(name: string, target?: SplitIO.Target): SplitIO.Config {
41+
if (target) {
42+
// Serve config with target
43+
if (validateTarget(log, target, GET_CONFIG)) {
44+
const result = ffClient.getTreatmentWithConfig(target.key, name, target.attributes, target) as SplitIO.TreatmentWithConfig;
45+
return parseConfig(result.config);
46+
} else {
47+
log.error('Invalid target for getConfig.');
48+
}
49+
}
50+
51+
// Serve config without target
52+
const config = ffManager.split(name) as SplitIO.SplitView;
53+
if (!config) {
54+
log.error('Provided config name does not exist. Serving empty config object.');
55+
return parseConfig({});
56+
}
57+
58+
log.info('Serving default config variant, ' + config.defaultTreatment + ' for config ' + name);
59+
const defaultConfigVariant = config.configs[config.defaultTreatment];
60+
return parseConfig(defaultConfigVariant);
61+
},
62+
63+
track(key: SplitIO.SplitKey, trafficType: string, eventType: string, value?: number, properties?: SplitIO.Properties): boolean {
64+
return ffClient.track(key, trafficType, eventType, value, properties) as boolean;
65+
}
66+
}
67+
);
68+
}

src/sdkManager/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ function objectToView(splitObject: ISplit | null): SplitIO.SplitView | null {
2929
killed: splitObject.killed,
3030
changeNumber: splitObject.changeNumber || 0,
3131
treatments: collectTreatments(splitObject),
32-
configs: splitObject.configurations || {},
32+
configs: splitObject.configurations as Record<string, string> || {},
3333
sets: splitObject.sets || [],
3434
defaultTreatment: splitObject.defaultTreatment,
3535
impressionsDisabled: splitObject.impressionsDisabled === true,

src/services/__tests__/splitApi.spec.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,27 @@ describe('splitApi', () => {
4545
assertHeaders(settings, headers);
4646
expect(url).toBe(expectedFlagsUrl(-1, 100, settings.validateFilters || false, settings, -1));
4747

48+
splitApi.fetchConfigs(-1, false, 100, -1);
49+
[url, { headers }] = fetchMock.mock.calls[4];
50+
assertHeaders(settings, headers);
51+
expect(url).toBe(expectedConfigsUrl(-1, 100, settings.validateFilters || false, settings, -1));
52+
4853
splitApi.postEventsBulk('fake-body');
49-
assertHeaders(settings, fetchMock.mock.calls[4][1].headers);
54+
assertHeaders(settings, fetchMock.mock.calls[5][1].headers);
5055

5156
splitApi.postTestImpressionsBulk('fake-body');
52-
assertHeaders(settings, fetchMock.mock.calls[5][1].headers);
53-
expect(fetchMock.mock.calls[5][1].headers['SplitSDKImpressionsMode']).toBe(settings.sync.impressionsMode);
57+
assertHeaders(settings, fetchMock.mock.calls[6][1].headers);
58+
expect(fetchMock.mock.calls[6][1].headers['SplitSDKImpressionsMode']).toBe(settings.sync.impressionsMode);
5459

5560
splitApi.postTestImpressionsCount('fake-body');
56-
assertHeaders(settings, fetchMock.mock.calls[6][1].headers);
61+
assertHeaders(settings, fetchMock.mock.calls[7][1].headers);
5762

5863
splitApi.postMetricsConfig('fake-body');
59-
assertHeaders(settings, fetchMock.mock.calls[7][1].headers);
60-
splitApi.postMetricsUsage('fake-body');
6164
assertHeaders(settings, fetchMock.mock.calls[8][1].headers);
65+
splitApi.postMetricsUsage('fake-body');
66+
assertHeaders(settings, fetchMock.mock.calls[9][1].headers);
6267

63-
expect(telemetryTrackerMock.trackHttp).toBeCalledTimes(9);
68+
expect(telemetryTrackerMock.trackHttp).toBeCalledTimes(10);
6469

6570
telemetryTrackerMock.trackHttp.mockClear();
6671
fetchMock.mockClear();
@@ -70,6 +75,11 @@ describe('splitApi', () => {
7075
const filterQueryString = settings.sync.__splitFiltersValidation && settings.sync.__splitFiltersValidation.queryString;
7176
return `sdk/splitChanges?s=1.1&since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${usesFilter ? filterQueryString : ''}${till ? '&till=' + till : ''}`;
7277
}
78+
79+
function expectedConfigsUrl(since: number, till: number, usesFilter: boolean, settings: ISettings, rbSince?: number) {
80+
const filterQueryString = settings.sync.__splitFiltersValidation && settings.sync.__splitFiltersValidation.queryString;
81+
return `sdk/configs?${settings.sync.flagSpecVersion ? `s=${settings.sync.flagSpecVersion}&` : ''}since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${usesFilter ? filterQueryString : ''}${till ? '&till=' + till : ''}`;
82+
}
7383
});
7484

7585
test('rejects requests if fetch Api is not provided', (done) => {

src/services/splitApi.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { splitHttpClientFactory } from './splitHttpClient';
44
import { ISplitApi } from './types';
55
import { objectAssign } from '../utils/lang/objectAssign';
66
import { ITelemetryTracker } from '../trackers/types';
7-
import { SPLITS, IMPRESSIONS, IMPRESSIONS_COUNT, EVENTS, TELEMETRY, TOKEN, SEGMENT, MEMBERSHIPS } from '../utils/constants';
7+
import { SPLITS, CONFIGS, IMPRESSIONS, IMPRESSIONS_COUNT, EVENTS, TELEMETRY, TOKEN, SEGMENT, MEMBERSHIPS } from '../utils/constants';
88
import { ERROR_TOO_MANY_SETS } from '../logger/constants';
99

1010
const noCacheHeaderOptions = { headers: { 'Cache-Control': 'no-cache' } };
@@ -61,6 +61,11 @@ export function splitApiFactory(
6161
});
6262
},
6363

64+
fetchConfigs(since: number, noCache?: boolean, till?: number, rbSince?: number) {
65+
const url = `${urls.sdk}/configs?${settings.sync.flagSpecVersion ? `s=${settings.sync.flagSpecVersion}&` : ''}since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${filterQueryString || ''}${till ? '&till=' + till : ''}`;
66+
return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined, telemetryTracker.trackHttp(CONFIGS));
67+
},
68+
6469
fetchSegmentChanges(since: number, segmentName: string, noCache?: boolean, till?: number) {
6570
const url = `${urls.sdk}/segmentChanges/${segmentName}?since=${since}${till ? '&till=' + till : ''}`;
6671
return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined, telemetryTracker.trackHttp(SEGMENT));

src/services/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export interface ISplitApi {
6060
getEventsAPIHealthCheck: IHealthCheckAPI
6161
fetchAuth: IFetchAuth
6262
fetchSplitChanges: IFetchSplitChanges
63+
fetchConfigs: IFetchSplitChanges
6364
fetchSegmentChanges: IFetchSegmentChanges
6465
fetchMemberships: IFetchMemberships
6566
postEventsBulk: IPostEventsBulk

src/storages/KeyBuilderSS.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const METHOD_NAMES: Record<Method, string> = {
1111
tfs: 'treatmentsByFlagSets',
1212
tcf: 'treatmentsWithConfigByFlagSet',
1313
tcfs: 'treatmentsWithConfigByFlagSets',
14+
c: 'config',
1415
tr: 'track'
1516
};
1617

0 commit comments

Comments
 (0)