Skip to content

Commit dda4bb2

Browse files
authored
feat: ab (#44)
1 parent 6d9c88a commit dda4bb2

18 files changed

+727
-68
lines changed

pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@
5454
<version>3.21.7</version>
5555
</dependency>
5656

57+
<dependency>
58+
<groupId>org.json</groupId>
59+
<artifactId>json</artifactId>
60+
<version>20140107</version>
61+
</dependency>
62+
5763
<dependency>
5864
<groupId>junit</groupId>
5965
<artifactId>junit</artifactId>

src/main/java/io/growing/sdk/java/GrowingAPI.java

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
package io.growing.sdk.java;
22

3+
import io.growing.sdk.java.ab.ABTaskController;
4+
import io.growing.sdk.java.ab.ABTestCallback;
35
import io.growing.sdk.java.constants.RunMode;
46
import io.growing.sdk.java.dto.GIOMessage;
5-
import io.growing.sdk.java.dto.GioCDPMessage;
67
import io.growing.sdk.java.exception.GIOSendBeRejectedException;
78
import io.growing.sdk.java.logger.GioLogger;
89
import io.growing.sdk.java.sender.FixThreadPoolSender;
910
import io.growing.sdk.java.store.StoreStrategy;
1011
import io.growing.sdk.java.store.StoreStrategyClient;
1112
import io.growing.sdk.java.utils.ConfigUtils;
12-
import io.growing.sdk.java.utils.StringUtils;
13+
import io.growing.sdk.java.utils.MessageUtils;
1314
import io.growing.sdk.java.utils.VersionInfo;
1415

1516
import java.util.Properties;
@@ -26,6 +27,8 @@ public class GrowingAPI {
2627
private final String dataSourceId;
2728

2829
private static StoreStrategy strategy;
30+
private static ABTaskController abTaskController;
31+
private static boolean abEnabled = false;
2932

3033
static {
3134
ConfigUtils.initDefault();
@@ -34,6 +37,10 @@ public class GrowingAPI {
3437
private GrowingAPI(Builder builder) {
3538
this.validDefaultConfig = validDefaultConfig();
3639
strategy = StoreStrategyClient.getStoreInstance(StoreStrategyClient.CURRENT_STRATEGY);
40+
abEnabled = ConfigUtils.getBooleanValue("ab.enabled", false);
41+
if (abEnabled) {
42+
abTaskController = new ABTaskController(strategy);
43+
}
3744
this.dataSourceId = builder.dataSourceId;
3845
this.projectKey = builder.projectKey;
3946
}
@@ -76,23 +83,30 @@ public void sendMaybeRejected(GIOMessage msg) throws GIOSendBeRejectedException
7683
}
7784
}
7885

79-
private boolean businessVerification(GIOMessage msg) {
80-
if (StringUtils.nonBlank(this.projectKey)) {
81-
msg.setProjectKey(this.projectKey);
82-
} else {
83-
GioLogger.error("projectKey cant be null or empty string");
84-
return false;
86+
public void getABTest(String layerId, String dataSourceId, String distinctId, ABTestCallback callback) {
87+
if (!abEnabled) {
88+
return;
8589
}
90+
try {
91+
abTaskController.submitABTaskAsync(this.projectKey, dataSourceId, layerId, distinctId, callback);
92+
} catch (Exception e) {
93+
GioLogger.error("getABTest failed: " + e.getLocalizedMessage());
94+
}
95+
}
8696

87-
if (msg instanceof GioCDPMessage) {
88-
if (StringUtils.nonBlank(this.dataSourceId)) {
89-
((GioCDPMessage<?>) msg).setDataSourceId(this.dataSourceId);
90-
} else {
91-
GioLogger.error("cdp message datasourceId cant be null or empty string");
92-
return false;
93-
}
97+
public void getABTestSync(String layerId, String dataSourceId, String distinctId, ABTestCallback callback) {
98+
if (!abEnabled) {
99+
return;
94100
}
95-
return true;
101+
try {
102+
abTaskController.submitABTaskSync(this.projectKey, dataSourceId, layerId, distinctId, callback);
103+
} catch (Exception e) {
104+
GioLogger.error("getABTest failed: " + e.getLocalizedMessage());
105+
}
106+
}
107+
108+
private boolean businessVerification(GIOMessage msg) {
109+
return MessageUtils.businessVerification(msg, this.projectKey, this.dataSourceId);
96110
}
97111

98112
public static class Builder {
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package io.growing.sdk.java.ab;
2+
3+
import java.util.Collections;
4+
import java.util.Map;
5+
import java.util.Set;
6+
7+
public class ABExperiment {
8+
9+
private final String layerId;
10+
private final long strategyId;
11+
private final long experimentId;
12+
private String expLayerName;
13+
private String expName;
14+
private String expStrategyName;
15+
private final Map<String, String> variables;
16+
17+
public ABExperiment(String layerId, long strategyId, long experimentId, Map<String, String> variables) {
18+
this.layerId = layerId;
19+
this.strategyId = strategyId;
20+
this.experimentId = experimentId;
21+
if (variables == null) {
22+
this.variables = Collections.emptyMap();
23+
} else {
24+
this.variables = variables;
25+
}
26+
}
27+
28+
public void setExperimentNames(String expLayerName, String expName, String expStrategyName) {
29+
this.expLayerName = expLayerName;
30+
this.expName = expName;
31+
this.expStrategyName = expStrategyName;
32+
}
33+
34+
public String getLayerId() {
35+
return layerId;
36+
}
37+
38+
public long getStrategyId() {
39+
return strategyId;
40+
}
41+
42+
public long getExperimentId() {
43+
return experimentId;
44+
}
45+
46+
public Map<String, String> getVariables() {
47+
return variables;
48+
}
49+
50+
public String getExpLayerName() {
51+
return expLayerName;
52+
}
53+
54+
public String getExpName() {
55+
return expName;
56+
}
57+
58+
public String getExpStrategyName() {
59+
return expStrategyName;
60+
}
61+
62+
@Override
63+
public boolean equals(Object obj) {
64+
if (obj instanceof ABExperiment) {
65+
ABExperiment abExperiment = (ABExperiment) obj;
66+
return abExperiment.getLayerId().equals(layerId)
67+
&& abExperiment.getStrategyId() == strategyId
68+
&& abExperiment.getExperimentId() == experimentId
69+
&& abExperiment.getVariables().equals(variables);
70+
}
71+
return super.equals(obj);
72+
}
73+
74+
@Override
75+
public String toString() {
76+
StringBuilder sb = new StringBuilder();
77+
sb.append(layerId).append("$").append(strategyId).append("$").append(experimentId);
78+
if (variables != null && !variables.isEmpty()) {
79+
Set<String> keySets = variables.keySet();
80+
sb.append("$");
81+
for (String key : keySets) {
82+
sb.append(key).append("=").append(variables.get(key));
83+
}
84+
}
85+
return sb.toString();
86+
}
87+
88+
89+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package io.growing.sdk.java.ab;
2+
3+
import io.growing.sdk.java.dto.GioCdpEventMessage;
4+
import io.growing.sdk.java.logger.GioLogger;
5+
import io.growing.sdk.java.store.StoreStrategy;
6+
import io.growing.sdk.java.thread.GioThreadNamedFactory;
7+
import io.growing.sdk.java.utils.MessageUtils;
8+
import io.growing.sdk.java.utils.StringUtils;
9+
10+
import java.util.concurrent.ExecutorService;
11+
import java.util.concurrent.Executors;
12+
13+
public class ABTaskController {
14+
private static final ExecutorService abTaskThreadPool = Executors.newSingleThreadExecutor(new GioThreadNamedFactory("gio-ab-sender"));
15+
private static final ABTestHttpUrlProvider netProvider = new ABTestHttpUrlProvider();
16+
private StoreStrategy strategy;
17+
18+
public ABTaskController(StoreStrategy strategy) {
19+
this.strategy = strategy;
20+
}
21+
22+
public void submitABTask(final String projectId, final String dataSourceId, final String layerId, final String distinctId, final ABTestCallback callback) {
23+
if (StringUtils.isBlank(projectId) || StringUtils.isBlank(dataSourceId) || StringUtils.isBlank(distinctId) || callback == null) {
24+
GioLogger.error("submitABTask:params is illegal");
25+
return;
26+
}
27+
28+
try {
29+
ABTestResponse response = netProvider.requestABTestExperimentData(projectId, dataSourceId, layerId, distinctId);
30+
if (response.isSucceed()) {
31+
callback.onABExperimentReceived(response.abExperiment);
32+
sendAbTestTrackEvent(response.abExperiment, projectId, dataSourceId, distinctId);
33+
} else {
34+
callback.onABExperimentFailed(new IllegalAccessException(response.getErrorMsg()));
35+
}
36+
} catch (Exception e) {
37+
GioLogger.error("submitABTask failed: " + e.getLocalizedMessage());
38+
}
39+
}
40+
41+
public void submitABTaskSync(final String projectId, final String dataSourceId, final String layerId, final String distinctId, final ABTestCallback callback) {
42+
submitABTask(projectId, dataSourceId, layerId, distinctId, callback);
43+
}
44+
45+
public void submitABTaskAsync(final String projectId, final String dataSourceId, final String layerId, final String distinctId, final ABTestCallback callback) {
46+
abTaskThreadPool.submit(new Runnable() {
47+
@Override
48+
public void run() {
49+
submitABTask(projectId, dataSourceId, layerId, distinctId, callback);
50+
}
51+
});
52+
}
53+
54+
private void sendAbTestTrackEvent(ABExperiment abExperiment, String projectId, String dataSourceId, String distinctId) {
55+
if (abExperiment.getExperimentId() == 0 && abExperiment.getStrategyId() == 0) {
56+
return;
57+
}
58+
GioCdpEventMessage eventMessage = new GioCdpEventMessage.Builder()
59+
.eventKey("$exp_hit")
60+
.anonymousId(distinctId)
61+
.addEventVariable("$exp_id", String.valueOf(abExperiment.getExperimentId()))
62+
.addEventVariable("$exp_strategy_id", String.valueOf(abExperiment.getStrategyId()))
63+
.addEventVariable("$exp_layer_id", abExperiment.getLayerId())
64+
.addEventVariable("$exp_layer_name", abExperiment.getExpLayerName())
65+
.addEventVariable("$exp_name", abExperiment.getExpName())
66+
.addEventVariable("$exp_strategy_name", abExperiment.getExpStrategyName())
67+
.build();
68+
69+
if (MessageUtils.businessVerification(eventMessage, projectId, dataSourceId)) {
70+
strategy.push(eventMessage);
71+
}
72+
}
73+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package io.growing.sdk.java.ab;
2+
3+
public interface ABTestCallback {
4+
5+
void onABExperimentReceived(ABExperiment experiment);
6+
7+
void onABExperimentFailed(Exception error);
8+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package io.growing.sdk.java.ab;
2+
3+
import io.growing.sdk.java.sender.net.BaseNetProvider;
4+
import io.growing.sdk.java.utils.ConfigUtils;
5+
6+
import java.io.*;
7+
import java.net.HttpURLConnection;
8+
import java.net.Proxy;
9+
import java.net.URL;
10+
import java.net.URLEncoder;
11+
import java.nio.charset.Charset;
12+
13+
public class ABTestHttpUrlProvider extends BaseNetProvider {
14+
15+
private static int getConnectionTimeout() {
16+
return ConfigUtils.getIntValue("ab.connection.timeout", 5000);
17+
}
18+
19+
private static int getReadTimeout() {
20+
return ConfigUtils.getIntValue("ab.read.timeout", 5000);
21+
}
22+
23+
public ABTestResponse requestABTestExperimentData(String projectId, String dataSourceId, String layerId, String distinctId) {
24+
ABTestResponse outABTestResponse = new ABTestResponse();
25+
DataOutputStream os = null;
26+
BufferedReader br = null;
27+
try {
28+
String body = "accountId=" + URLEncoder.encode(projectId, Charset.forName("UTF-8").toString()) +
29+
"&datasourceId=" + URLEncoder.encode(dataSourceId, Charset.forName("UTF-8").toString()) +
30+
"&distinctId=" + URLEncoder.encode(distinctId, Charset.forName("UTF-8").toString()) +
31+
"&layerId=" + URLEncoder.encode(layerId, Charset.forName("UTF-8").toString());
32+
33+
HttpURLConnection httpConn = getConnection(apiHost());
34+
httpConn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
35+
httpConn.setUseCaches(false);
36+
httpConn.setRequestMethod("POST");
37+
httpConn.setConnectTimeout(getConnectionTimeout());
38+
httpConn.setReadTimeout(getReadTimeout());
39+
httpConn.setRequestProperty("Content-Length", String.valueOf(body.getBytes().length));
40+
httpConn.setDoOutput(true);
41+
42+
os = new DataOutputStream(httpConn.getOutputStream());
43+
os.write(body.getBytes());
44+
os.flush();
45+
46+
int responseCode = httpConn.getResponseCode();
47+
if (responseCode >= 200 && responseCode < 300) {
48+
br = new BufferedReader(new InputStreamReader(httpConn.getInputStream()));
49+
String inputLine;
50+
StringBuilder response = new StringBuilder();
51+
while ((inputLine = br.readLine()) != null) {
52+
response.append(inputLine);
53+
}
54+
ABTestResponse abTestResponse = ABTestResponse.parseHttpJson(layerId, response.toString());
55+
if (!abTestResponse.isSucceed()) {
56+
abTestResponse.setErrorMsg("ABExperiment data failed with: " + abTestResponse.getErrorMsg());
57+
}
58+
return abTestResponse;
59+
} else {
60+
outABTestResponse.setErrorMsg("ABTest request failed: " + responseCode);
61+
}
62+
63+
} catch (Exception e) {
64+
outABTestResponse.setErrorMsg("ABTest request failed: " + e.getLocalizedMessage());
65+
} finally {
66+
if (os != null) {
67+
try {
68+
os.close();
69+
} catch (IOException e) {
70+
outABTestResponse.setErrorMsg("ABTest request failed: " + e.getLocalizedMessage());
71+
}
72+
os = null;
73+
}
74+
if (br != null) {
75+
try {
76+
br.close();
77+
} catch (IOException e) {
78+
outABTestResponse.setErrorMsg("ABTest request failed: " + e.getLocalizedMessage());
79+
}
80+
br = null;
81+
}
82+
}
83+
return outABTestResponse;
84+
}
85+
86+
private String apiDomain() {
87+
String apiHost = ConfigUtils.getStringValue("ab.api.host", "");
88+
if (apiHost.endsWith("/")) {
89+
return apiHost.substring(0, apiHost.length() - 1);
90+
} else {
91+
return apiHost;
92+
}
93+
}
94+
95+
private String apiHost() {
96+
return apiDomain() + "/diversion/specified-layer-variables";
97+
}
98+
99+
private HttpURLConnection getConnection(String url) throws IOException {
100+
Proxy proxy = ProxyInfo.getProxy();
101+
102+
HttpURLConnection httpConn;
103+
if (proxy == null) {
104+
httpConn = (HttpURLConnection) new URL(url).openConnection();
105+
} else {
106+
httpConn = (HttpURLConnection) new URL(url).openConnection(proxy);
107+
}
108+
109+
return httpConn;
110+
}
111+
}

0 commit comments

Comments
 (0)