Skip to content

Commit 110a51c

Browse files
committed
feat: added evaluation hooks
1 parent 6dbb6cf commit 110a51c

File tree

10 files changed

+1488
-22
lines changed

10 files changed

+1488
-22
lines changed

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

Lines changed: 40 additions & 12 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.*;
@@ -17,16 +19,15 @@
1719
import retrofit2.Response;
1820

1921
import java.io.IOException;
20-
import java.util.Collections;
21-
import java.util.HashMap;
22-
import java.util.Map;
22+
import java.util.*;
2323

2424
public final class DevCycleCloudClient implements IDevCycleClient {
2525

2626
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
2727
private final IDevCycleApi api;
2828
private final DevCycleCloudOptions dvcOptions;
2929
private final DevCycleProvider openFeatureProvider;
30+
private final EvalHooksRunner evalHooksRunner;
3031

3132
public DevCycleCloudClient(String sdkKey) {
3233
this(sdkKey, DevCycleCloudOptions.builder().build());
@@ -50,6 +51,7 @@ public DevCycleCloudClient(String sdkKey, DevCycleCloudOptions options) {
5051
OBJECT_MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
5152

5253
this.openFeatureProvider = new DevCycleProvider(this);
54+
this.evalHooksRunner = new EvalHooksRunner();
5355
}
5456

5557
/**
@@ -109,23 +111,46 @@ public <T> Variable<T> variable(DevCycleUser user, String key, T defaultValue) {
109111
}
110112

111113
TypeEnum variableType = TypeEnum.fromClass(defaultValue.getClass());
112-
Variable<T> variable;
114+
Variable<T> variable = null;
115+
HookContext<T> context = new HookContext<T>(user, key, defaultValue);
116+
ArrayList<EvalHook<T>> hooks = new ArrayList<EvalHook<T>>(evalHooksRunner.getHooks());
117+
ArrayList<EvalHook<T>> reversedHooks = new ArrayList<>(hooks);
118+
Collections.reverse(reversedHooks);
113119

114120
try {
121+
Throwable beforeError = null;
122+
123+
try {
124+
context = context.merge(evalHooksRunner.executeBefore(hooks, context));
125+
} catch (Throwable e) {
126+
beforeError = e;
127+
}
128+
115129
Call<Variable> response = api.getVariableByKey(user, key, dvcOptions.getEnableEdgeDB());
116130
variable = getResponseWithRetries(response, 5);
117131
if (variable.getType() != variableType) {
118132
throw new IllegalArgumentException("Variable type mismatch, returning default value");
119133
}
134+
if (beforeError != null) {
135+
throw beforeError;
136+
}
137+
138+
evalHooksRunner.executeAfter(reversedHooks, context, variable);
120139
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();
140+
} catch (Throwable exception) {
141+
if (!(exception instanceof BeforeHookError || exception instanceof AfterHookError)) {
142+
variable = (Variable<T>) Variable.builder()
143+
.key(key)
144+
.type(variableType)
145+
.value(defaultValue)
146+
.defaultValue(defaultValue)
147+
.isDefaulted(true)
148+
.build();
149+
}
150+
151+
evalHooksRunner.executeError(reversedHooks, context, exception);
152+
} finally {
153+
evalHooksRunner.executeFinally(reversedHooks, context, Optional.ofNullable(variable));
129154
}
130155
return variable;
131156
}
@@ -226,6 +251,9 @@ private <T> T getResponseWithRetries(Call<T> call, int maxRetries) throws DevCyc
226251
throw new DevCycleException(HttpResponseCode.SERVER_ERROR, errorResponse);
227252
}
228253

254+
public void addHook(EvalHook hook) {
255+
this.evalHooksRunner.addHook(hook);
256+
}
229257

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