Skip to content
46 changes: 46 additions & 0 deletions src/dtos/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,52 @@ export interface IRBSegment {
} | null
}

// Superset of ISplit (i.e., ISplit extends IConfig)
// - with optional fields related to targeting information and
// - an optional link fields that binds configurations to other entities
export interface IConfig {
name: string,
changeNumber: number,
status?: 'ACTIVE' | 'ARCHIVED',
conditions?: ISplitCondition[] | null,
prerequisites?: null | {
n: string,
ts: string[]
}[]
killed?: boolean,
defaultTreatment: string,
trafficTypeName?: string,
seed?: number,
trafficAllocation?: number,
trafficAllocationSeed?: number
configurations?: {
[treatmentName: string]: string
},
sets?: string[],
impressionsDisabled?: boolean,
// a map of entities (e.g., pipeline, feature-flag, etc) to configuration variants
links?: {
[entityType: string]: {
[entityName: string]: string
}
}
}

/** Interface of the parsed JSON response of `/configs` */
export interface IConfigsResponse {
configs?: {
t: number,
s?: number,
d: IConfig[]
},
rbs?: {
t: number,
s?: number,
d: IRBSegment[]
}
}

// @TODO: rename to IDefinition (Configs and Feature Flags are definitions)
export interface ISplit {
name: string,
changeNumber: number,
Expand Down
24 changes: 17 additions & 7 deletions src/services/__tests__/splitApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,27 @@ describe('splitApi', () => {
assertHeaders(settings, headers);
expect(url).toBe(expectedFlagsUrl(-1, 100, settings.validateFilters || false, settings, -1));

splitApi.fetchConfigs(-1, false, 100, -1);
[url, { headers }] = fetchMock.mock.calls[4];
assertHeaders(settings, headers);
expect(url).toBe(expectedConfigsUrl(-1, 100, settings.validateFilters || false, settings, -1));

splitApi.postEventsBulk('fake-body');
assertHeaders(settings, fetchMock.mock.calls[4][1].headers);
assertHeaders(settings, fetchMock.mock.calls[5][1].headers);

splitApi.postTestImpressionsBulk('fake-body');
assertHeaders(settings, fetchMock.mock.calls[5][1].headers);
expect(fetchMock.mock.calls[5][1].headers['SplitSDKImpressionsMode']).toBe(settings.sync.impressionsMode);
assertHeaders(settings, fetchMock.mock.calls[6][1].headers);
expect(fetchMock.mock.calls[6][1].headers['SplitSDKImpressionsMode']).toBe(settings.sync.impressionsMode);

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

splitApi.postMetricsConfig('fake-body');
assertHeaders(settings, fetchMock.mock.calls[7][1].headers);
splitApi.postMetricsUsage('fake-body');
assertHeaders(settings, fetchMock.mock.calls[8][1].headers);
splitApi.postMetricsUsage('fake-body');
assertHeaders(settings, fetchMock.mock.calls[9][1].headers);

expect(telemetryTrackerMock.trackHttp).toBeCalledTimes(9);
expect(telemetryTrackerMock.trackHttp).toBeCalledTimes(10);

telemetryTrackerMock.trackHttp.mockClear();
fetchMock.mockClear();
Expand All @@ -70,6 +75,11 @@ describe('splitApi', () => {
const filterQueryString = settings.sync.__splitFiltersValidation && settings.sync.__splitFiltersValidation.queryString;
return `sdk/splitChanges?s=1.1&since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${usesFilter ? filterQueryString : ''}${till ? '&till=' + till : ''}`;
}

function expectedConfigsUrl(since: number, till: number, usesFilter: boolean, settings: ISettings, rbSince?: number) {
const filterQueryString = settings.sync.__splitFiltersValidation && settings.sync.__splitFiltersValidation.queryString;
return `sdk/configs?${settings.sync.flagSpecVersion ? `s=${settings.sync.flagSpecVersion}&` : ''}since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${usesFilter ? filterQueryString : ''}${till ? '&till=' + till : ''}`;
}
});

test('rejects requests if fetch Api is not provided', (done) => {
Expand Down
7 changes: 6 additions & 1 deletion src/services/splitApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { splitHttpClientFactory } from './splitHttpClient';
import { ISplitApi } from './types';
import { objectAssign } from '../utils/lang/objectAssign';
import { ITelemetryTracker } from '../trackers/types';
import { SPLITS, IMPRESSIONS, IMPRESSIONS_COUNT, EVENTS, TELEMETRY, TOKEN, SEGMENT, MEMBERSHIPS } from '../utils/constants';
import { SPLITS, CONFIGS, IMPRESSIONS, IMPRESSIONS_COUNT, EVENTS, TELEMETRY, TOKEN, SEGMENT, MEMBERSHIPS } from '../utils/constants';
import { ERROR_TOO_MANY_SETS } from '../logger/constants';

const noCacheHeaderOptions = { headers: { 'Cache-Control': 'no-cache' } };
Expand Down Expand Up @@ -61,6 +61,11 @@ export function splitApiFactory(
});
},

fetchConfigs(since: number, noCache?: boolean, till?: number, rbSince?: number) {
const url = `${urls.sdk}/configs?${settings.sync.flagSpecVersion ? `s=${settings.sync.flagSpecVersion}&` : ''}since=${since}${rbSince ? '&rbSince=' + rbSince : ''}${filterQueryString || ''}${till ? '&till=' + till : ''}`;
return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined, telemetryTracker.trackHttp(CONFIGS));
},

fetchSegmentChanges(since: number, segmentName: string, noCache?: boolean, till?: number) {
const url = `${urls.sdk}/segmentChanges/${segmentName}?since=${since}${till ? '&till=' + till : ''}`;
return splitHttpClient(url, noCache ? noCacheHeaderOptions : undefined, telemetryTracker.trackHttp(SEGMENT));
Expand Down
1 change: 1 addition & 0 deletions src/services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export interface ISplitApi {
getEventsAPIHealthCheck: IHealthCheckAPI
fetchAuth: IFetchAuth
fetchSplitChanges: IFetchSplitChanges
fetchConfigs: IFetchSplitChanges
fetchSegmentChanges: IFetchSegmentChanges
fetchMemberships: IFetchMemberships
postEventsBulk: IPostEventsBulk
Expand Down
70 changes: 70 additions & 0 deletions src/sync/polling/fetchers/configsFetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { IConfig, IConfigsResponse, ISplitChangesResponse, ISplitCondition } from '../../../dtos/types';
import { IFetchSplitChanges, IResponse } from '../../../services/types';
import { ISplitChangesFetcher } from './types';

/**
* Factory of Configs fetcher.
* Configs fetcher is a wrapper around `configs` API service that parses the response and handle errors.
*/
export function configsFetcherFactory(fetchConfigs: IFetchSplitChanges): ISplitChangesFetcher {

return function configsFetcher(
since: number,
noCache?: boolean,
till?: number,
rbSince?: number,
// Optional decorator for `fetchSplitChanges` promise, such as timeout or time tracker
decorator?: (promise: Promise<IResponse>) => Promise<IResponse>
): Promise<ISplitChangesResponse> {

let configsPromise = fetchConfigs(since, noCache, till, rbSince);
if (decorator) configsPromise = decorator(configsPromise);

return configsPromise
.then((resp: IResponse) => resp.json())
.then(convertConfigsResponseToSplitChangesResponse);
};

}

function defaultCondition(treatment: string): ISplitCondition {
return {
conditionType: 'ROLLOUT',
matcherGroup: {
combiner: 'AND',
matchers: [{
keySelector: null,
matcherType: 'ALL_KEYS',
negate: false
}],
},
partitions: [{ treatment, size: 100 }],
label: 'default rule',
};
}

function convertConfigToDefinitionDTO(config: IConfig) {
const defaultTreatment = config.defaultTreatment || 'default';

return {
...config,
defaultTreatment,
trafficTypeName: config.trafficTypeName || 'user',
conditions: config.conditions && config.conditions.length > 0 ? config.conditions : [defaultCondition(defaultTreatment)],
killed: config.killed || false,
seed: config.seed || 0,
trafficAllocation: config.trafficAllocation || 100,
trafficAllocationSeed: config.trafficAllocationSeed || 0,
};
}

function convertConfigsResponseToSplitChangesResponse(configs: IConfigsResponse): ISplitChangesResponse {
return {
...configs,
ff: configs.configs ? {
...configs.configs,
d: configs.configs.d?.map(convertConfigToDefinitionDTO)
} : undefined,
rbs: configs.rbs
};
}
3 changes: 2 additions & 1 deletion src/sync/submitters/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ export type TELEMETRY = 'te';
export type TOKEN = 'to';
export type SEGMENT = 'se';
export type MEMBERSHIPS = 'ms';
export type OperationType = SPLITS | IMPRESSIONS | IMPRESSIONS_COUNT | EVENTS | TELEMETRY | TOKEN | SEGMENT | MEMBERSHIPS;
export type CONFIGS = 'cf';
export type OperationType = SPLITS | IMPRESSIONS | IMPRESSIONS_COUNT | EVENTS | TELEMETRY | TOKEN | SEGMENT | MEMBERSHIPS | CONFIGS;

export type LastSync = Partial<Record<OperationType, number | undefined>>
export type HttpErrors = Partial<Record<OperationType, { [statusCode: string]: number }>>
Expand Down
1 change: 1 addition & 0 deletions src/utils/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export const TELEMETRY = 'te';
export const TOKEN = 'to';
export const SEGMENT = 'se';
export const MEMBERSHIPS = 'ms';
export const CONFIGS = 'cf';

export const TREATMENT = 't';
export const TREATMENTS = 'ts';
Expand Down