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 @@ -3,8 +3,8 @@
import com.devcycle.sdk.server.cloud.model.DevCycleCloudOptions;
import com.devcycle.sdk.server.common.api.APIUtils;
import com.devcycle.sdk.server.common.api.IDevCycleApi;
import com.devcycle.sdk.server.common.api.ObjectMapperUtils;
import com.devcycle.sdk.server.common.interceptor.AuthorizationHeaderInterceptor;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
Expand All @@ -14,14 +14,13 @@

public final class DevCycleCloudApiClient {

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final ObjectMapper OBJECT_MAPPER = ObjectMapperUtils.createDefaultObjectMapper();
private static final String BUCKETING_URL = "https://bucketing-api.devcycle.com/";
private final OkHttpClient.Builder okBuilder;
private final Retrofit.Builder adapterBuilder;
private String bucketingUrl;

public DevCycleCloudApiClient(String apiKey, DevCycleCloudOptions options) {
OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
okBuilder = new OkHttpClient.Builder();

APIUtils.applyRestOptions(options.getRestOptions(), okBuilder);
Expand All @@ -38,7 +37,7 @@ public DevCycleCloudApiClient(String apiKey, DevCycleCloudOptions options) {

adapterBuilder = new Retrofit.Builder()
.baseUrl(bucketingUrl)
.addConverterFactory(JacksonConverterFactory.create());
.addConverterFactory(JacksonConverterFactory.create(OBJECT_MAPPER));
}

public IDevCycleApi initialize() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
import com.devcycle.sdk.server.cloud.model.DevCycleCloudOptions;
import com.devcycle.sdk.server.common.api.IDevCycleApi;
import com.devcycle.sdk.server.common.api.IDevCycleClient;
import com.devcycle.sdk.server.common.api.ObjectMapperUtils;
import com.devcycle.sdk.server.common.exception.AfterHookError;
import com.devcycle.sdk.server.common.exception.BeforeHookError;
import com.devcycle.sdk.server.common.exception.DevCycleException;
import com.devcycle.sdk.server.common.logging.DevCycleLogger;
import com.devcycle.sdk.server.common.model.*;
import com.devcycle.sdk.server.common.model.Variable.TypeEnum;
import com.devcycle.sdk.server.openfeature.DevCycleProvider;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
Expand All @@ -23,7 +23,7 @@

public final class DevCycleCloudClient implements IDevCycleClient {

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final ObjectMapper OBJECT_MAPPER = ObjectMapperUtils.createDefaultObjectMapper();
private final IDevCycleApi api;
private final DevCycleCloudOptions dvcOptions;
private final DevCycleProvider openFeatureProvider;
Expand All @@ -48,7 +48,6 @@ public DevCycleCloudClient(String sdkKey, DevCycleCloudOptions options) {

this.dvcOptions = options;
api = new DevCycleCloudApiClient(sdkKey, options).initialize();
OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);

this.openFeatureProvider = new DevCycleProvider(this);
this.evalHooksRunner = new EvalHooksRunner(dvcOptions.getHooks());
Expand Down Expand Up @@ -112,7 +111,7 @@ public <T> Variable<T> variable(DevCycleUser user, String key, T defaultValue) {

TypeEnum variableType = TypeEnum.fromClass(defaultValue.getClass());
Variable<T> variable = null;
HookContext<T> context = new HookContext<T>(user, key, defaultValue);
HookContext<T> context = new HookContext<T>(user, key, defaultValue, null);
ArrayList<EvalHook<T>> hooks = new ArrayList<EvalHook<T>>(evalHooksRunner.getHooks());
ArrayList<EvalHook<T>> reversedHooks = new ArrayList<>(hooks);
Collections.reverse(reversedHooks);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.devcycle.sdk.server.common.api;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

/**
* Utility class for providing pre-configured ObjectMapper instances
* with consistent settings across the DevCycle SDK.
*/
public class ObjectMapperUtils {

/**
* Creates a new ObjectMapper with DevCycle SDK default configuration:
* - Ignores unknown properties during deserialization
* - Excludes null values from serialization
* - Uses consistent date/time formatting
*
* @return A pre-configured ObjectMapper instance
*/
public static ObjectMapper createDefaultObjectMapper() {
ObjectMapper mapper = new ObjectMapper();

// Ignore unknown properties to handle API changes gracefully
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

// Don't include null values in JSON output
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

return mapper;
}

/**
* Creates an ObjectMapper specifically configured for event processing
* with additional date formatting settings.
*
* @return A pre-configured ObjectMapper for events
*/
public static ObjectMapper createEventObjectMapper() {
ObjectMapper mapper = createDefaultObjectMapper();

// Disable timestamp serialization for events
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

return mapper;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import java.util.Map;

import com.devcycle.sdk.server.local.model.ConfigMetadata;

/**
* Context object passed to hooks during variable evaluation.
* Contains the user, variable key, default value, and additional context data.
Expand All @@ -10,19 +12,22 @@ public class HookContext<T> {
private DevCycleUser user;
private final String key;
private final T defaultValue;
private final ConfigMetadata metadata;
private Variable<T> variableDetails;

public HookContext(DevCycleUser user, String key, T defaultValue) {
public HookContext(DevCycleUser user, String key, T defaultValue, ConfigMetadata metadata) {
this.user = user;
this.key = key;
this.defaultValue = defaultValue;
this.metadata = metadata;
}

public HookContext(DevCycleUser user, String key, T defaultValue, Variable<T> variable) {
public HookContext(DevCycleUser user, String key, T defaultValue, Variable<T> variable, ConfigMetadata metadata) {
this.user = user;
this.key = key;
this.defaultValue = defaultValue;
this.variableDetails = variable;
this.metadata = metadata;
}

public DevCycleUser getUser() {
Expand All @@ -39,10 +44,14 @@ public T getDefaultValue() {

public Variable<T> getVariableDetails() { return variableDetails; }

public ConfigMetadata getMetadata() {
return metadata;
}

public HookContext<T> merge(HookContext<T> other) {
if (other == null) {
return this;
}
return new HookContext<>(other.getUser(), key, defaultValue, variableDetails);
return new HookContext<>(other.getUser(), key, defaultValue, variableDetails, metadata);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.devcycle.sdk.server.common.model;

import com.devcycle.sdk.server.local.model.Environment;
import com.devcycle.sdk.server.local.model.Project;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand All @@ -15,10 +18,12 @@
public class ProjectConfig {

@Schema(description = "Project Settings")
private Object project;
@JsonProperty("project")
private Project project;

@Schema(description = "Environment Key & ID")
private Object environment;
@JsonProperty("environment")
private Environment environment;

@Schema(description = "List of Features in this Project")
private Object[] features;
Expand All @@ -34,5 +39,13 @@ public class ProjectConfig {

@Schema(description = "SSE Configuration")
private SSE sse;

public Project getProject() {
return project;
}

public Environment getEnvironment() {
return environment;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

import com.devcycle.sdk.server.common.api.APIUtils;
import com.devcycle.sdk.server.common.api.IDevCycleApi;
import com.devcycle.sdk.server.common.api.ObjectMapperUtils;
import com.devcycle.sdk.server.local.model.DevCycleLocalOptions;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
Expand All @@ -14,7 +14,7 @@

public final class DevCycleLocalApiClient {

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final ObjectMapper OBJECT_MAPPER = ObjectMapperUtils.createDefaultObjectMapper();
private static final String CONFIG_URL = "https://config-cdn.devcycle.com/";
private static final int DEFAULT_TIMEOUT_MS = 10000;
private static final int MIN_INTERVALS_MS = 1000;
Expand All @@ -25,7 +25,6 @@ public final class DevCycleLocalApiClient {

private DevCycleLocalApiClient(DevCycleLocalOptions options) {

OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
okBuilder = new OkHttpClient.Builder();

APIUtils.applyRestOptions(options.getRestOptions(), okBuilder);
Expand All @@ -42,7 +41,7 @@ private DevCycleLocalApiClient(DevCycleLocalOptions options) {

adapterBuilder = new Retrofit.Builder()
.baseUrl(configUrl)
.addConverterFactory(JacksonConverterFactory.create());
.addConverterFactory(JacksonConverterFactory.create(OBJECT_MAPPER));
}

public DevCycleLocalApiClient(String sdkKey, DevCycleLocalOptions options) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.devcycle.sdk.server.local.managers.EnvironmentConfigManager;
import com.devcycle.sdk.server.local.managers.EventQueueManager;
import com.devcycle.sdk.server.local.model.BucketedUserConfig;
import com.devcycle.sdk.server.local.model.ConfigMetadata;
import com.devcycle.sdk.server.local.model.DevCycleLocalOptions;
import com.devcycle.sdk.server.local.protobuf.SDKVariable_PB;
import com.devcycle.sdk.server.local.protobuf.VariableForUserParams_PB;
Expand All @@ -28,6 +29,7 @@ public final class DevCycleLocalClient implements IDevCycleClient {
private final EnvironmentConfigManager configManager;
private EventQueueManager eventQueueManager;
private final String clientUUID;
// raw type here is okay because we're using a generic type for the variable
private EvalHooksRunner evalHooksRunner;

public DevCycleLocalClient(String sdkKey) {
Expand Down Expand Up @@ -156,7 +158,7 @@ public <T> Variable<T> variable(DevCycleUser user, String key, T defaultValue) {
.setShouldTrackEvent(true)
.build();

HookContext<T> hookContext = new HookContext<T>(user, key, defaultValue);
HookContext<T> hookContext = new HookContext<T>(user, key, defaultValue, getMetadata());
Variable<T> variable = null;
ArrayList<EvalHook<T>> hooks = new ArrayList<EvalHook<T>>(evalHooksRunner.getHooks());
ArrayList<EvalHook<T>> reversedHooks = new ArrayList<EvalHook<T>>(evalHooksRunner.getHooks());
Expand Down Expand Up @@ -192,14 +194,20 @@ public <T> Variable<T> variable(DevCycleUser user, String key, T defaultValue) {
if (!(e instanceof BeforeHookError)) {
DevCycleLogger.error("Unable to evaluate Variable " + key + " due to error: " + e, e);
}
evalHooksRunner.executeError(reversedHooks, hookContext, e);
// For BeforeHookError, pass the original cause to error hooks, not the wrapper
Throwable errorToPass = (e instanceof BeforeHookError && e.getCause() != null) ? e.getCause() : e;
evalHooksRunner.executeError(reversedHooks, hookContext, errorToPass);
} finally {
if (variable == null) {
variable = defaultVariable;
}
evalHooksRunner.executeFinally(reversedHooks, hookContext, Optional.of(variable));
return variable;
}
return variable;
}

public ConfigMetadata getMetadata() {
return configManager.getConfigMetadata();
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

import com.devcycle.sdk.server.common.api.APIUtils;
import com.devcycle.sdk.server.common.api.IDevCycleApi;
import com.devcycle.sdk.server.common.api.ObjectMapperUtils;
import com.devcycle.sdk.server.common.interceptor.AuthorizationHeaderInterceptor;
import com.devcycle.sdk.server.local.model.DevCycleLocalOptions;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.OkHttpClient;
import retrofit2.Retrofit;
Expand All @@ -14,14 +14,13 @@

public final class DevCycleLocalEventsApiClient {

private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private static final ObjectMapper OBJECT_MAPPER = ObjectMapperUtils.createEventObjectMapper();
private static final String EVENTS_API_URL = "https://events.devcycle.com/";
private final OkHttpClient.Builder okBuilder;
private final Retrofit.Builder adapterBuilder;
private String eventsApiUrl;

public DevCycleLocalEventsApiClient(String sdkKey, DevCycleLocalOptions options) {
OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
okBuilder = new OkHttpClient.Builder();

APIUtils.applyRestOptions(options.getRestOptions(), okBuilder);
Expand All @@ -35,9 +34,7 @@ public DevCycleLocalEventsApiClient(String sdkKey, DevCycleLocalOptions options)

adapterBuilder = new Retrofit.Builder()
.baseUrl(eventsApiUrl)
.addConverterFactory(JacksonConverterFactory.create());


.addConverterFactory(JacksonConverterFactory.create(OBJECT_MAPPER));
}

public IDevCycleApi initialize() {
Expand Down
Loading
Loading