Skip to content
Open
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
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
53 changes: 53 additions & 0 deletions src/sync/polling/fetchers/configsFetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { IConfig, IConfigsResponse, ISplitChangesResponse } 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((configs: IConfigsResponse) => {
return convertConfigsToSplits(configs);
});
};

}

function convertConfigsToSplits(configs: IConfigsResponse): ISplitChangesResponse {
return {
...configs,
ff: configs.configs ? {
...configs.configs,
d: configs.configs.d?.map((config: IConfig) => {
// @TODO: review defaults
return {
...config,
defaultTreatment: config.defaultTreatment,
conditions: config.conditions || [],
killed: config.killed || false,
trafficTypeName: config.trafficTypeName || 'user',
seed: config.seed || 0,
trafficAllocation: config.trafficAllocation || 0,
trafficAllocationSeed: config.trafficAllocationSeed || 0,
};
})
} : 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