Skip to content

Commit aabd919

Browse files
Add IConfig interface and fetchConfigs method
1 parent bd199f0 commit aabd919

5 files changed

Lines changed: 124 additions & 9 deletions

File tree

src/dtos/types.ts

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

218+
// Superset of ISplit (i.e., ISplit extends IConfig)
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+
[treatmentName: string]: string | 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+
249+
/** Interface of the parsed JSON response of `/configs` */
250+
export interface IConfigsResponse {
251+
configs?: {
252+
t: number,
253+
s?: number,
254+
d: IConfig[]
255+
},
256+
rbs?: {
257+
t: number,
258+
s?: number,
259+
d: IRBSegment[]
260+
}
261+
}
262+
263+
// @TODO: rename to IDefinition (Configs and Feature Flags are definitions)
218264
export interface ISplit {
219265
name: string,
220266
changeNumber: number,
@@ -231,7 +277,7 @@ export interface ISplit {
231277
trafficAllocation?: number,
232278
trafficAllocationSeed?: number
233279
configurations?: {
234-
[treatmentName: string]: string
280+
[treatmentName: string]: string | null
235281
},
236282
sets?: string[],
237283
impressionsDisabled?: boolean

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
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { IConfig, IConfigsResponse, ISplitChangesResponse } from '../../../dtos/types';
2+
import { IFetchSplitChanges, IResponse } from '../../../services/types';
3+
import { ISplitChangesFetcher } from './types';
4+
5+
/**
6+
* Factory of Configs fetcher.
7+
* Configs fetcher is a wrapper around `configs` API service that parses the response and handle errors.
8+
*/
9+
export function configsFetcherFactory(fetchConfigs: IFetchSplitChanges): ISplitChangesFetcher {
10+
11+
return function configsFetcher(
12+
since: number,
13+
noCache?: boolean,
14+
till?: number,
15+
rbSince?: number,
16+
// Optional decorator for `fetchSplitChanges` promise, such as timeout or time tracker
17+
decorator?: (promise: Promise<IResponse>) => Promise<IResponse>
18+
): Promise<ISplitChangesResponse> {
19+
20+
let configsPromise = fetchConfigs(since, noCache, till, rbSince);
21+
if (decorator) configsPromise = decorator(configsPromise);
22+
23+
return configsPromise
24+
.then((resp: IResponse) => resp.json())
25+
.then((configs: IConfigsResponse) => {
26+
return convertConfigsToSplits(configs);
27+
});
28+
};
29+
30+
}
31+
32+
function convertConfigsToSplits(configs: IConfigsResponse): ISplitChangesResponse {
33+
return {
34+
...configs,
35+
ff: configs.configs ? {
36+
...configs.configs,
37+
d: configs.configs.d?.map((config: IConfig) => {
38+
// @TODO: review defaults
39+
return {
40+
...config,
41+
defaultTreatment: config.defaultTreatment,
42+
conditions: config.conditions || [],
43+
killed: config.killed || false,
44+
trafficTypeName: config.trafficTypeName || 'user',
45+
seed: config.seed || 0,
46+
trafficAllocation: config.trafficAllocation || 0,
47+
trafficAllocationSeed: config.trafficAllocationSeed || 0,
48+
};
49+
})
50+
} : undefined,
51+
rbs: configs.rbs
52+
};
53+
}

0 commit comments

Comments
 (0)