Scout24/featurebee-scala

Name: featurebee-scala

Owner: AutoScout24

Owner: AutoScout24

Description: FeatureBee client for scala applications

Created: 2015-03-27 14:55:43.0

Updated: 2017-10-02 13:53:46.0

Pushed: 2016-09-12 11:58:37.0

Homepage: null

Size: 1142

Language: Scala

GitHub Committers

UserMost Recent Commit# Commits

Other Committers

UserEmailMost Recent Commit# Commits

README

FeatureBee For Scala

FeatureBee client for Scala applications

Status

Build Status Coverage Status Download

Setup

Add to your build.sbt following resolver with dependency:

lvers += Resolver.bintrayRepo("tatsu-kondor", "maven")

aryDependencies += "com.autoscout24" %% "featurebee" % "(see version number above)",
                   "com.autoscout24" %% "featurebee-s3-registry" % "(see version number above)" // if you want the S3 and the reloading feature registry

Now you can use FeatureBee library.

How To Use

Write a Features trait to access all your features in one place, e.g.:

t Features {
f languageDropdown: Feature
f survey: Feature


ct Features extends Features {
ivate implicit lazy val featureRegistry = StaticJsonFeatureRegistry("featureBee.json")
erride def languageDropdown = Feature("language-dropdown").getOrElse(AlwaysOffFeature)
erride def survey = Feature("survey").getOrElse(AlwaysOffFeature)

Hint: If you want a dynamic S3 based json file registry, see farther below.

Add file featureBee.json at your resources or conf folder with JSON that explains behaviour of your feature flags, for example:



"name": "survey",
"description": "Enables survey",
"tags": ["our-team", "awesome-feature"],
"activation": [{ "default": true}]


"name": "language-dropdown",
"description": "Shows language dropdown",
"tags": ["our-team", "awesome-feature"],
"activation": [{ "default": false}]


For deatil information regarding format of JSON see Contract paragraph.

Write a support object which defines how the request from the client is used to extract relevant feature toggle info, like e.g. the language or the browser. For Play apps you may use the already defined PlayClientInfoSupport:

ct ControllerClientInfoSupport {
plicit def requestToClientInfo(implicit requestHeader: RequestHeader): ClientInfo = {
import PlayClientInfoSupport._
ClientInfoImpl(userAgent, localeFromCookieValue("culture"), uuidFromCookieValue("as24Visitor"), forcedFeatureToggle)


Currently only a static json file inside your deployment is supported, see Contract section below. See the usage of StaticJsonFeatureRegistry above for infos how you specify the location of the feature config file.

Forced Feature Toggling (GodMode)

If you use the PlayClientInfoSupport, you may force feature activation regardless of the conditions you specify in your JSON feature config by setting a query param, a request Header, or a cookie. This order of mentioning the variants is also the order of precedence, so query param has precedence over cookie. All the keys are case insensitive.

IMPORTANT: Please be aware that GodMode will work even if that Feature is not defined in the Registry or the Registry fails to load. This means that AlwaysOnFeature/AlwaysOffFeature objects will be overridden by the GodMode.

Query Param

Use query param featurebee to specify forced / god mode activation of features:

://yourUrl?featurebee=feature1%3Dtrue%7Cfeature2%3Dfalse 

Which decodes to:

://yourUrl?featurebee=feature1=true|feature2=false)

= is used to assign the true / false value to a feature with the given name and | is used to separate the different features from each other. So we need URL encoding here, so the above forced feature string would decode to:

ure1=true|feature2=false
Request Header

Use header name featurebee or X-Featurebee (case insensitiv) to specify the forced / god mode feature activation.

Example request header value:

ure1=true|feature2=false
Cookie

Use a cookie with name featurebee to specify the forced/god mode feature activation.

Example cookie value:

ure1=true|feature2=false
Contract

The FeatureBee Server returns a list of all defined features.


ame": "Name of the Feature",
escription": "Some additional description",
ags": ["Team Name", "Or Service name"],
ctivation": [{"culture": ["de-DE"]}]

Conditions is an array of type and values. Supported types are default, culture, userAgentFragments and trafficDistribution.

Each condition can have its own format how the values should look like. Each condition could have multiple values. All conditions have to be fulfilled (logical AND).

Format of conditions

The JSON has to fulfill the following requirements:

Reloadable Feature Registry based on files in S3

Featurebee supports dynamic, periodic re-loading of the feature registry from a S3 bucket possibly containing several feature json files in the format as described above. For that to work you it's best to move the creation of the FeatureRegistry to the play guice context.

S3 loading is supplied by S3JsonFeatureRegistry and the reloading feature is implemented by ReloadingFeatureRegistry.

ReloadingFeatureRegistry

ReloadingFeatureRegistry inspects the last modification date of the inital and the re-creator function, adds the activationDelay duration to it and activates the registry on the resulting point in time. With that approach it should be possible to achieve that all instances of a service activate a new feature registry at the same time and by that minimizing the problems experienced by end users. Would the instances switch the registry at different points in time some problems in their experience could arise.

See below for an example to enable periodic reloading of feature json files from S3.

rt java.time.LocalDateTime
rt java.util.concurrent.Executors
rt akka.actor.ActorSystem
rt com.amazonaws.services.s3.AmazonS3Client
rt com.autoscout24.classifiedlist.TypedEvents.{FeatureRegistryLoadedFromS3WithIgnoredErrors, FeatureRegistryLoadingFromS3Failed, FeatureRegistrySuccessfullyLoadedFromS3}
rt com.autoscout24.eventpublisher24.events._
rt com.google.inject.{AbstractModule, Provides, Singleton}
rt featurebee.api.FeatureRegistry
rt featurebee.registry.DefaultFeatureValueFeatureRegistry
rt featurebee.registry.s3.S3JsonFeatureRegistry.S3File
rt featurebee.registry.s3.{ReloadingFeatureRegistry, S3JsonFeatureRegistry}
rt org.scalactic.{Bad, Good}
rt scala.concurrent.ExecutionContext
rt scala.concurrent.duration._
rt scala.language.postfixOps

s FeatureRegistryModule extends AbstractModule {

ivate val bucketName = "as24prod-features-eu-west-1"

f configure() = {}

rovides
ingleton
f s3Client(configuration: Configuration): AmazonS3Client = new AmazonS3Client()

rovides
ingleton
f featureRegistry(amazonS3Client: AmazonS3Client, actorSystem: ActorSystem, eventPublisher: TypedEventPublisher): FeatureRegistry = {

val initialRegistry = s3FeatureRegistry(amazonS3Client, eventPublisher) match {
  case Some((registry, lastModified)) => (registry, lastModified)
  case None => (DefaultFeatureValueFeatureRegistry, LocalDateTime.MIN)
}

new ReloadingFeatureRegistry(initialRegistry, () => s3FeatureRegistry(amazonS3Client, eventPublisher),
  actorSystem.scheduler, reloadAfter = 2 minutes, activationDelay = 2 min 10 seconds, singleThreadExecContext
)


 S3 Feature registry returns a merge of all feature json files and the latest modification date of all of them
 in case of failures it returns None
ivate val s3FeatureRegistry: (AmazonS3Client, TypedEventPublisher) => Option[(FeatureRegistry, LocalDateTime)] = {
(amazonS3Client, eventPublisher) =>
  S3JsonFeatureRegistry(
    Seq(
      S3File(bucketName, "classified-list-featurebee.json", ignoreOnFailures = false),
      S3File(bucketName, "classified-list-gecloud-featurebee.json", ignoreOnFailures = true)
    )
  )(amazonS3Client) match {
    case Good(featureRegistryBuilt) =>
      val errorString = featureRegistryBuilt.failedIgnoredFiles.map(_.toString)
      if (errorString.nonEmpty) eventPublisher.publish(FeatureRegistryLoadedFromS3WithIgnoredErrors(errorString))
      else eventPublisher.publish(FeatureRegistrySuccessfullyLoadedFromS3())
      Some((featureRegistryBuilt.featureRegistry, featureRegistryBuilt.lastModified))

    case Bad(errors) =>
      eventPublisher.publish(FeatureRegistryLoadingFromS3Failed(errors.mkString("The following errors occured loading the features from S3", ";", "")))
      None
  }


ivate val singleThreadExecContext = new ExecutionContext {
val threadPool = Executors.newFixedThreadPool(1)
def execute(runnable: Runnable) { threadPool.submit(runnable) }
def reportFailure(t: Throwable) {}


Please note that you are able to define files that don't break the reloading in case of errors (ignoreOnFailures). With that you are able to separate features for downstream fragments (see below) and you're own features in two files with possibly different access policies and/or failure handling (ignore or not, as in above example)

Fragment Services & Features

When working with Fragments you may need to pass the feature toggles through to Fragment Service.

To do this you can define the services your feature is required in. From there you can call the Feature Registry and get the Feature String for a particular service.

e.g:


ame": "name-of-the-feature",
escription": "Some additional description",
ags": ["Team Name", "Or Service name"],
ctivation": [{"default": true}],
ervices": ["content-service"]


ame": "name-of-another-feature",
escription": "Some other description",
ags": ["Team Name", "Or Service name"],
ctivation": [{"default": true}],
ervices": []

Where you are including your fragment you can now do:

featureString = featureRegistry.featureStringForService("content-service")
l(s"""<!--#include virtual="/fragment/contentservice/header.html?featurebee=${featureString}" -->""")

This will only pass across the appropriate features to the fragment service. e.g:

/fragment/contentservice/header.html?featurebee=name-of-the-feature=true

Copyright

Copyright © 2016 AutoScout24 GmbH.

Distributed under the MIT License.

FeatureBee For Scala

FeatureBee client for Scala applications

Status

Build Status Coverage Status Download

Setup

Add to your build.sbt following resolver with dependency:

lvers += Resolver.bintrayRepo("tatsu-kondor", "maven")

aryDependencies += "com.autoscout24" %% "featurebee" % "(see version number above)",
                   "com.autoscout24" %% "featurebee-s3-registry" % "(see version number above)" // if you want the S3 and the reloading feature registry

Now you can use FeatureBee library.

How To Use

Write a Features trait to access all your features in one place, e.g.:

t Features {
f languageDropdown: Feature
f survey: Feature


ct Features extends Features {
ivate implicit lazy val featureRegistry = StaticJsonFeatureRegistry("featureBee.json")
erride def languageDropdown = Feature("language-dropdown").getOrElse(AlwaysOffFeature)
erride def survey = Feature("survey").getOrElse(AlwaysOffFeature)

Hint: If you want a dynamic S3 based json file registry, see farther below.

Add file featureBee.json at your resources or conf folder with JSON that explains behaviour of your feature flags, for example:



"name": "survey",
"description": "Enables survey",
"tags": ["our-team", "awesome-feature"],
"activation": [{ "default": true}]


"name": "language-dropdown",
"description": "Shows language dropdown",
"tags": ["our-team", "awesome-feature"],
"activation": [{ "default": false}]


For deatil information regarding format of JSON see Contract paragraph.

Write a support object which defines how the request from the client is used to extract relevant feature toggle info, like e.g. the language or the browser. For Play apps you may use the already defined PlayClientInfoSupport:

ct ControllerClientInfoSupport {
plicit def requestToClientInfo(implicit requestHeader: RequestHeader): ClientInfo = {
import PlayClientInfoSupport._
ClientInfoImpl(userAgent, localeFromCookieValue("culture"), uuidFromCookieValue("as24Visitor"), forcedFeatureToggle)


Currently only a static json file inside your deployment is supported, see Contract section below. See the usage of StaticJsonFeatureRegistry above for infos how you specify the location of the feature config file.

Forced Feature Toggling (GodMode)

If you use the PlayClientInfoSupport, you may force feature activation regardless of the conditions you specify in your JSON feature config by setting a query param, a request Header, or a cookie. This order of mentioning the variants is also the order of precedence, so query param has precedence over cookie. All the keys are case insensitive.

IMPORTANT: Please be aware that GodMode will work even if that Feature is not defined in the Registry or the Registry fails to load. This means that AlwaysOnFeature/AlwaysOffFeature objects will be overridden by the GodMode.

Query Param

Use query param featurebee to specify forced / god mode activation of features:

://yourUrl?featurebee=feature1%3Dtrue%7Cfeature2%3Dfalse 

Which decodes to:

://yourUrl?featurebee=feature1=true|feature2=false)

= is used to assign the true / false value to a feature with the given name and | is used to separate the different features from each other. So we need URL encoding here, so the above forced feature string would decode to:

ure1=true|feature2=false
Request Header

Use header name featurebee or X-Featurebee (case insensitiv) to specify the forced / god mode feature activation.

Example request header value:

ure1=true|feature2=false
Cookie

Use a cookie with name featurebee to specify the forced/god mode feature activation.

Example cookie value:

ure1=true|feature2=false
Contract

The FeatureBee Server returns a list of all defined features.


ame": "Name of the Feature",
escription": "Some additional description",
ags": ["Team Name", "Or Service name"],
ctivation": [{"culture": ["de-DE"]}]

Conditions is an array of type and values. Supported types are default, culture, userAgentFragments and trafficDistribution.

Each condition can have its own format how the values should look like. Each condition could have multiple values. All conditions have to be fulfilled (logical AND).

Format of conditions

The JSON has to fulfill the following requirements:

Reloadable Feature Registry based on files in S3

Featurebee supports dynamic, periodic re-loading of the feature registry from a S3 bucket possibly containing several feature json files in the format as described above. For that to work you it's best to move the creation of the FeatureRegistry to the play guice context.

S3 loading is supplied by S3JsonFeatureRegistry and the reloading feature is implemented by ReloadingFeatureRegistry.

ReloadingFeatureRegistry

ReloadingFeatureRegistry inspects the last modification date of the inital and the re-creator function, adds the activationDelay duration to it and activates the registry on the resulting point in time. With that approach it should be possible to achieve that all instances of a service activate a new feature registry at the same time and by that minimizing the problems experienced by end users. Would the instances switch the registry at different points in time some problems in their experience could arise.

See below for an example to enable periodic reloading of feature json files from S3.

rt java.time.LocalDateTime
rt java.util.concurrent.Executors
rt akka.actor.ActorSystem
rt com.amazonaws.services.s3.AmazonS3Client
rt com.autoscout24.classifiedlist.TypedEvents.{FeatureRegistryLoadedFromS3WithIgnoredErrors, FeatureRegistryLoadingFromS3Failed, FeatureRegistrySuccessfullyLoadedFromS3}
rt com.autoscout24.eventpublisher24.events._
rt com.google.inject.{AbstractModule, Provides, Singleton}
rt featurebee.api.FeatureRegistry
rt featurebee.registry.DefaultFeatureValueFeatureRegistry
rt featurebee.registry.s3.S3JsonFeatureRegistry.S3File
rt featurebee.registry.s3.{ReloadingFeatureRegistry, S3JsonFeatureRegistry}
rt org.scalactic.{Bad, Good}
rt scala.concurrent.ExecutionContext
rt scala.concurrent.duration._
rt scala.language.postfixOps

s FeatureRegistryModule extends AbstractModule {

ivate val bucketName = "as24prod-features-eu-west-1"

f configure() = {}

rovides
ingleton
f s3Client(configuration: Configuration): AmazonS3Client = new AmazonS3Client()

rovides
ingleton
f featureRegistry(amazonS3Client: AmazonS3Client, actorSystem: ActorSystem, eventPublisher: TypedEventPublisher): FeatureRegistry = {

val initialRegistry = s3FeatureRegistry(amazonS3Client, eventPublisher) match {
  case Some((registry, lastModified)) => (registry, lastModified)
  case None => (DefaultFeatureValueFeatureRegistry, LocalDateTime.MIN)
}

new ReloadingFeatureRegistry(initialRegistry, () => s3FeatureRegistry(amazonS3Client, eventPublisher),
  actorSystem.scheduler, reloadAfter = 2 minutes, activationDelay = 2 min 10 seconds, singleThreadExecContext
)


 S3 Feature registry returns a merge of all feature json files and the latest modification date of all of them
 in case of failures it returns None
ivate val s3FeatureRegistry: (AmazonS3Client, TypedEventPublisher) => Option[(FeatureRegistry, LocalDateTime)] = {
(amazonS3Client, eventPublisher) =>
  S3JsonFeatureRegistry(
    Seq(
      S3File(bucketName, "classified-list-featurebee.json", ignoreOnFailures = false),
      S3File(bucketName, "classified-list-gecloud-featurebee.json", ignoreOnFailures = true)
    )
  )(amazonS3Client) match {
    case Good(featureRegistryBuilt) =>
      val errorString = featureRegistryBuilt.failedIgnoredFiles.map(_.toString)
      if (errorString.nonEmpty) eventPublisher.publish(FeatureRegistryLoadedFromS3WithIgnoredErrors(errorString))
      else eventPublisher.publish(FeatureRegistrySuccessfullyLoadedFromS3())
      Some((featureRegistryBuilt.featureRegistry, featureRegistryBuilt.lastModified))

    case Bad(errors) =>
      eventPublisher.publish(FeatureRegistryLoadingFromS3Failed(errors.mkString("The following errors occured loading the features from S3", ";", "")))
      None
  }


ivate val singleThreadExecContext = new ExecutionContext {
val threadPool = Executors.newFixedThreadPool(1)
def execute(runnable: Runnable) { threadPool.submit(runnable) }
def reportFailure(t: Throwable) {}


Please note that you are able to define files that don't break the reloading in case of errors (ignoreOnFailures). With that you are able to separate features for downstream fragments (see below) and you're own features in two files with possibly different access policies and/or failure handling (ignore or not, as in above example)

Fragment Services & Features

When working with Fragments you may need to pass the feature toggles through to Fragment Service.

To do this you can define the services your feature is required in. From there you can call the Feature Registry and get the Feature String for a particular service.

e.g:


ame": "name-of-the-feature",
escription": "Some additional description",
ags": ["Team Name", "Or Service name"],
ctivation": [{"default": true}],
ervices": ["content-service"]


ame": "name-of-another-feature",
escription": "Some other description",
ags": ["Team Name", "Or Service name"],
ctivation": [{"default": true}],
ervices": []

Where you are including your fragment you can now do:

featureString = featureRegistry.featureStringForService("content-service")
l(s"""<!--#include virtual="/fragment/contentservice/header.html?featurebee=${featureString}" -->""")

This will only pass across the appropriate features to the fragment service. e.g:

/fragment/contentservice/header.html?featurebee=name-of-the-feature=true

Copyright

Copyright © 2016 AutoScout24 GmbH.

Distributed under the MIT License.


This work is supported by the National Institutes of Health's National Center for Advancing Translational Sciences, Grant Number U24TR002306. This work is solely the responsibility of the creators and does not necessarily represent the official views of the National Institutes of Health.