@splitsoftware/splitio-commons 2.7.2-rc.0 → 2.7.9-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGES.txt +4 -2
- package/cjs/evaluator/fallbackTreatmentsCalculator/constants.js +8 -0
- package/cjs/evaluator/fallbackTreatmentsCalculator/fallbackSanitizer/index.js +47 -0
- package/cjs/evaluator/fallbackTreatmentsCalculator/index.js +48 -0
- package/cjs/logger/constants.js +2 -2
- package/cjs/logger/messages/info.js +1 -1
- package/cjs/logger/messages/warn.js +3 -3
- package/cjs/readiness/readinessManager.js +3 -5
- package/cjs/readiness/sdkReadinessManager.js +33 -0
- package/cjs/sdkClient/client.js +12 -5
- package/cjs/sdkClient/clientInputValidation.js +18 -7
- package/cjs/sdkClient/sdkClient.js +1 -1
- package/cjs/sdkFactory/index.js +3 -1
- package/cjs/sdkManager/index.js +3 -3
- package/cjs/sync/polling/syncTasks/segmentsSyncTask.js +1 -1
- package/cjs/sync/polling/updaters/segmentChangesUpdater.js +5 -16
- package/cjs/sync/polling/updaters/splitChangesUpdater.js +2 -2
- package/cjs/sync/streaming/SSEHandler/index.js +1 -1
- package/cjs/sync/submitters/telemetrySubmitter.js +3 -3
- package/cjs/trackers/telemetryTracker.js +6 -6
- package/cjs/utils/inputValidation/index.js +2 -1
- package/cjs/utils/inputValidation/isOperational.js +9 -4
- package/cjs/utils/inputValidation/splitExistence.js +3 -3
- package/cjs/utils/labels/index.js +3 -1
- package/esm/evaluator/fallbackTreatmentsCalculator/constants.js +5 -0
- package/esm/evaluator/fallbackTreatmentsCalculator/fallbackSanitizer/index.js +44 -0
- package/esm/evaluator/fallbackTreatmentsCalculator/index.js +45 -0
- package/esm/logger/constants.js +1 -1
- package/esm/logger/messages/info.js +1 -1
- package/esm/logger/messages/warn.js +3 -3
- package/esm/readiness/readinessManager.js +3 -5
- package/esm/readiness/sdkReadinessManager.js +33 -0
- package/esm/sdkClient/client.js +12 -5
- package/esm/sdkClient/clientInputValidation.js +20 -9
- package/esm/sdkClient/sdkClient.js +1 -1
- package/esm/sdkFactory/index.js +3 -1
- package/esm/sdkManager/index.js +4 -4
- package/esm/sync/polling/syncTasks/segmentsSyncTask.js +1 -1
- package/esm/sync/polling/updaters/segmentChangesUpdater.js +5 -16
- package/esm/sync/polling/updaters/splitChangesUpdater.js +2 -2
- package/esm/sync/streaming/SSEHandler/index.js +1 -1
- package/esm/sync/submitters/telemetrySubmitter.js +3 -3
- package/esm/trackers/telemetryTracker.js +6 -6
- package/esm/utils/inputValidation/index.js +1 -1
- package/esm/utils/inputValidation/isOperational.js +8 -4
- package/esm/utils/inputValidation/splitExistence.js +4 -4
- package/esm/utils/labels/index.js +2 -0
- package/package.json +1 -1
- package/src/evaluator/fallbackTreatmentsCalculator/constants.ts +4 -0
- package/src/evaluator/fallbackTreatmentsCalculator/fallbackSanitizer/index.ts +62 -0
- package/src/evaluator/fallbackTreatmentsCalculator/index.ts +57 -0
- package/src/logger/constants.ts +1 -1
- package/src/logger/messages/info.ts +1 -1
- package/src/logger/messages/warn.ts +3 -3
- package/src/readiness/readinessManager.ts +3 -4
- package/src/readiness/sdkReadinessManager.ts +30 -0
- package/src/sdkClient/client.ts +14 -5
- package/src/sdkClient/clientInputValidation.ts +24 -9
- package/src/sdkClient/sdkClient.ts +2 -1
- package/src/sdkFactory/index.ts +4 -1
- package/src/sdkFactory/types.ts +2 -0
- package/src/sdkManager/index.ts +4 -4
- package/src/sync/polling/syncTasks/segmentsSyncTask.ts +0 -2
- package/src/sync/polling/updaters/segmentChangesUpdater.ts +4 -17
- package/src/sync/polling/updaters/splitChangesUpdater.ts +5 -4
- package/src/sync/streaming/SSEHandler/index.ts +1 -1
- package/src/sync/submitters/telemetrySubmitter.ts +3 -3
- package/src/trackers/telemetryTracker.ts +6 -6
- package/src/utils/inputValidation/index.ts +1 -1
- package/src/utils/inputValidation/isOperational.ts +9 -4
- package/src/utils/inputValidation/splitExistence.ts +4 -4
- package/src/utils/labels/index.ts +3 -0
- package/types/splitio.d.ts +37 -6
|
@@ -3,10 +3,10 @@ import { timer } from '../utils/timeTracker/timer';
|
|
|
3
3
|
import { TOKEN_REFRESH, AUTH_REJECTION } from '../utils/constants';
|
|
4
4
|
export function telemetryTrackerFactory(telemetryCache, now) {
|
|
5
5
|
if (telemetryCache && now) {
|
|
6
|
-
var
|
|
6
|
+
var sessionTimer_1 = timer(now);
|
|
7
7
|
return {
|
|
8
8
|
trackEval: function (method) {
|
|
9
|
-
var
|
|
9
|
+
var evalTimer = timer(now);
|
|
10
10
|
return function (label) {
|
|
11
11
|
switch (label) {
|
|
12
12
|
case EXCEPTION:
|
|
@@ -16,13 +16,13 @@ export function telemetryTrackerFactory(telemetryCache, now) {
|
|
|
16
16
|
if (telemetryCache.recordNonReadyUsage)
|
|
17
17
|
telemetryCache.recordNonReadyUsage();
|
|
18
18
|
}
|
|
19
|
-
telemetryCache.recordLatency(method,
|
|
19
|
+
telemetryCache.recordLatency(method, evalTimer());
|
|
20
20
|
};
|
|
21
21
|
},
|
|
22
22
|
trackHttp: function (operation) {
|
|
23
|
-
var
|
|
23
|
+
var httpTimer = timer(now);
|
|
24
24
|
return function (error) {
|
|
25
|
-
telemetryCache.recordHttpLatency(operation,
|
|
25
|
+
telemetryCache.recordHttpLatency(operation, httpTimer());
|
|
26
26
|
if (error && error.statusCode)
|
|
27
27
|
telemetryCache.recordHttpError(operation, error.statusCode);
|
|
28
28
|
else
|
|
@@ -31,7 +31,7 @@ export function telemetryTrackerFactory(telemetryCache, now) {
|
|
|
31
31
|
},
|
|
32
32
|
sessionLength: function () {
|
|
33
33
|
if (telemetryCache.recordSessionLength)
|
|
34
|
-
telemetryCache.recordSessionLength(
|
|
34
|
+
telemetryCache.recordSessionLength(sessionTimer_1());
|
|
35
35
|
},
|
|
36
36
|
streamingEvent: function (e, d) {
|
|
37
37
|
if (e === AUTH_REJECTION) {
|
|
@@ -7,7 +7,7 @@ export { validateKey } from './key';
|
|
|
7
7
|
export { validateSplit } from './split';
|
|
8
8
|
export { validateSplits } from './splits';
|
|
9
9
|
export { validateTrafficType } from './trafficType';
|
|
10
|
-
export { validateIfNotDestroyed, validateIfOperational } from './isOperational';
|
|
10
|
+
export { validateIfNotDestroyed, validateIfReadyFromCache, validateIfOperational } from './isOperational';
|
|
11
11
|
export { validateSplitExistence } from './splitExistence';
|
|
12
12
|
export { validateTrafficTypeExistence } from './trafficTypeExistence';
|
|
13
13
|
export { validateEvaluationOptions } from './eventProperties';
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import { ERROR_CLIENT_DESTROYED,
|
|
1
|
+
import { ERROR_CLIENT_DESTROYED, CLIENT_NOT_READY_FROM_CACHE } from '../../logger/constants';
|
|
2
2
|
export function validateIfNotDestroyed(log, readinessManager, method) {
|
|
3
3
|
if (!readinessManager.isDestroyed())
|
|
4
4
|
return true;
|
|
5
5
|
log.error(ERROR_CLIENT_DESTROYED, [method]);
|
|
6
6
|
return false;
|
|
7
7
|
}
|
|
8
|
-
export function
|
|
9
|
-
if (readinessManager.
|
|
8
|
+
export function validateIfReadyFromCache(log, readinessManager, method, featureFlagNameOrNames) {
|
|
9
|
+
if (readinessManager.isReadyFromCache())
|
|
10
10
|
return true;
|
|
11
|
-
log.warn(
|
|
11
|
+
log.warn(CLIENT_NOT_READY_FROM_CACHE, [method, featureFlagNameOrNames ? " for feature flag " + featureFlagNameOrNames.toString() : '']);
|
|
12
12
|
return false;
|
|
13
13
|
}
|
|
14
|
+
// Operational means that the SDK is ready to evaluate (not destroyed and ready from cache)
|
|
15
|
+
export function validateIfOperational(log, readinessManager, method, featureFlagNameOrNames) {
|
|
16
|
+
return validateIfNotDestroyed(log, readinessManager, method) && validateIfReadyFromCache(log, readinessManager, method, featureFlagNameOrNames);
|
|
17
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { SPLIT_NOT_FOUND } from '../labels';
|
|
1
|
+
import { FALLBACK_SPLIT_NOT_FOUND, SPLIT_NOT_FOUND } from '../labels';
|
|
2
2
|
import { WARN_NOT_EXISTENT_SPLIT } from '../../logger/constants';
|
|
3
3
|
/**
|
|
4
4
|
* This is defined here and in this format mostly because of the logger and the fact that it's considered a validation at product level.
|
|
5
|
-
* But it's not going to run on the input validation layer. In any case, the most
|
|
5
|
+
* But it's not going to run on the input validation layer. In any case, the most compelling reason to use it as we do is to avoid going to Redis and get a split twice.
|
|
6
6
|
*/
|
|
7
7
|
export function validateSplitExistence(log, readinessManager, splitName, labelOrSplitObj, method) {
|
|
8
|
-
if (readinessManager.isReady()) { // Only if it's ready we validate this, otherwise it may just be that the SDK is
|
|
9
|
-
if (labelOrSplitObj === SPLIT_NOT_FOUND || labelOrSplitObj == null) {
|
|
8
|
+
if (readinessManager.isReady()) { // Only if it's ready (synced with BE) we validate this, otherwise it may just be that the SDK is still syncing
|
|
9
|
+
if (labelOrSplitObj === SPLIT_NOT_FOUND || labelOrSplitObj === FALLBACK_SPLIT_NOT_FOUND || labelOrSplitObj == null) {
|
|
10
10
|
log.warn(WARN_NOT_EXISTENT_SPLIT, [method, splitName]);
|
|
11
11
|
return false;
|
|
12
12
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FALLBACK_PREFIX } from '../../evaluator/fallbackTreatmentsCalculator';
|
|
1
2
|
export var SPLIT_KILLED = 'killed';
|
|
2
3
|
export var NO_CONDITION_MATCH = 'default rule';
|
|
3
4
|
export var SPLIT_NOT_FOUND = 'definition not found';
|
|
@@ -7,3 +8,4 @@ export var SPLIT_ARCHIVED = 'archived';
|
|
|
7
8
|
export var NOT_IN_SPLIT = 'not in split';
|
|
8
9
|
export var UNSUPPORTED_MATCHER_TYPE = 'targeting rule type unsupported by sdk';
|
|
9
10
|
export var PREREQUISITES_NOT_MET = 'prerequisites not met';
|
|
11
|
+
export var FALLBACK_SPLIT_NOT_FOUND = FALLBACK_PREFIX + SPLIT_NOT_FOUND;
|
package/package.json
CHANGED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Treatment, TreatmentWithConfig } from '../../../../types/splitio';
|
|
2
|
+
import { ILogger } from '../../../logger/types';
|
|
3
|
+
import { isObject, isString } from '../../../utils/lang';
|
|
4
|
+
import { FallbackDiscardReason } from '../constants';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export class FallbacksSanitizer {
|
|
8
|
+
|
|
9
|
+
private static readonly pattern = /^[0-9]+[.a-zA-Z0-9_-]*$|^[a-zA-Z]+[a-zA-Z0-9_-]*$/;
|
|
10
|
+
|
|
11
|
+
private static isValidFlagName(name: string): boolean {
|
|
12
|
+
return name.length <= 100 && !name.includes(' ');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private static isValidTreatment(t?: Treatment | TreatmentWithConfig): boolean {
|
|
16
|
+
const treatment = isObject(t) ? (t as TreatmentWithConfig).treatment : t;
|
|
17
|
+
|
|
18
|
+
if (!isString(treatment) || treatment.length > 100) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return FallbacksSanitizer.pattern.test(treatment);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static sanitizeGlobal(logger: ILogger, treatment?: Treatment | TreatmentWithConfig): Treatment | TreatmentWithConfig | undefined {
|
|
25
|
+
if (!this.isValidTreatment(treatment)) {
|
|
26
|
+
logger.error(
|
|
27
|
+
`Fallback treatments - Discarded fallback: ${FallbackDiscardReason.Treatment}`
|
|
28
|
+
);
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return treatment;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
static sanitizeByFlag(
|
|
35
|
+
logger: ILogger,
|
|
36
|
+
byFlagFallbacks: Record<string, Treatment | TreatmentWithConfig>
|
|
37
|
+
): Record<string, Treatment | TreatmentWithConfig> {
|
|
38
|
+
const sanitizedByFlag: Record<string, Treatment | TreatmentWithConfig> = {};
|
|
39
|
+
|
|
40
|
+
const entries = Object.keys(byFlagFallbacks);
|
|
41
|
+
entries.forEach((flag) => {
|
|
42
|
+
const t = byFlagFallbacks[flag];
|
|
43
|
+
if (!this.isValidFlagName(flag)) {
|
|
44
|
+
logger.error(
|
|
45
|
+
`Fallback treatments - Discarded flag '${flag}': ${FallbackDiscardReason.FlagName}`
|
|
46
|
+
);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!this.isValidTreatment(t)) {
|
|
51
|
+
logger.error(
|
|
52
|
+
`Fallback treatments - Discarded treatment for flag '${flag}': ${FallbackDiscardReason.Treatment}`
|
|
53
|
+
);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
sanitizedByFlag[flag] = t;
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return sanitizedByFlag;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { FallbackTreatmentConfiguration, Treatment, TreatmentWithConfig } from '../../../types/splitio';
|
|
2
|
+
import { FallbacksSanitizer } from './fallbackSanitizer';
|
|
3
|
+
import { CONTROL } from '../../utils/constants';
|
|
4
|
+
import { isString } from '../../utils/lang';
|
|
5
|
+
import { ILogger } from '../../logger/types';
|
|
6
|
+
|
|
7
|
+
export type IFallbackTreatmentsCalculator = {
|
|
8
|
+
resolve(flagName: string, label: string): TreatmentWithConfig & { label: string };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const FALLBACK_PREFIX = 'fallback - ';
|
|
12
|
+
|
|
13
|
+
export class FallbackTreatmentsCalculator implements IFallbackTreatmentsCalculator {
|
|
14
|
+
private readonly fallbacks: FallbackTreatmentConfiguration;
|
|
15
|
+
|
|
16
|
+
constructor(logger: ILogger, fallbacks?: FallbackTreatmentConfiguration) {
|
|
17
|
+
const sanitizedGlobal = fallbacks?.global ? FallbacksSanitizer.sanitizeGlobal(logger, fallbacks.global) : undefined;
|
|
18
|
+
const sanitizedByFlag = fallbacks?.byFlag ? FallbacksSanitizer.sanitizeByFlag(logger, fallbacks.byFlag) : {};
|
|
19
|
+
this.fallbacks = {
|
|
20
|
+
global: sanitizedGlobal,
|
|
21
|
+
byFlag: sanitizedByFlag
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
resolve(flagName: string, label: string): TreatmentWithConfig & { label: string } {
|
|
26
|
+
const treatment = this.fallbacks.byFlag?.[flagName];
|
|
27
|
+
if (treatment) {
|
|
28
|
+
return this.copyWithLabel(treatment, label);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (this.fallbacks.global) {
|
|
32
|
+
return this.copyWithLabel(this.fallbacks.global, label);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
treatment: CONTROL,
|
|
37
|
+
config: null,
|
|
38
|
+
label,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private copyWithLabel(fallback: Treatment | TreatmentWithConfig, label: string): TreatmentWithConfig & { label: string } {
|
|
43
|
+
if (isString(fallback)) {
|
|
44
|
+
return {
|
|
45
|
+
treatment: fallback,
|
|
46
|
+
config: null,
|
|
47
|
+
label: `${FALLBACK_PREFIX}${label}`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
treatment: fallback.treatment,
|
|
53
|
+
config: fallback.config,
|
|
54
|
+
label: `${FALLBACK_PREFIX}${label}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/logger/constants.ts
CHANGED
|
@@ -60,7 +60,7 @@ export const SUBMITTERS_PUSH_PAGE_HIDDEN = 125;
|
|
|
60
60
|
export const ENGINE_VALUE_INVALID = 200;
|
|
61
61
|
export const ENGINE_VALUE_NO_ATTRIBUTES = 201;
|
|
62
62
|
export const CLIENT_NO_LISTENER = 202;
|
|
63
|
-
export const
|
|
63
|
+
export const CLIENT_NOT_READY_FROM_CACHE = 203;
|
|
64
64
|
export const SYNC_MYSEGMENTS_FETCH_RETRY = 204;
|
|
65
65
|
export const SYNC_SPLITS_FETCH_FAILS = 205;
|
|
66
66
|
export const STREAMING_PARSING_ERROR_FAILS = 206;
|
|
@@ -22,7 +22,7 @@ export const codesInfo: [number, string][] = codesWarn.concat([
|
|
|
22
22
|
[c.POLLING_SMART_PAUSING, c.LOG_PREFIX_SYNC_POLLING + 'Turning segments data polling %s.'],
|
|
23
23
|
[c.POLLING_START, c.LOG_PREFIX_SYNC_POLLING + 'Starting polling'],
|
|
24
24
|
[c.POLLING_STOP, c.LOG_PREFIX_SYNC_POLLING + 'Stopping polling'],
|
|
25
|
-
[c.SYNC_SPLITS_FETCH_RETRY, c.LOG_PREFIX_SYNC_SPLITS + 'Retrying
|
|
25
|
+
[c.SYNC_SPLITS_FETCH_RETRY, c.LOG_PREFIX_SYNC_SPLITS + 'Retrying download of feature flags #%s. Reason: %s'],
|
|
26
26
|
[c.SUBMITTERS_PUSH_FULL_QUEUE, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Flushing full %s queue and resetting timer.'],
|
|
27
27
|
[c.SUBMITTERS_PUSH, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Pushing %s.'],
|
|
28
28
|
[c.SUBMITTERS_PUSH_PAGE_HIDDEN, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Flushing %s because page became hidden.'],
|
|
@@ -6,7 +6,7 @@ export const codesWarn: [number, string][] = codesError.concat([
|
|
|
6
6
|
[c.ENGINE_VALUE_INVALID, c.LOG_PREFIX_ENGINE_VALUE + 'Value %s doesn\'t match with expected type.'],
|
|
7
7
|
[c.ENGINE_VALUE_NO_ATTRIBUTES, c.LOG_PREFIX_ENGINE_VALUE + 'Defined attribute `%s`. No attributes received.'],
|
|
8
8
|
// synchronizer
|
|
9
|
-
[c.SYNC_MYSEGMENTS_FETCH_RETRY, c.LOG_PREFIX_SYNC_MYSEGMENTS + 'Retrying
|
|
9
|
+
[c.SYNC_MYSEGMENTS_FETCH_RETRY, c.LOG_PREFIX_SYNC_MYSEGMENTS + 'Retrying download of segments #%s. Reason: %s'],
|
|
10
10
|
[c.SYNC_SPLITS_FETCH_FAILS, c.LOG_PREFIX_SYNC_SPLITS + 'Error while doing fetch of feature flags. %s'],
|
|
11
11
|
[c.STREAMING_PARSING_ERROR_FAILS, c.LOG_PREFIX_SYNC_STREAMING + 'Error parsing SSE error notification: %s'],
|
|
12
12
|
[c.STREAMING_PARSING_MESSAGE_FAILS, c.LOG_PREFIX_SYNC_STREAMING + 'Error parsing SSE message notification: %s'],
|
|
@@ -14,8 +14,8 @@ export const codesWarn: [number, string][] = codesError.concat([
|
|
|
14
14
|
[c.SUBMITTERS_PUSH_FAILS, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Dropping %s after retry. Reason: %s.'],
|
|
15
15
|
[c.SUBMITTERS_PUSH_RETRY, c.LOG_PREFIX_SYNC_SUBMITTERS + 'Failed to push %s, keeping data to retry on next iteration. Reason: %s.'],
|
|
16
16
|
// client status
|
|
17
|
-
[c.
|
|
18
|
-
[c.CLIENT_NO_LISTENER, 'No listeners for
|
|
17
|
+
[c.CLIENT_NOT_READY_FROM_CACHE, '%s: the SDK is not ready to evaluate. Results may be incorrect%s. Make sure to wait for SDK readiness before using this method.'],
|
|
18
|
+
[c.CLIENT_NO_LISTENER, 'No listeners for SDK_READY event detected. Incorrect control treatments could have been logged if you called getTreatment/s while the SDK was not yet synchronized with the backend.'],
|
|
19
19
|
// input validation
|
|
20
20
|
[c.WARN_SETTING_NULL, '%s: Property "%s" is of invalid type. Setting value to null.'],
|
|
21
21
|
[c.WARN_TRIMMING_PROPERTIES, '%s: more than 300 properties were provided. Some of them will be trimmed when processed.'],
|
|
@@ -3,7 +3,6 @@ import { ISettings } from '../types';
|
|
|
3
3
|
import SplitIO from '../../types/splitio';
|
|
4
4
|
import { SDK_SPLITS_ARRIVED, SDK_SPLITS_CACHE_LOADED, SDK_SEGMENTS_ARRIVED, SDK_READY_TIMED_OUT, SDK_READY_FROM_CACHE, SDK_UPDATE, SDK_READY } from './constants';
|
|
5
5
|
import { IReadinessEventEmitter, IReadinessManager, ISegmentsEventEmitter, ISplitsEventEmitter } from './types';
|
|
6
|
-
import { STORAGE_LOCALSTORAGE } from '../utils/constants';
|
|
7
6
|
|
|
8
7
|
function splitsEventEmitterFactory(EventEmitter: new () => SplitIO.IEventEmitter): ISplitsEventEmitter {
|
|
9
8
|
const splitsEventEmitter = objectAssign(new EventEmitter(), {
|
|
@@ -91,7 +90,7 @@ export function readinessManagerFactory(
|
|
|
91
90
|
if (!isReady && !isDestroyed) {
|
|
92
91
|
try {
|
|
93
92
|
syncLastUpdate();
|
|
94
|
-
gate.emit(SDK_READY_FROM_CACHE);
|
|
93
|
+
gate.emit(SDK_READY_FROM_CACHE, isReady);
|
|
95
94
|
} catch (e) {
|
|
96
95
|
// throws user callback exceptions in next tick
|
|
97
96
|
setTimeout(() => { throw e; }, 0);
|
|
@@ -115,9 +114,9 @@ export function readinessManagerFactory(
|
|
|
115
114
|
isReady = true;
|
|
116
115
|
try {
|
|
117
116
|
syncLastUpdate();
|
|
118
|
-
if (!isReadyFromCache
|
|
117
|
+
if (!isReadyFromCache) {
|
|
119
118
|
isReadyFromCache = true;
|
|
120
|
-
gate.emit(SDK_READY_FROM_CACHE);
|
|
119
|
+
gate.emit(SDK_READY_FROM_CACHE, isReady);
|
|
121
120
|
}
|
|
122
121
|
gate.emit(SDK_READY);
|
|
123
122
|
} catch (e) {
|
|
@@ -9,6 +9,7 @@ import { ERROR_CLIENT_LISTENER, CLIENT_READY_FROM_CACHE, CLIENT_READY, CLIENT_NO
|
|
|
9
9
|
|
|
10
10
|
const NEW_LISTENER_EVENT = 'newListener';
|
|
11
11
|
const REMOVE_LISTENER_EVENT = 'removeListener';
|
|
12
|
+
const TIMEOUT_ERROR = new Error(SDK_READY_TIMED_OUT);
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* SdkReadinessManager factory, which provides the public status API of SDK clients and manager: ready promise, readiness event emitter and constants (SDK_READY, etc).
|
|
@@ -38,6 +39,8 @@ export function sdkReadinessManagerFactory(
|
|
|
38
39
|
} else if (event === SDK_READY) {
|
|
39
40
|
readyCbCount++;
|
|
40
41
|
}
|
|
42
|
+
} else if (event === SDK_READY_FROM_CACHE && readinessManager.isReadyFromCache()) {
|
|
43
|
+
log.error(ERROR_CLIENT_LISTENER, ['SDK_READY_FROM_CACHE']);
|
|
41
44
|
}
|
|
42
45
|
});
|
|
43
46
|
|
|
@@ -93,6 +96,7 @@ export function sdkReadinessManagerFactory(
|
|
|
93
96
|
SDK_READY_TIMED_OUT,
|
|
94
97
|
},
|
|
95
98
|
|
|
99
|
+
// @TODO: remove in next major
|
|
96
100
|
ready() {
|
|
97
101
|
if (readinessManager.hasTimedout()) {
|
|
98
102
|
if (!readinessManager.isReady()) {
|
|
@@ -104,6 +108,32 @@ export function sdkReadinessManagerFactory(
|
|
|
104
108
|
return readyPromise;
|
|
105
109
|
},
|
|
106
110
|
|
|
111
|
+
whenReady() {
|
|
112
|
+
return new Promise<void>((resolve, reject) => {
|
|
113
|
+
if (readinessManager.isReady()) {
|
|
114
|
+
resolve();
|
|
115
|
+
} else if (readinessManager.hasTimedout()) {
|
|
116
|
+
reject(TIMEOUT_ERROR);
|
|
117
|
+
} else {
|
|
118
|
+
readinessManager.gate.once(SDK_READY, resolve);
|
|
119
|
+
readinessManager.gate.once(SDK_READY_TIMED_OUT, () => reject(TIMEOUT_ERROR));
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
whenReadyFromCache() {
|
|
125
|
+
return new Promise<boolean>((resolve, reject) => {
|
|
126
|
+
if (readinessManager.isReadyFromCache()) {
|
|
127
|
+
resolve(readinessManager.isReady());
|
|
128
|
+
} else if (readinessManager.hasTimedout()) {
|
|
129
|
+
reject(TIMEOUT_ERROR);
|
|
130
|
+
} else {
|
|
131
|
+
readinessManager.gate.once(SDK_READY_FROM_CACHE, () => resolve(readinessManager.isReady()));
|
|
132
|
+
readinessManager.gate.once(SDK_READY_TIMED_OUT, () => reject(TIMEOUT_ERROR));
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
},
|
|
136
|
+
|
|
107
137
|
__getStatus() {
|
|
108
138
|
return {
|
|
109
139
|
isReady: readinessManager.isReady(),
|
package/src/sdkClient/client.ts
CHANGED
|
@@ -35,7 +35,7 @@ function stringify(options?: SplitIO.EvaluationOptions) {
|
|
|
35
35
|
* Creator of base client with getTreatments and track methods.
|
|
36
36
|
*/
|
|
37
37
|
export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | SplitIO.IAsyncClient {
|
|
38
|
-
const { sdkReadinessManager: { readinessManager }, storage, settings, impressionsTracker, eventTracker, telemetryTracker } = params;
|
|
38
|
+
const { sdkReadinessManager: { readinessManager }, storage, settings, impressionsTracker, eventTracker, telemetryTracker, fallbackTreatmentsCalculator } = params;
|
|
39
39
|
const { log, mode } = settings;
|
|
40
40
|
const isAsync = isConsumerMode(mode);
|
|
41
41
|
|
|
@@ -51,7 +51,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
|
|
|
51
51
|
return treatment;
|
|
52
52
|
};
|
|
53
53
|
|
|
54
|
-
const evaluation = readinessManager.
|
|
54
|
+
const evaluation = readinessManager.isReadyFromCache() ?
|
|
55
55
|
evaluateFeature(log, key, featureFlagName, attributes, storage) :
|
|
56
56
|
isAsync ? // If the SDK is not ready, treatment may be incorrect due to having splits but not segments data, or storage is not connected
|
|
57
57
|
Promise.resolve(treatmentNotReady) :
|
|
@@ -80,7 +80,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
|
|
|
80
80
|
return treatments;
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
-
const evaluations = readinessManager.
|
|
83
|
+
const evaluations = readinessManager.isReadyFromCache() ?
|
|
84
84
|
evaluateFeatures(log, key, featureFlagNames, attributes, storage) :
|
|
85
85
|
isAsync ? // If the SDK is not ready, treatment may be incorrect due to having splits but not segments data, or storage is not connected
|
|
86
86
|
Promise.resolve(treatmentsNotReady(featureFlagNames)) :
|
|
@@ -109,7 +109,7 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
|
|
|
109
109
|
return treatments;
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
-
const evaluations = readinessManager.
|
|
112
|
+
const evaluations = readinessManager.isReadyFromCache() ?
|
|
113
113
|
evaluateFeaturesByFlagSets(log, key, flagSetNames, attributes, storage, methodName) :
|
|
114
114
|
isAsync ?
|
|
115
115
|
Promise.resolve({}) :
|
|
@@ -143,7 +143,16 @@ export function clientFactory(params: ISdkFactoryContext): SplitIO.IClient | Spl
|
|
|
143
143
|
const matchingKey = getMatching(key);
|
|
144
144
|
const bucketingKey = getBucketing(key);
|
|
145
145
|
|
|
146
|
-
const {
|
|
146
|
+
const { changeNumber, impressionsDisabled } = evaluation;
|
|
147
|
+
let { treatment, label, config = null } = evaluation;
|
|
148
|
+
|
|
149
|
+
if (treatment === CONTROL) {
|
|
150
|
+
const fallbackTreatment = fallbackTreatmentsCalculator.resolve(featureFlagName, label);
|
|
151
|
+
treatment = fallbackTreatment.treatment;
|
|
152
|
+
label = fallbackTreatment.label;
|
|
153
|
+
config = fallbackTreatment.config;
|
|
154
|
+
}
|
|
155
|
+
|
|
147
156
|
log.info(IMPRESSION, [featureFlagName, matchingKey, treatment, label]);
|
|
148
157
|
|
|
149
158
|
if (validateSplitExistence(log, readinessManager, featureFlagName, label, invokingMethodName)) {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { objectAssign } from '../utils/lang/objectAssign';
|
|
2
1
|
import {
|
|
3
2
|
validateAttributes,
|
|
4
3
|
validateEvent,
|
|
@@ -9,23 +8,24 @@ import {
|
|
|
9
8
|
validateSplits,
|
|
10
9
|
validateTrafficType,
|
|
11
10
|
validateIfNotDestroyed,
|
|
12
|
-
|
|
11
|
+
validateIfReadyFromCache,
|
|
13
12
|
validateEvaluationOptions
|
|
14
13
|
} from '../utils/inputValidation';
|
|
15
14
|
import { startsWith } from '../utils/lang';
|
|
16
|
-
import {
|
|
15
|
+
import { GET_TREATMENT, GET_TREATMENTS, GET_TREATMENTS_BY_FLAG_SET, GET_TREATMENTS_BY_FLAG_SETS, GET_TREATMENTS_WITH_CONFIG, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SET, GET_TREATMENTS_WITH_CONFIG_BY_FLAG_SETS, GET_TREATMENT_WITH_CONFIG, TRACK_FN_LABEL } from '../utils/constants';
|
|
17
16
|
import { IReadinessManager } from '../readiness/types';
|
|
18
17
|
import { MaybeThenable } from '../dtos/types';
|
|
19
18
|
import { ISettings } from '../types';
|
|
20
19
|
import SplitIO from '../../types/splitio';
|
|
21
20
|
import { isConsumerMode } from '../utils/settingsValidation/mode';
|
|
22
21
|
import { validateFlagSets } from '../utils/settingsValidation/splitFilters';
|
|
22
|
+
import { IFallbackTreatmentsCalculator } from '../evaluator/fallbackTreatmentsCalculator';
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* Decorator that validates the input before actually executing the client methods.
|
|
26
26
|
* We should "guard" the client here, while not polluting the "real" implementation of those methods.
|
|
27
27
|
*/
|
|
28
|
-
export function clientInputValidationDecorator<TClient extends SplitIO.IClient | SplitIO.IAsyncClient>(settings: ISettings, client: TClient, readinessManager: IReadinessManager): TClient {
|
|
28
|
+
export function clientInputValidationDecorator<TClient extends SplitIO.IClient | SplitIO.IAsyncClient>(settings: ISettings, client: TClient, readinessManager: IReadinessManager, fallbackTreatmentsCalculator: IFallbackTreatmentsCalculator): TClient {
|
|
29
29
|
|
|
30
30
|
const { log, mode } = settings;
|
|
31
31
|
const isAsync = isConsumerMode(mode);
|
|
@@ -46,7 +46,7 @@ export function clientInputValidationDecorator<TClient extends SplitIO.IClient |
|
|
|
46
46
|
const isNotDestroyed = validateIfNotDestroyed(log, readinessManager, methodName);
|
|
47
47
|
const options = validateEvaluationOptions(log, maybeOptions, methodName);
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
validateIfReadyFromCache(log, readinessManager, methodName, nameOrNames);
|
|
50
50
|
|
|
51
51
|
const valid = isNotDestroyed && key && nameOrNames && attributes !== false;
|
|
52
52
|
|
|
@@ -59,6 +59,19 @@ export function clientInputValidationDecorator<TClient extends SplitIO.IClient |
|
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function evaluateFallBackTreatment(featureFlagName: string, withConfig: boolean): SplitIO.Treatment | SplitIO.TreatmentWithConfig {
|
|
63
|
+
const { treatment, config } = fallbackTreatmentsCalculator.resolve(featureFlagName, '');
|
|
64
|
+
|
|
65
|
+
if (withConfig) {
|
|
66
|
+
return {
|
|
67
|
+
treatment,
|
|
68
|
+
config
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return treatment;
|
|
73
|
+
}
|
|
74
|
+
|
|
62
75
|
function wrapResult<T>(value: T): MaybeThenable<T> {
|
|
63
76
|
return isAsync ? Promise.resolve(value) : value;
|
|
64
77
|
}
|
|
@@ -69,7 +82,8 @@ export function clientInputValidationDecorator<TClient extends SplitIO.IClient |
|
|
|
69
82
|
if (params.valid) {
|
|
70
83
|
return client.getTreatment(params.key as SplitIO.SplitKey, params.nameOrNames as string, params.attributes as SplitIO.Attributes | undefined, params.options);
|
|
71
84
|
} else {
|
|
72
|
-
|
|
85
|
+
const result = evaluateFallBackTreatment(params.nameOrNames as string, false);
|
|
86
|
+
return wrapResult(result);
|
|
73
87
|
}
|
|
74
88
|
}
|
|
75
89
|
|
|
@@ -79,7 +93,8 @@ export function clientInputValidationDecorator<TClient extends SplitIO.IClient |
|
|
|
79
93
|
if (params.valid) {
|
|
80
94
|
return client.getTreatmentWithConfig(params.key as SplitIO.SplitKey, params.nameOrNames as string, params.attributes as SplitIO.Attributes | undefined, params.options);
|
|
81
95
|
} else {
|
|
82
|
-
|
|
96
|
+
const result = evaluateFallBackTreatment(params.nameOrNames as string, true);
|
|
97
|
+
return wrapResult(result);
|
|
83
98
|
}
|
|
84
99
|
}
|
|
85
100
|
|
|
@@ -90,7 +105,7 @@ export function clientInputValidationDecorator<TClient extends SplitIO.IClient |
|
|
|
90
105
|
return client.getTreatments(params.key as SplitIO.SplitKey, params.nameOrNames as string[], params.attributes as SplitIO.Attributes | undefined, params.options);
|
|
91
106
|
} else {
|
|
92
107
|
const res: SplitIO.Treatments = {};
|
|
93
|
-
if (params.nameOrNames) (params.nameOrNames as string[]).forEach((split: string) => res[split] =
|
|
108
|
+
if (params.nameOrNames) (params.nameOrNames as string[]).forEach((split: string) => res[split] = evaluateFallBackTreatment(split, false) as SplitIO.Treatment);
|
|
94
109
|
|
|
95
110
|
return wrapResult(res);
|
|
96
111
|
}
|
|
@@ -103,7 +118,7 @@ export function clientInputValidationDecorator<TClient extends SplitIO.IClient |
|
|
|
103
118
|
return client.getTreatmentsWithConfig(params.key as SplitIO.SplitKey, params.nameOrNames as string[], params.attributes as SplitIO.Attributes | undefined, params.options);
|
|
104
119
|
} else {
|
|
105
120
|
const res: SplitIO.TreatmentsWithConfig = {};
|
|
106
|
-
if (params.nameOrNames) (params.nameOrNames as string[]).forEach(split => res[split] =
|
|
121
|
+
if (params.nameOrNames) (params.nameOrNames as string[]).forEach(split => res[split] = evaluateFallBackTreatment(split, true) as SplitIO.TreatmentWithConfig);
|
|
107
122
|
|
|
108
123
|
return wrapResult(res);
|
|
109
124
|
}
|
|
@@ -43,7 +43,8 @@ export function sdkClientFactory(params: ISdkFactoryContext, isSharedClient?: bo
|
|
|
43
43
|
clientInputValidationDecorator(
|
|
44
44
|
settings,
|
|
45
45
|
clientFactory(params),
|
|
46
|
-
sdkReadinessManager.readinessManager
|
|
46
|
+
sdkReadinessManager.readinessManager,
|
|
47
|
+
params.fallbackTreatmentsCalculator
|
|
47
48
|
),
|
|
48
49
|
|
|
49
50
|
// Sdk destroy
|
package/src/sdkFactory/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { DEBUG, OPTIMIZED } from '../utils/constants';
|
|
|
17
17
|
import { setRolloutPlan } from '../storages/setRolloutPlan';
|
|
18
18
|
import { IStorageSync } from '../storages/types';
|
|
19
19
|
import { getMatching } from '../utils/key';
|
|
20
|
+
import { FallbackTreatmentsCalculator } from '../evaluator/fallbackTreatmentsCalculator';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Modular SDK factory
|
|
@@ -60,6 +61,8 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA
|
|
|
60
61
|
}
|
|
61
62
|
});
|
|
62
63
|
|
|
64
|
+
const fallbackTreatmentsCalculator = new FallbackTreatmentsCalculator(settings.log, settings.fallbackTreatments);
|
|
65
|
+
|
|
63
66
|
if (initialRolloutPlan) {
|
|
64
67
|
setRolloutPlan(log, initialRolloutPlan, storage as IStorageSync, key && getMatching(key));
|
|
65
68
|
if ((storage as IStorageSync).splits.getChangeNumber() > -1) readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
|
|
@@ -85,7 +88,7 @@ export function sdkFactory(params: ISdkFactoryParams): SplitIO.ISDK | SplitIO.IA
|
|
|
85
88
|
// splitApi is used by SyncManager and Browser signal listener
|
|
86
89
|
const splitApi = splitApiFactory && splitApiFactory(settings, platform, telemetryTracker);
|
|
87
90
|
|
|
88
|
-
const ctx: ISdkFactoryContext = { clients, splitApi, eventTracker, impressionsTracker, telemetryTracker, uniqueKeysTracker, sdkReadinessManager, readiness, settings, storage, platform };
|
|
91
|
+
const ctx: ISdkFactoryContext = { clients, splitApi, eventTracker, impressionsTracker, telemetryTracker, uniqueKeysTracker, sdkReadinessManager, readiness, settings, storage, platform, fallbackTreatmentsCalculator };
|
|
89
92
|
|
|
90
93
|
const syncManager = syncManagerFactory && syncManagerFactory(ctx as ISdkFactoryContextSync);
|
|
91
94
|
ctx.syncManager = syncManager;
|
package/src/sdkFactory/types.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { ISignalListener } from '../listeners/types';
|
|
|
3
3
|
import { IReadinessManager, ISdkReadinessManager } from '../readiness/types';
|
|
4
4
|
import type { sdkManagerFactory } from '../sdkManager';
|
|
5
5
|
import type { splitApiFactory } from '../services/splitApi';
|
|
6
|
+
import type { IFallbackTreatmentsCalculator } from '../evaluator/fallbackTreatmentsCalculator';
|
|
6
7
|
import { IFetch, ISplitApi, IEventSourceConstructor } from '../services/types';
|
|
7
8
|
import { IStorageAsync, IStorageSync, IStorageFactoryParams } from '../storages/types';
|
|
8
9
|
import { ISyncManager } from '../sync/types';
|
|
@@ -51,6 +52,7 @@ export interface ISdkFactoryContext {
|
|
|
51
52
|
splitApi?: ISplitApi
|
|
52
53
|
syncManager?: ISyncManager,
|
|
53
54
|
clients: Record<string, SplitIO.IBasicClient>,
|
|
55
|
+
fallbackTreatmentsCalculator: IFallbackTreatmentsCalculator
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
export interface ISdkFactoryContextSync extends ISdkFactoryContext {
|
package/src/sdkManager/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { objectAssign } from '../utils/lang/objectAssign';
|
|
2
2
|
import { thenable } from '../utils/promise/thenable';
|
|
3
3
|
import { find } from '../utils/lang';
|
|
4
|
-
import { validateSplit, validateSplitExistence,
|
|
4
|
+
import { validateSplit, validateSplitExistence, validateIfOperational } from '../utils/inputValidation';
|
|
5
5
|
import { ISplitsCacheAsync, ISplitsCacheSync } from '../storages/types';
|
|
6
6
|
import { ISdkReadinessManager } from '../readiness/types';
|
|
7
7
|
import { ISplit } from '../dtos/types';
|
|
@@ -66,7 +66,7 @@ export function sdkManagerFactory<TSplitCache extends ISplitsCacheSync | ISplits
|
|
|
66
66
|
*/
|
|
67
67
|
split(featureFlagName: string) {
|
|
68
68
|
const splitName = validateSplit(log, featureFlagName, SPLIT_FN_LABEL);
|
|
69
|
-
if (!
|
|
69
|
+
if (!validateIfOperational(log, readinessManager, SPLIT_FN_LABEL) || !splitName) {
|
|
70
70
|
return isAsync ? Promise.resolve(null) : null;
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -87,7 +87,7 @@ export function sdkManagerFactory<TSplitCache extends ISplitsCacheSync | ISplits
|
|
|
87
87
|
* Get the feature flag objects present on the factory storage
|
|
88
88
|
*/
|
|
89
89
|
splits() {
|
|
90
|
-
if (!
|
|
90
|
+
if (!validateIfOperational(log, readinessManager, SPLITS_FN_LABEL)) {
|
|
91
91
|
return isAsync ? Promise.resolve([]) : [];
|
|
92
92
|
}
|
|
93
93
|
const currentSplits = splits.getAll();
|
|
@@ -100,7 +100,7 @@ export function sdkManagerFactory<TSplitCache extends ISplitsCacheSync | ISplits
|
|
|
100
100
|
* Get the feature flag names present on the factory storage
|
|
101
101
|
*/
|
|
102
102
|
names() {
|
|
103
|
-
if (!
|
|
103
|
+
if (!validateIfOperational(log, readinessManager, NAMES_FN_LABEL)) {
|
|
104
104
|
return isAsync ? Promise.resolve([]) : [];
|
|
105
105
|
}
|
|
106
106
|
const splitNames = splits.getSplitNames();
|
|
@@ -23,8 +23,6 @@ export function segmentsSyncTaskFactory(
|
|
|
23
23
|
segmentChangesFetcherFactory(fetchSegmentChanges),
|
|
24
24
|
storage.segments,
|
|
25
25
|
readiness,
|
|
26
|
-
settings.startup.requestTimeoutBeforeReady,
|
|
27
|
-
settings.startup.retriesOnFailureBeforeReady,
|
|
28
26
|
),
|
|
29
27
|
settings.scheduler.segmentsRefreshRate,
|
|
30
28
|
'segmentChangesUpdater'
|