Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,8 @@ private <ValueType> void dispatchExperimentViewed(
}
PluginRegistry pluginRegistry = context.getPluginRegistry();
if (pluginRegistry != null) {
pluginRegistry.fireExperimentViewed(experiment, result);
pluginRegistry.fireExperimentViewed(experiment, result,
context.getUser() != null ? context.getUser().getAttributes() : null);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,8 @@ private <T> void dispatchFeatureUsage(EvaluationContext context, String key, Fea
}
PluginRegistry registry = context.getPluginRegistry();
if (registry != null) {
registry.fireFeatureEvaluated(key, result);
registry.fireFeatureEvaluated(key, result,
context.getUser() != null ? context.getUser().getAttributes() : null);
}
}

Expand Down
29 changes: 29 additions & 0 deletions lib/src/main/java/growthbook/sdk/java/plugin/GrowthBookPlugin.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package growthbook.sdk.java.plugin;

import com.google.gson.JsonObject;
import growthbook.sdk.java.model.Experiment;
import growthbook.sdk.java.model.ExperimentResult;
import growthbook.sdk.java.model.FeatureResult;

import javax.annotation.Nullable;

/**
* A plugin that can observe experiment and feature evaluations on a GrowthBook
* instance. Mirrors the plugin contracts in the GrowthBook Python and Go SDKs.
Expand Down Expand Up @@ -31,12 +34,38 @@ default void init() {
default <V> void onExperimentViewed(Experiment<V> experiment, ExperimentResult<V> result) {
}

/**
* Invoked after a user is bucketed into an experiment, with the user's attributes at evaluation
* time. The default delegates to {@link #onExperimentViewed(Experiment, ExperimentResult)} so
* existing plugins keep working; override this overload to receive the attributes.
*
* @param userAttributes the evaluated user's attributes, or {@code null} when unavailable
*/
default <V> void onExperimentViewed(Experiment<V> experiment,
ExperimentResult<V> result,
@Nullable JsonObject userAttributes) {
onExperimentViewed(experiment, result);
}

/**
* Invoked every time a feature is evaluated.
*/
default <V> void onFeatureEvaluated(String featureKey, FeatureResult<V> result) {
}

/**
* Invoked on every feature evaluation, with the user's attributes at evaluation time. The default
* delegates to {@link #onFeatureEvaluated(String, FeatureResult)} so existing plugins keep working;
* override this overload to receive the attributes.
*
* @param userAttributes the evaluated user's attributes, or {@code null} when unavailable
*/
default <V> void onFeatureEvaluated(String featureKey,
FeatureResult<V> result,
@Nullable JsonObject userAttributes) {
onFeatureEvaluated(featureKey, result);
}

/**
* Invoked when the owning GrowthBook instance is closed. Implementations
* should flush any buffered work synchronously. MUST be safe to call
Expand Down
17 changes: 15 additions & 2 deletions lib/src/main/java/growthbook/sdk/java/plugin/PluginRegistry.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package growthbook.sdk.java.plugin;

import com.google.gson.JsonObject;
import growthbook.sdk.java.model.Experiment;
import growthbook.sdk.java.model.ExperimentResult;
import growthbook.sdk.java.model.FeatureResult;
Expand Down Expand Up @@ -60,12 +61,18 @@ public void initAll() {
}

public <V> void fireExperimentViewed(Experiment<V> experiment, ExperimentResult<V> result) {
fireExperimentViewed(experiment, result, null);
}

public <V> void fireExperimentViewed(Experiment<V> experiment,
ExperimentResult<V> result,
@Nullable JsonObject userAttributes) {
if (plugins.isEmpty()) {
return;
}
for (GrowthBookPlugin plugin : plugins) {
try {
plugin.onExperimentViewed(experiment, result);
plugin.onExperimentViewed(experiment, result, userAttributes);
} catch (Exception e) {
log.warn("Plugin {} onExperimentViewed failed",
plugin.getClass().getName(), e);
Expand All @@ -74,12 +81,18 @@ public <V> void fireExperimentViewed(Experiment<V> experiment, ExperimentResult<
}

public <V> void fireFeatureEvaluated(String featureKey, FeatureResult<V> result) {
fireFeatureEvaluated(featureKey, result, null);
}

public <V> void fireFeatureEvaluated(String featureKey,
FeatureResult<V> result,
@Nullable JsonObject userAttributes) {
if (plugins.isEmpty()) {
return;
}
for (GrowthBookPlugin plugin : plugins) {
try {
plugin.onFeatureEvaluated(featureKey, result);
plugin.onFeatureEvaluated(featureKey, result, userAttributes);
} catch (Exception e) {
log.warn("Plugin {} onFeatureEvaluated failed",
plugin.getClass().getName(), e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import growthbook.sdk.java.util.GrowthBookJsonUtils;
import growthbook.sdk.java.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import okhttp3.HttpUrl;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
Expand Down Expand Up @@ -36,9 +37,9 @@

/**
* Batches experiment/feature evaluation events and POSTs them to the
* GrowthBook data-warehouse ingest endpoint. Wire contract mirrors the Go
* SDK: {@code POST {ingestorHost}/events} with JSON body
* {@code {"client_key": ..., "events": [...]}}.
* GrowthBook ingest endpoint. Wire contract mirrors the JS and Go SDKs:
* {@code POST {ingestorHost}/track?client_key={clientKey}} with a JSON array
* of events as the body.
*
* <p>Resources (HTTP client, scheduler, flush executor) are created in
* {@link #init()}, not the constructor, so an unregistered plugin never leaks
Expand Down Expand Up @@ -149,18 +150,32 @@ public void init() {

@Override
public <V> void onExperimentViewed(Experiment<V> experiment, ExperimentResult<V> result) {
onExperimentViewed(experiment, result, null);
}

@Override
public <V> void onExperimentViewed(Experiment<V> experiment,
ExperimentResult<V> result,
@Nullable JsonObject userAttributes) {
if (!isActive()) {
return;
}
enqueue(TrackingEvent.forExperiment(experiment, result));
enqueue(TrackingEvent.forExperiment(experiment, result, userAttributes));
}

@Override
public <V> void onFeatureEvaluated(String featureKey, FeatureResult<V> result) {
onFeatureEvaluated(featureKey, result, null);
}

@Override
public <V> void onFeatureEvaluated(String featureKey,
FeatureResult<V> result,
@Nullable JsonObject userAttributes) {
if (!isActive()) {
return;
}
enqueue(TrackingEvent.forFeature(featureKey, result));
enqueue(TrackingEvent.forFeature(featureKey, result, userAttributes));
}

@Override
Expand Down Expand Up @@ -320,20 +335,22 @@ private void flushBatch(List<TrackingEvent> batch, @Nullable OkHttpClient client
if (batch.isEmpty() || client == null) {
return;
}
String url = config.resolvedIngestorHost() + "/events";
HttpUrl base = HttpUrl.parse(config.resolvedIngestorHost() + "/track");
if (base == null) {
log.warn("Tracking ingest host is not a valid URL: {}", config.resolvedIngestorHost());
return;
}
HttpUrl url = base.newBuilder().addQueryParameter("client_key", config.getClientKey()).build();
try {
JsonObject body = new JsonObject();
body.addProperty("client_key", config.getClientKey());
JsonArray events = new JsonArray();
for (TrackingEvent event : batch) {
events.add(GrowthBookJsonUtils.getInstance().gson.toJsonTree(event));
}
body.add("events", events);

Request request = new Request.Builder()
.url(url)
.header("User-Agent", SdkMetadata.USER_AGENT)
.post(RequestBody.create(body.toString(), JSON))
.post(RequestBody.create(events.toString(), JSON))
.build();

try (Response response = client.newCall(request).execute()) {
Expand Down
Loading
Loading