Skip to content

Commit

Permalink
java-doc: java doc and read me (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
dsun0720 authored Mar 2, 2022
1 parent f0688e8 commit 5d302b3
Show file tree
Hide file tree
Showing 41 changed files with 1,158 additions and 76 deletions.
202 changes: 201 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,45 @@
# ffc-java-sdk
# JAVA Server Side SDK

## Introduction

This is the Java Server Side SDK for the feature management platform [feature-flags.co](feature-flags.co). It is
intended for use in a multiple-users Java server applications.

This SDK has two main purposes:

- Store the available feature flags and evaluate the feature flags by given user in the server side SDK
- Sends feature flags usage, and custom events for the insights and A/B/n testing.

## Data synchonization

We use websocket to make the local data synchronized with the server, and then store them in the memory by default.
Whenever there is any changes to a feature flag or his related data, the changes would be pushed to the SDK, the average
synchronization time is less than **100** ms. Be aware the websocket connection can be interrupted by any error or
internet interruption, but it would be restored automatically right after the problem is gone.

## Offline mode support

In the offline mode, SDK DOES not exchange any data with [feature-flags.co](feature-flags.co)

In the following situation, the SDK would work when there is no internet connection: it has been initialized in
using `co.featureflags.server.exterior.FFCClient#initializeFromExternalJson(json)`

To open the offline mode:

```
FFCConfig config = new FFCConfig.Builder()
.offline(false)
.build()
FFCClient client = new FFCClientImp(envSecret, config);
```

## Evaluation of a feature flag

SDK will initialize all the related data(feature flags, segments etc.) in the bootstrapping and receive the data updates
in real time, as mentioned in the above

After initialization, the SDK has all the feature flags in the memory and all evaluation is done locally and
synchronously, the average evaluation time is < **10** ms.

## Installation

Expand All @@ -21,3 +62,162 @@ install the sdk in using maven
</dependency>
</dependencies>
```

## SDK

### FFCClient

Applications SHOULD instantiate a single instance for the lifetime of the application. In the case where an application
needs to evaluate feature flags from different environments, you may create multiple clients, but they should still be
retained for the lifetime of the application rather than created per request or per thread.

### Bootstrapping

The bootstrapping is in fact the call of constructor of `FFCClientImp`, in which the SDK will be initialized, using
streaming from [feature-flags.co](feature-flags.co).

The constructor will return when it successfully connects, or when the timeout set
by `FFCConfig.Builder#startWaitTime(Duration)`
(default: 15 seconds) expires, whichever comes first. If it has not succeeded in connecting when the timeout elapses,
you will receive the client in an uninitialized state where feature flags will return default values; it will still
continue trying to connect in the background unless there has been an `java.net.ProtocolException` or you close the
client(using `close()`). You can detect whether initialization has succeeded by calling `isInitialized()`.

```
FFCClient client = new FFCClient(sdkKey, config);
if(client.isInitialized()){
// do whatever is appropriate
}
```

If you prefer to have the constructor return immediately, and then wait for initialization to finish at some other
point, you can use `getDataUpdateStatusProvider()`, which provides an asynchronous way, as follows:

```
FFCConfig config = new FFCConfig.Builder()
.startWait(Duration.ZERO)
.build();
FFCClient client = new FFCClient(sdkKey, config);
// later, when you want to wait for initialization to finish:
boolean inited = client.getDataUpdateStatusProvider().waitForOKState(Duration.ofSeconds(15))
if (!inited) {
// do whatever is appropriate if initialization has timed out
}
```

Note that the _**sdkKey(envSecret)**_ is mandatory.

### FFCConfig and Components

`FFCConfig` exposes advanced configuration options for the `FFCClient`.

`startWaitTime`: how long the constructor will block awaiting a successful data sync. Setting this to a zero or negative
duration will not block and cause the constructor to return immediately.

`offline`: Set whether SDK is offline. when set to true no connection to feature-flag.co anymore

We strongly recommend to use the default configuration or just set `startWaitTime` or `offline` if necessary.



```
// default configuration
FFCConfig config = FFCConfig.DEFAULT
// set startWaitTime and offline
FFCConfig config = new FFCConfig.Builder()
.startWaitTime(Duration.ZERO)
.offline(false)
.build()
FFCClient client = new FFCClient(sdkKey, config);
// default configuration
FFCClient client = new FFCClient(sdkKey);
```

`FFCConfig` provides advanced configuration options for setting the SDK component or you want to customize the behavior
of build-in components.

`HttpConfigFactory`: Interface for a factory that creates an `HttpConfig`. SDK sets the SDK's networking configuration,
using a factory object. This object by defaut is a configuration builder obtained from `Factory#httpConfigFactory()`.
With `HttpConfig`, Sets connection/read/write timeout, proxy or insecure/secure socket.

```
HttpConfigFactory factory = Factory.httpConfigFactory()
.connectTime(Duration.ofMillis(3000))
.httpProxy("my-proxy", 9000)
FFCConfig config = new FFCConfig.Builder()
.httpConfigFactory(factory)
.build();
```


`DataStorageFactory` Interface for a factory that creates some implementation of `DataStorage`, that holds feature flags,
user segments or any other related data received by the SDK. SDK sets the implementation of the data storage, using `Factory#inMemoryDataStorageFactory()`
to instantiate a memory data storage. Developers can customize the data storage to persist received data in redis, mongodb, etc.

```
FFCConfig config = new FFCConfig.Builder()
.dataStorageFactory(factory)
.build();
```

`UpdateProcessorFactory` SDK sets the implementation of the `UpdateProcessor` that receives feature flag data from feature-flag.co,
using a factory object. The default is `Factory#streamingBuilder()`, which will create a streaming, using websocket.
If Developers would like to know what the implementation is, they can read the javadoc and source code.

`InsightProcessorFactory` SDK sets the implementation of `InsightProcessor` to be used for processing analytics events,
using a factory object. The default is `Factory#insightProcessorFactory()`. If Developers would like to know what the implementation is,
they can read the javadoc and source code.

###Evaluation

SDK calculates the value of a feature flag for a given user, and returns a flag vlaue/an object that describes the way
that the value was determined.

`FFUser`: A collection of attributes that can affect flag evaluation, usually corresponding to a user of your application.
This object contains built-in properties(`key`, `userName`, `email` and `country`). The only mandatory property is the key,
which must uniquely identify each user; this could be a username or email address for authenticated users, or a ID for anonymous users.
All other built-in properties are optional, it's strongly recommended to set userName in order to search your user quickly
You may also define custom properties with arbitrary names and values.

```
FFCClient client = new FFCClientImp(envSecret);
// FFUser creation
FFCClient user = new FFCClient.Builder("key")
.userName("name")
.country("country")
.email("[email protected]")
.custom("property", "value")
.build()
// Evaluation details
FlagState<String> res = client.variationDetail("flag key", user, "Not Found");
// Flag value
String res = client.variation("flag key", user, "Not Found");
```

If evaluation called before Java SDK client initialized or you set the wrong flag key or user for the evaluation, SDK will return
the default value you set. The `FlagState` will explain the reason of the last evaluation error.

SDK support the String, Boolean, and Number as the return type of flag values, see JavaDocs for more details.

### Experiments (A/B/n Testing)
We support automatic experiments for pageviews and clicks, you just need to set your experiment on our SaaS platform, then you should be able to see the result in near real time after the experiment is started.

In case you need more control over the experiment data sent to our server, we offer a method to send custom event.
```
client.trackMetric(user, eventName, numericValue);
```
**numericValue** is not mandatory, the default value is **1**.

Make sure `trackMetric` is called after the related feature flag is called by simply calling `variation` or `variationDetail`
otherwise, the custom event won't be included into the experiment result.


Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1 +1 @@
31924decb4745685509f3a6a17da81f3
596e9cb4c5bf3447a78efb7117d3f07c
Original file line number Diff line number Diff line change
@@ -1 +1 @@
a3e448f5686e77497649232bcd466add76fbb66e
1f565d07296a4829817b20833785c8142f0b0e6f
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1 +1 @@
6f920804887a2a7165f1aef07f8bb4e7
0c86ee186b970f9eb309b8fe40582307
Original file line number Diff line number Diff line change
@@ -1 +1 @@
e364cf172317e64b0f95675dfbf87588dd86bf64
a9ba552804bfd684142874f877facef3df233480
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -1 +1 @@
a1d97088259e78babbf333b9c14fc3ed
a11b951211a7f81926f828957f43cf8b
Original file line number Diff line number Diff line change
@@ -1 +1 @@
819c8b1b2232697814ced0daf24c7ebb040a635b
df013663be8cec42f9124ae354a2ca3b7a083c88
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
<versions>
<version>1.0</version>
</versions>
<lastUpdated>20220224112944</lastUpdated>
<lastUpdated>20220302204354</lastUpdated>
</versioning>
</metadata>
Original file line number Diff line number Diff line change
@@ -1 +1 @@
5462b4675b9a3abe371c3f741a5188db
f040fe1eb0dbfb515853d7354c5e2ccc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
bd0bae12efd50fdd446c7acfa3e6dbd7165b4b0d
9b4817726df80e8f66f0b987f3a99212a70fcacf
4 changes: 2 additions & 2 deletions src/main/java/co/featureflags/server/ContextImp.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
import co.featureflags.server.exterior.Context;
import co.featureflags.server.exterior.HttpConfig;

public final class ContextImp implements Context {
final class ContextImp implements Context {
private final HttpConfig httpConfig;
private final BasicConfig basicConfig;

public ContextImp(String envSecret, FFCConfig config) {
ContextImp(String envSecret, FFCConfig config) {
this.basicConfig = new BasicConfig(envSecret, config.isOffline());
this.httpConfig = config.getHttpConfigFactory().createHttpConfig(basicConfig);
}
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/co/featureflags/server/DataModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,46 @@ public abstract class DataModel {
private DataModel() {
}

/**
* interface for the object to represent a versioned/timestamped data
*/
public interface TimestampData {
Integer FFC_FEATURE_FLAG = 100;
Integer FFC_ARCHIVED_VDATA = 200;
Integer FFC_PERSISTENT_VDATA = 300;

/**
* return the unique id
*
* @return a string
*/
String getId();

/**
* return true if object is archived
*
* @return true if object is archived
*/
boolean isArchived();

/**
* return the version/timestamp of the object
*
* @return a long value
*/
Long getTimestamp();

/**
* return the type of versioned/timestamped object
*
* @return an integer
*/
Integer getType();
}

/**
* the object is an implementation of{@link TimestampData}, to represent the archived data
*/
public final static class ArchivedTimestampData implements TimestampData {
private final String id;
private final Long timestamp;
Expand Down
55 changes: 54 additions & 1 deletion src/main/java/co/featureflags/server/FFCClientImp.java
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ public Status.DataUpdateStatusProvider getDataUpdateStatusProvider() {

@Override
public boolean initializeFromExternalJson(String json) {
if (offline) {
if (offline && StringUtils.isNotBlank(json)) {
DataModel.All all = JsonHelper.deserialize(json, DataModel.All.class);
if (all.isProcessData()) {
DataModel.Data allData = all.data();
Expand Down Expand Up @@ -379,4 +379,57 @@ public AllFlagStates<String> getAllLatestFlagsVariations(FFCUser user) {
}
return AllFlagStates.of(success, errorString, builder.build());
}

@Override
public void flush() {
this.insightProcessor.flush();
}

@Override
public void trackMetric(FFCUser user, String eventName) {
trackMetric(user, eventName, 1.0);
}

@Override
public void trackMetric(FFCUser user, String eventName, double metricValue) {
if (user == null || StringUtils.isBlank(eventName) || metricValue <= 0) {
Loggers.CLIENT.warn("event/user/metric invalid");
return;
}
InsightTypes.Event event = InsightTypes.MetricEvent.of(user)
.add(InsightTypes.Metric.of(eventName, metricValue));
insightProcessor.send(event);
}

@Override
public void trackMetrics(FFCUser user, String... eventNames) {
if (user == null || eventNames == null || eventNames.length == 0) {
Loggers.CLIENT.warn("user/events invalid");
return;
}
InsightTypes.Event event = InsightTypes.MetricEvent.of(user);
for (String eventName : eventNames) {
if (StringUtils.isNotBlank(eventName)) {
event.add(InsightTypes.Metric.of(eventName, 1.0));
}
}
insightProcessor.send(event);
}

@Override
public void trackMetrics(FFCUser user, Map<String, Double> metrics) {
if (user == null || metrics == null || metrics.isEmpty()) {
Loggers.CLIENT.warn("user/metrics invalid");
return;
}
InsightTypes.Event event = InsightTypes.MetricEvent.of(user);
for (Map.Entry<String, Double> entry : metrics.entrySet()) {
String eventName = entry.getKey();
Double metricValue = entry.getValue();
if (StringUtils.isNotBlank(eventName) && metricValue != null && metricValue > 0D) {
event.add(InsightTypes.Metric.of(eventName, metricValue));
}
}
insightProcessor.send(event);
}
}
Loading

0 comments on commit 5d302b3

Please sign in to comment.