Java & Scala

It’s simple to build your client using the APIs. If you’re application is built on jvm you can also use the built-in Izanami client.

This client offer nice strategies for better performances.

You need to add repository

Graddle
repositories {
    jcenter()
    maven {
        url 'https://raw.githubusercontent.com/mathieuancelin/json-lib-javaslang/master/repository/releases/'
    }
}
Sbt
resolvers ++= Seq(
  "jsonlib-repo" at "https://raw.githubusercontent.com/mathieuancelin/json-lib-javaslang/master/repository/releases",
  Resolver.jcenterRepo
)

Add the following dependency to your project

sbt
libraryDependencies += "fr.maif" %% "izanami-client" % "1.11.0"
Maven
<dependency>
  <groupId>fr.maif</groupId>
  <artifactId>izanami-client_2.13</artifactId>
  <version>1.11.0</version>
</dependency>
Gradle
dependencies {
  compile group: 'fr.maif', name: 'izanami-client_2.13', version: '1.11.0'
}

The client can be used in java or scala. There is two distinct dsl. Be sure to import the correct one :

Java
import izanami.*;
import izanami.javadsl.*;
Scala
import izanami._
import izanami.scaladsl._

Izanami client is built with

  • Scala: As programming language
  • Akka: to handle global state, scheduler …
  • Akka http: for http request, sse …

The scaladsl rely on :

The javadsl rely on :

  • vavr: For functional structures like future, either, option …
  • play json java: For json handling

Setup the Izanami client

The first thing to do is to create a client. The client own the shared http client between config client, feature client and the experiment client.

You need to create a single client for all your application.

Java
izanamiClient = IzanamiClient.client(
    system,
    ClientConfig.create("http://localhost:8089")
        .withClientId("xxxx")
        .withClientIdHeaderName("Another-Client-Id-Header")
        .withClientSecret("xxxx")
        .withClientSecretHeaderName("Another-Client-Secret-Header")
        .sseBackend()
        .withDispatcher("izanami-example.blocking-io-dispatcher")
        .withPageSize(50)
        .withZoneId(ZoneId.of("Europe/Paris"))
);
Scala
implicit val system = ActorSystem(
  "izanami-client",
  ConfigFactory.parseString("""
    izanami-example.blocking-io-dispatcher {
      type = Dispatcher
      executor = "thread-pool-executor"
      thread-pool-executor {
        fixed-pool-size = 32
      }
      throughput = 1
    }
  """)
)

val client = IzanamiClient(
  ClientConfig(
    host = "http://localhost:9000",
    clientId = Some("xxxx"),
    clientIdHeaderName = "Another-Client-Id-Header",
    clientSecret = Some("xxxx"),
    clientSecretHeaderName = "Another-Client-Id-Header",
    backend = SseBackend,
    pageSize = 50,
    zoneId = ZoneId.of("Europe/Paris"),
    dispatcher = "izanami-example.blocking-io-dispatcher"
  )
)
Field Description
clientId The client id to access izanami APIs see Manage APIs keys
clientSecretIdName A custom header for the client id
clientSecret The client secret to access izanami APIs see Manage APIs keys
clientSecretHeaderName A custom header for the client secret
sseBackend Enable sse to get events from the server
dispatcher Reference a dispatcher to manage thread pool
pageSize Change the size of the pages when fetching from the server
zoneId Zone Id to handle date

Configs client

The config client is used to access the shared config in Izanami. To understand how configs work, just visit this

Setup the client

Java
ConfigClient configClient = izanamiClient.configClient(
    Strategies.fetchStrategy(),
    Configs.configs(
        Config.config("my:config", Json.obj(
            Syntax.$("value", "Fallback value")
        ))
    )
);
Scala
val configClient = client.configClient(
  strategy = FetchStrategy(),
  fallback = Configs(
    "test2" -> Json.obj("value" -> 2)
  )
)

When you set up a client you have to choose a strategy :

Strategy Description
Fetch Call izanami for each request
Fetch with cache Keep response in cache
Smart cache with polling Keep data in memory and poll izanami to refresh the cache asynchronously.
Smart cache with sse Keep data in memory and refresh the cache with the events from the izanami server.

The fetch strategy

The fetch strategy will call izanami for each request.

Java
ConfigClient configFetchStrategy = izanamiClient.configClient(
    Strategies.fetchStrategy(),
    Configs.configs(
        Config.config("my:config", Json.obj(
            Syntax.$("value", "Fallback value")
        ))
    )
);
Scala
val configClient = client.configClient(
  strategy = FetchStrategy(),
  fallback = Configs(
    "test2" -> Json.obj("value" -> 2)
  )
)

The fetch with cache strategy

The fetch with cache will do dumb cache by http call. You have to provide a the max elements in cache and a TTL.

Java
Integer maxElementInCache = 100;
FiniteDuration ttl = FiniteDuration.create(20, TimeUnit.MINUTES);
ConfigClient fetchWithCacheStrategy = izanamiClient.configClient(
    Strategies.fetchWithCacheStrategy(maxElementInCache, ttl),
    Configs.configs(
        Config.config("my:config", Json.obj(
            Syntax.$("value", "Fallback value")
        ))
    )
);
Scala
val strategy = IzanamiClient(
  ClientConfig(ctx.host)
).configClient(
  strategy = FetchWithCacheStrategy(maxElement = 2, duration = 1.second),
  fallback = Configs(
    "test2" -> Json.obj("value" -> 2)
  )
)

The smart cache strategy

When you choose the smart cache, you have to provide patterns :

Java
ConfigClient configClient = izanamiClient.configClient(
    Strategies.smartCacheWithPollingStrategy(
        FiniteDuration.create(20, TimeUnit.SECONDS),
        "my:configs:*", "other:pattern"
    ),
    Configs.configs(
        Config.config("my:config", Json.obj(
            Syntax.$("value", "Fallback value")
        ))
    )
);
Scala
val configClient = client.configClient(
  strategy = CacheWithPollingStrategy(
    patterns = Seq("*"),
    pollingInterval = 3.second
  ),
  fallback = fallback
)

The client will cache all the configs matching this patterns.

  • With a poll strategy, the client will request the server to get change so you have to set a delay.
  • With a SSE strategy, the client will listen events from the server to refresh the cache.
Warning

There is no TTL using this strategy so you have to choose the right patterns to be sure that all datas fit in memory.

Handling errors

An error handling strategy could be provided. You can choose between :

  • RecoverWithFallback: If the call crash, the fallback is used
  • Crash: The call will finish on error if an error occured while evaluating the feature, config or experiment.
Java
ConfigClient configErrorStrategy = izanamiClient.configClient(
    Strategies.fetchStrategy(ErrorStrategies.crash())
);
Scala
val configClient = izanamiClient.configClient(
  strategy = FetchStrategy(Crash),
  fallback = Configs(
    "test2" -> Json.obj("value" -> 2)
  )
)

Client usage

Get configs for a pattern

Java
Future<Configs> futureConfigs = configClient.configs("my:*");
futureConfigs.onSuccess((Configs configs) -> {
  JsValue config = configs.config("my:config");
  System.out.println(config.field("value").asString());
});
Scala
val configs: Future[Configs] = configClient.configs("*")
configs.onComplete {
  case Success(c) => println(c)
  case Failure(e) => e.printStackTrace()
}

Get one config

Java
Future<JsValue> futureConfig = configClient.config("my:config");
futureConfig.onSuccess((JsValue config) -> {
  System.out.println(config.field("value").asString());
});
Scala
val futureConfig: Future[JsValue] = izanamiClient.config("test")
futureConfig.onComplete {
  case Success(c) => println(c)
  case Failure(e) => e.printStackTrace()
}

Create / Update / Delete configs

Create config using json

Java
JsValue createdJson = configClient.createConfig("my:config", Json.obj(Syntax.$("value", "A configuration"))).get();
Scala
val configCreated = client.createConfig("test", Json.obj("value" -> 1))

Create config using a config object

Java
Config created = configClient.createConfig(Config.config("my:config", Json.obj(Syntax.$("value", "A configuration")))).get();
Scala
val jsoncreated = client.createConfig(Config("test", Json.obj("value" -> 1)))

Update config using json

Java
JsValue updatedJson = configClient.updateConfig("my:previous:config", "my:config", Json.obj(Syntax.$("value", "A configuration"))).get();
Scala
val configUpdated = client.updateConfig("test", "newtest", Json.obj("value" -> 1))

Update config using a config object

Java
Config updated = configClient.updateConfig("my:previous:config", Config.config("my:config", Json.obj(Syntax.$("value", "A configuration")))).get();
Scala
val configUpdated = client.updateConfig("test", Config("newtest", Json.obj("value" -> 1)))

Delete a config

Java
Done deleted = configClient.deleteConfig("my:config").get();
Scala
val configDeleted = client.deleteConfig("test")

Autocreate configs

You can autocreate configs that are define as fallback. To enable this you need set the autocreate parameter when the client is created.

Java
Boolean autocreate = true;
ConfigClient configClient = izanamiClient.configClient(
    Strategies.fetchStrategy(),
    Configs.configs(
        Config.config("my:config", Json.obj(
            Syntax.$("value", "Fallback value")
        ))
    ),
    autocreate
);
Scala
val izanamiClient = client.configClient(
  strategy = Strategies.fetchStrategy(),
  fallback = Configs(
    "test" -> Json.obj("value" -> 2)
  ),
  autocreate = true
)

Features client

To understand how features work, just visit this

Setup the client

Java
FeatureClient featureClient = izanamiClient.featureClient(
    Strategies.fetchStrategy(),
    Features.features(
        Features.feature("my:feature", false)
    )
);
Scala
val featureClient = client.featureClient(
  strategy = FetchStrategy(),
  fallback = Features(
    DefaultFeature("test2", true)
  )
)

When you set up a client you have to choose a strategy :

Strategy Description
Fetch Call izanami for each request
Fetch with cache Keep response in cache
Smart cache with polling Keep data in memory and poll izanami to refresh the cache asynchronously. The features that need a context are not cached because it can needs a huge amount of memory
Smart cache with sse Keep data in memory and refresh the cache with the events from the izanami server. The features that need a context are not cached because it can needs a huge amount of memory

The fetch strategy

The fetch strategy will call izanami for each request

Java
FeatureClient featureClient = izanamiClient.featureClient(
    Strategies.fetchStrategy(),
    Features.features(
        Features.feature("my:feature", false)
    )
);
Scala
val strategy = IzanamiClient(
  ClientConfig(host, pageSize = 2)
).featureClient(
  Strategies.fetchStrategy()
)

The fetch with cache strategy

The fetch with cache will do dumb cache by http call. You have to provide a the max elements in cache and a TTL.

Java
Integer maxElementInCache = 100;
FiniteDuration ttl = FiniteDuration.create(20, TimeUnit.MINUTES);
FeatureClient featureClientWithCache = izanamiClient.featureClient(
    Strategies.fetchWithCacheStrategy(maxElementInCache, ttl),
    Features.features(
        Features.feature("my:feature", false)
    )
);
Scala
val strategy = IzanamiClient(
  ClientConfig(ctx.host)
).featureClient(
  strategy = FetchWithCacheStrategy(maxElement = 2, duration = 1.second),
  fallback = Features(
    DefaultFeature("test2", true)
  )
)

The smart cache strategy

When you choose the smart cache, you have to provide patterns to select the keys that will be in cache:

Java
FeatureClient featureClient = izanamiClient.featureClient(
    Strategies.smartCacheWithSseStrategy(
        scala.Option.apply(FiniteDuration.create(1, TimeUnit.MINUTES)),
        "my:features:*", "other:pattern"
    ),
    Features.features(
        Features.feature("my:feature", false)
    )
);
Scala
val featureClient = client.featureClient(
  strategy = CacheWithPollingStrategy(
    patterns = Seq("*"),
    pollingInterval = 3.second
  ),
  fallback = Features(fallback: _*)
)

The client will cache all the configs matching this patterns.

  • With a poll strategy, the client will request the server to get change so you have to set a delay.
  • With a SSE strategy, the client will listen events from the server to refresh the cache.
Note

The feature that need a context to be evaluated are not cached. The cache is used only for simple features or feature with release date.

Warning

There is no TTL using this strategy so you have to choose the right patterns to be sure that all datas fit in memory.

Handling errors

An error handling strategy could be provided. You can choose between :

  • RecoverWithFallback: If the call crash, the fallback is used
  • Crash: The call will finish on error if an error occured while evaluating the feature, config or experiment.
Java
FeatureClient featureClientWithErrorHandling = izanamiClient.featureClient(
    Strategies.fetchStrategy(ErrorStrategies.crash())
);
Scala
val featureClient = izanamiClient.featureClient(
  FetchStrategy(Crash)
)

Client usage

List features

Java
Future<Features> futureFeatures = featureClient.features("my:feature:*");

futureFeatures.onSuccess(features -> {
  boolean active = features.isActive("my:feature:test");
  if (active) {
    System.out.println("Feature my:feature:test is active");
  } else {
    System.out.println("Feature my:feature:test is active");
  }
  JsObject tree = features.tree();
  System.out.println("Tree is " + Json.prettyPrint(tree));
});
Scala
val futureFeatures: Future[Features] = featureClient.features("*")
futureFeatures.onComplete {
  case Success(features) =>
    val active: Boolean = features.isActive("test")
    if (active)
      println(s"Feature test is active")
    else
      println(s"Feature test is not active")

    val tree: JsObject = features.tree()
    println(s"All features: ${Json.prettyPrint(tree)}")

  case Failure(e) =>
    e.printStackTrace()
}

Check feature

Java
Future<Boolean> futureCheck = featureClient.checkFeature("my:feature");
Scala
val futureCheck: Future[Boolean] = featureClient.checkFeature("test")

If the feature needs a context to be evaluated:

Java
Future<Boolean> futureCheckContext = featureClient.checkFeature("my:feature", Json.obj(
    Syntax.$("context", true)
));
Scala
val context                           = Json.obj("context" -> true)
val checkWithContext: Future[Boolean] = featureClient.checkFeature("test", context)

Conditional code on feature

This execute a code and return a value if a feature is active:

Java
Future<String> conditonal = featureClient.featureOrElse("my:feature",
    () -> "Feature is active",
    () -> "Feature is not active"
);
Scala
val conditonal: Future[String] = featureClient.featureOrElse("test") {
  "Feature is active"
} {
  "Feature is not active"
}

Or with a context

Java
Future<String> conditonalContext = featureClient.featureOrElse(
    "my:feature",
    Json.obj(Syntax.$("context", true)),
    () -> "Feature is active",
    () -> "Feature is not active"
);
Scala
val conditonalWithContext: Future[String] = featureClient.featureOrElse("test", context) {
  "Feature is active"
} {
  "Feature is not active"
}

Create / update / delete

With the client you can mutate features.

Create with raw data:

Java
Feature createdJson = featureClient.createJsonFeature(
    "my:feature",
    true,
    Features.hourRangeType(),
    Option.of(Json.obj(
        Syntax.$("startAt", "05:25"),
        Syntax.$("endAt", "16:30")
    ))
).get();
Scala
val featureCreated = featureClient.createJsonFeature(
  "feature:test",
  true,
  FeatureType.HOUR_RANGE,
  Some(Json.obj("startAt" -> "05:25", "endAt" -> "16:30"))
)
Or with a feature object: Java
Feature created = featureClient.createFeature(
    Features.hourRange("my:feature", true, LocalTime.of(5, 25), LocalTime.of(16, 30))
).get();
Scala
val featureCreated = featureClient.createFeature(
  DateRangeFeature("test2", true, LocalDateTime.of(2019, 4, 12, 0, 0, 0), LocalDateTime.of(2019, 5, 13, 0, 0, 0))
)

Update a feature :

Java
Feature updated = featureClient.updateFeature("my:previous:feature", Features.hourRange("my:feature:test", true, LocalTime.of(5, 25), LocalTime.of(16, 30))).get();
Scala
val featureCreated = featureClient.updateFeature(
  "test",
  DateRangeFeature("test2", true, LocalDateTime.of(2019, 4, 12, 0, 0, 0), LocalDateTime.of(2019, 5, 13, 0, 0, 0))
)

Delete a feature :

Java
Done deleted = featureClient.deleteFeature("my:feature").get();
Scala
val deleted = featureClient.deleteFeature("test")

You can also activate or deactivate a feature

Java
Feature activated = featureClient.switchFeature("my:feature", true).get();
Scala
val activated = featureClient.switchFeature("test", false)

Autocreate features

You can autocreate features that are define as fallback. To enable this you need set the autocreate parameter when the client is created.

Java
Boolean autocreate = true;
FeatureClient featureClient = izanamiClient.featureClient(
    Strategies.fetchStrategy(),
    Features.features(
        Features.feature("my:feature", false)
    ),
    autocreate
);
Scala
val featureClient = client
  .featureClient(
    FetchStrategy(Crash),
    autocreate = true,
    fallback = Features(feature)
  )

Experiments client

To understand how experiments work, just visit this

Setup the client

Java
ExperimentsClient experimentsClient = izanamiClient.experimentClient(Strategies.fetchStrategy());
Scala
val experimentClient = IzanamiClient(ClientConfig(host))
  .experimentClient(Strategies.fetchStrategy())

For experiments, there is only two strategies available : fetch or dev.

Variants

Get a variant for a client

Java
Future<Option<Variant>> futureVariant = experimentsClient.getVariantFor("my:experiment", "clientId");
futureVariant.onSuccess(mayBeVariant ->
    Match(mayBeVariant).of(
        Case($Some($()), exist -> {
          String phrase = "Variant is " + exist;
          System.out.println(phrase);
          return phrase;
        }),
        Case($None(), __ -> {
          String phrase = "Variant not found";
          System.out.println(phrase);
          return phrase;
        })
    )
);
Scala
val mayBeFutureVariant: Future[Option[Variant]] = experimentClient.getVariantFor("test", "client1")
mayBeFutureVariant.onComplete {
  case Success(mayBeVariant) => println(mayBeVariant)
  case Failure(e)            => e.printStackTrace()
}

Mark variant displayed

Java
Future<ExperimentVariantDisplayed> futureDisplayed = experimentsClient.markVariantDisplayed("my:experiment", "clientId");
futureDisplayed.onSuccess(event ->
    System.out.println(event)
);
Scala
val futureDisplayed: Future[ExperimentVariantDisplayed] =
  experimentClient.markVariantDisplayed("test", "client1")
futureDisplayed.onComplete {
  case Success(event) => println(event)
  case Failure(e)     => e.printStackTrace()
}

Mark variant won

Java
Future<ExperimentVariantWon> futureWon = experimentsClient.markVariantWon("my:experiment", "clientId");
futureWon.onSuccess(event ->
    System.out.println(event)
);
Scala
val futureWon: Future[ExperimentVariantWon] = experimentClient.markVariantWon("test", "client1")
futureWon.onComplete {
  case Success(event) => println(event)
  case Failure(e)     => e.printStackTrace()
}

Work with experiment

Get the experiment

Java
Future<Option<ExperimentClient>> futureExperiment = experimentsClient.experiment("my:experiment");
futureExperiment.onSuccess(mayBeExperiment ->
    Match(mayBeExperiment).of(
        Case($Some($()), exist -> {
          String phrase = "Experiment is " + exist;
          System.out.println(phrase);
          return phrase;
        }),
        Case($None(), __ -> {
          String phrase = "Experiment not found";
          System.out.println(phrase);
          return phrase;
        })
    )
);
Scala
val futureExperiment: Future[Option[ExperimentClient]] = experimentClient.experiment("test")
futureExperiment.onComplete {
  case Success(Some(exp)) => println(s"Experiment is $exp")
  case Success(None)      => println("Experiment not Found")
  case Failure(e)         => e.printStackTrace()
}

Once you get the experiment, you can get a variant for a client, mark variant displayed or mark variant won :

Java
ExperimentClient experiment = mayExperiment.get();
Future<Option<Variant>> clientId = experiment.getVariantFor("clientId");
Future<ExperimentVariantDisplayed> displayed = experiment.markVariantDisplayed("clientId");
Future<ExperimentVariantWon> won = experiment.markVariantWon("clientId");
Scala
val experiment: ExperimentClient                  = mayBeExperiment.get
val futureVariant: Future[Option[Variant]]        = experiment.getVariantFor("client1")
val displayed: Future[ExperimentVariantDisplayed] = experiment.markVariantDisplayed("client1")
val won: Future[ExperimentVariantWon]             = experiment.markVariantWon("client1")

Experiment tree

You can get the experiments tree with associated variant for a client :

Java
Future<JsValue> futureTree = experimentsClient.tree("*", "clientId");
futureTree.onSuccess(tree -> {
  assertThat(tree).isEqualTo(
      Json.obj(
          Syntax.$("my", Json.obj(
              Syntax.$("experiment", Json.obj(
                  Syntax.$("variant", "A")
              ))
          ))
      )
  );
});
Scala
val experimentsTree = client.tree("*", "client1").futureValue
experimentsTree must be(
  Json.obj(
    "izanami" -> Json.obj(
      "ab" -> Json.obj(
        "test" -> Json.obj(
          "variant" -> "A"
        )
      )
    )
  )
)
val experimentsTree = client.tree("*", "client1").futureValue
experimentsTree must be(
  Json.obj()
)

Exposing izanami with a Proxy

When you have to use Izanami from the client side, you can’t call Izanami directly from the browser because it means the API keys are exposed to anyone.

The best solution is to use your backend as a proxy. You can do this with the jvm client.

Java
ConfigClient configClient = izanamiClient.configClient(
    Strategies.dev(),
    Configs.configs(
        Config.config("configs:test", Json.obj(
            Syntax.$("value", 2)
        ))
    )
);
FeatureClient featureClient = izanamiClient.featureClient(
    Strategies.dev(),
    Features.features(
        Features.feature("features:test1", true)
    )
);
ExperimentsClient experimentsClient = izanamiClient.experimentClient(
    Strategies.dev(),
    Experiments.create(
        ExperimentFallback.create(
            "experiments:id",
            "Experiment",
            "An Experiment",
            true,
            Variant.create("A", "Variant A", scala.Option.apply("Variant A"))
        )));

Proxy proxy = izanamiClient.proxy()
    .withConfigClient(configClient)
    .withConfigPattern("configs:*")
    .withFeatureClient(featureClient)
    .withFeaturePattern("features:*")
    .withExperimentsClient(experimentsClient)
    .withExperimentPattern("experiments:*");

Future<Tuple2<Integer, JsValue>> fJsonResponse = proxy.statusAndJsonResponse();
fJsonResponse.onSuccess(t ->
    System.out.println("Code = " + t._1 + ", json body = " + t._2)
);

//Or with string response and additional infos :
Future<Tuple2<Integer, String>> fStringResponse = proxy.statusAndStringResponse(Json.obj().with("id", "ragnard.lodbrock@gmail.com"), "ragnard.lodbrock@gmail.com");
fStringResponse.onSuccess(t ->
    System.out.println("Code = " + t._1 + ", string body = " + t._2)
);
// Experiment proxy

Future<Tuple2<Integer, JsValue>> markVariantDisplayed = proxy.markVariantDisplayed("experiments:id", "ragnars.lodbrock@gmail.com");
Future<Tuple2<Integer, JsValue>> markVariantWon = proxy.markVariantWon("experiments:id", "ragnars.lodbrock@gmail.com");
Scala
val client = IzanamiClient(
  ClientConfig("")
)

val featureClient: FeatureClient = client.featureClient(
  strategy = Strategies.dev(),
  fallback = Features(
    DefaultFeature("features:test1", true)
  )
)

val configClient: ConfigClient = client.configClient(
  Strategies.dev(),
  fallback = Configs(
    "configs:test" -> Json.obj("value" -> 2)
  )
)

val experimentsClient: ExperimentsClient = client.experimentClient(
  strategy = Strategies.dev(),
  fallback = Experiments(
    ExperimentFallback(
      "experiments:id",
      "Experiment",
      "An experiment",
      true,
      Variant("A", "Variant A", Some("Variant A"))
    )
  )
)

val proxy: Proxy = client
  .proxy()
  .withConfigClient(configClient)
  .withConfigPattern("configs:*")
  .withFeatureClient(featureClient)
  .withFeaturePattern("features:*")
  .withExperimentsClient(experimentsClient)
  .withExperimentPattern("experiments:*")

val fResponseJson: Future[(Int, JsValue)] = proxy.statusAndJsonResponse()
fResponseJson.onComplete {
  case Success((status, responseBody)) =>
    println(s"Izanami respond with status $status and json body $responseBody")
  case _ => println("Oups something wrong happened")
}

//Or for a string response and additional infos
val fResponseString: Future[(Int, String)] = proxy.statusAndStringResponse(
  context = Some(Json.obj("user" -> "ragnard.lodbrock@gmail.com")),
  userId = Some("ragnard.lodbrock@gmail.com")
)
fResponseString.onComplete {
  case Success((status, responseBody)) =>
    println(s"Izanami respond with status $status and string body $responseBody")
  case _ => println("Oups something wrong happened")
}

// Experiment proxy

val fDisplayed: Future[(Int, JsValue)] =
  proxy.markVariantDisplayed("experiments:id", "ragnard.lodbrock@gmail.com")
val fWon: Future[(Int, JsValue)] = proxy.markVariantWon("experiments:id", "ragnard.lodbrock@gmail.com")
The source code for this page can be found here.