Skip to content

Commit 01eb07e

Browse files
authored
feat: add config metadata (#178)
* feat: add config metadata * fix: local config server missing the headers for the metadata * add getters for config metadata variables and cleanup code * revert local example testing file * remove getters for final variables as its not needed
1 parent f791d35 commit 01eb07e

16 files changed

+601
-49
lines changed

src/main/java/com/devcycle/sdk/server/cloud/api/DevCycleCloudApiClient.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
import com.devcycle.sdk.server.cloud.model.DevCycleCloudOptions;
44
import com.devcycle.sdk.server.common.api.APIUtils;
55
import com.devcycle.sdk.server.common.api.IDevCycleApi;
6+
import com.devcycle.sdk.server.common.api.ObjectMapperUtils;
67
import com.devcycle.sdk.server.common.interceptor.AuthorizationHeaderInterceptor;
7-
import com.fasterxml.jackson.annotation.JsonInclude;
88
import com.fasterxml.jackson.databind.ObjectMapper;
99
import okhttp3.OkHttpClient;
1010
import retrofit2.Retrofit;
@@ -14,14 +14,13 @@
1414

1515
public final class DevCycleCloudApiClient {
1616

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

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

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

3938
adapterBuilder = new Retrofit.Builder()
4039
.baseUrl(bucketingUrl)
41-
.addConverterFactory(JacksonConverterFactory.create());
40+
.addConverterFactory(JacksonConverterFactory.create(OBJECT_MAPPER));
4241
}
4342

4443
public IDevCycleApi initialize() {

src/main/java/com/devcycle/sdk/server/cloud/api/DevCycleCloudClient.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
import com.devcycle.sdk.server.cloud.model.DevCycleCloudOptions;
44
import com.devcycle.sdk.server.common.api.IDevCycleApi;
55
import com.devcycle.sdk.server.common.api.IDevCycleClient;
6+
import com.devcycle.sdk.server.common.api.ObjectMapperUtils;
67
import com.devcycle.sdk.server.common.exception.AfterHookError;
78
import com.devcycle.sdk.server.common.exception.BeforeHookError;
89
import com.devcycle.sdk.server.common.exception.DevCycleException;
910
import com.devcycle.sdk.server.common.logging.DevCycleLogger;
1011
import com.devcycle.sdk.server.common.model.*;
1112
import com.devcycle.sdk.server.common.model.Variable.TypeEnum;
1213
import com.devcycle.sdk.server.openfeature.DevCycleProvider;
13-
import com.fasterxml.jackson.annotation.JsonInclude;
1414
import com.fasterxml.jackson.core.JsonProcessingException;
1515
import com.fasterxml.jackson.databind.ObjectMapper;
1616
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
@@ -23,7 +23,7 @@
2323

2424
public final class DevCycleCloudClient implements IDevCycleClient {
2525

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

4949
this.dvcOptions = options;
5050
api = new DevCycleCloudApiClient(sdkKey, options).initialize();
51-
OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
5251

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

113112
TypeEnum variableType = TypeEnum.fromClass(defaultValue.getClass());
114113
Variable<T> variable = null;
115-
HookContext<T> context = new HookContext<T>(user, key, defaultValue);
114+
HookContext<T> context = new HookContext<T>(user, key, defaultValue, null);
116115
ArrayList<EvalHook<T>> hooks = new ArrayList<EvalHook<T>>(evalHooksRunner.getHooks());
117116
ArrayList<EvalHook<T>> reversedHooks = new ArrayList<>(hooks);
118117
Collections.reverse(reversedHooks);
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.devcycle.sdk.server.common.api;
2+
3+
import com.fasterxml.jackson.annotation.JsonInclude;
4+
import com.fasterxml.jackson.databind.DeserializationFeature;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import com.fasterxml.jackson.databind.SerializationFeature;
7+
8+
/**
9+
* Utility class for providing pre-configured ObjectMapper instances
10+
* with consistent settings across the DevCycle SDK.
11+
*/
12+
public class ObjectMapperUtils {
13+
14+
/**
15+
* Creates a new ObjectMapper with DevCycle SDK default configuration:
16+
* - Ignores unknown properties during deserialization
17+
* - Excludes null values from serialization
18+
* - Uses consistent date/time formatting
19+
*
20+
* @return A pre-configured ObjectMapper instance
21+
*/
22+
public static ObjectMapper createDefaultObjectMapper() {
23+
ObjectMapper mapper = new ObjectMapper();
24+
25+
// Ignore unknown properties to handle API changes gracefully
26+
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
27+
28+
// Don't include null values in JSON output
29+
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
30+
31+
return mapper;
32+
}
33+
34+
/**
35+
* Creates an ObjectMapper specifically configured for event processing
36+
* with additional date formatting settings.
37+
*
38+
* @return A pre-configured ObjectMapper for events
39+
*/
40+
public static ObjectMapper createEventObjectMapper() {
41+
ObjectMapper mapper = createDefaultObjectMapper();
42+
43+
// Disable timestamp serialization for events
44+
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
45+
46+
return mapper;
47+
}
48+
}

src/main/java/com/devcycle/sdk/server/common/model/HookContext.java

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import java.util.Map;
44

5+
import com.devcycle.sdk.server.local.model.ConfigMetadata;
6+
57
/**
68
* Context object passed to hooks during variable evaluation.
79
* Contains the user, variable key, default value, and additional context data.
@@ -10,19 +12,22 @@ public class HookContext<T> {
1012
private DevCycleUser user;
1113
private final String key;
1214
private final T defaultValue;
15+
private final ConfigMetadata metadata;
1316
private Variable<T> variableDetails;
1417

15-
public HookContext(DevCycleUser user, String key, T defaultValue) {
18+
public HookContext(DevCycleUser user, String key, T defaultValue, ConfigMetadata metadata) {
1619
this.user = user;
1720
this.key = key;
1821
this.defaultValue = defaultValue;
22+
this.metadata = metadata;
1923
}
2024

21-
public HookContext(DevCycleUser user, String key, T defaultValue, Variable<T> variable) {
25+
public HookContext(DevCycleUser user, String key, T defaultValue, Variable<T> variable, ConfigMetadata metadata) {
2226
this.user = user;
2327
this.key = key;
2428
this.defaultValue = defaultValue;
2529
this.variableDetails = variable;
30+
this.metadata = metadata;
2631
}
2732

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

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

47+
public ConfigMetadata getMetadata() {
48+
return metadata;
49+
}
50+
4251
public HookContext<T> merge(HookContext<T> other) {
4352
if (other == null) {
4453
return this;
4554
}
46-
return new HookContext<>(other.getUser(), key, defaultValue, variableDetails);
55+
return new HookContext<>(other.getUser(), key, defaultValue, variableDetails, metadata);
4756
}
4857
}

src/main/java/com/devcycle/sdk/server/common/model/ProjectConfig.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package com.devcycle.sdk.server.common.model;
22

3+
import com.devcycle.sdk.server.local.model.Environment;
4+
import com.devcycle.sdk.server.local.model.Project;
35
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
6+
import com.fasterxml.jackson.annotation.JsonProperty;
47
import io.swagger.v3.oas.annotations.media.Schema;
58
import lombok.AllArgsConstructor;
69
import lombok.Builder;
@@ -15,10 +18,12 @@
1518
public class ProjectConfig {
1619

1720
@Schema(description = "Project Settings")
18-
private Object project;
21+
@JsonProperty("project")
22+
private Project project;
1923

2024
@Schema(description = "Environment Key & ID")
21-
private Object environment;
25+
@JsonProperty("environment")
26+
private Environment environment;
2227

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

3540
@Schema(description = "SSE Configuration")
3641
private SSE sse;
42+
43+
public Project getProject() {
44+
return project;
45+
}
46+
47+
public Environment getEnvironment() {
48+
return environment;
49+
}
3750
}
3851

src/main/java/com/devcycle/sdk/server/local/api/DevCycleLocalApiClient.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import com.devcycle.sdk.server.common.api.APIUtils;
44
import com.devcycle.sdk.server.common.api.IDevCycleApi;
5+
import com.devcycle.sdk.server.common.api.ObjectMapperUtils;
56
import com.devcycle.sdk.server.local.model.DevCycleLocalOptions;
6-
import com.fasterxml.jackson.annotation.JsonInclude;
77
import com.fasterxml.jackson.databind.ObjectMapper;
88
import okhttp3.OkHttpClient;
99
import retrofit2.Retrofit;
@@ -14,7 +14,7 @@
1414

1515
public final class DevCycleLocalApiClient {
1616

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

2626
private DevCycleLocalApiClient(DevCycleLocalOptions options) {
2727

28-
OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
2928
okBuilder = new OkHttpClient.Builder();
3029

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

4342
adapterBuilder = new Retrofit.Builder()
4443
.baseUrl(configUrl)
45-
.addConverterFactory(JacksonConverterFactory.create());
44+
.addConverterFactory(JacksonConverterFactory.create(OBJECT_MAPPER));
4645
}
4746

4847
public DevCycleLocalApiClient(String sdkKey, DevCycleLocalOptions options) {

src/main/java/com/devcycle/sdk/server/local/api/DevCycleLocalClient.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.devcycle.sdk.server.local.managers.EnvironmentConfigManager;
1010
import com.devcycle.sdk.server.local.managers.EventQueueManager;
1111
import com.devcycle.sdk.server.local.model.BucketedUserConfig;
12+
import com.devcycle.sdk.server.local.model.ConfigMetadata;
1213
import com.devcycle.sdk.server.local.model.DevCycleLocalOptions;
1314
import com.devcycle.sdk.server.local.protobuf.SDKVariable_PB;
1415
import com.devcycle.sdk.server.local.protobuf.VariableForUserParams_PB;
@@ -28,6 +29,7 @@ public final class DevCycleLocalClient implements IDevCycleClient {
2829
private final EnvironmentConfigManager configManager;
2930
private EventQueueManager eventQueueManager;
3031
private final String clientUUID;
32+
// raw type here is okay because we're using a generic type for the variable
3133
private EvalHooksRunner evalHooksRunner;
3234

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

159-
HookContext<T> hookContext = new HookContext<T>(user, key, defaultValue);
161+
HookContext<T> hookContext = new HookContext<T>(user, key, defaultValue, getMetadata());
160162
Variable<T> variable = null;
161163
ArrayList<EvalHook<T>> hooks = new ArrayList<EvalHook<T>>(evalHooksRunner.getHooks());
162164
ArrayList<EvalHook<T>> reversedHooks = new ArrayList<EvalHook<T>>(evalHooksRunner.getHooks());
@@ -192,14 +194,20 @@ public <T> Variable<T> variable(DevCycleUser user, String key, T defaultValue) {
192194
if (!(e instanceof BeforeHookError)) {
193195
DevCycleLogger.error("Unable to evaluate Variable " + key + " due to error: " + e, e);
194196
}
195-
evalHooksRunner.executeError(reversedHooks, hookContext, e);
197+
// For BeforeHookError, pass the original cause to error hooks, not the wrapper
198+
Throwable errorToPass = (e instanceof BeforeHookError && e.getCause() != null) ? e.getCause() : e;
199+
evalHooksRunner.executeError(reversedHooks, hookContext, errorToPass);
196200
} finally {
197201
if (variable == null) {
198202
variable = defaultVariable;
199203
}
200204
evalHooksRunner.executeFinally(reversedHooks, hookContext, Optional.of(variable));
201-
return variable;
202205
}
206+
return variable;
207+
}
208+
209+
public ConfigMetadata getMetadata() {
210+
return configManager.getConfigMetadata();
203211
}
204212

205213

src/main/java/com/devcycle/sdk/server/local/api/DevCycleLocalEventsApiClient.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
import com.devcycle.sdk.server.common.api.APIUtils;
44
import com.devcycle.sdk.server.common.api.IDevCycleApi;
5+
import com.devcycle.sdk.server.common.api.ObjectMapperUtils;
56
import com.devcycle.sdk.server.common.interceptor.AuthorizationHeaderInterceptor;
67
import com.devcycle.sdk.server.local.model.DevCycleLocalOptions;
7-
import com.fasterxml.jackson.annotation.JsonInclude;
88
import com.fasterxml.jackson.databind.ObjectMapper;
99
import okhttp3.OkHttpClient;
1010
import retrofit2.Retrofit;
@@ -14,14 +14,13 @@
1414

1515
public final class DevCycleLocalEventsApiClient {
1616

17-
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
17+
private static final ObjectMapper OBJECT_MAPPER = ObjectMapperUtils.createEventObjectMapper();
1818
private static final String EVENTS_API_URL = "https://events.devcycle.com/";
1919
private final OkHttpClient.Builder okBuilder;
2020
private final Retrofit.Builder adapterBuilder;
2121
private String eventsApiUrl;
2222

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

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

3635
adapterBuilder = new Retrofit.Builder()
3736
.baseUrl(eventsApiUrl)
38-
.addConverterFactory(JacksonConverterFactory.create());
39-
40-
37+
.addConverterFactory(JacksonConverterFactory.create(OBJECT_MAPPER));
4138
}
4239

4340
public IDevCycleApi initialize() {

0 commit comments

Comments
 (0)