Skip to content
Open
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
46 changes: 33 additions & 13 deletions example/src/main/java/com/launchdarkly/example/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@

import java.util.Date;
import java.util.Locale;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;

import timber.log.Timber;
Expand All @@ -45,6 +41,9 @@ public class MainActivity extends AppCompatActivity {

private static final String DEFAULT_USER_KEY = "user key";

/** How long startup blocks waiting for the first flags to arrive. */
private static final int INIT_WAIT_SECONDS = 10;

private LDClient ldClient;
private LDStatusListener ldStatusListener;
private LDAllFlagsListener allFlagsListener;
Expand Down Expand Up @@ -102,6 +101,7 @@ public void onCreate(Bundle savedInstanceState) {
setupFlushButton();
setupTrackButton();
setupIdentifyButton();
setupKillUnsentButton();
setupOfflineSwitch();
setupListeners();
updateDedupeStatus();
Expand Down Expand Up @@ -142,15 +142,12 @@ public void onCreate(Bundle savedInstanceState) {
.set("email", "fake@example.com")
.build();

Future<LDClient> initFuture = LDClient.init(this.getApplication(), ldConfig, context);
try {
ldClient = initFuture.get(10, TimeUnit.SECONDS);
updateStatusString(ldClient.getConnectionInformation());
ldClient.registerStatusListener(ldStatusListener);
ldClient.registerAllFlagsListener(allFlagsListener);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
Timber.e(e, "Exception when awaiting LaunchDarkly Client initialization");
}
// Returns the client either way: if the flags have not arrived within the wait, it is usable
// with whatever it has cached.
ldClient = LDClient.init(this.getApplication(), ldConfig, context, INIT_WAIT_SECONDS);
updateStatusString(ldClient.getConnectionInformation());
ldClient.registerStatusListener(ldStatusListener);
ldClient.registerAllFlagsListener(allFlagsListener);
}

private void setupListeners() {
Expand Down Expand Up @@ -215,6 +212,29 @@ private void setupTrackButton() {
});
}

/**
* Reproduces in-memory event loss: evaluate (exposure) and track (stand-in for an error),
* wait 5s so both calls are queued, then kill the process before the 30s flush.
* {@code finish()} or backgrounding would run the SDK's background flush, so this uses
* {@link android.os.Process#killProcess}.
*/
private void setupKillUnsentButton() {
Button killUnsentButton = findViewById(R.id.kill_unsent_button);
killUnsentButton.setOnClickListener(v -> {
final String typedKey = ((EditText) findViewById(R.id.feature_flag_key)).getText().toString().trim();
final String flagKey = typedKey.isEmpty() ? "kill-flag" : typedKey;
Timber.w("eval+track+kill flag=%s", flagKey);
doSafeClientAction(() -> {
ldClient.boolVariation(flagKey, false);
ldClient.track("$ld:telemetry:error");
ldClient.flush();
new Handler(Looper.getMainLooper()).postDelayed(
() -> android.os.Process.killProcess(android.os.Process.myPid()),
5_000);
});
});
}

private void setupIdentifyButton() {
Button identify = findViewById(R.id.identify_button);
identify.setOnClickListener(v -> {
Expand Down
13 changes: 12 additions & 1 deletion example/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,17 @@
android:layout_alignParentRight="true"
android:minLines="4" />

<Button
android:id="@+id/kill_unsent_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@id/identify_button"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:text="Eval+track+kill" />

<Space
android:id="@+id/space"
android:layout_width="0dp"
Expand All @@ -128,7 +139,7 @@
android:id="@+id/userKey_editText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@+id/identify_button"
android:layout_above="@+id/kill_unsent_button"
android:layout_alignEnd="@+id/result_textView"
android:layout_alignRight="@+id/result_textView"
android:layout_toEndOf="@+id/textView2"
Expand Down
2 changes: 1 addition & 1 deletion launchdarkly-android-client-sdk/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ ext.versions = [
"jacksonDatabind": "2.10.5.1",
"junit": "4.13",
"launchdarklyJavaSdkCommon": "2.4.0",
"launchdarklyJavaSdkInternal": "1.9.0",
"launchdarklyJavaSdkInternal": "1.12.0",
"launchdarklyLogging": "1.1.1",
"okhttp": "4.12.0",
"testHelpers": "2.1.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,9 @@
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;
import com.launchdarkly.sdk.internal.events.EventsConfiguration;
import com.launchdarkly.sdk.internal.events.EventSender;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

Expand Down Expand Up @@ -101,33 +98,30 @@ static final class EventProcessorBuilderImpl extends EventProcessorBuilder
@Override
public EventProcessor build(ClientContext clientContext) {
ClientContextImpl clientContextImpl = ClientContextImpl.get(clientContext);
EventsConfiguration eventsConfig = new EventsConfiguration(
allAttributesPrivate,
capacity,
null, // contextDeduplicator - not needed for client-side use
diagnosticRecordingIntervalMillis,
clientContextImpl.getDiagnosticStore(),
new DefaultEventSender(
LDUtil.makeHttpProperties(clientContext),
StandardEndpoints.ANALYTICS_EVENTS_REQUEST_PATH,
StandardEndpoints.DIAGNOSTIC_EVENTS_REQUEST_PATH,
0L, // use default retry delay
false, // disable gzip compression for Android
EventSender eventSender = new DefaultEventSender(
LDUtil.makeHttpProperties(clientContext),
StandardEndpoints.ANALYTICS_EVENTS_REQUEST_PATH,
StandardEndpoints.DIAGNOSTIC_EVENTS_REQUEST_PATH,
0L, // use default retry delay
false, // disable gzip compression for Android
clientContext.getBaseLogger());
return new DirectEventProcessor(
new OutboundEventBuffer(
capacity,
allAttributesPrivate,
privateAttributes,
true, // perContextSummarization - enable for client SDK
clientContext.getBaseLogger()),
1, // eventSendingThreadPoolSize
eventSender,
clientContext.getServiceEndpoints().getEventsBaseUri(),
clientContextImpl.getDiagnosticStore(),
flushIntervalMillis,
diagnosticRecordingIntervalMillis,
clientContext.isInBackground(),
true, // initiallyOffline
privateAttributes,
true // perContextSummarization - enable for client SDK
);
return new DefaultEventProcessorWrapper(new DefaultEventProcessor(
eventsConfig,
EventUtil.makeEventsTaskExecutor(),
Thread.NORM_PRIORITY, // note, we may want to make this configurable as it is in java-server-sdk
clientContext.getBaseLogger()
));
);
}

@Override
Expand All @@ -140,72 +134,6 @@ public LDValue describeConfiguration(ClientContext clientContext) {
.put("eventsFlushIntervalMillis", flushIntervalMillis)
.build();
}

/**
* Adapter from the public component interface of EventProcessor to the internal
* implementation class from java-sdk-internal.
*/
private final class DefaultEventProcessorWrapper implements EventProcessor {
private final DefaultEventProcessor eventProcessor;

DefaultEventProcessorWrapper(DefaultEventProcessor eventProcessor) {
this.eventProcessor = eventProcessor;
}

@Override
public void recordEvaluationEvent(
LDContext context,
String flagKey,
int flagVersion,
int variation,
LDValue value,
EvaluationReason reason,
LDValue defaultValue,
boolean requireFullEvent,
Long debugEventsUntilDate
) {
eventProcessor.sendEvent(new Event.FeatureRequest(
System.currentTimeMillis(), flagKey, context, flagVersion, variation,
value, defaultValue, reason, null, requireFullEvent,
debugEventsUntilDate, false));
}

@Override
public void recordIdentifyEvent(LDContext context) {
eventProcessor.sendEvent(new Event.Identify(System.currentTimeMillis(), context));
}

@Override
public void recordCustomEvent(LDContext context, String eventKey, LDValue data, Double metricValue) {
eventProcessor.sendEvent(new Event.Custom(System.currentTimeMillis(), eventKey,
context, data, metricValue));
}

@Override
public void setInBackground(boolean inBackground) {
eventProcessor.setInBackground(inBackground);
}

@Override
public void setOffline(boolean offline) {
eventProcessor.setOffline(offline);
}

@Override
public void flush() {
eventProcessor.flushAsync();
}

@Override
public void blockingFlush() {
eventProcessor.flushBlocking();
}

@Override
public void close() throws IOException {
eventProcessor.close();
}
}
}

static final class HttpConfigurationBuilderImpl extends HttpConfigurationBuilder
Expand Down
Loading
Loading