Skip to content

chore: adds experimental plugin functionality #303

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
@@ -0,0 +1,169 @@
package com.launchdarkly.sdk.android;

import static org.junit.Assert.assertEquals;

import android.app.Application;

import androidx.annotation.NonNull;
import androidx.test.core.app.ApplicationProvider;

import com.launchdarkly.sdk.EvaluationDetail;
import com.launchdarkly.sdk.EvaluationReason;
import com.launchdarkly.sdk.LDContext;
import com.launchdarkly.sdk.LDValue;
import com.launchdarkly.sdk.android.integrations.EnvironmentMetadata;
import com.launchdarkly.sdk.android.integrations.EvaluationSeriesContext;
import com.launchdarkly.sdk.android.integrations.Hook;
import com.launchdarkly.sdk.android.integrations.IdentifySeriesContext;
import com.launchdarkly.sdk.android.integrations.IdentifySeriesResult;
import com.launchdarkly.sdk.android.integrations.Plugin;
import com.launchdarkly.sdk.android.integrations.PluginMetadata;
import com.launchdarkly.sdk.android.integrations.TrackSeriesContext;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

public class LDClientPluginsTest {

private static final String mobileKey = "test-mobile-key";
private static final LDContext ldContext = LDContext.create("userKey");
private Application application;

@Rule
public LogCaptureRule logging = new LogCaptureRule();

@Before
public void setUp() {
application = ApplicationProvider.getApplicationContext();
}

@Test
public void registerIsCalledForPlugins() throws Exception {

MockHook testHook = new MockHook();
MockPlugin testPlugin = new MockPlugin(Collections.singletonList(testHook));

try (LDClient ldClient = LDClient.init(application, makeOfflineConfig(List.of(testPlugin)), ldContext, 1)) {
ldClient.boolVariation("test-flag", false);
assertEquals(1, testPlugin.registerCalls.size());
assertEquals(1, testHook.beforeEvaluationCalls.size());
assertEquals(1, testHook.afterEvaluationCalls.size());

assertEquals(ldClient, testPlugin.registerCalls.get(0).get("client"));
EnvironmentMetadata environmentMetadata = (EnvironmentMetadata) testPlugin.registerCalls.get(0).get("environmentMetadata");
assertEquals(mobileKey, environmentMetadata.getCredential());
assertEquals("android-client-sdk", environmentMetadata.getSdkMetadata().getName());

logging.assertNoWarningsLogged();
logging.assertNoErrorsLogged();
}
}

private LDConfig makeOfflineConfig(List<Plugin> plugins) {
LDConfig.Builder builder = new LDConfig.Builder(LDConfig.Builder.AutoEnvAttributes.Disabled)
.mobileKey(mobileKey)
.offline(true)
.events(Components.noEvents())
.logAdapter(logging.logAdapter);

if (plugins != null) {
builder.plugins(Components.plugins().setPlugins(plugins));
}

return builder.build();
}

private static class MockPlugin extends Plugin {


private final List<Hook> hooks;
public final List<Map<String, Object>> registerCalls = new ArrayList<>();

public MockPlugin(List<Hook> hooks) {
this.hooks = hooks;
}

@NonNull
@Override
public PluginMetadata getMetadata() {
return new PluginMetadata("mock-plugin");
}

@Override
public void register(LDClient client, EnvironmentMetadata metadata) {
registerCalls.add(Map.of(
"client", client,
"environmentMetadata", metadata
));
}

@NonNull
@Override
public List<Hook> getHooks() {
return this.hooks;
}
}

private static class MockHook extends Hook {
public final List<Map<String, Object>> beforeEvaluationCalls = new ArrayList<>();
public final List<Map<String, Object>> afterEvaluationCalls = new ArrayList<>();
public final List<Map<String, Object>> beforeIdentifyCalls = new ArrayList<>();
public final List<Map<String, Object>> afterIdentifyCalls = new ArrayList<>();
public final List<Map<String, Object>> afterTrackCalls = new ArrayList<>();

public MockHook() {
super("MockHook");
}

@Override
public Map<String, Object> beforeEvaluation(EvaluationSeriesContext seriesContext, Map<String, Object> seriesData) {
beforeEvaluationCalls.add(Map.of(
"seriesContext", seriesContext,
"seriesData", seriesData
));
return Collections.unmodifiableMap(Collections.emptyMap());
}

@Override
public Map<String, Object> afterEvaluation(EvaluationSeriesContext seriesContext, Map<String, Object> seriesData, EvaluationDetail<LDValue> evaluationDetail) {
afterEvaluationCalls.add(Map.of(
"seriesContext", seriesContext,
"seriesData", seriesData,
"evaluationDetail", evaluationDetail
));
return Collections.unmodifiableMap(Collections.emptyMap());
}

@Override
public Map<String, Object> beforeIdentify(IdentifySeriesContext seriesContext, Map<String, Object> seriesData) {
beforeIdentifyCalls.add(Map.of(
"seriesContext", seriesContext,
"seriesData", seriesData
));
return Collections.unmodifiableMap(Collections.emptyMap());
}

@Override
public Map<String, Object> afterIdentify(IdentifySeriesContext seriesContext, Map<String, Object> seriesData, IdentifySeriesResult result) {
afterIdentifyCalls.add(Map.of(
"seriesContext", seriesContext,
"seriesData", seriesData,
"result", result
));
return Collections.unmodifiableMap(Collections.emptyMap());
}

@Override
public void afterTrack(TrackSeriesContext seriesContext) {
afterTrackCalls.add(Map.of(
"seriesContext", seriesContext
));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.launchdarkly.sdk.android.integrations.EventProcessorBuilder;
import com.launchdarkly.sdk.android.integrations.HooksConfigurationBuilder;
import com.launchdarkly.sdk.android.integrations.HttpConfigurationBuilder;
import com.launchdarkly.sdk.android.integrations.PluginsConfigurationBuilder;
import com.launchdarkly.sdk.android.integrations.PollingDataSourceBuilder;
import com.launchdarkly.sdk.android.integrations.ServiceEndpointsBuilder;
import com.launchdarkly.sdk.android.integrations.StreamingDataSourceBuilder;
Expand Down Expand Up @@ -200,4 +201,11 @@ public static StreamingDataSourceBuilder streamingDataSource() {
public static HooksConfigurationBuilder hooks() {
return new ComponentsImpl.HooksConfigurationBuilderImpl();
}

// TODO: add experimental comment from other SDKs // A list of plugins to be used with the SDK.
//
// Plugin support is currently experimental and subject to change.
public static PluginsConfigurationBuilder plugins() {
return new ComponentsImpl.PluginsConfigurationBuilderImpl();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.launchdarkly.sdk.android.integrations.EventProcessorBuilder;
import com.launchdarkly.sdk.android.integrations.HooksConfigurationBuilder;
import com.launchdarkly.sdk.android.integrations.HttpConfigurationBuilder;
import com.launchdarkly.sdk.android.integrations.PluginsConfigurationBuilder;
import com.launchdarkly.sdk.android.integrations.PollingDataSourceBuilder;
import com.launchdarkly.sdk.android.integrations.ServiceEndpointsBuilder;
import com.launchdarkly.sdk.android.integrations.StreamingDataSourceBuilder;
Expand All @@ -18,6 +19,7 @@
import com.launchdarkly.sdk.android.subsystems.EventProcessor;
import com.launchdarkly.sdk.android.subsystems.HookConfiguration;
import com.launchdarkly.sdk.android.subsystems.HttpConfiguration;
import com.launchdarkly.sdk.android.subsystems.PluginsConfiguration;
import com.launchdarkly.sdk.internal.events.DefaultEventProcessor;
import com.launchdarkly.sdk.internal.events.DefaultEventSender;
import com.launchdarkly.sdk.internal.events.Event;
Expand Down Expand Up @@ -365,6 +367,14 @@ public HookConfiguration build() {
}
}

static final class PluginsConfigurationBuilderImpl extends PluginsConfigurationBuilder {

@Override
public PluginsConfiguration build() {
return new PluginsConfiguration(plugins);
}
}

// Marker interface for data source implementations that will require a FeatureFetcher
interface DataSourceRequiresFeatureFetcher {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@
import com.launchdarkly.sdk.LDValue;
import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder;
import com.launchdarkly.sdk.android.env.IEnvironmentReporter;
import com.launchdarkly.sdk.android.integrations.EnvironmentMetadata;
import com.launchdarkly.sdk.android.integrations.Hook;
import com.launchdarkly.sdk.android.integrations.IdentifySeriesResult;
import com.launchdarkly.sdk.android.integrations.Plugin;
import com.launchdarkly.sdk.android.integrations.SdkMetadata;
import com.launchdarkly.sdk.android.subsystems.ApplicationInfo;
import com.launchdarkly.sdk.android.subsystems.Callback;
import com.launchdarkly.sdk.android.DataModel.Flag;
import com.launchdarkly.sdk.android.subsystems.EventProcessor;
Expand All @@ -24,7 +28,9 @@
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -64,6 +70,7 @@ public class LDClient implements LDClientInterface, Closeable {
private final ConnectivityManager connectivityManager;
private final LDLogger logger;
private final HookRunner hookRunner;
private final List<Plugin> plugins;
// If 15 seconds or more is passed as a timeout to init, we will log a warning.
private static final int EXCESSIVE_INIT_WAIT_SECONDS = 15;

Expand Down Expand Up @@ -116,6 +123,9 @@ public static Future<LDClient> init(@NonNull Application application,
LDClient primaryClient;
LDContext modifiedContext;

// used for plugin registration after instances are created
final Map<LDClient, EnvironmentMetadata> instanceMetadatas = new HashMap<>();

// Acquire the `initLock` to ensure that if `init()` is called multiple times, we will only
// initialize the client(s) once.
synchronized (initLock) {
Expand Down Expand Up @@ -143,6 +153,8 @@ public static Future<LDClient> init(@NonNull Application application,
reporterBuilder.enableCollectionFromPlatform(application);
}
IEnvironmentReporter environmentReporter = reporterBuilder.build();
ApplicationInfo applicationInfo = environmentReporter.getApplicationInfo();
SdkMetadata sdkMetadata = new SdkMetadata(LDPackageConsts.SDK_NAME, BuildConfig.VERSION_NAME);

if (config.isAutoEnvAttributes()) {
autoEnvContextModifier = new AutoEnvContextModifier(persistentData, environmentReporter, logger);
Expand All @@ -154,12 +166,13 @@ public static Future<LDClient> init(@NonNull Application application,
modifiedContext = autoEnvContextModifier.modifyContext(context);
modifiedContext = anonymousKeyContextModifier.modifyContext(modifiedContext);

Set<Map.Entry<String, String>> envAndMobileKeys = config.getMobileKeys().entrySet();
// Create, but don't start, every LDClient instance
final Map<String, LDClient> newInstances = new HashMap<>();
final Map<String, LDClient> newInstances = new HashMap<>(envAndMobileKeys.size());

LDClient createdPrimaryClient = null;
for (Map.Entry<String, String> mobileKeys : config.getMobileKeys().entrySet()) {
String envName = mobileKeys.getKey(), mobileKey = mobileKeys.getValue();
for (Map.Entry<String, String> entry : envAndMobileKeys) {
String envName = entry.getKey(), mobileKey = entry.getValue();
try {
final LDClient instance = new LDClient(
sharedPlatformState,
Expand All @@ -175,6 +188,10 @@ public static Future<LDClient> init(@NonNull Application application,
if (mobileKey.equals(config.getMobileKey())) {
createdPrimaryClient = instance;
}

// metadata created per environment since mobile key varies
instanceMetadatas.put(instance, new EnvironmentMetadata(applicationInfo, sdkMetadata, mobileKey));

} catch (LaunchDarklyException e) {
resultFuture.setException(e);
return resultFuture;
Expand All @@ -187,6 +204,27 @@ public static Future<LDClient> init(@NonNull Application application,
instances = newInstances;
}

// after instances have been created, set up hooks for each plugin of the instance and call register
for (Map.Entry<LDClient, EnvironmentMetadata> entry : instanceMetadatas.entrySet()) {
LDClient instance = entry.getKey();
EnvironmentMetadata metadata = entry.getValue();

for (Plugin plugin : instance.plugins) {
// try is for each plugin so that if one plugin has an issue, the others will have an opportunity to be used
try {
List<Hook> pluginHooks = plugin.getHooks();
for (Hook hook : pluginHooks) {
instance.hookRunner.addHook(hook);
}

} catch (Exception e) {
logger.error("Exception thrown getting hooks for plugin " + plugin.getMetadata().getName() + ". Unable to get hooks, plugin may not operate correctly.");
}

plugin.register(instance, metadata);
}
}

final AtomicInteger initCounter = new AtomicInteger(config.getMobileKeys().size());
Callback<Void> completeWhenCounterZero = new Callback<Void>() {
@Override
Expand Down Expand Up @@ -368,6 +406,7 @@ protected LDClient(
);

hookRunner = new HookRunner(logger, config.hooks.getHooks());
plugins = config.pluginsConfig.getPlugins();
}

@Override
Expand Down
Loading