Skip to content

Commit 44092a3

Browse files
committed
feat: added evaluation hooks
1 parent 6dbb6cf commit 44092a3

File tree

10 files changed

+1475
-16
lines changed

10 files changed

+1475
-16
lines changed

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

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
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.exception.AfterHookError;
7+
import com.devcycle.sdk.server.common.exception.BeforeHookError;
68
import com.devcycle.sdk.server.common.exception.DevCycleException;
79
import com.devcycle.sdk.server.common.logging.DevCycleLogger;
810
import com.devcycle.sdk.server.common.model.*;
@@ -20,13 +22,15 @@
2022
import java.util.Collections;
2123
import java.util.HashMap;
2224
import java.util.Map;
25+
import java.util.Optional;
2326

2427
public final class DevCycleCloudClient implements IDevCycleClient {
2528

2629
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
2730
private final IDevCycleApi api;
2831
private final DevCycleCloudOptions dvcOptions;
2932
private final DevCycleProvider openFeatureProvider;
33+
private final EvalHooksRunner evalHooksRunner;
3034

3135
public DevCycleCloudClient(String sdkKey) {
3236
this(sdkKey, DevCycleCloudOptions.builder().build());
@@ -50,6 +54,7 @@ public DevCycleCloudClient(String sdkKey, DevCycleCloudOptions options) {
5054
OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
5155

5256
this.openFeatureProvider = new DevCycleProvider(this);
57+
this.evalHooksRunner = new EvalHooksRunner();
5358
}
5459

5560
/**
@@ -109,23 +114,46 @@ public <T> Variable<T> variable(DevCycleUser user, String key, T defaultValue) {
109114
}
110115

111116
TypeEnum variableType = TypeEnum.fromClass(defaultValue.getClass());
112-
Variable<T> variable;
117+
Variable<T> variable = null;
118+
HookContext context = new HookContext(user, key, defaultValue);
113119

114120
try {
121+
Throwable beforeError = null;
122+
ArrayList<EvalHook> hooks = new ArrayList<>(evalHooksRunner.hooks);
123+
ArrayList<EvalHook> reversedHooks = new ArrayList<>(hooks);
124+
Collections.reverse(reversedHooks);
125+
126+
try {
127+
context = context.merge(evalHooksRunner.executeBefore(hooks,context));
128+
} catch (Throwable e) {
129+
beforeError = e;
130+
}
131+
115132
Call<Variable> response = api.getVariableByKey(user, key, dvcOptions.getEnableEdgeDB());
116133
variable = getResponseWithRetries(response, 5);
117134
if (variable.getType() != variableType) {
118135
throw new IllegalArgumentException("Variable type mismatch, returning default value");
119136
}
137+
if (beforeError != null) {
138+
throw beforeError;
139+
}
140+
141+
evalHooksRunner.executeAfter(reversedHooks, context, variable);
120142
variable.setIsDefaulted(false);
121-
} catch (Exception exception) {
122-
variable = (Variable<T>) Variable.builder()
123-
.key(key)
124-
.type(variableType)
125-
.value(defaultValue)
126-
.defaultValue(defaultValue)
127-
.isDefaulted(true)
128-
.build();
143+
} catch (Throwable exception) {
144+
if (!(exception instanceof BeforeHookError || exception instanceof AfterHookError)) {
145+
variable = (Variable<T>) Variable.builder()
146+
.key(key)
147+
.type(variableType)
148+
.value(defaultValue)
149+
.defaultValue(defaultValue)
150+
.isDefaulted(true)
151+
.build();
152+
}
153+
154+
evalHooksRunner.executeError(reversedHooks, context, exception);
155+
} finally {
156+
evalHooksRunner.executeFinally(reversedHooks, context, Optional.ofNullable(variable));
129157
}
130158
return variable;
131159
}
@@ -226,6 +254,9 @@ private <T> T getResponseWithRetries(Call<T> call, int maxRetries) throws DevCyc
226254
throw new DevCycleException(HttpResponseCode.SERVER_ERROR, errorResponse);
227255
}
228256

257+
public void addHook(EvalHook hook) {
258+
this.evalHooksRunner.addHook(hook);
259+
}
229260

230261
private <T> T getResponse(Call<T> call) throws DevCycleException {
231262
ErrorResponse errorResponse = ErrorResponse.builder().build();
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.devcycle.sdk.server.common.exception;
2+
3+
/**
4+
* Exception thrown when an after hook fails during variable evaluation.
5+
*/
6+
public class AfterHookError extends RuntimeException {
7+
public AfterHookError(String message) {
8+
super(message);
9+
}
10+
11+
public AfterHookError(String message, Throwable cause) {
12+
super(message, cause);
13+
}
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.devcycle.sdk.server.common.exception;
2+
3+
/**
4+
* Exception thrown when a before hook fails during variable evaluation.
5+
*/
6+
public class BeforeHookError extends RuntimeException {
7+
public BeforeHookError(String message) {
8+
super(message);
9+
}
10+
11+
public BeforeHookError(String message, Throwable cause) {
12+
super(message, cause);
13+
}
14+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.devcycle.sdk.server.common.model;
2+
3+
import java.util.Optional;
4+
5+
public interface EvalHook<T> {
6+
7+
default Optional<HookContext<T>> before(HookContext<T> ctx) {
8+
return Optional.empty();
9+
}
10+
default void after(HookContext<T> ctx, Variable<T> variable) {}
11+
default void error(HookContext<T> ctx, Throwable e) {}
12+
default void onFinally(HookContext<T> ctx, Optional<Variable<T>> variable) {}
13+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package com.devcycle.sdk.server.common.model;
2+
3+
import com.devcycle.sdk.server.common.exception.AfterHookError;
4+
import com.devcycle.sdk.server.common.exception.BeforeHookError;
5+
import com.devcycle.sdk.server.common.logging.DevCycleLogger;
6+
7+
import javax.swing.text.html.Option;
8+
import java.util.ArrayList;
9+
import java.util.Collections;
10+
import java.util.List;
11+
import java.util.Optional;
12+
import java.util.concurrent.CompletableFuture;
13+
14+
/**
15+
* A class that manages evaluation hooks for the DevCycle SDK.
16+
* Provides functionality to add and clear hooks, storing them in an array.
17+
*/
18+
public class EvalHooksRunner {
19+
private List<EvalHook> hooks;
20+
21+
/**
22+
* Default constructor initializes an empty list of hooks.
23+
*/
24+
public EvalHooksRunner() {
25+
this.hooks = new ArrayList<>();
26+
}
27+
28+
/**
29+
* Adds a single hook to the collection.
30+
*
31+
* @param hook The hook to add
32+
*/
33+
public void addHook(EvalHook hook) {
34+
if (hook != null) {
35+
hooks.add(hook);
36+
}
37+
}
38+
39+
/**
40+
* Clears all hooks from the collection.
41+
*/
42+
public void clearHooks() {
43+
hooks.clear();
44+
}
45+
46+
/**
47+
* Runs all before hooks in order.
48+
*
49+
* @param context The context to pass to the hooks
50+
* @param <T> The type of the variable value
51+
* @return The potentially modified context
52+
*/
53+
public <T> HookContext<T> executeBefore(ArrayList<EvalHook<T>> hooks, HookContext<T> context) {
54+
HookContext<T> beforeContext = context;
55+
for (EvalHook<T> hook : hooks) {
56+
try {
57+
Optional<HookContext<T>> newContext = hook.before(beforeContext);
58+
if (newContext.isPresent()) {
59+
beforeContext = beforeContext.merge(newContext.get());
60+
}
61+
} catch (Exception e) {
62+
throw new BeforeHookError("Before hook failed", e);
63+
}
64+
}
65+
return beforeContext;
66+
}
67+
68+
/**
69+
* Runs all after hooks in reverse order.
70+
*
71+
* @param context The context to pass to the hooks
72+
* @param variable The variable result to pass to the hooks
73+
* @param <T> The type of the variable value
74+
*/
75+
public <T> void executeAfter(ArrayList<EvalHook<T>> hooks, HookContext<T> context, Variable<T> variable) {
76+
for (EvalHook<T> hook : hooks) {
77+
try {
78+
hook.after(context, variable);
79+
} catch (Exception e) {
80+
throw new AfterHookError("After hook failed", e);
81+
}
82+
}
83+
}
84+
85+
/**
86+
* Runs all error hooks in reverse order.
87+
*
88+
* @param context The context to pass to the hooks
89+
* @param error The error that occurred
90+
* @param <T> The type of the variable value
91+
*/
92+
public <T> void executeError(ArrayList<EvalHook<T>> hooks, HookContext<T> context, Throwable error) {
93+
for (EvalHook<T> hook : hooks) {
94+
try {
95+
hook.error(context, error);
96+
} catch (Exception hookError) {
97+
// Log hook error but don't throw to avoid masking the original error
98+
DevCycleLogger.error("Error hook failed: " + hookError.getMessage(), hookError);
99+
}
100+
}
101+
}
102+
103+
/**
104+
* Runs all finally hooks in reverse order.
105+
*
106+
* @param context The context to pass to the hooks
107+
* @param variable The variable result to pass to the hooks (may be null)
108+
*/
109+
public <T> void executeFinally(ArrayList<EvalHook<T>> hooks, HookContext<T> context, Optional<Variable<T>> variable) {
110+
for (EvalHook<T> hook : hooks) {
111+
try {
112+
hook.onFinally(context, variable);
113+
} catch (Exception e) {
114+
// Log finally hook error but don't throw
115+
DevCycleLogger.error("Finally hook failed: " + e.getMessage(), e);
116+
}
117+
}
118+
}
119+
}
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.model;
2+
3+
import java.util.Map;
4+
5+
/**
6+
* Context object passed to hooks during variable evaluation.
7+
* Contains the user, variable key, default value, and additional context data.
8+
*/
9+
public class HookContext<T> {
10+
private DevCycleUser user;
11+
private final String key;
12+
private final T defaultValue;
13+
private Variable<T> variableDetails;
14+
15+
public HookContext(DevCycleUser user, String key, T defaultValue) {
16+
this.user = user;
17+
this.key = key;
18+
this.defaultValue = defaultValue;
19+
}
20+
21+
public HookContext(DevCycleUser user, String key, T defaultValue, Variable<T> variable) {
22+
this.user = user;
23+
this.key = key;
24+
this.defaultValue = defaultValue;
25+
this.variableDetails = variable;
26+
}
27+
28+
public DevCycleUser getUser() {
29+
return user;
30+
}
31+
32+
public String getKey() {
33+
return key;
34+
}
35+
36+
public T getDefaultValue() {
37+
return defaultValue;
38+
}
39+
40+
public Variable<T> getVariableDetails() { return variableDetails; }
41+
42+
public HookContext<T> merge(HookContext<T> other) {
43+
if (other == null) {
44+
return this;
45+
}
46+
return new HookContext<>(other.getUser(), key, defaultValue, variableDetails);
47+
}
48+
}

0 commit comments

Comments
 (0)