Custom Properties

Custom properties allow you to expose any of your application properties to CloudBees Feature Flags and then use these properties as a criteria for feature releases. Custom properties can be used either in Target Groups or in the Audiences tab as a criteria for feature release.

For example, if you want to open a feature only for internal employees, the way you can identify internal employees is by the email domain they have (e.g: @cloudbees.com).  In this example you would define a custom property called email and then use it when deploying the feature.

Creating Custom Properties

You can create your own custom properties that can be used when defining Target Groups or in the Audience tab as a criteria for feature release. Custom properties can be created either from the code or the dashboard. If you add a custom property to the code and run/compile the application they will automatically be available in the dashboard.

Explicit Custom Property - setCustomProperty

Explicit property is a Custom Property which is defined entirely within your code by using the CloudBees Feature Flags API. The SDK supplies a function for every type of property. The available property types are as follows:

  • Boolean

  • Int

  • Double

  • String

  • SemVer

The type Number is used in some platforms instead of Double and Int.

Once the above code is running you will be able to use the Custom Property from within the CloudBees Feature Flags dashboard.

The following are examples of how to define Explicit Custom Properties in the code on different platforms:

Swift Objective-C Android React Native JavaScript Node.js JavaScript SSR JVM .NET Python Go Ruby PHP C C++
ROX.setCustomProperty(key:"intProp", value: 100)
ROX.setCustomProperty(key:"stringProp", value: "something")
ROX.setCustomProperty(key:"boolProp", value: true)

ROX.setCustomProperty(key: "isPaying") { () -> Bool in
  return Account.sharedInstance().paying
}

ROX.setCustomProperty(key: "email") { () -> String in
  return Account.sharedInstance().email
}
[ROXCore setCustomIntProperty:100 forKey:@"intProp"];
[ROXCore setCustomStringProperty:@"something" forKey:@"stringProp"];
[ROXCore setCustomBooleanProperty:YES forKey:@"boolProp"];

[ROXCore setCustomComputedBooleanProperty:^Bool *{
  return [[Account sharedInstance] isPaying];
} forKey:@"payingUser"];

[ROXCore setCustomComputedIntProperty:^int *{
  return [[Account sharedInstance] age];
} forKey:@"age"];

[ROXCore setCustomComputedStringProperty:^NSString *{
  return [[Account sharedInstance] email];
} forKey:@"email"];
Rox.setCustomStringProperty("userName", user.getName());

Rox.setCustomBooleanProperty("isPayingUser", user.isPayingUser());

Rox.setCustomComputedStringProperty("userNameComputed", new CustomPropertyGenerator<String>() {
 @Override
  public String generateProperty() {
    return database.getUserName();
  }
});
Rox.setCustomNumberProperty("numberProperty", 100)
Rox.setCustomStringProperty("stringProperty", "something")
Rox.setCustomBooleanProperty("booleanProperty", true)

Rox.setCustomBooleanProperty("isPaying", () => Account.sharedInstance().paying)

Rox.setCustomStringProperty("email", () => Account.sharedInstance().email())
Rox.setCustomNumberProperty("numberProperty", 100)
Rox.setCustomStringProperty("stringProperty", "something")
Rox.setCustomBooleanProperty("booleanProperty", true)

Rox.setCustomBooleanProperty("isPaying", () => Account.sharedInstance().paying)
Rox.setCustomBooleanProperty("isPayingUser", context => context.user.isPayingUser())

Rox.setCustomStringProperty("email", () => Account.sharedInstance().email())
Rox.setCustomStringProperty("serviceName", serviceName)

Rox.setCustomBooleanProperty("isPayingUser", context => context.user.isPayingUser())
import {Rox} from 'rox-ssr';

Rox.setCustomStringProperty("serviceName", serviceName)

Rox.setCustomBooleanProperty("isPayingUser", context => context.user.isPayingUser())
Rox.setCustomStringProperty("serviceName", serviceName);

Rox.setCustomComputedBooleanProperty("isPayingUser", new CustomPropertyGeneratorWithContext() {

  @Override
  public Boolean generateProperty(Context context) {
    return context.user.isPayingUser();
  }
});
Rox.SetCustomStringProperty("serviceName", serviceName);

Rox.SetCustomComputedBooleanProperty("isPayingUser", (context) =>
{
    return database.user.isPayingUser();
});
# simple String property
Rox.set_custom_string_property('serviceName', serviceName);

# simple Boolean computed property
Rox.set_custom_boolean_property('isPaying', lambda context :
        context['is_paying_user']
)
rox.SetCustomStringProperty("serviceName", serviceName)

rox.SetCustomComputedBooleanProperty("isPaying", func(context context.Context) bool {
        value, _ := context.Get("is_paying_user").(bool)
        return value
    })
# simple String property
Rox::Server::RoxServer.set_custom_string_property("serviceName", serviceName)

# simple Boolean computed property
Rox::Server::RoxServer.set_custom_boolean_property('isPaying') do |context|
  context['is_paying_user'] == true
end
# simple String property
Rox::Server::RoxServer.set_custom_string_property("serviceName", serviceName)

# simple Boolean computed property
Rox::Server::RoxServer.set_custom_boolean_property("isPaying", function (ContextInterface $context) {
    return (bool)($context->get("is_paying_user"));
});
rox_set_custom_integer_property("intProp", 100);
rox_set_custom_string_property("stringProp", "something");
rox_set_custom_boolean_property("boolProp", true);
rox_set_custom_double_property("doubleProp", 3.14);
rox_set_custom_semver_property("smvrProp", "3.11.0");

RoxDynamicValue *check_if_playing(void *target, RoxContext *context) {
    return rox_dynamic_value_create_boolean(rox_context_get(context, "playing"));
}

rox_set_custom_computed_boolean_property("isPlayingProperty", NULL, &check_if_playing);

RoxDynamicValue *user_email(void *target, RoxContext *context) {
    return rox_dynamic_value_create_string_copy(rox_context_get(context, "email"));
}

rox_set_custom_computed_string_property("emailProperty", NULL, &user_email);
Rox::SetCustomProperty<int>("intProp", 100);
Rox::SetCustomProperty<const char *>("stringProp", "something");
Rox::SetCustomProperty<bool>("boolProp", true);
Rox::SetCustomProperty<double>("doubleProp", 3.14);
Rox::SetCustomSemverProperty("smvrProp", "3.11.0");

class CheckIfPlaying : public Rox::CustomPropertyGeneratorInterface {
public:
    explicit CheckIfPlaying() {}

    Rox::DynamicValue *operator()(Rox::Context *context) override {
        return rox_context_get(context, "playing");
    }
};

CheckIfPlaying checkIfPlaying = CheckIfPlaying();
Rox::SetCustomComputedProperty<bool>("isPlayingProperty", &CheckIfPlaying);

class UserEmail : public Rox::CustomPropertyGeneratorInterface {
public:
    explicit UserEmail() {}

    Rox::DynamicValue *operator()(Rox::Context *context) override {
        return rox_context_get(context, "email");
    }
};

UserEmail userEmail = UserEmail();
Rox::SetCustomComputedProperty<const char *>("emailProperty", &CheckIfPlaying);

Implicit Custom Property - DynamicPropertiesRule

The value of that Custom Property is defined by the DynamicPropertiesRule in the code.

The Dynamic Custom Property Rule handler is called when an explicit Custom Property definition does not exist on the client side.

If you do not set the setDynamicCustomPropertyRule it will then activate the default function which tries to extract the property value from the context by its name.

A generic implementation of that handler can be described by the following snippet:

(propName, context) => context ? context[propName] : undefined

The following examples are code snippets to create this rule on different platforms:

JavaScript JavaScript SSR C# Node.js PHP C C++
Rox.setDynamicCustomPropertyRule((propName, context) =>
                                 getLoggedInUser().properties[propName]);
import {Rox} from 'rox-ssr';

Rox.setDynamicCustomPropertyRule((propName, context) =>
                                 getLoggedInUser().properties[propName]);
RoxOptions options = new RoxOptions(new RoxOptions.RoxOptionsBuilder{
    DynamicPropertiesRule = (property, context) => {
        //return context == null ? null : context.Get(property); <- default behavior
        // in case the property we use on Dashboard is a property on the user object in the context
        return context.Get("userData")[property]);
    }
});

await Rox.Setup(appKey, options);
Rox.setDynamicCustomPropertyRule((propName, context) => {
  if (propName.startsWith('account.')) {
    const accountProp = propName.split(".")[0];
    return context['account'].properties[accountProp]);
  }
  return null;
})
$roxOptionsBuilder = (new RoxOptionsBuilder())
    ->setDynamicPropertiesRule(function ($propName, ContextInterface $ctx) {
        return ($ctx->get("user"))->{$propName};
    });

Rox::setup(ROLLOUT_KEY, new RoxOptions($roxOptionsBuilder));
RoxDynamicValue *dynamic_rule(char *propName, void *target, RoxContext *context) {
  return rox_context_get(context, propName);
}

RoxOptions *options = rox_options_create();
rox_options_set_dynamic_properties_rule(options, NULL, &dynamic_rule);
class DynamicPropertyRule : public Rox::DynamicPropertiesRuleInterface {
  public:
    explicit DynamicPropertiesRuleInterface() {}
  public:
    DynamicValue *Invoke(const char *propName, Context *context) override {
            return rox_context_get(context, propName);
    }
};

int main(int argc, char **argv) {
  DynamicPropertyRule dynamicPropertyRule = DynamicPropertyRule();
    Rox::Options *options = Rox::OptionsBuilder()
        .SetDynamicPropertiesRule(&dynamicPropertyRule)
    .Build();

    Rox::Setup(DEFAULT_API_KEY, options);
    Rox::Shutdown();
}

As mentioned, you can also define custom properties in the dashboard. To add a custom property from dashboard: . Go to CloudBees Feature Flags dashboard. . Go to App Settings > Custom Properties. . Scroll the to the bottom and click Add new Custom Property

Custom Properties

You would still have to add the custom property to the codebase for it to actually work.

Changing Custom Properties

You can change your Custom Property in the dashboard instead of changing your property in the code. For example, if you defined a Custom Property of type Int, you can change the type to String in the dashboard.

To change Custom Properties, do the following:

  1. Go to CloudBees Feature Flags dashboard.

  2. Go to App Settings > Custom Properties. See a list of Custom Properties.

  3. Click on a Custom Property from the list to change. The Update Custom Property window pops up.

  4. In the Update Custom Property window:

    1. Change the type.

    2. Add a description

Hiding / Deleting Custom Properties

Since custom properties are added to the code, to prevent code < > dashboard discrepancies, custom properties cannot be deleted. Custom properties can however be hidden, which means they would not be available in the Target Group screen or in the inline targeting on the audience page.

You can still hide a custom property even if it’s been used.

To hide or unhide a custom property: . Go to CloudBees Feature Flags dashboard. . Go to App Settings > Custom Properties. . View the list of Custom Properties. . For each custom property there is a corresponding drop down menu to select if this property is visible or not.  Click on the dropdown to change custom property visibility.

Custom Properties

Server-side properties

Servers handle requests from multiple users as opposed to client applications that are built for a single user.

Sometimes the flag value might be related to the context of the call and will need additional information (in order to be evaluated correctly) which is available only at the time the flag is checked. For example, a flag value may only be determined once a user is authenticated.

This feature is only available for the following SDKs

Server side SDKs:

  • NodeJS

  • JavaScript SSR

  • Java (JVM)

  • .NET

  • Python

  • Go

  • Ruby

  • PHP

Client side SDK:

  • JavaScript

  • JavaScript SSR

Context object

Context is an object that is provided to a flag in order to evaluate the flag correctly for the specific call. Here is how you create a context:

NodeJS JavaScript JavaScript SSR JVM .NET Python Go Ruby PHP C C++
const userId = session.userId
const user = db.readUserSync(userId);
const shoppingCart = db.readShoppingCartSync(userId);

const context = { user, shoppingCart }
let user = 'John'
let email = '[email protected]'

let context = { user, email }
const userId = session.userId
const user = db.readUserSync(userId);
const shoppingCart = db.readShoppingCartSync(userId);

const context = { user, shoppingCart }
String userId = session.userId;
User user = db.readUser(userId);
ShoppingCart shoppingCart = db.readShoppingCart(userId);

Map<String, Object> map = new HashMap<>();
map.put("user", user);
map.put("shoppingCart", shoppingCart);
Context context = new RoxContext.Builder().from(map);
var context = new ContextBuilder().Build(
    new Dictionary<string, object> { { "user", user } }
);
context = {'user': user}
someContext := context.NewContext(map[string]interface{}{"user": user})
someContext = {'user' => user}
$context = (new ContextBuilder())->build([
    "user" => user
]);
RoxContext *userContext = rox_context_create_from_map(
            ROX_MAP(ROX_COPY("userId"), rox_dynamic_value_create_int(555)));
Rox::Context *userContext = Rox::ContextBuilder()
  .AddIntValue("userId", 555)
  .Build();

Context keys are String’s and values can be any type that is required in order to evaluate the flag correctly.

Providing context to a flag

When checking the flag value, the flag expects a context object that will be used for the evaluation.

NodeJS JavaScript JavaScript SSR JVM .NET Python Go Ruby PHP C C++
const userId = session.userId
const user = db.readUserSync(userId);
const shoppingCart = db.readShoppingCartSync(userId);

const context = { user, shoppingCart }

const myContainer = {
  specialDiscount: new Rox.Flag()
};

if (myContainer.specialDiscount.isEnabled(context)) {
  // enable special discount offer
}
let user = 'John'
let email = '[email protected]'

let context = { user, email }

let myContainer = {
  specialDiscount: new Rox.Flag()
};

if (myContainer.specialDiscount.isEnabled(context)) {
  // enable special discount offer
}
import {Flag} from 'rox-ssr';

const userId = session.userId
const user = db.readUserSync(userId);
const shoppingCart = db.readShoppingCartSync(userId);

const context = { user, shoppingCart }

const myContainer = {
  specialDiscount: new Flag()
};

if (myContainer.specialDiscount.isEnabled(context)) {
  // enable special discount offer
}
String userId = session.userId;
User user = db.readUser(userId);
ShoppingCart shoppingCart = db.readShoppingCart(userId);

Map<String, Object> map = new HashMap<>();
map.put("user", user);
map.put("shoppingCart", shoppingCart);
Context context = new RoxContext.Builder().from(map);

if (myContainer.specialDiscount.isEnabled(context)) {
  // enable special discount offer
}
var context = new ContextBuilder().Build(new Dictionary<string, object> { { "user", user } });

if (container.specialDiscount.isEnabled(context))
{
  // this user is entitled for special discount
}
context = {'user': user}

if container.special_discount.is_enabled(context) :
  # this user is entitled for special discount
someContext := context.NewContext(map[string]interface{}{"user": user})

if (container.specialDiscount.isEnabled(someContext)) {
  // this user is entitled for special discount
}
context = {'user': user}

if container.spe.enabled?(context)
  #this user is entitled for special discount
end
$context = (new ContextBuilder())->build([
    "user" => user
]);

if ($container->specialDiscount->getValue($context)) {
    // this user is entitled for special discount
}
RoxVariant *specialDiscount = rox_add_flag("billing.isSpecialDiscount", false);

RoxContext *userContext = rox_context_create_from_map(
            ROX_MAP(ROX_COPY("userId"), rox_dynamic_value_create_int(555)));

bool isDiscounted = rox_flag_is_enabled_ctx(specialDiscount, userContext);
Rox::Flag *specialDiscount = Rox::Flag::Create("billing.isSpecialDiscount", false);

Rox::Context *userContext = Rox::ContextBuilder()
  .AddIntValue("userId", 555)
  .Build();

bool isDiscounted = specialDiscount->IsEnabled(userContext);

Experiments and the configuration audience are configured using Target Groups. Now that the context has been defined, continue reading to learn how to use the context in properties.

Using context in properties

Target Groups let you define your audience based on properties. Property value can be different based on the context it receives. For example, we can define property that will be true for users with items in their shopping cart.

NodeJS JavaScript JavaScript SSR JVM .NET Python Go Ruby PHP C C++
Rox.setCustomBooleanProperty("hasItemsInShoppingCart", context => !context.shoppingCart.isEmpty())
Rox.setCustomBooleanProperty("hasItemsInShoppingCart", context => !context.shoppingCart.isEmpty())
import {Rox} from 'rox-ssr';

Rox.setCustomBooleanProperty("hasItemsInShoppingCart", context => !context.shoppingCart.isEmpty())
Rox.setCustomComputedBooleanProperty("hasItemsInShoppingCart", new CustomPropertyGeneratorWithContext() {

  @Override
  public Boolean generateProperty(Context context) {
    return ! ((shoppingCart) context.get("shoppingCart")).isEmpty();
  }
});
Rox.SetCustomComputedBooleanProperty("hasItemsInShoppingCart", (context) => { return ! ((ShoppingCart) context.Get("shoppingCart")).isEmpty()})
Rox.set_custom_string_property('hasItemsInShoppingCart', lambda context: context['shopping_cart'].is_empty())
  rox.SetCustomComputedBooleanProperty("hasItemsInShoppingCart", func(ctx context.Context) bool {
    value, _ := ctx.Get("shoppingCart").(*ShoppingCart)
    return value.IsEmpty() })
Rox::Server::RoxServer.set_custom_boolean_property('hasItemsInShoppingCart') do

context

context[:user].shopping_cart.empty? end ----

Rox::setCustomComputedBooleanProperty("hasItemsInShoppingCart", function (ContextInterface $context) {
    return (bool)($context->get("user")->getShoppingCart->isEmpty());
});

With this property, we can target all users which have items in their shopping cart and offer them a special discount. As you can see, the context that we passed to specialDiscount flag will be used to calculate the value of the hasItemsInShoppingCart property specific for this user.

What do you do if there is one or more key-value which should be the same for all property calculations? For this case, we have the global context.

Global context

You can think of global context as a default context. Whenever the context that is passed to a flag is missing something that is required for property calculation, if this information exists in the global context, it will be available for the property. Creating a global context is exactly the same as a regular context. The only difference is that you need to create it and set it properly when the SDK initialized.

NodeJS JavaScript JavaScript SSR JVM .NET Python Go Ruby PHP C C++
const user = createAnonymousUser();
const globalContext = { user };

Rox.setContext(globalContext);
let user = 'John';
let globalContext = { user };

Rox.setContext(globalContext);
import {Rox} from 'rox-ssr';

const user = createAnonymousUser();
const globalContext = { user };

Rox.setContext(globalContext);
Map<String, Object> map = new HashMap<>();
map.put("user", user);
Context globalContext = new RoxContext.Builder().from(map);
Rox.setContext(globalContext);
var globalContext = new ContextBuilder().Build(new Dictionary<string, object> { { "user", user } });

Rox.setContext(globalContext)
global_context = {'user': user}

Rox.set_context(global_context)
globalContext := context.NewContext(map[string]interface{}{"user": user})

rox.SetContext(globalContext)
global_context = {'user': user}

Rox::Server::RoxServer.context = global_context
$context = (new ContextBuilder())->build([
    "user" => user
]);

Rox::setContext($context);
RoxContext *userContext = rox_context_create_from_map(
            ROX_MAP(ROX_COPY("userId"), rox_dynamic_value_create_int(555)));

rox_set_context(userContext)
Rox::Context *userContext = Rox::ContextBuilder()
  .AddIntValue("userId", 555)
  .Build();

Rox::SetContext(userContext);

As a developer, you don’t need to worry about multiple contexts. The SDK ensures that the context that is passed to properties will include key-values from the local context and also from the Global Context.

In a case where a key exists in both contexts, the key-value from the Local Context that is the one that will take precedence.

Setting distinct_id from context

rox.distinct_id is a default Custom Property that comes out of the box and is used internally by the system for splitting values by percentage (in the experiment audience screen). On client side SDKs rox.distinct_id is automatically generated and stored locally, this is important in order to create a consistent user behavior when running an experiment with split values.

On the server side, it is the developer responsibility to supply a consistent string value that can be used for percentage splitting.

In this example below, we have chosen the userId as the value to be used for percentage splitting.

NodeJS JavaScript SSR JVM .NET Python Go Ruby PHP C C++
Rox.setCustomStringProperty("rox.distinct_id", context  => {
  return context["user"].getId();
});
await Rox.setup(envKey);
import {Rox} from 'rox-ssr';

Rox.setCustomStringProperty("rox.distinct_id", context  => {
  return context["user"].getId();
});
await Rox.setup(envKey);
Rox.setCustomComputedStringProperty("rox.distinct_id", context -> {
  return ((UserDetails)context.get("user")).getId();
});
Rox.setup(envKey).get()
Rox.SetCustomComputedStringProperty("rox.distinct_id", (context) => {
  return ((UserDetails) context.Get("user")).getId();
});
await Rox.setup(envKey);
# taking into account that context has user in it
Rox.set_custom_string_property('rox.distinct_id', lambda context: context['user'].get_id());
Rox.setup(envKey).result();
rox.SetCustomComputedStringProperty("rox.distinct_id", func(ctx context.Context) string {
    return ctx.Get("user").(*User).id
})
<-rox.setup(envKey, options)
# taking into account that context has user in it
Rox::Server::RoxServer.set_custom_string_property('rox.distinct_id') do

context

context['user'].id end Rox::Server::RoxServer.setup(envKey).join ----

Rox::setCustomComputedStringProperty("rox.distinct_id", function (ContextInterface $context) {
    return $context->get("user")->getId();
});

Rox::setup(envKey);