@splitsoftware/splitio-commons 2.7.2-rc.0 → 2.7.9-rc.0

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.
Files changed (49) hide show
  1. package/CHANGES.txt +2 -5
  2. package/cjs/evaluator/fallbackTreatmentsCalculator/constants.js +8 -0
  3. package/cjs/evaluator/fallbackTreatmentsCalculator/fallbackSanitizer/index.js +47 -0
  4. package/cjs/evaluator/fallbackTreatmentsCalculator/index.js +48 -0
  5. package/cjs/logger/index.js +3 -0
  6. package/cjs/logger/messages/info.js +1 -1
  7. package/cjs/logger/messages/warn.js +1 -1
  8. package/cjs/sdkClient/client.js +9 -2
  9. package/cjs/sdkClient/clientInputValidation.js +17 -6
  10. package/cjs/sdkClient/sdkClient.js +1 -1
  11. package/cjs/sdkFactory/index.js +3 -1
  12. package/cjs/sync/polling/syncTasks/segmentsSyncTask.js +1 -1
  13. package/cjs/sync/polling/updaters/segmentChangesUpdater.js +5 -16
  14. package/cjs/sync/polling/updaters/splitChangesUpdater.js +2 -2
  15. package/cjs/utils/inputValidation/splitExistence.js +1 -1
  16. package/cjs/utils/labels/index.js +3 -1
  17. package/esm/evaluator/fallbackTreatmentsCalculator/constants.js +5 -0
  18. package/esm/evaluator/fallbackTreatmentsCalculator/fallbackSanitizer/index.js +44 -0
  19. package/esm/evaluator/fallbackTreatmentsCalculator/index.js +45 -0
  20. package/esm/logger/index.js +3 -0
  21. package/esm/logger/messages/info.js +1 -1
  22. package/esm/logger/messages/warn.js +1 -1
  23. package/esm/sdkClient/client.js +9 -2
  24. package/esm/sdkClient/clientInputValidation.js +18 -7
  25. package/esm/sdkClient/sdkClient.js +1 -1
  26. package/esm/sdkFactory/index.js +3 -1
  27. package/esm/sync/polling/syncTasks/segmentsSyncTask.js +1 -1
  28. package/esm/sync/polling/updaters/segmentChangesUpdater.js +5 -16
  29. package/esm/sync/polling/updaters/splitChangesUpdater.js +2 -2
  30. package/esm/utils/inputValidation/splitExistence.js +2 -2
  31. package/esm/utils/labels/index.js +2 -0
  32. package/package.json +1 -1
  33. package/src/evaluator/fallbackTreatmentsCalculator/constants.ts +4 -0
  34. package/src/evaluator/fallbackTreatmentsCalculator/fallbackSanitizer/index.ts +62 -0
  35. package/src/evaluator/fallbackTreatmentsCalculator/index.ts +57 -0
  36. package/src/logger/index.ts +2 -0
  37. package/src/logger/messages/info.ts +1 -1
  38. package/src/logger/messages/warn.ts +1 -1
  39. package/src/sdkClient/client.ts +11 -2
  40. package/src/sdkClient/clientInputValidation.ts +22 -7
  41. package/src/sdkClient/sdkClient.ts +2 -1
  42. package/src/sdkFactory/index.ts +4 -1
  43. package/src/sdkFactory/types.ts +2 -0
  44. package/src/sync/polling/syncTasks/segmentsSyncTask.ts +0 -2
  45. package/src/sync/polling/updaters/segmentChangesUpdater.ts +4 -17
  46. package/src/sync/polling/updaters/splitChangesUpdater.ts +5 -4
  47. package/src/utils/inputValidation/splitExistence.ts +2 -2
  48. package/src/utils/labels/index.ts +3 -0
  49. package/types/splitio.d.ts +17 -1
@@ -14,6 +14,7 @@ import { uniqueKeysTrackerFactory } from '../trackers/uniqueKeysTracker';
14
14
  import { DEBUG, OPTIMIZED } from '../utils/constants';
15
15
  import { setRolloutPlan } from '../storages/setRolloutPlan';
16
16
  import { getMatching } from '../utils/key';
17
+ import { FallbackTreatmentsCalculator } from '../evaluator/fallbackTreatmentsCalculator';
17
18
  /**
18
19
  * Modular SDK factory
19
20
  */
@@ -48,6 +49,7 @@ export function sdkFactory(params) {
48
49
  readiness.splits.emit(SDK_SPLITS_CACHE_LOADED);
49
50
  }
50
51
  });
52
+ var fallbackTreatmentsCalculator = new FallbackTreatmentsCalculator(settings.log, settings.fallbackTreatments);
51
53
  if (initialRolloutPlan) {
52
54
  setRolloutPlan(log, initialRolloutPlan, storage, key && getMatching(key));
53
55
  if (storage.splits.getChangeNumber() > -1)
@@ -68,7 +70,7 @@ export function sdkFactory(params) {
68
70
  var eventTracker = eventTrackerFactory(settings, storage.events, whenInit, integrationsManager, storage.telemetry);
69
71
  // splitApi is used by SyncManager and Browser signal listener
70
72
  var splitApi = splitApiFactory && splitApiFactory(settings, platform, telemetryTracker);
71
- var ctx = { clients: clients, splitApi: splitApi, eventTracker: eventTracker, impressionsTracker: impressionsTracker, telemetryTracker: telemetryTracker, uniqueKeysTracker: uniqueKeysTracker, sdkReadinessManager: sdkReadinessManager, readiness: readiness, settings: settings, storage: storage, platform: platform };
73
+ var ctx = { clients: clients, splitApi: splitApi, eventTracker: eventTracker, impressionsTracker: impressionsTracker, telemetryTracker: telemetryTracker, uniqueKeysTracker: uniqueKeysTracker, sdkReadinessManager: sdkReadinessManager, readiness: readiness, settings: settings, storage: storage, platform: platform, fallbackTreatmentsCalculator: fallbackTreatmentsCalculator };
72
74
  var syncManager = syncManagerFactory && syncManagerFactory(ctx);
73
75
  ctx.syncManager = syncManager;
74
76
  var signalListener = SignalListener && new SignalListener(syncManager, settings, storage, splitApi);
@@ -5,5 +5,5 @@ import { segmentChangesUpdaterFactory } from '../updaters/segmentChangesUpdater'
5
5
  * Creates a sync task that periodically executes a `segmentChangesUpdater` task
6
6
  */
7
7
  export function segmentsSyncTaskFactory(fetchSegmentChanges, storage, readiness, settings) {
8
- return syncTaskFactory(settings.log, segmentChangesUpdaterFactory(settings.log, segmentChangesFetcherFactory(fetchSegmentChanges), storage.segments, readiness, settings.startup.requestTimeoutBeforeReady, settings.startup.retriesOnFailureBeforeReady), settings.scheduler.segmentsRefreshRate, 'segmentChangesUpdater');
8
+ return syncTaskFactory(settings.log, segmentChangesUpdaterFactory(settings.log, segmentChangesFetcherFactory(fetchSegmentChanges), storage.segments, readiness), settings.scheduler.segmentsRefreshRate, 'segmentChangesUpdater');
9
9
  }
@@ -1,6 +1,5 @@
1
1
  import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants';
2
2
  import { LOG_PREFIX_INSTANTIATION, LOG_PREFIX_SYNC_SEGMENTS } from '../../../logger/constants';
3
- import { timeout } from '../../../utils/promise/timeout';
4
3
  /**
5
4
  * Factory of SegmentChanges updater, a task that:
6
5
  * - fetches segment changes using `segmentChangesFetcher`
@@ -12,33 +11,22 @@ import { timeout } from '../../../utils/promise/timeout';
12
11
  * @param segments - segments storage, with sync or async methods
13
12
  * @param readiness - optional readiness manager. Not required for synchronizer or producer mode.
14
13
  */
15
- export function segmentChangesUpdaterFactory(log, segmentChangesFetcher, segments, readiness, requestTimeoutBeforeReady, retriesOnFailureBeforeReady) {
14
+ export function segmentChangesUpdaterFactory(log, segmentChangesFetcher, segments, readiness) {
16
15
  var readyOnAlreadyExistentState = true;
17
- function _promiseDecorator(promise) {
18
- if (readyOnAlreadyExistentState && requestTimeoutBeforeReady)
19
- promise = timeout(requestTimeoutBeforeReady, promise);
20
- return promise;
21
- }
22
- function updateSegment(segmentName, noCache, till, fetchOnlyNew, retries) {
16
+ function updateSegment(segmentName, noCache, till, fetchOnlyNew) {
23
17
  log.debug(LOG_PREFIX_SYNC_SEGMENTS + "Processing segment " + segmentName);
24
18
  var sincePromise = Promise.resolve(segments.getChangeNumber(segmentName));
25
19
  return sincePromise.then(function (since) {
26
20
  // if fetchOnlyNew flag, avoid processing already fetched segments
27
21
  return fetchOnlyNew && since !== undefined ?
28
22
  false :
29
- segmentChangesFetcher(since || -1, segmentName, noCache, till, _promiseDecorator).then(function (changes) {
23
+ segmentChangesFetcher(since || -1, segmentName, noCache, till).then(function (changes) {
30
24
  return Promise.all(changes.map(function (x) {
31
25
  log.debug(LOG_PREFIX_SYNC_SEGMENTS + "Processing " + segmentName + " with till = " + x.till + ". Added: " + x.added.length + ". Removed: " + x.removed.length);
32
26
  return segments.update(segmentName, x.added, x.removed, x.till);
33
27
  })).then(function (updates) {
34
28
  return updates.some(function (update) { return update; });
35
29
  });
36
- }).catch(function (error) {
37
- if (retries) {
38
- log.warn(LOG_PREFIX_SYNC_SEGMENTS + "Retrying fetch of segment " + segmentName + " (attempt #" + retries + "). Reason: " + error);
39
- return updateSegment(segmentName, noCache, till, fetchOnlyNew, retries - 1);
40
- }
41
- throw error;
42
30
  });
43
31
  });
44
32
  }
@@ -58,7 +46,8 @@ export function segmentChangesUpdaterFactory(log, segmentChangesFetcher, segment
58
46
  // If not a segment name provided, read list of available segments names to be updated.
59
47
  var segmentsPromise = Promise.resolve(segmentName ? [segmentName] : segments.getRegisteredSegments());
60
48
  return segmentsPromise.then(function (segmentNames) {
61
- var updaters = segmentNames.map(function (segmentName) { return updateSegment(segmentName, noCache, till, fetchOnlyNew, readyOnAlreadyExistentState ? retriesOnFailureBeforeReady : 0); });
49
+ // Async fetchers
50
+ var updaters = segmentNames.map(function (segmentName) { return updateSegment(segmentName, noCache, till, fetchOnlyNew); });
62
51
  return Promise.all(updaters).then(function (shouldUpdateFlags) {
63
52
  // if at least one segment fetch succeeded, mark segments ready
64
53
  if (shouldUpdateFlags.some(function (update) { return update; }) || readyOnAlreadyExistentState) {
@@ -166,14 +166,14 @@ export function splitChangesUpdaterFactory(log, splitChangesFetcher, storage, sp
166
166
  });
167
167
  })
168
168
  .catch(function (error) {
169
+ log.warn(SYNC_SPLITS_FETCH_FAILS, [error]);
169
170
  if (startingUp && retriesOnFailureBeforeReady > retry) {
170
171
  retry += 1;
171
- log.warn(SYNC_SPLITS_FETCH_RETRY, [retry, error]);
172
+ log.info(SYNC_SPLITS_FETCH_RETRY, [retry, error]);
172
173
  return _splitChangesUpdater(sinces, retry);
173
174
  }
174
175
  else {
175
176
  startingUp = false;
176
- log.warn(SYNC_SPLITS_FETCH_FAILS, [error]);
177
177
  }
178
178
  return false;
179
179
  });
@@ -1,4 +1,4 @@
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.
@@ -6,7 +6,7 @@ import { WARN_NOT_EXISTENT_SPLIT } from '../../logger/constants';
6
6
  */
7
7
  export function validateSplitExistence(log, readinessManager, splitName, labelOrSplitObj, method) {
8
8
  if (readinessManager.isReady()) { // Only if it's ready we validate this, otherwise it may just be that the SDK is not ready yet.
9
- if (labelOrSplitObj === SPLIT_NOT_FOUND || labelOrSplitObj == null) {
9
+ if (labelOrSplitObj === SPLIT_NOT_FOUND || labelOrSplitObj == null || labelOrSplitObj === FALLBACK_SPLIT_NOT_FOUND) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splitsoftware/splitio-commons",
3
- "version": "2.7.2-rc.0",
3
+ "version": "2.7.9-rc.0",
4
4
  "description": "Split JavaScript SDK common components",
5
5
  "main": "cjs/index.js",
6
6
  "module": "esm/index.js",
@@ -0,0 +1,4 @@
1
+ export enum FallbackDiscardReason {
2
+ FlagName = 'Invalid flag name (max 100 chars, no spaces)',
3
+ Treatment = 'Invalid treatment (max 100 chars and must match pattern)',
4
+ }
@@ -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
+ }
@@ -72,6 +72,8 @@ export class Logger implements ILogger {
72
72
  if (logger) {
73
73
  if (isLogger(logger)) {
74
74
  this.logger = logger;
75
+ // If custom logger is set, all logs are either enabled or disabled
76
+ if (this.logLevel !== LogLevelIndexes.NONE) this.setLogLevel(LogLevels.DEBUG);
75
77
  return;
76
78
  } else {
77
79
  this.error('Invalid `logger` instance. It must be an object with `debug`, `info`, `warn` and `error` methods. Defaulting to `console.log`');
@@ -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 fetch of feature flags (attempt #%s). Reason: %s'],
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 fetch of memberships (attempt #%s). Reason: %s'],
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'],
@@ -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
 
@@ -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 { treatment, label, changeNumber, config = null, impressionsDisabled } = evaluation;
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 ? fallbackTreatment.label : 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,
@@ -13,19 +12,20 @@ import {
13
12
  validateEvaluationOptions
14
13
  } from '../utils/inputValidation';
15
14
  import { startsWith } from '../utils/lang';
16
- import { CONTROL, CONTROL_WITH_CONFIG, 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';
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);
@@ -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
- return wrapResult(CONTROL);
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
- return wrapResult(objectAssign({}, CONTROL_WITH_CONFIG));
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] = CONTROL);
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] = objectAssign({}, CONTROL_WITH_CONFIG));
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
@@ -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;
@@ -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 {
@@ -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'
@@ -4,7 +4,6 @@ import { IReadinessManager } from '../../../readiness/types';
4
4
  import { SDK_SEGMENTS_ARRIVED } from '../../../readiness/constants';
5
5
  import { ILogger } from '../../../logger/types';
6
6
  import { LOG_PREFIX_INSTANTIATION, LOG_PREFIX_SYNC_SEGMENTS } from '../../../logger/constants';
7
- import { timeout } from '../../../utils/promise/timeout';
8
7
 
9
8
  type ISegmentChangesUpdater = (fetchOnlyNew?: boolean, segmentName?: string, noCache?: boolean, till?: number) => Promise<boolean>
10
9
 
@@ -24,18 +23,11 @@ export function segmentChangesUpdaterFactory(
24
23
  segmentChangesFetcher: ISegmentChangesFetcher,
25
24
  segments: ISegmentsCacheBase,
26
25
  readiness?: IReadinessManager,
27
- requestTimeoutBeforeReady?: number,
28
- retriesOnFailureBeforeReady?: number,
29
26
  ): ISegmentChangesUpdater {
30
27
 
31
28
  let readyOnAlreadyExistentState = true;
32
29
 
33
- function _promiseDecorator<T>(promise: Promise<T>) {
34
- if (readyOnAlreadyExistentState && requestTimeoutBeforeReady) promise = timeout(requestTimeoutBeforeReady, promise);
35
- return promise;
36
- }
37
-
38
- function updateSegment(segmentName: string, noCache?: boolean, till?: number, fetchOnlyNew?: boolean, retries?: number): Promise<boolean> {
30
+ function updateSegment(segmentName: string, noCache?: boolean, till?: number, fetchOnlyNew?: boolean): Promise<boolean> {
39
31
  log.debug(`${LOG_PREFIX_SYNC_SEGMENTS}Processing segment ${segmentName}`);
40
32
  let sincePromise = Promise.resolve(segments.getChangeNumber(segmentName));
41
33
 
@@ -43,19 +35,13 @@ export function segmentChangesUpdaterFactory(
43
35
  // if fetchOnlyNew flag, avoid processing already fetched segments
44
36
  return fetchOnlyNew && since !== undefined ?
45
37
  false :
46
- segmentChangesFetcher(since || -1, segmentName, noCache, till, _promiseDecorator).then((changes) => {
38
+ segmentChangesFetcher(since || -1, segmentName, noCache, till).then((changes) => {
47
39
  return Promise.all(changes.map(x => {
48
40
  log.debug(`${LOG_PREFIX_SYNC_SEGMENTS}Processing ${segmentName} with till = ${x.till}. Added: ${x.added.length}. Removed: ${x.removed.length}`);
49
41
  return segments.update(segmentName, x.added, x.removed, x.till);
50
42
  })).then((updates) => {
51
43
  return updates.some(update => update);
52
44
  });
53
- }).catch(error => {
54
- if (retries) {
55
- log.warn(`${LOG_PREFIX_SYNC_SEGMENTS}Retrying fetch of segment ${segmentName} (attempt #${retries}). Reason: ${error}`);
56
- return updateSegment(segmentName, noCache, till, fetchOnlyNew, retries - 1);
57
- }
58
- throw error;
59
45
  });
60
46
  });
61
47
  }
@@ -77,7 +63,8 @@ export function segmentChangesUpdaterFactory(
77
63
  let segmentsPromise = Promise.resolve(segmentName ? [segmentName] : segments.getRegisteredSegments());
78
64
 
79
65
  return segmentsPromise.then(segmentNames => {
80
- const updaters = segmentNames.map(segmentName => updateSegment(segmentName, noCache, till, fetchOnlyNew, readyOnAlreadyExistentState ? retriesOnFailureBeforeReady : 0));
66
+ // Async fetchers
67
+ const updaters = segmentNames.map(segmentName => updateSegment(segmentName, noCache, till, fetchOnlyNew));
81
68
 
82
69
  return Promise.all(updaters).then(shouldUpdateFlags => {
83
70
  // if at least one segment fetch succeeded, mark segments ready
@@ -120,8 +120,8 @@ export function splitChangesUpdaterFactory(
120
120
  storage: Pick<IStorageBase, 'splits' | 'rbSegments' | 'segments' | 'save'>,
121
121
  splitFiltersValidation: ISplitFiltersValidation,
122
122
  splitsEventEmitter?: ISplitsEventEmitter,
123
- requestTimeoutBeforeReady = 0,
124
- retriesOnFailureBeforeReady = 0,
123
+ requestTimeoutBeforeReady: number = 0,
124
+ retriesOnFailureBeforeReady: number = 0,
125
125
  isClientSide?: boolean
126
126
  ): SplitChangesUpdater {
127
127
  const { splits, rbSegments, segments } = storage;
@@ -201,13 +201,14 @@ export function splitChangesUpdaterFactory(
201
201
  });
202
202
  })
203
203
  .catch(error => {
204
+ log.warn(SYNC_SPLITS_FETCH_FAILS, [error]);
205
+
204
206
  if (startingUp && retriesOnFailureBeforeReady > retry) {
205
207
  retry += 1;
206
- log.warn(SYNC_SPLITS_FETCH_RETRY, [retry, error]);
208
+ log.info(SYNC_SPLITS_FETCH_RETRY, [retry, error]);
207
209
  return _splitChangesUpdater(sinces, retry);
208
210
  } else {
209
211
  startingUp = false;
210
- log.warn(SYNC_SPLITS_FETCH_FAILS, [error]);
211
212
  }
212
213
  return false;
213
214
  });
@@ -1,4 +1,4 @@
1
- import { SPLIT_NOT_FOUND } from '../labels';
1
+ import { FALLBACK_SPLIT_NOT_FOUND, SPLIT_NOT_FOUND } from '../labels';
2
2
  import { IReadinessManager } from '../../readiness/types';
3
3
  import { ILogger } from '../../logger/types';
4
4
  import { WARN_NOT_EXISTENT_SPLIT } from '../../logger/constants';
@@ -9,7 +9,7 @@ import { WARN_NOT_EXISTENT_SPLIT } from '../../logger/constants';
9
9
  */
10
10
  export function validateSplitExistence(log: ILogger, readinessManager: IReadinessManager, splitName: string, labelOrSplitObj: any, method: string): boolean {
11
11
  if (readinessManager.isReady()) { // Only if it's ready we validate this, otherwise it may just be that the SDK is not ready yet.
12
- if (labelOrSplitObj === SPLIT_NOT_FOUND || labelOrSplitObj == null) {
12
+ if (labelOrSplitObj === SPLIT_NOT_FOUND || labelOrSplitObj == null || labelOrSplitObj === FALLBACK_SPLIT_NOT_FOUND) {
13
13
  log.warn(WARN_NOT_EXISTENT_SPLIT, [method, splitName]);
14
14
  return false;
15
15
  }
@@ -1,3 +1,5 @@
1
+ import { FALLBACK_PREFIX } from '../../evaluator/fallbackTreatmentsCalculator';
2
+
1
3
  export const SPLIT_KILLED = 'killed';
2
4
  export const NO_CONDITION_MATCH = 'default rule';
3
5
  export const SPLIT_NOT_FOUND = 'definition not found';
@@ -7,3 +9,4 @@ export const SPLIT_ARCHIVED = 'archived';
7
9
  export const NOT_IN_SPLIT = 'not in split';
8
10
  export const UNSUPPORTED_MATCHER_TYPE = 'targeting rule type unsupported by sdk';
9
11
  export const PREREQUISITES_NOT_MET = 'prerequisites not met';
12
+ export const FALLBACK_SPLIT_NOT_FOUND = FALLBACK_PREFIX + SPLIT_NOT_FOUND;
@@ -93,7 +93,6 @@ interface ISharedSettings {
93
93
  urls?: SplitIO.UrlSettings;
94
94
  /**
95
95
  * Custom logger object. If not provided, the SDK will use the default `console.log` method for all log levels.
96
- * Set together with `debug` option to `true` or a log level string to enable logging.
97
96
  */
98
97
  logger?: SplitIO.Logger;
99
98
  }
@@ -146,6 +145,8 @@ interface IPluggableSharedSettings {
146
145
  * config.debug = ErrorLogger()
147
146
  * ```
148
147
  *
148
+ * When combined with the `logger` option, any log level other than `NONE` (false) will be set to `DEBUG` (true), delegating log level control to the custom logger.
149
+ *
149
150
  * @defaultValue `false`
150
151
  */
151
152
  debug?: boolean | SplitIO.LogLevel | SplitIO.ILogger;
@@ -169,6 +170,8 @@ interface INonPluggableSharedSettings {
169
170
  * config.debug = 'WARN'
170
171
  * ```
171
172
  *
173
+ * When combined with the `logger` option, any log level other than `NONE` (false) will be set to `DEBUG` (true), delegating log level control to the custom logger.
174
+ *
172
175
  * @defaultValue `false`
173
176
  */
174
177
  debug?: boolean | SplitIO.LogLevel;
@@ -618,6 +621,10 @@ declare namespace SplitIO {
618
621
  * User consent status if using in client-side. Undefined if using in server-side (Node.js).
619
622
  */
620
623
  readonly userConsent?: ConsentStatus;
624
+ /**
625
+ * Fallback treatments to be used when the SDK is not ready or the flag is not found.
626
+ */
627
+ readonly fallbackTreatments?: FallbackTreatmentConfiguration;
621
628
  }
622
629
  /**
623
630
  * Log levels.
@@ -1225,6 +1232,15 @@ declare namespace SplitIO {
1225
1232
  * User consent status.
1226
1233
  */
1227
1234
  type ConsentStatus = 'GRANTED' | 'DECLINED' | 'UNKNOWN';
1235
+ /**
1236
+ * Fallback treatments to be used when the SDK is not ready or the flag is not found.
1237
+ */
1238
+ type FallbackTreatmentConfiguration = {
1239
+ global?: Treatment | TreatmentWithConfig,
1240
+ byFlag?: {
1241
+ [featureFlagName: string]: Treatment | TreatmentWithConfig
1242
+ }
1243
+ }
1228
1244
  /**
1229
1245
  * Logger. Its interface details are not part of the public API. It shouldn't be used directly.
1230
1246
  */