FeaturevisorFeaturevisor

SDKs

Roku SDK

BrightScript SDK for Roku is meant to be used with kopytko-framework.

However, if you don't use it, you can simply copy all SDK files and their dependencies to your project (a version will be prepared in the future if anyone is interested).

Installation

Install with npm:

npm i -P @featurevisor/roku

Introduction

The BrightScript implementation is a bit different than the JS one, as in BrightScript to be able to keep our instance separated and keep it globally, the SDK would need to be a SceneGraph Node. But to provide a bit similar API to the JS SDK we have introduced 2 main entities. One is the FeaturevisorInstance node, and the other is the function FeaturevisorSDK (that returns an object with methods mirroring JS SDK functions). The FeaturevisorSDK operates on the FeaturevisorInstance that is created by it or passed to it.

That's why all functions presented in this documentation are FeaturevisorSDK methods (or exactly, of the object returned by FeaturevisorSDK())

There are a couple of methods to handle Featurevisor in your Roku App, for example:

  • create a new SceneGraph Node that will save in its context the FeaturevisorSDK() and create your abstraction based on FeaturevisorSDK. Later save this whole node globally
  • create with FeaturevisorSDK an instance that you will save globally and later pass each time to FeaturevisorSDK().createInstance({}, existingInstance) before using its methods.

For this documentation, we assume there will be a new node created for the integration (first variant). We will call it MyFeaturevisorInstance and all code will be invoked inside this node (in the MyFeaturevisorInstance.brs for the below example). The value returned by FeaturevisorSDK() will be called f, or if needed saved in the context - m.f.

XML definition:

<?xml version="1.0" encoding="utf-8" ?>

<component name="MyFeaturevisorInstance" extends="Node">
  <interface>
    <!-- Fields and Functions you want to expose -->
  </interface>

  <script type="text/brightscript" uri="MyFeaturevisorInstance.brs" />
</component>

Initialization

The SDK can create a new instance or accept an existing one to be able to invoke its methods using this instance.

Understanding datafiles

You are recommended to learn more about building datafiles before proceeding further.

Create a new instance (creates FeaturevisorInstance node):

' @import /components/libs/featurevisor/FeaturevisorSDK.brs from @featurevisor/roku

sub init()
  f = FeaturevisorSDK()
  f.createInstance({
    datafileUrl: "<featurevisor-datafile-url>",
  })
end sub

Or use existing instance:

' @import /components/libs/featurevisor/FeaturevisorSDK.brs from @featurevisor/roku

sub init()
  ' define existingInstance

  f = FeaturevisorSDK()
  f.createInstance({}, existingInstance)
end sub

Context

Contexts are attribute values that we pass to SDK for evaluating features.

They are objects where keys are the attribute keys, and values are the attribute values.

context = {
  myAttributeKey: "myAttributeValue",
  anotherAttributeKey: "anotherAttributeValue",
}

Methods using the context (attributes) described above:

  • f.isEnabled(featureKey as String, context = {} as Object) as Boolean
  • f.getVariation(feature as Dynamic, context = {} as Object) as Dynamic
  • f.getVariable(feature as Dynamic, variableKey as String, context = {} as Object) as Object
  • f.getVariableBoolean(feature as Dynamic, variableKey as String, context = {} as Object) as Boolean
  • f.getVariableString(feature as Dynamic, variableKey as String, context = {} as Object) as Dynamic
  • f.getVariableInteger(feature as Dynamic, variableKey as String, context = {} as Object) as Integer
  • f.getVariableDouble(feature as Dynamic, variableKey as String, context = {} as Object) as Float
  • f.getVariableArray(feature as Dynamic, variableKey as String, context = {} as Object) as Object
  • f.getVariableObject(feature as Dynamic, variableKey as String, context = {} as Object) as Object
  • f.getVariableJSON(feature as Dynamic, variableKey as String, context = {} as Object) as Dynamic

Checking if enabled

Once the SDK is initialized, you can check if a feature is enabled or not:

function isMyFeatureEnabled() as Boolean
  ' m.f is a FeaturevisorSDK with an initialized FeaturevisorInstance

  featureKey = "my_feature"
  context = {
    userId: "123",
    country: "nl",
  }

  return m.f.isEnabled(featureKey, context)
end function

Getting variations

If your feature has any variations defined, you can get evaluate them as follows:

function getMyFeatureVariation() as Dynamic
  ' m.f is a FeaturevisorSDK with an initialized FeaturevisorInstance

  featureKey = "my_feature"
  context = {
    userId: "123",
    country: "nl",
  }

  return m.f.getVariation(featureKey, context)
end function

Getting variables

Your features may also include variables:

function getBgColorValue() as Dynamic
  ' m.f is a FeaturevisorSDK with an initialized FeaturevisorInstance

  featureKey = "my_feature"
  variableKey = "bgColor"
  context = {
    userId: "123",
    country: "nl",
  }

  return m.f.getVariable(featureKey, variableKey, context)
end function

Type specific methods

Next to generic f.getVariable(feature as Dynamic, variableKey as String, context = {} as Object) as Object methods, there are also type specific methods available for convenience:

boolean

f.getVariableBoolean(feature as Dynamic, variableKey as String, context = {} as Object) as Boolean

string

f.getVariableString(feature as Dynamic, variableKey as String, context = {} as Object) as Dynamic

integer

f.getVariableInteger(feature as Dynamic, variableKey as String, context = {} as Object) as Integer

double

f.getVariableDouble(feature as Dynamic, variableKey as String, context = {} as Object) as Float

array

f.getVariableArray(feature as Dynamic, variableKey as String, context = {} as Object) as Object

object

f.getVariableObject(feature as Dynamic, variableKey as String, context = {} as Object) as Object

json

f.getVariableJSON(feature as Dynamic, variableKey as String, context = {} as Object) as Dynamic

Observe initialization

Remember that if defined with f.onReady method, it should be called before createInstance

By the f.onReady(func as Function, context = Invalid as Object)

' @import /components/libs/featurevisor/Featurevisor.facade.brs from @featurevisor/roku

sub init()
  f = FeaturevisorSDK()
  f.onReady(sub ()
    ' instance has been initialized and it is ready
  end sub) ' context can be added as an optional second argument
end sub

By the f.createInstance(options as Object) options:

' @import /components/libs/featurevisor/Featurevisor.facade.brs from @featurevisor/roku

sub init()
  f = FeaturevisorSDK()
  f.createInstance({
    onReady: {
      callback: sub ()
        ' instance has been initialized and it is ready
      end sub,
      context: {}, ' optional context for the callback
    },
  })
end sub

Activation

Activation is useful when you want to track what features and their variations are exposed to your users.

It works the same as f.getVariation() method, but it will also bubble an event up that you can listen to.

' @import /components/libs/featurevisor/FeaturevisorSDK.brs from @featurevisor/roku

sub init()
  f = FeaturevisorSDK()
  f.onActivation(sub (data as Object)
    ' feature has been activated
  end sub) ' context can be added as an optional second argument
end sub

or

' @import /components/libs/featurevisor/FeaturevisorSDK.brs from @featurevisor/roku

sub init()
  f = FeaturevisorSDK()
  f.createInstance({
    onActivation: {
      callback: sub (data as Object)
        ' feature has been activated
      end sub,
      context: {}, ' optional context for the callback
    },
  })
end sub

data Object fields:

  • captureContext - attributes that you want to capture, marked as capture: true in Attribute YAMLs
  • feature - activated feature
  • context - all the attributes used for evaluating
  • variationValue - variation of the activated feature

Initial features

You may want to initialize your SDK with a set of features before SDK has successfully fetched the datafile (if using datafileUrl option).

This helps in cases when you fail to fetch the datafile, but you still wish your SDK instance to continue serving a set of sensible default values. And as soon as the datafile is fetched successfully, the SDK will start serving values from there.

' @import /components/libs/featurevisor/FeaturevisorSDK.brs from @featurevisor/roku

sub init()
  f = FeaturevisorSDK()
  f.createInstance({
    ' ...
    initialFeatures: {
      myFeatureKey: {
        enabled: true,

        ' optional
        variation: "treatment",
        variables: {
          myVariableKey: "my-variable-value",
        },
      },
    },
  })
end sub

Stickiness

Featurevisor relies on consistent bucketing making sure the same user always sees the same variation in a deterministic way. You can learn more about it in Bucketing section.

But there are times when your targeting conditions (segments) can change and this may lead to some users being re-bucketed into a different variation. This is where stickiness becomes important.

If you have already identified your user in your application, and know what features should be exposed to them in what variations, you can initialize the SDK with a set of sticky features:

' @import /components/libs/featurevisor/FeaturevisorSDK.brs from @featurevisor/roku

sub init()
  f = FeaturevisorSDK()
  f.createInstance({
    ' ...
    stickyFeatures: {
      myFeatureKey: {
        enabled: true,

        ' optional
        variation: "treatment",
        variables: {
          myVariableKey: "my-variable-value",
        },
      },
    },
  })
end sub

Once initialized with sticky features, the SDK will look for values there first before evaluating the targeting conditions and going through the bucketing process.

You can also set sticky features after the SDK is initialized:

f.setStickyFeatures({
  myFeatureKey: {
    enabled: true,
    variation: "treatment",
    variables: {},
  },
  anotherFeatureKey: {
    enabled: false,
  }
})

This will be handy when you want to:

  • update sticky features in the SDK without re-initializing it (or restarting the app), and
  • handle evaluation of features for multiple users from the same instance of the SDK (e.g. in a server dealing with incoming requests from multiple users)

Intercepting context

You can intercept context before they are used for evaluation:

' @import /components/libs/featurevisor/FeaturevisorSDK.brs from @featurevisor/roku

sub init()
  defaultContext = {
    platform: "roku",
    locale: "en_US",
    country: "US",
    timezone: "America/New_York",
  }
  f = FeaturevisorSDK()
  f.createInstance({
    configureAndInterceptStaticContext: defaultContext,
    interceptContext: function (context as Object) as Object
      joinedContext = {}
      joinedContext.append(m) ' defaultContext
      joinedContext.append(context)

      return joinedContext
    end function,
  })
end sub

This is useful when you wish to add a default set of attributes as context for all your evaluations, giving you the convenience of not having to pass them in every time.

Refreshing datafile

Refreshing the datafile is convenient when you want to update the datafile in runtime, for example when you want to update the feature variations and variables config without having to restart your application.

It is only possible to refresh datafile in Featurevisor if you are using the datafileUrl option when creating your SDK instance.

Manual refresh

' @import /components/libs/featurevisor/FeaturevisorSDK.brs from @featurevisor/roku

sub init()
  m.f = FeaturevisorSDK()
  m.f.createInstance({
    datafileUrl: "<featurevisor-datafile-url>",
  })
end sub

sub refresh()
  m.f.refresh()
end sub

Refresh by interval

If you want to refresh your datafile every X number of seconds, you can pass the refreshInterval option when creating your SDK instance:

' @import /components/libs/featurevisor/FeaturevisorSDK.brs from @featurevisor/roku

sub init()
  f = FeaturevisorSDK()
  f.createInstance({
    datafileUrl: "<featurevisor-datafile-url>",
    refreshInterval: 60 * 5, ' every 5 minutes
  })
end sub

You can stop the interval by calling:

f.stopRefreshing()

If you want to resume refreshing:

f.startRefreshing()

Listening for updates

Every successful refresh will trigger the onRefresh() option:

' @import /components/libs/featurevisor/Featurevisor.facade.brs from @featurevisor/roku

sub init()
  f = FeaturevisorSDK()
  f.onRefresh(sub ()
    ' datafile has been refreshed
  end sub) ' context can be added as an optional second argument
end sub

or

' @import /components/libs/featurevisor/Featurevisor.facade.brs from @featurevisor/roku

sub init()
  f = FeaturevisorSDK()
  f.createInstance({
    onRefresh: {
      callback: sub ()
        ' datafile has been refreshed
      end sub,
      context: {}, ' optional context for the callback
    },
  })
end sub

Not every refresh is going to be of a new datafile version. If you want to know if datafile content has changed in any particular refresh, you can listen to onUpdate option:

' @import /components/libs/featurevisor/Featurevisor.facade.brs from @featurevisor/roku

sub init()
  f = FeaturevisorSDK()
  f.onUpdate(sub ()
    ' datafile has been updated (the revision has changed)
  end sub) ' context can be added as an optional second argument
end sub

or

' @import /components/libs/featurevisor/Featurevisor.facade.brs from @featurevisor/roku

sub init()
  f = FeaturevisorSDK()
  f.createInstance({
    onUpdate: {
      callback: sub ()
        ' datafile has been updated (the revision has changed)
      end sub,
      context: {}, ' optional context for the callback
    },
  })
end sub
Previous
Swift