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
User | Most Recent Commit | # Commits |
---|
Other Committers
User | Most Recent Commit | # Commits |
---|
FeatureBee client for Scala applications
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.
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.
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.
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
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
Use a cookie with name featurebee
to specify the forced/god mode feature activation.
Example cookie value:
ure1=true|feature2=false
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).
default
: JSON Boolean (true
or false
) or JSON String ("on"
or "off"
)
This defines a default ON or OFF activation of the feature for all clients. This state/activation may be overwritten by using god mode/forced feature toggling
IMPORTANT: Please be aware that god mode only works if the feature is defined in the registry, i.e. the json in classpath or S3 contains the given feature. If
the feature is NOT present, and you define your features like above `override def languageDropdown = Feature("language-dropdown").getOrElse(AlwaysOffFeature)
`
then the state defined by getOrElse(STATE) wins and forcing the feature to a specific state will not work. So this is a big difference between defining the
default state of the feature in code with getOrElse and the default condition in the JSON!{ "default": true }
or { "default": false }
{ "default": "on" }
or { "default": "off" }
culture
: JSON Array of Strings in the form "lang-COUNTRY"
or "lang"
(only lower case) or "COUNTRY"
(only upper case){ "culture": ["de-AT"] }
{ "culture": ["AT"] }
{ "culture": ["de"] }
{ "culture": ["de-DE", "de-AT] }
userAgentFragments
: JSON Array of Strings that must be contained in the user agent{"userAgentFragments": ["Firefox"]}
trafficDistribution
: JSON Array or a single JSON String in the from "FROM-TO"
where TO
> FROM
and 1 <= FROM,TO <= 100
.
Be aware that in the standard impl of PlayClientInfoSupport this is derived by looking at a cookie value which is expected to be a UUID. If this cookie
is not present, a random UUID is generated which means that the feature using trafficDistribution will not be stable for this client!{ "trafficDistribution": "1-100" }
{ "trafficDistribution": "51-100" }
{ "trafficDistribution": "1-20" }
{ "trafficDistribution": ["1-20", "80-85"] }
The JSON
has to fulfill the following requirements:
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 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)
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 © 2016 AutoScout24 GmbH.
Distributed under the MIT License.
FeatureBee client for Scala applications
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.
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.
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.
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
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
Use a cookie with name featurebee
to specify the forced/god mode feature activation.
Example cookie value:
ure1=true|feature2=false
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).
default
: JSON Boolean (true
or false
) or JSON String ("on"
or "off"
)
This defines a default ON or OFF activation of the feature for all clients. This state/activation may be overwritten by using god mode/forced feature toggling
IMPORTANT: Please be aware that god mode only works if the feature is defined in the registry, i.e. the json in classpath or S3 contains the given feature. If
the feature is NOT present, and you define your features like above `override def languageDropdown = Feature("language-dropdown").getOrElse(AlwaysOffFeature)
`
then the state defined by getOrElse(STATE) wins and forcing the feature to a specific state will not work. So this is a big difference between defining the
default state of the feature in code with getOrElse and the default condition in the JSON!{ "default": true }
or { "default": false }
{ "default": "on" }
or { "default": "off" }
culture
: JSON Array of Strings in the form "lang-COUNTRY"
or "lang"
(only lower case) or "COUNTRY"
(only upper case){ "culture": ["de-AT"] }
{ "culture": ["AT"] }
{ "culture": ["de"] }
{ "culture": ["de-DE", "de-AT] }
userAgentFragments
: JSON Array of Strings that must be contained in the user agent{"userAgentFragments": ["Firefox"]}
trafficDistribution
: JSON Array or a single JSON String in the from "FROM-TO"
where TO
> FROM
and 1 <= FROM,TO <= 100
.
Be aware that in the standard impl of PlayClientInfoSupport this is derived by looking at a cookie value which is expected to be a UUID. If this cookie
is not present, a random UUID is generated which means that the feature using trafficDistribution will not be stable for this client!{ "trafficDistribution": "1-100" }
{ "trafficDistribution": "51-100" }
{ "trafficDistribution": "1-20" }
{ "trafficDistribution": ["1-20", "80-85"] }
The JSON
has to fulfill the following requirements:
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 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)
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 © 2016 AutoScout24 GmbH.
Distributed under the MIT License.