Skip to content
Draft
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 @@ -45,11 +45,13 @@ public static class SdkConfigParams {
public static class SdkConfigStreamParams {
String baseUri;
Long initialRetryDelayMs;
String filter;
}

public static class SdkConfigPollParams {
String baseUri;
Long pollIntervalMs;
String filter;
}

public static class SdkConfigEventParams {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,12 @@ private LDConfig buildSdkConfig(SdkConfigParams params, LDLogAdapter logAdapter,
builder.events(eventsBuilder);
}

if (params.streaming != null && params.streaming.filter != null) {
builder.payloadFilter(params.streaming.filter);
} else if (params.polling != null && params.polling.filter != null) {
builder.payloadFilter(params.polling.filter);
}

builder.evaluationReasons(params.clientSide.evaluationReasons);
builder.http(
Components.httpConfiguration()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ public class TestService extends NanoHTTPD {
"client-event-source-http-errors",
"retry-conformance-fdv1-streaming",
"retry-conformance-fdv1-polling",
"client-use-post"
"client-use-post",
"filtering",
"filtering-strict"
};
private static final String MIME_JSON = "application/json";
static final Gson gson = new GsonBuilder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ final class DataSourceBuildInputsInternal extends DataSourceBuildInputs {
@Nullable
private final PersistentDataStoreWrapper.ReadOnlyPerEnvironmentData perEnvironmentData;

@Nullable
private final String payloadFilter;

DataSourceBuildInputsInternal(
LDContext evaluationContext,
ServiceEndpoints serviceEndpoints,
Expand All @@ -39,11 +42,13 @@ final class DataSourceBuildInputsInternal extends DataSourceBuildInputs {
ScheduledExecutorService sharedExecutor,
@NonNull File cacheDir,
LDLogger baseLogger,
@Nullable PersistentDataStoreWrapper.ReadOnlyPerEnvironmentData perEnvironmentData
@Nullable PersistentDataStoreWrapper.ReadOnlyPerEnvironmentData perEnvironmentData,
@Nullable String payloadFilter
) {
super(evaluationContext, serviceEndpoints, http, evaluationReasons,
selectorSource, sharedExecutor, cacheDir, baseLogger);
this.perEnvironmentData = perEnvironmentData;
this.payloadFilter = payloadFilter;
}

/**
Expand All @@ -64,6 +69,7 @@ static DataSourceBuildInputsInternal get(DataSourceBuildInputs inputs) {
inputs.getSharedExecutor(),
inputs.getCacheDir(),
inputs.getBaseLogger(),
null,
null
);
}
Expand All @@ -72,4 +78,9 @@ static DataSourceBuildInputsInternal get(DataSourceBuildInputs inputs) {
PersistentDataStoreWrapper.ReadOnlyPerEnvironmentData getPerEnvironmentDataIfAvailable() {
return perEnvironmentData;
}

@Nullable
String getPayloadFilter() {
return payloadFilter;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,9 @@ public Synchronizer build(DataSourceBuildInputs inputs) {
StandardEndpoints.FDV2_STREAMING_REQUEST_BASE_PATH,
requestor,
initialReconnectDelayMillis,
inputs.isEvaluationReasons(), inputs.getHttp().shouldUsePost(),
inputs.isEvaluationReasons(),
DataSourceBuildInputsInternal.get(inputs).getPayloadFilter(),
inputs.getHttp().shouldUsePost(),
httpProps, inputs.getSharedExecutor(),
inputs.getBaseLogger(), null);
}
Expand All @@ -165,6 +167,7 @@ public Synchronizer build(DataSourceBuildInputs inputs) {
FeatureFetcher fetcher = new HttpFeatureFlagFetcher(
inputs.getServiceEndpoints().getPollingBaseUri(),
inputs.isEvaluationReasons(),
DataSourceBuildInputsInternal.get(inputs).getPayloadFilter(),
inputs.getHttp().shouldUsePost(),
LDUtil.makeHttpProperties(inputs.getHttp()),
inputs.getCacheDir(),
Expand Down Expand Up @@ -366,6 +369,8 @@ private static FDv2Requestor makePollingRequestor(DataSourceBuildInputs inputs,
StandardEndpoints.FDV2_POLLING_REQUEST_GET_BASE_PATH,
StandardEndpoints.FDV2_POLLING_REQUEST_POST_BASE_PATH,
httpProps, inputs.getHttp().shouldUsePost(),
inputs.isEvaluationReasons(), null, inputs.getBaseLogger());
inputs.isEvaluationReasons(),
DataSourceBuildInputsInternal.get(inputs).getPayloadFilter(),
inputs.getBaseLogger());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,8 @@ private DataSourceBuildInputsInternal makeInputs(ClientContext clientContext) {
sharedExecutor,
impl.getPlatformState().getCacheDir(),
clientContext.getBaseLogger(),
impl.getPerEnvironmentDataIfAvailable()
impl.getPerEnvironmentDataIfAvailable(),
LDUtil.payloadFilter(clientContext)
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ final class FDv2StreamingSynchronizer implements Synchronizer {
@Nullable
private final FDv2Requestor requestor;
private final boolean evaluationReasons;
private final String payloadFilter;
private final int initialReconnectDelayMillis;
@Nullable
private final DiagnosticStore diagnosticStore;
Expand All @@ -92,6 +93,7 @@ final class FDv2StreamingSynchronizer implements Synchronizer {
* @param requestor optional requestor for handling ping events via poll; may be null
* @param initialReconnectDelayMillis delay before reconnecting after an error, in milliseconds
* @param evaluationReasons true to request evaluation reasons in the stream
* @param payloadFilter optional payload filter key; sent as {@code filter} query param
* @param usePost true to send the context in the request body via HTTP POST
* @param httpProperties HTTP configuration for the stream request
* @param executor executor used to run the streaming loop on a background
Expand All @@ -107,6 +109,7 @@ final class FDv2StreamingSynchronizer implements Synchronizer {
@Nullable FDv2Requestor requestor,
int initialReconnectDelayMillis,
boolean evaluationReasons,
@Nullable String payloadFilter,
boolean usePost,
@NonNull HttpProperties httpProperties,
@NonNull Executor executor,
Expand All @@ -120,6 +123,7 @@ final class FDv2StreamingSynchronizer implements Synchronizer {
this.requestor = requestor;
this.initialReconnectDelayMillis = initialReconnectDelayMillis;
this.evaluationReasons = evaluationReasons;
this.payloadFilter = payloadFilter;
this.usePost = usePost;
this.httpProperties = httpProperties;
this.executor = executor;
Expand Down Expand Up @@ -250,7 +254,7 @@ private URI getStreamUri() {
if (evaluationReasons) {
uri = HttpHelpers.addQueryParam(uri, "withReasons", "true");
}
return uri;
return LDUtil.withPayloadFilter(uri, payloadFilter);
}

private void recordStreamInit(boolean failed) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.launchdarkly.sdk.android;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.launchdarkly.logging.LDLogger;
import com.launchdarkly.sdk.LDContext;
Expand Down Expand Up @@ -32,6 +33,7 @@ class HttpFeatureFlagFetcher implements FeatureFetcher {

private final URI pollUri;
private final boolean evaluationReasons;
private final String payloadFilter;
private final boolean useReport;
private final HttpProperties httpProperties;
private final OkHttpClient client;
Expand All @@ -43,6 +45,7 @@ class HttpFeatureFlagFetcher implements FeatureFetcher {
this(
clientContext.getServiceEndpoints().getPollingBaseUri(),
clientContext.isEvaluationReasons(),
LDUtil.payloadFilter(clientContext),
clientContext.getHttp().isUseReport(),
LDUtil.makeHttpProperties(clientContext),
ClientContextImpl.get(clientContext).getPlatformState().getCacheDir(),
Expand All @@ -53,13 +56,15 @@ class HttpFeatureFlagFetcher implements FeatureFetcher {
HttpFeatureFlagFetcher(
@NonNull URI pollUri,
boolean evaluationReasons,
@Nullable String payloadFilter,
boolean useReport,
@NonNull HttpProperties httpProperties,
@NonNull File platformCacheDir,
@NonNull LDLogger logger
) {
this.pollUri = pollUri;
this.evaluationReasons = evaluationReasons;
this.payloadFilter = payloadFilter;
this.useReport = useReport;
this.httpProperties = httpProperties;
this.logger = logger;
Expand Down Expand Up @@ -159,6 +164,7 @@ private Request getDefaultRequest(LDContext ldContext) throws IOException {
if (evaluationReasons) {
uri = URI.create(uri.toString() + "?withReasons=true");
}
uri = LDUtil.withPayloadFilter(uri, payloadFilter);
logger.debug("Attempting to fetch Feature flags using uri: {}", uri);
return new Request.Builder().url(uri.toURL())
.headers(httpProperties.toHeadersBuilder().build())
Expand All @@ -170,6 +176,7 @@ private Request getReportRequest(LDContext ldContext) throws IOException {
if (evaluationReasons) {
uri = URI.create(uri.toString() + "?withReasons=true");
}
uri = LDUtil.withPayloadFilter(uri, payloadFilter);
logger.debug("Attempting to report user using uri: {}", uri);
String contextJson = JsonSerialization.serialize(ldContext);
RequestBody reportBody = RequestBody.create(contextJson, JSON);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import com.launchdarkly.logging.LDLogAdapter;
import com.launchdarkly.logging.LDLogLevel;
import com.launchdarkly.logging.LDLogger;
import com.launchdarkly.logging.Logs;
import com.launchdarkly.sdk.ContextKind;
import com.launchdarkly.sdk.LDContext;
Expand All @@ -28,6 +29,7 @@
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

import okhttp3.MediaType;

Expand Down Expand Up @@ -64,6 +66,7 @@ public class LDConfig {

static final int DEFAULT_MAX_CACHED_CONTEXTS = 5;
static final int DEFAULT_CONNECTION_TIMEOUT_MILLIS = 10_000; // 10 seconds
private static final Pattern PAYLOAD_FILTER_PATTERN = Pattern.compile("^[a-zA-Z0-9][._\\-a-zA-Z0-9]*$");

private final Map<String, String> mobileKeys;

Expand All @@ -88,6 +91,7 @@ public class LDConfig {
private final int maxCachedContexts;
private final boolean offline;
private final long connectionModeStateDebounceMs;
private final String payloadFilter;
private final PersistentDataStore persistentDataStore; // configurable for testing only

LDConfig(Map<String, String> mobileKeys,
Expand All @@ -107,6 +111,7 @@ public class LDConfig {
boolean generateAnonymousKeys,
boolean autoEnvAttributes,
long connectionModeStateDebounceMs,
String payloadFilter,
PersistentDataStore persistentDataStore,
LDLogAdapter logAdapter,
String loggerName) {
Expand All @@ -127,6 +132,7 @@ public class LDConfig {
this.generateAnonymousKeys = generateAnonymousKeys;
this.autoEnvAttributes = autoEnvAttributes;
this.connectionModeStateDebounceMs = connectionModeStateDebounceMs;
this.payloadFilter = payloadFilter;
this.persistentDataStore = persistentDataStore;
this.logAdapter = logAdapter;
this.loggerName = loggerName;
Expand Down Expand Up @@ -181,6 +187,14 @@ public boolean isEvaluationReasons() {
return evaluationReasons;
}

/**
* @return the payload filter key, or null if no valid filter was configured
* @see Builder#payloadFilter(String)
*/
public String getPayloadFilter() {
return payloadFilter;
}

/**
* @return true if opting out of diagnostics, false otherwise
*/
Expand Down Expand Up @@ -278,6 +292,8 @@ public enum AutoEnvAttributes {

private long connectionModeStateDebounceMs = StateDebounceManager.DEFAULT_DEBOUNCE_MS;

private String payloadFilter = null;

private LDLogAdapter logAdapter = defaultLogAdapter();
private String loggerName = LDPackageConsts.DEFAULT_LOGGER_NAME;
private LDLogLevel logLevel = null;
Expand Down Expand Up @@ -594,6 +610,25 @@ public Builder evaluationReasons(boolean evaluationReasons) {
return this;
}

/**
* Sets the key of a payload filter to apply to flag data requests.
* <p>
* Payload filters are defined per project in LaunchDarkly. When a filter key is set, the SDK
* adds it as the {@code filter} query parameter to its streaming and polling requests, and
* LaunchDarkly limits the flags it sends to the ones selected by that filter.
* <p>
* The key must contain only alphanumeric characters, periods, underscores, and hyphens, and
* must start with an alphanumeric character. If the key is invalid, the SDK logs an error and
* requests the full environment instead.
*
* @param payloadFilter the payload filter key, or null for no filter
* @return the builder
*/
public Builder payloadFilter(String payloadFilter) {
this.payloadFilter = payloadFilter;
return this;
}

/**
* Set to true to opt out of sending diagnostics data.
*
Expand Down Expand Up @@ -774,6 +809,19 @@ private static LDLogAdapter defaultLogAdapter() {
return LDTimberLogging.adapter();
}

private String validPayloadFilterOrNull(LDLogAdapter actualLogAdapter) {
if (payloadFilter == null || payloadFilter.isEmpty()) {
return null;
}
if (!PAYLOAD_FILTER_PATTERN.matcher(payloadFilter).matches()) {
LDLogger.withAdapter(actualLogAdapter, loggerName).error(
"Payload filter \"{}\" is invalid; the full environment will be requested instead",
payloadFilter);
return null;
}
return payloadFilter;
}

/**
* Returns the configured {@link LDConfig} object.
* @return the configuration
Expand Down Expand Up @@ -840,6 +888,7 @@ public LDConfig build() {
generateAnonymousKeys,
autoEnvAttributes,
connectionModeStateDebounceMs,
validPayloadFilterOrNull(actualLogAdapter),
persistentDataStore,
actualLogAdapter,
loggerName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@
import com.launchdarkly.sdk.android.subsystems.ClientContext;
import com.launchdarkly.sdk.android.subsystems.HttpConfiguration;
import com.launchdarkly.sdk.internal.http.HeadersTransformer;
import com.launchdarkly.sdk.internal.http.HttpHelpers;
import com.launchdarkly.sdk.internal.http.HttpProperties;
import com.launchdarkly.sdk.json.JsonSerialization;

import java.net.URI;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
Expand Down Expand Up @@ -179,6 +181,16 @@ static String urlSafeBase64(LDContext context) {
Base64.URL_SAFE + Base64.NO_WRAP);
}

@Nullable
static String payloadFilter(ClientContext clientContext) {
LDConfig config = clientContext.getConfig();
return config == null ? null : config.getPayloadFilter();
}

static URI withPayloadFilter(URI uri, @Nullable String payloadFilter) {
return payloadFilter == null ? uri : HttpHelpers.addQueryParam(uri, "filter", payloadFilter);
}

static HttpProperties makeHttpProperties(ClientContext clientContext) {
return makeHttpProperties(clientContext.getHttp());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ final class StreamingDataSource implements DataSource {
private final LDContext context;
private final HttpProperties httpProperties;
private final boolean evaluationReasons;
private final String payloadFilter;
final int initialReconnectDelayMillis; // visible for testing
private final RetryDelayStrategy normalRetryDelay;
private final RetryDelayStrategy extendedRetryDelay;
Expand Down Expand Up @@ -106,6 +107,7 @@ final class StreamingDataSource implements DataSource {
this.streamUri = clientContext.getServiceEndpoints().getStreamingBaseUri();
this.httpProperties = LDUtil.makeHttpProperties(clientContext);
this.evaluationReasons = clientContext.isEvaluationReasons();
this.payloadFilter = LDUtil.payloadFilter(clientContext);
this.useReport = clientContext.getHttp().isUseReport();
this.initialReconnectDelayMillis = initialReconnectDelayMillis;
this.normalRetryDelay = RetryDelayStrategy.defaultStrategy()
Expand Down Expand Up @@ -230,7 +232,7 @@ private URI getUri(@Nullable LDContext context) {
uri = URI.create(uri.toString() + "?withReasons=true");
}

return uri;
return LDUtil.withPayloadFilter(uri, payloadFilter);
}

@VisibleForTesting
Expand Down
Loading
Loading