Usage

Feature Toggles Class

The main class of the library is FeatureToggles, all synchronization for a given configuration happens within an instance of this class. Constructing an instance requires a unique name, so that multiple instances communicating with Redis don’t access the same data, unless this is intended. Whenever two or more instances use the same unique name, they must be initialized with the same configuration.

const toggles = require("@cap-js-community/feature-toggle-library");
const FeatureToggles = toggles.constructor;
const instance = new FeatureToggles({ uniqueName: "snowflake" });

The library prepares a convenient singleton instance of FeatureToggles for out-of-the-box usage, where the Cloud Foundry app name is used as unique name. This should be sufficient for most use-cases and is the default export of the library.

const toggles = require("@cap-js-community/feature-toggle-library");

Be aware that using the singleton instance and changing the app name will invalidate the Redis state and set back all toggles to their fallback values.

Configuration

We recommend maintaining the configuration in a version-tracked, YAML- or JSON-file, which only changes during deployments. The configuration is a key-value map describing each individual feature toggle. Here is an example in YAML.

/srv/util/logger/logLevel:
  type: string
  fallbackValue: info
  appUrl: \.cfapps\.sap\.hana\.ondemand\.com$
  validations:
    - regex: ^(error|warn|info|verbose|debug)$

The semantics of these properties are as follows.

property required meaning
type true one of the allowed types boolean, number, string
fallbackValue true emergency fallback value
active   if this is false, the corresponding feature toggle is inactive
appUrl   activate toggle only if appUrl regex is matched
validations   list of validations

type
You can use the type string to encode complex data types like arrays or objects, but need to take care of serialization/deserialization yourself. In these cases, make sure to use external validation so that new values can be deserialized correctly.

fallbackValue
This value gets set initially when the feature toggle is introduced, and it is also used as a fallback when communication with Redis is interrupted during startup.

active
Using active or appUrl to block the activation of a feature toggle, will cause all usage code reading it to always get the fallback value.

appUrl
Regular expression for activating a feature toggle only if at least one of its Cloud Foundry application’s urls match. When the library is not running in CF_REDIS integration mode, this check is disabled. Here are some examples:

  • for CANARY landscape \.cfapps\.sap\.hana\.ondemand\.com$,
  • for EU10 landscape \.cfapps\.eu10\.hana\.ondemand\.com$,
  • specific CANARY app <cf-app-name>\.cfapps\.sap\.hana\.ondemand\.com$.

validations
List of validations that will guard all changes of the associated feature toggle. All validations must pass successfully for a change to occur. Each kind of validation can happen multiple times. Here is a practical example with all possible validation kinds:

# info: check api priority; 0 means access is disabled
/check/priority:
  type: number
  fallbackValue: 0
  validations:
    - scopes: [user, tenant]
    - regex: ^\d+$
    - { module: "$CONFIG_DIR/validators.js", call: validateTenantScope }

The semantics of these properties are as follows.

property meaning
scopes restrict which scopes are allowed
regex value converted to string must match regex
module register external validation module

module
Module points to a module, where an external validation is implemented. These external checks get registered during initialization and will be called during change attempts. You can specify just the module and export the validation function directly. Alternatively, you can specify both the module and a property to call on the module.

For the module path, you can specify it either relative to the runtime working directory (usually the project root), e.g., module: ./path-from-root/validations.js, or you can use the location of the configuration file as a relative anchor, e.g., module: $CONFIG_DIR/validation.js.

Initialization

For CAP Projects

CAP projects will use the library as a CDS-Plugin. The initialization of CAP Feature Toggles, meaning toggles that govern the active CDS model extensions, will happen automatically based on the fts/ project subdirectory structure. These toggles do not require a configuration file.

If you need further, arbitrary runtime toggles, you can expand on the automatically recognized CAP toggles with an initialization setting in package.json. For example:

{
  "cds": {
    "featureToggles": {
      "configFile": "./srv/feature/features.yaml"
    }
  }
}

In this example, the path ./srv/feature/feature.yaml points to the previously discussed configuration file.

Either way, with either automatic CAP toggles or configured arbitrary runtime toggles, the singleton instance of the library will be initialized and is ready for usage at and after the bootstrap event.

Using the feature toggles in CAP projects also enables a REST service, where toggles can be read and manipulated.

For Non-CAP Projects

Other projects will need to use the corresponding filepath, in order to initialize the feature toggles instance in code.

const pathlib = require("path");
const toggles = require("@cap-js-community/feature-toggle-library");
const FEATURES_FILEPATH = pathlib.join(__dirname, ".toggles.yml");

// ... during application bootstrap
await toggles.initializeFeatures({ configFile: FEATURES_FILEPATH });

Alternatively, a runtime configuration object is also supported.

const toggles = require("@cap-js-community/feature-toggle-library");

// ... during application bootstrap
await toggles.initializeFeatures({
  config: {
    runNewCode: {
      type: "boolean",
      fallbackValue: false,
    },
    maxConsumers: {
      type: "number",
      fallbackValue: 100,
    },
  },
});

In rare cases, it can make sense to read the configuration from file and manipulate it in code before passing it to the feature toggles.

const pathlib = require("path");
const toggles = require("@cap-js-community/feature-toggle-library");
const FeatureToggles = toggles.constructor;
const FEATURES_FILEPATH = pathlib.join(__dirname, ".toggles.yml");

// ... during application bootstrap
const config = await FeatureToggles.readConfigFromFile(FEATURES_FILEPATH);
// ... manipulate
await toggles.initializeFeatures({ config });

Integration Mode

After successful initialization, the library will write one info log of the form:

13:40:13.775 | INFO | /FeatureToggles | finished initialization with 2 feature toggles with NO_REDIS

It tells you both how many toggles where initialized, and the integration mode, that the library detected. Here are all possible modes:

mode meaning
NO_REDIS no redis detected, all changes are only in memory and will be lost on restart
LOCAL_REDIS local redis server detected, changes are persisted in that local redis
CF_REDIS Cloud Foundry redis service binding detected, changes will be persisted normally

Environment Variables

The following environment variables can be used to fine-tune the library’s behavior:

variable default meaning
BTP_FEATURES_UNIQUE_NAME <cfAppName> override uniqueName of singleton (see Class)
BTP_FEATURES_REDIS_KEY features-<uniqueName> override Redis key for central state
BTP_FEATURES_REDIS_CHANNEL features-<uniqueName> override Redis channel for synchronization

User Code

In this section, we will assume that the initialization has happened and the configuration contained a feature toggle with the key /srv/util/logger/logLevel, similar to the one described here.

Reading Feature Value

You can read the current in memory state of any feature toggle:

const toggles = require("@cap-js-community/feature-toggle-library");

// ... in some function
const logLevel = toggles.getFeatureValue("/srv/util/logger/logLevel");

// ... with runtime scope information
const logLevel = toggles.getFeatureValue("/srv/util/logger/logLevel", {
  tenant: cds.context.tenant,
  user: cds.context.user.id,
});

While getFeatureValue is synchronous, and could happen on the top-level of a module. The function will throw, if it is called before initializeFeatures, which is asynchronous. So, it’s never sensible to have this on top-level.

Observing Feature Value Changes

You can register a callback for all updates to a feature toggle:

const toggles = require("@cap-js-community/feature-toggle-library");

toggles.registerFeatureValueChangeHandler("/srv/util/logger/logLevel", (newValue, oldValue, scopeMap) => {
  console.log("changing log level from %s to %s (scope %j)", oldValue, newValue, scopeMap);
  updateLogLevel(newValue);
});

// ... or for async APIs
toggles.registerFeatureValueChangeHandler("/srv/util/logger/logLevel", async (newValue, oldValue, scopeMap) => {
  await updateLogLevel(newValue);
});

Some important caveats for change handlers:

  • scopes: The handler will always receive the new value that is relevant for the scopeMap that is passed along. Meaning it will be called with new values that can be highly restricted and getFeatureValue calls with the same feature key can return different values for other scopeMaps or no scopeMap.
  • deletion: If values are deleted, by setting them to null, the change handler will receive the new actual value for the relevant scopeMap and not the change value of null.

Registering any callback will not require that the feature toggles are initialized, so this can happen on top-level.

Updating Feature Value

Finally, updating the feature toggle value:

const toggles = require("@cap-js-community/feature-toggle-library");

// optionally pass in a scopeMap, which describes the least specific scope where the change should happen
async function changeIt(newValue, scopeMap) {
  const validationErrors = await toggles.changeFeatureValue("/srv/util/logger/logLevel", newValue, scopeMap);
  if (Array.isArray(validationErrors) && validationErrors.length > 0) {
    for (const { errorMessage, errorMessageValues } of validationErrors) {
      // show errors to the user, the change did not happen
    }
  }
}

The change API changeFeatureValue will return when the change is published to Redis, so there may be a slight processing delay until the change is picked up by all subscribers.

Setting a feature value to null will delete the associated remote state and effectively reset it to its fallback value.

Option: clearSubScopes

Since setting values for scope-combinations happens additively, it can become hard to keep track of which combinations have maintained values attached to them. If you want to set a value and make sure that there isn’t a more specific scope-combination, which overrides that value, then you can use the option { clearSubScopes: true } as a third argument. For example

await toggles.changeFeatureValue("/srv/util/logger/logLevel", "error", {}, { clearSubScopes: true });

will set the root-scope value to "error" and remove all sub-scopes. See scoping for context.

Option: remoteOnly

When you find toggle values in Redis that are not configured, marked with { "SOURCE": "NONE" }, it usually makes sense to remove them. In this situation, we want to change just Redis and bypass the local server state update, the usual validation, change handlers, and server instance change propagation. To achieve this, you can use the { remoteOnly: true } option. For example

await toggles.changeFeatureValue("/legacy-key", null, {}, { clearSubScopes: true, remoteOnly: true });

will remove all maintained values associated with the /legacy-key key in Redis.

Changes with the { remoteOnly: true } option will be blocked for configured toggles. This happens to avoid situations where the remote state of these toggles is accidentally changed in a way that bypasses validations and server state updates.

Resetting Feature Value

There is a convenience reset API just to reset a feature toggle and remove all associated persisted values. Reading the feature toggle afterward will only yield the fallback value until new changes are made.

const toggles = require("@cap-js-community/feature-toggle-library");

// ... in some function
await toggles.resetFeatureValue("/srv/util/logger/logLevel");

// this is functionally equivalent to
await toggles.changeFeatureValue("/srv/util/logger/logLevel", null, {}, { clearSubScopes: true });

External Validation

The string-type feature toggles can theoretically encode very complex data structures, so it’s sensible to validate inputs in-depth before allowing changes to be published and propagated.

const toggles = require("@cap-js-community/feature-toggle-library");

toggles.registerFeatureValueValidation("/srv/util/logger/logLevel", (newValue) => {
  if (isBad(newValue)) {
    return { errorMessage: "got bad value" };
  }
  if (isWorse(newValue)) {
    return { errorMessage: 'got bad value with parameter "{0}"', errorMessageValues: [paramFromValue(newValue)] };
  }
});

Simple validation rules that can be expressed as a regular expression should use the associated validation configuration instead.