diff --git a/src/dtos/types.ts b/src/dtos/types.ts index 2b0ee4ef..3974d37f 100644 --- a/src/dtos/types.ts +++ b/src/dtos/types.ts @@ -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, diff --git a/src/services/__tests__/splitApi.spec.ts b/src/services/__tests__/splitApi.spec.ts index 196266a3..c2f63500 100644 --- a/src/services/__tests__/splitApi.spec.ts +++ b/src/services/__tests__/splitApi.spec.ts @@ -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(); @@ -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) => { diff --git a/src/services/splitApi.ts b/src/services/splitApi.ts index 6860b022..67d7834f 100644 --- a/src/services/splitApi.ts +++ b/src/services/splitApi.ts @@ -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' } }; @@ -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)); diff --git a/src/services/types.ts b/src/services/types.ts index b747dbb5..fa2261fb 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -60,6 +60,7 @@ export interface ISplitApi { getEventsAPIHealthCheck: IHealthCheckAPI fetchAuth: IFetchAuth fetchSplitChanges: IFetchSplitChanges + fetchConfigs: IFetchSplitChanges fetchSegmentChanges: IFetchSegmentChanges fetchMemberships: IFetchMemberships postEventsBulk: IPostEventsBulk diff --git a/src/sync/polling/fetchers/configsFetcher.ts b/src/sync/polling/fetchers/configsFetcher.ts new file mode 100644 index 00000000..0daeee7e --- /dev/null +++ b/src/sync/polling/fetchers/configsFetcher.ts @@ -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) => Promise + ): Promise { + + 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 + }; +} diff --git a/src/sync/submitters/types.ts b/src/sync/submitters/types.ts index 36a76c9b..57e8bfd5 100644 --- a/src/sync/submitters/types.ts +++ b/src/sync/submitters/types.ts @@ -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> export type HttpErrors = Partial> diff --git a/src/utils/constants/index.ts b/src/utils/constants/index.ts index 6686c68e..b9c8edc1 100644 --- a/src/utils/constants/index.ts +++ b/src/utils/constants/index.ts @@ -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';