Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
7 changes: 7 additions & 0 deletions src/dtos/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,9 @@ export interface IRBSegment extends TargetingEntity {
} | null
}

export type ConfigType = 'STANDARD' | 'AI';
export type ConfigSubtype = 'LLM_CALL';

export interface IDefinition extends TargetingEntity {
trafficTypeName: string;
sets?: string[] | null;
Expand All @@ -231,6 +234,10 @@ export interface IDefinition extends TargetingEntity {
configurations?: {
[treatmentName: string]: string | SplitIO.JsonObject
} | null;
/** Definition classification. Absent means a feature flag. */
type?: ConfigType;
/** Only meaningful when `type === 'AI'`. */
subtype?: ConfigSubtype;
}

/** Interface of the parsed JSON response of `/splitChanges` */
Expand Down
96 changes: 32 additions & 64 deletions src/evaluator/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,29 +47,14 @@ export function evaluateFeature(
return EVALUATION_EXCEPTION;
}

if (thenable(definition)) {
return definition.then((definition) => getEvaluation(
log,
key,
definition,
attributes,
storage,
options,
)).catch(
// Exception on async storage. For example, when the storage is redis or
// pluggable and there is a connection issue and we can't retrieve the split to be evaluated
() => EVALUATION_EXCEPTION
);
}

return getEvaluation(
log,
key,
definition,
attributes,
storage,
options,
);
return thenable(definition) ?
definition.then((definition) => getEvaluation(log, key, definition, attributes, storage, options))
.catch(
// Exception on async storage. For example, when the storage is redis or
// pluggable and there is a connection issue and we can't retrieve the split to be evaluated
() => EVALUATION_EXCEPTION
) :
getEvaluation(log, key, definition, attributes, storage, options);
}

export function evaluateFeatures(
Expand All @@ -91,11 +76,11 @@ export function evaluateFeatures(

return thenable(definitions) ?
definitions.then(definitions => getEvaluations(log, key, definitionNames, definitions, attributes, storage, options))
.catch(() => {
.catch(
// Exception on async storage. For example, when the storage is redis or
// pluggable and there is a connection issue and we can't retrieve the split to be evaluated
return treatmentsException(definitionNames);
}) :
() => treatmentsException(definitionNames)
) :
getEvaluations(log, key, definitionNames, definitions, attributes, storage, options);
}

Expand Down Expand Up @@ -137,12 +122,21 @@ export function evaluateFeaturesByFlagSets(
// evaluate related features
return thenable(storedFlagNames) ?
storedFlagNames.then((storedFlagNames) => evaluate(storedFlagNames))
.catch(() => {
return {};
}) :
.catch(() => ({})) :
evaluate(storedFlagNames);
}

function setEvaluationDataFromDefinition(evaluation: IEvaluationResult, definition: IDefinition, options?: SplitIO.EvaluationOptions): IEvaluationResult {
evaluation.changeNumber = definition.changeNumber;
evaluation.config = definition.configurations && definition.configurations[evaluation.treatment] || null;
evaluation.type = definition.type;
evaluation.subtype = definition.subtype;
// @ts-expect-error impressionsDisabled is not exposed in the public typings yet.
evaluation.impressionsDisabled = options?.impressionsDisabled || definition.impressionsDisabled;

return evaluation;
}

function getEvaluation(
log: ILogger,
key: SplitIO.SplitKey,
Expand All @@ -156,24 +150,9 @@ function getEvaluation(
const split = engineParser(log, definition, storage);
const evaluation = split.getTreatment(key, attributes, evaluateFeature);

// If the storage is async and the evaluated definition uses segments or dependencies, evaluation is thenable
if (thenable(evaluation)) {
return evaluation.then(result => {
result.changeNumber = definition.changeNumber;
result.config = definition.configurations && definition.configurations[result.treatment] || null;
// @ts-expect-error impressionsDisabled is not exposed in the public typings yet.
result.impressionsDisabled = options?.impressionsDisabled || definition.impressionsDisabled;

return result;
});
} else {
evaluation.changeNumber = definition.changeNumber;
evaluation.config = definition.configurations && definition.configurations[evaluation.treatment] || null;
// @ts-expect-error impressionsDisabled is not exposed in the public typings yet.
evaluation.impressionsDisabled = options?.impressionsDisabled || definition.impressionsDisabled;
}

return evaluation;
return thenable(evaluation) ?
evaluation.then(result => setEvaluationDataFromDefinition(result, definition, options)) :
setEvaluationDataFromDefinition(evaluation, definition, options);
}

return EVALUATION_DEFINITION_NOT_FOUND;
Expand All @@ -191,21 +170,12 @@ function getEvaluations(
const result: Record<string, IEvaluationResult> = {};
const thenables: Promise<void>[] = [];
definitionNames.forEach(definitionName => {
const evaluation = getEvaluation(
log,
key,
definitions[definitionName],
attributes,
storage,
options
);
if (thenable(evaluation)) {
const evaluation = getEvaluation(log, key, definitions[definitionName], attributes, storage, options);
thenable(evaluation) ?
thenables.push(evaluation.then(res => {
result[definitionName] = res;
}));
} else {
})) :
result[definitionName] = evaluation;
}
});

return thenables.length > 0 ? Promise.all(thenables).then(() => result) : result;
Expand All @@ -232,12 +202,10 @@ function getDefaultTreatment(
definition: IDefinition | null,
): MaybeThenable<IEvaluationResult> {
if (definition) {
return {
return setEvaluationDataFromDefinition({
treatment: definition.defaultTreatment,
label: NO_CONDITION_MATCH, // "default rule"
config: definition.configurations && definition.configurations[definition.defaultTreatment] || null,
changeNumber: definition.changeNumber
};
label: NO_CONDITION_MATCH // "default rule"
}, definition);
}

return EVALUATION_DEFINITION_NOT_FOUND;
Expand Down
4 changes: 3 additions & 1 deletion src/evaluator/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IBetweenMatcherData, IBetweenStringMatcherData, IDependencyMatcherData, MaybeThenable } from '../dtos/types';
import { IBetweenMatcherData, IBetweenStringMatcherData, IDefinition, IDependencyMatcherData, MaybeThenable } from '../dtos/types';
import { IStorageAsync, IStorageSync } from '../storages/types';
import SplitIO from '../../types/splitio';
import { ILogger } from '../logger/types';
Expand All @@ -23,6 +23,8 @@ export interface IEvaluation {
label: string,
changeNumber?: number,
config?: string | null | SplitIO.JsonObject
type?: IDefinition['type']
subtype?: IDefinition['subtype']
}

export type IEvaluationResult = IEvaluation & { treatment: string; impressionsDisabled?: boolean }
Expand Down
4 changes: 2 additions & 2 deletions src/listeners/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ export class BrowserSignalListener implements ISignalListener {
private serviceApi: IServiceApi;
private fromImpressionsCollector: (data: SplitIO.ImpressionDTO[]) => ImpressionsPayload;

constructor({ syncManager, settings, storage, serviceApi, entityType }: ISdkFactoryContextSync) {
constructor({ syncManager, settings, storage, serviceApi }: ISdkFactoryContextSync) {
this.syncManager = syncManager;
this.settings = settings;
this.storage = storage;
this.serviceApi = serviceApi;
this.flushData = this.flushData.bind(this);
this.flushDataIfHidden = this.flushDataIfHidden.bind(this);
this.fromImpressionsCollector = fromImpressionsCollector.bind(undefined, settings.core.labelsEnabled, entityType);
this.fromImpressionsCollector = fromImpressionsCollector.bind(undefined, settings.core.labelsEnabled);
}

/**
Expand Down
4 changes: 0 additions & 4 deletions src/sdkFactory/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,6 @@ export interface IPlatform {
SignalListener?: new (params: ISdkFactoryContext) => ISignalListener, // Used by BrowserSignalListener
}

// Definition type
export type EntityType = 'config' | 'flag';

export interface ISdkFactoryContext {
platform: IPlatform,
sdkReadinessManager: ISdkReadinessManager,
Expand All @@ -59,7 +56,6 @@ export interface ISdkFactoryContext {
syncManager?: ISyncManager,
clients: Record<string, SplitIO.IBasicClient>,
fallbackCalculator: IFallbackCalculator,
entityType?: EntityType
}

export interface ISdkFactoryContextSync extends ISdkFactoryContext {
Expand Down
2 changes: 1 addition & 1 deletion src/services/authProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function authProviderFactory(settings: ISettings, splitHttpClient: ISplit
const { urls, log } = settings;

function fetchAuth() {
let url = `${urls.auth}/api/v3/auth?capabilities=config`;
let url = `${urls.auth}/api/v3/auth?capabilities=config,aiconfig`;
return splitHttpClient(url, undefined, telemetryTracker.trackHttp(TOKEN), false, true);
}

Expand Down
1 change: 1 addition & 0 deletions src/storages/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export function impressionsToJSON(impressions: SplitIO.ImpressionDTO[], metadata
m: impression.time,
pt: impression.pt,
properties: impression.properties
// @TODO set entityType
}
};

Expand Down
25 changes: 4 additions & 21 deletions src/sync/submitters/__tests__/impressionsSubmitter.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fromImpressionsCollector, impressionsSubmitterFactory } from '../impressionsSubmitter';
import { impressionsSubmitterFactory } from '../impressionsSubmitter';
import { loggerMock } from '../../../logger/__tests__/sdkLogger.mock';
import { ImpressionsCacheInMemory } from '../../../storages/inMemory/ImpressionsCacheInMemory';

Expand All @@ -11,7 +11,7 @@ const imp1 = {
time: 0
};
const imp2 = { ...imp1, keyName: 'k2' };
const imp3 = { ...imp1, keyName: 'k3' };
const imp3 = { ...imp1, keyName: 'k3', entityType: 'config' as const };

describe('Impressions submitter', () => {

Expand Down Expand Up @@ -41,7 +41,7 @@ describe('Impressions submitter', () => {
// POST with imp1
['[{"f":"someFeature","i":[{"k":"k1","t":"someTreatment","m":0,"c":123}]}]'],
// POST with imp2 and imp3
['[{"f":"someFeature","i":[{"k":"k2","t":"someTreatment","m":0,"c":123},{"k":"k3","t":"someTreatment","m":0,"c":123}]}]']]);
['[{"f":"someFeature","i":[{"k":"k2","t":"someTreatment","m":0,"c":123},{"k":"k3","t":"someTreatment","m":0,"c":123,"et":"config"}]}]']]);
impressionsSubmitter.stop();

done();
Expand All @@ -66,7 +66,7 @@ describe('Impressions submitter', () => {
// impression for imp1
['[{"f":"someFeature","i":[{"k":"k1","t":"someTreatment","m":0,"c":123}]}]'],
// impressions for imp1, imp2 and imp3
['[{"f":"someFeature","i":[{"k":"k1","t":"someTreatment","m":0,"c":123},{"k":"k2","t":"someTreatment","m":0,"c":123},{"k":"k3","t":"someTreatment","m":0,"c":123}]}]']]);
['[{"f":"someFeature","i":[{"k":"k1","t":"someTreatment","m":0,"c":123},{"k":"k2","t":"someTreatment","m":0,"c":123},{"k":"k3","t":"someTreatment","m":0,"c":123,"et":"config"}]}]']]);
impressionsSubmitter.stop();

done();
Expand Down Expand Up @@ -96,20 +96,3 @@ describe('Impressions submitter', () => {
});

});

describe('fromImpressionsCollector', () => {

test('includes entityType in payload when provided', () => {
const impressions = [imp1, imp2];
const result = fromImpressionsCollector(false, 'config', impressions);

expect(result).toEqual([{
f: 'someFeature',
i: [
{ k: 'k1', t: 'someTreatment', m: 0, c: 123, et: 'config' },
{ k: 'k2', t: 'someTreatment', m: 0, c: 123, et: 'config' },
]
}]);
});

});
12 changes: 6 additions & 6 deletions src/sync/submitters/impressionsSubmitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import SplitIO from '../../../types/splitio';
import { submitterFactory } from './submitter';
import { ImpressionsPayload } from './types';
import { SUBMITTERS_PUSH_FULL_QUEUE } from '../../logger/constants';
import { EntityType, ISdkFactoryContextSync } from '../../sdkFactory/types';
import { ISdkFactoryContextSync } from '../../sdkFactory/types';

/**
* Converts `impressions` data from cache into request payload.
*/
export function fromImpressionsCollector(sendLabels: boolean, entityType: EntityType | undefined, data: SplitIO.ImpressionDTO[]): ImpressionsPayload {
export function fromImpressionsCollector(sendLabels: boolean, data: SplitIO.ImpressionDTO[]): ImpressionsPayload {
let groupedByFeature = groupBy(data, 'feature');
let dto: ImpressionsPayload = [];

Expand All @@ -25,7 +25,8 @@ export function fromImpressionsCollector(sendLabels: boolean, entityType: Entity
b: entry.bucketingKey, // Bucketing Key
pt: entry.pt, // Previous time
properties: entry.properties, // Properties
et: entityType, // Definition type
// @ts-expect-error - entityType is not yet public. @TODO: add to SplitIO.ImpressionDTO type
et: entry.entityType, // Definition type
};
})
});
Expand All @@ -42,12 +43,11 @@ export function impressionsSubmitterFactory(params: ISdkFactoryContextSync) {
const {
settings: { log, scheduler: { impressionsRefreshRate }, core: { labelsEnabled } },
serviceApi: { postTestImpressionsBulk },
storage: { impressions },
entityType
storage: { impressions }
} = params;

// retry impressions only once.
const syncTask = submitterFactory(log, postTestImpressionsBulk, impressions, impressionsRefreshRate, fromImpressionsCollector.bind(undefined, labelsEnabled, entityType), 1);
const syncTask = submitterFactory(log, postTestImpressionsBulk, impressions, impressionsRefreshRate, fromImpressionsCollector.bind(undefined, labelsEnabled), 1);

// register impressions submitter to be executed when impressions cache is full
impressions.setOnFullQueueCb(() => {
Expand Down
3 changes: 2 additions & 1 deletion src/sync/submitters/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import { IMetadata } from '../../dtos/types';
import SplitIO from '../../../types/splitio';
import { ISyncTask } from '../types';
import { EntityType } from '../../sdkFactory/types';

type EntityType = 'config' | 'flag' | 'ai-config';

type ImpressionPayload = {
/** Matching Key */
Expand Down