Skip to content

Commit 882279d

Browse files
authored
fix(analytics): allow capability to offload reportExposure to async thread (#85)
1 parent 0fae74e commit 882279d

4 files changed

Lines changed: 79 additions & 12 deletions

File tree

src/main/java/com/mixpanel/mixpanelapi/MixpanelAPI.java

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import java.net.URLConnection;
1111
import java.net.URLEncoder;
1212
import java.util.List;
13+
import java.util.concurrent.Executor;
1314
import java.util.logging.Level;
1415
import java.util.logging.Logger;
1516
import java.util.zip.GZIPOutputStream;
@@ -663,16 +664,42 @@ public RemoteFlagsProvider getRemoteFlags() {
663664
/**
664665
* Creates an EventSender that uses the provided MixpanelAPI instance for sending events.
665666
* This is shared by both local and remote flag evaluation modes.
667+
* <p>
668+
* If the config provides a non-null exposure executor, the HTTP send is dispatched on
669+
* the executor so flag evaluation does not block on network I/O. JSON construction
670+
* always runs on the calling thread because {@link MessageBuilder} is not thread-safe.
671+
* </p>
666672
*/
667673
private static EventSender createEventSender(BaseFlagsConfig config, MixpanelAPI api) {
668674
final MessageBuilder builder = new MessageBuilder(config.getProjectToken());
675+
final Executor exposureExecutor = config.getExposureExecutor();
669676

670677
return (distinctId, eventName, properties) -> {
678+
final JSONObject event;
671679
try {
672-
JSONObject event = builder.event(distinctId, eventName, properties);
673-
api.sendMessage(event);
674-
} catch (IOException e) {
675-
// Silently fail - exposure tracking should not break flag evaluation
680+
event = builder.event(distinctId, eventName, properties);
681+
} catch (RuntimeException e) {
682+
logger.log(Level.WARNING, "Failed to build exposure event " + eventName, e);
683+
return;
684+
}
685+
686+
Runnable send = () -> {
687+
try {
688+
api.sendMessage(event);
689+
} catch (IOException e) {
690+
logger.log(Level.WARNING, "Failed to send exposure event " + eventName, e);
691+
}
692+
};
693+
694+
if (exposureExecutor == null) {
695+
send.run();
696+
return;
697+
}
698+
699+
try {
700+
exposureExecutor.execute(send);
701+
} catch (Exception e) {
702+
logger.log(Level.WARNING, "Exposure event dropped — executor failed to accept task for " + eventName, e);
676703
}
677704
};
678705
}

src/main/java/com/mixpanel/mixpanelapi/featureflags/config/BaseFlagsConfig.java

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.mixpanel.mixpanelapi.featureflags.config;
22

3+
import java.util.concurrent.Executor;
4+
35
/**
46
* Base configuration for feature flags providers.
57
* <p>
@@ -10,18 +12,21 @@ public class BaseFlagsConfig {
1012
private final String projectToken;
1113
private final String apiHost;
1214
private final int requestTimeoutSeconds;
15+
private final Executor exposureExecutor;
1316

1417
/**
1518
* Creates a new BaseFlagsConfig with specified settings.
1619
*
1720
* @param projectToken the Mixpanel project token
1821
* @param apiHost the API endpoint host
1922
* @param requestTimeoutSeconds HTTP request timeout in seconds
23+
* @param exposureExecutor executor used to dispatch exposure event HTTP sends; may be null
2024
*/
21-
protected BaseFlagsConfig(String projectToken, String apiHost, int requestTimeoutSeconds) {
25+
protected BaseFlagsConfig(String projectToken, String apiHost, int requestTimeoutSeconds, Executor exposureExecutor) {
2226
this.projectToken = projectToken;
2327
this.apiHost = apiHost;
2428
this.requestTimeoutSeconds = requestTimeoutSeconds;
29+
this.exposureExecutor = exposureExecutor;
2530
}
2631

2732
/**
@@ -45,6 +50,13 @@ public int getRequestTimeoutSeconds() {
4550
return requestTimeoutSeconds;
4651
}
4752

53+
/**
54+
* @return the Executor used to dispatch exposure event HTTP sends, or null for synchronous dispatch
55+
*/
56+
public Executor getExposureExecutor() {
57+
return exposureExecutor;
58+
}
59+
4860
/**
4961
* Builder for BaseFlagsConfig.
5062
*
@@ -55,6 +67,7 @@ public static class Builder<T extends Builder<T>> {
5567
protected String projectToken;
5668
protected String apiHost = "api.mixpanel.com";
5769
protected int requestTimeoutSeconds = 10;
70+
protected Executor exposureExecutor;
5871

5972
/**
6073
* Sets the project token.
@@ -89,13 +102,34 @@ public T requestTimeoutSeconds(int requestTimeoutSeconds) {
89102
return (T) this;
90103
}
91104

105+
/**
106+
* Sets the executor used to dispatch exposure event HTTP sends.
107+
* <p>
108+
* When null (the default), exposure events are sent synchronously on the
109+
* calling thread — this adds HTTP latency to every flag evaluation when {@code reportExposure} is
110+
* enabled.
111+
* </p>
112+
* <p>
113+
* When set, the executor receives one {@link Runnable} per exposure event;
114+
* each {@code Runnable} performs a single HTTP POST. If the
115+
* executor fails to accept the task, the exposure event is dropped and a warning is logged.
116+
* </p>
117+
*
118+
* @param exposureExecutor executor for exposure event dispatch, or null for synchronous
119+
* @return this builder
120+
*/
121+
public T exposureExecutor(Executor exposureExecutor) {
122+
this.exposureExecutor = exposureExecutor;
123+
return (T) this;
124+
}
125+
92126
/**
93127
* Builds the BaseFlagsConfig instance.
94128
*
95129
* @return a new BaseFlagsConfig
96130
*/
97131
public BaseFlagsConfig build() {
98-
return new BaseFlagsConfig(projectToken, apiHost, requestTimeoutSeconds);
132+
return new BaseFlagsConfig(projectToken, apiHost, requestTimeoutSeconds, exposureExecutor);
99133
}
100134
}
101135

src/main/java/com/mixpanel/mixpanelapi/featureflags/config/LocalFlagsConfig.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.mixpanel.mixpanelapi.featureflags.config;
22

3+
import java.util.concurrent.Executor;
4+
35
/**
46
* Configuration for local feature flags evaluation.
57
* <p>
@@ -17,11 +19,12 @@ public final class LocalFlagsConfig extends BaseFlagsConfig {
1719
* @param projectToken the Mixpanel project token
1820
* @param apiHost the API endpoint host
1921
* @param requestTimeoutSeconds HTTP request timeout in seconds
22+
* @param exposureExecutor executor used to dispatch exposure event HTTP sends; may be null
2023
* @param enablePolling whether to periodically refresh flag definitions
2124
* @param pollingIntervalSeconds time between refresh cycles in seconds
2225
*/
23-
private LocalFlagsConfig(String projectToken, String apiHost, int requestTimeoutSeconds, boolean enablePolling, int pollingIntervalSeconds) {
24-
super(projectToken, apiHost, requestTimeoutSeconds);
26+
private LocalFlagsConfig(String projectToken, String apiHost, int requestTimeoutSeconds, Executor exposureExecutor, boolean enablePolling, int pollingIntervalSeconds) {
27+
super(projectToken, apiHost, requestTimeoutSeconds, exposureExecutor);
2528
this.enablePolling = enablePolling;
2629
this.pollingIntervalSeconds = pollingIntervalSeconds;
2730
}
@@ -76,7 +79,7 @@ public Builder pollingIntervalSeconds(int pollingIntervalSeconds) {
7679
*/
7780
@Override
7881
public LocalFlagsConfig build() {
79-
return new LocalFlagsConfig(projectToken, apiHost, requestTimeoutSeconds, enablePolling, pollingIntervalSeconds);
82+
return new LocalFlagsConfig(projectToken, apiHost, requestTimeoutSeconds, exposureExecutor, enablePolling, pollingIntervalSeconds);
8083
}
8184
}
8285

src/main/java/com/mixpanel/mixpanelapi/featureflags/config/RemoteFlagsConfig.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.mixpanel.mixpanelapi.featureflags.config;
22

3+
import java.util.concurrent.Executor;
4+
35
/**
46
* Configuration for remote feature flags evaluation.
57
* <p>
@@ -15,9 +17,10 @@ public final class RemoteFlagsConfig extends BaseFlagsConfig {
1517
* @param projectToken the Mixpanel project token
1618
* @param apiHost the API endpoint host
1719
* @param requestTimeoutSeconds HTTP request timeout in seconds
20+
* @param exposureExecutor executor used to dispatch exposure event HTTP sends; may be null
1821
*/
19-
private RemoteFlagsConfig(String projectToken, String apiHost, int requestTimeoutSeconds) {
20-
super(projectToken, apiHost, requestTimeoutSeconds);
22+
private RemoteFlagsConfig(String projectToken, String apiHost, int requestTimeoutSeconds, Executor exposureExecutor) {
23+
super(projectToken, apiHost, requestTimeoutSeconds, exposureExecutor);
2124
}
2225

2326
/**
@@ -32,7 +35,7 @@ public static final class Builder extends BaseFlagsConfig.Builder<Builder> {
3235
*/
3336
@Override
3437
public RemoteFlagsConfig build() {
35-
return new RemoteFlagsConfig(projectToken, apiHost, requestTimeoutSeconds);
38+
return new RemoteFlagsConfig(projectToken, apiHost, requestTimeoutSeconds, exposureExecutor);
3639
}
3740
}
3841

0 commit comments

Comments
 (0)