Skip to content

Commit c287b58

Browse files
kinyoklionbeeme1mrlukas-reiningtoddbaert
authored
feat: Add hook-data concept for hooks. (#273)
<!-- Please use this template for your pull request. --> <!-- Please use the sections that you need and delete other sections --> ## This PR <!-- add the description of the PR here --> Add support for the hook-data concept for hooks. Hook-data allows for per-evaluation data to be propagated between hooks. This is especially useful for analytics purposes where you may want to measure things that happen between stages, or you want to do something like create a span in one stage and close it in another. This concept is similar to the `series data` concept for LaunchDarkly hooks. https://github.com/launchdarkly/open-sdk-specs/tree/main/specs/HOOK-hooks#evaluationseriesdata Unlike `series data` the data in this approach is mutable. This is because the `before` stage already has a return value. We could workaround this by specifying a return structure, but it maybe seems more complex. The data is only passed to a specific hook instance, so mutability is not of great concern. Some functional languages may still need to use an immutable with return values approach. I can create an OFEP if we think this merits discussion prior to proposal. ### Related Issues <!-- add here the GitHub issue that this PR resolves if applicable --> Related discussion in a PR comment. open-feature/java-sdk#1049 (comment) --------- Signed-off-by: Ryan Lamb <[email protected]> Co-authored-by: Michael Beemer <[email protected]> Co-authored-by: Lukas Reining <[email protected]> Co-authored-by: Todd Baert <[email protected]>
1 parent d261f68 commit c287b58

File tree

2 files changed

+126
-4
lines changed

2 files changed

+126
-4
lines changed

specification.json

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,7 @@
685685
{
686686
"id": "Requirement 4.1.1",
687687
"machine_id": "requirement_4_1_1",
688-
"content": "Hook context MUST provide: the `flag key`, `flag value type`, `evaluation context`, and the `default value`.",
688+
"content": "Hook context MUST provide: the `flag key`, `flag value type`, `evaluation context`, `default value`, and `hook data`.",
689689
"RFC 2119 keyword": "MUST",
690690
"children": []
691691
},
@@ -718,6 +718,13 @@
718718
}
719719
]
720720
},
721+
{
722+
"id": "Requirement 4.1.5",
723+
"machine_id": "requirement_4_1_5",
724+
"content": "The `hook data` MUST be mutable.",
725+
"RFC 2119 keyword": "MUST",
726+
"children": []
727+
},
721728
{
722729
"id": "Requirement 4.2.1",
723730
"machine_id": "requirement_4_2_1",
@@ -761,6 +768,13 @@
761768
"RFC 2119 keyword": "MUST",
762769
"children": []
763770
},
771+
{
772+
"id": "Requirement 4.3.2",
773+
"machine_id": "requirement_4_3_2",
774+
"content": "`Hook data` MUST must be created before the first `stage` invoked in a hook for a specific evaluation and propagated between each `stage` of the hook. The hook data is not shared between different hooks.",
775+
"RFC 2119 keyword": "MUST",
776+
"children": []
777+
},
764778
{
765779
"id": "Condition 4.3.2",
766780
"machine_id": "condition_4_3_2",
@@ -911,6 +925,13 @@
911925
"RFC 2119 keyword": "MUST NOT",
912926
"children": []
913927
},
928+
{
929+
"id": "Requirement 4.6.1",
930+
"machine_id": "requirement_4_6_1",
931+
"content": "`hook data` MUST be a structure supporting the definition of arbitrary properties, with keys of type `string`, and values of type `boolean | string | number | datetime | structure`.",
932+
"RFC 2119 keyword": "MUST",
933+
"children": []
934+
},
914935
{
915936
"id": "Requirement 5.1.1",
916937
"machine_id": "requirement_5_1_1",

specification/sections/04-hooks.md

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,20 @@ Hooks can be configured to run globally (impacting all flag evaluations), per cl
3232
### Definitions
3333

3434
**Hook**: Application author/integrator-supplied logic that is called by the OpenFeature framework at a specific stage.
35-
**Stage**: An explicit portion of the flag evaluation lifecycle. e.g. `before` being "before" the [resolution](../glossary.md#resolving-flag-values) is run.
35+
36+
**Stage**: An explicit portion of the flag evaluation lifecycle. e.g. `before` being "before the [resolution](../glossary.md#resolving-flag-values) is run.
37+
3638
**Invocation**: A single call to evaluate a flag. `client.getBooleanValue(..)` is an invocation.
39+
3740
**API**: The global API singleton.
3841

3942
### 4.1. Hook context
4043

41-
Hook context exists to provide hooks with information about the invocation.
44+
Hook context exists to provide hooks with information about the invocation and propagate data between hook stages.
4245

4346
#### Requirement 4.1.1
4447

45-
> Hook context **MUST** provide: the `flag key`, `flag value type`, `evaluation context`, and the `default value`.
48+
> Hook context **MUST** provide: the `flag key`, `flag value type`, `evaluation context`, `default value`, and `hook data`.
4649
4750
#### Requirement 4.1.2
4851

@@ -62,6 +65,22 @@ see: [dynamic-context paradigm](../glossary.md#dynamic-context-paradigm)
6265

6366
> The evaluation context **MUST** be mutable only within the `before` hook.
6467
68+
#### Requirement 4.1.5
69+
70+
> The `hook data` **MUST** be mutable.
71+
72+
Either the `hook data` reference itself must be mutable, or it must allow mutation of its contents.
73+
74+
Mutable reference:
75+
```
76+
hookContext.hookData = {'my-key': 'my-value'}
77+
```
78+
79+
Mutable content:
80+
```
81+
hookContext.hookData.set('my-key', 'my-value')
82+
```
83+
6584
### 4.2. Hook Hints
6685

6786
#### Requirement 4.2.1
@@ -90,6 +109,58 @@ see: [dynamic-context paradigm](../glossary.md#dynamic-context-paradigm)
90109

91110
> Hooks **MUST** specify at least one stage.
92111
112+
#### Requirement 4.3.2
113+
114+
> `Hook data` **MUST** must be created before the first `stage` invoked in a hook for a specific evaluation and propagated between each `stage` of the hook. The hook data is not shared between different hooks.
115+
116+
Example showing data between `before` and `after` stage for two different hooks.
117+
```mermaid
118+
sequenceDiagram
119+
actor Application
120+
participant Client
121+
participant HookA
122+
participant HookB
123+
124+
Application->>Client: getBooleanValue('my-bool', myContext, false)
125+
activate Client
126+
127+
Client-->>Client: create hook data for HookA
128+
129+
Client->>HookA: before(hookContext: {data: {}, ... })
130+
activate HookA
131+
132+
HookA-->>HookA: hookContext.hookData.set('key',' data for A')
133+
134+
HookA-->>Client: (return)
135+
deactivate HookA
136+
137+
Client-->>Client: create hook data for HookB
138+
139+
Client->>HookB: before(hookContext: {data: {}, ... }, hints)
140+
activate HookB
141+
142+
HookB-->>HookB: hookContext.hookData.set('key', 'data for B')
143+
deactivate HookB
144+
145+
Client-->>Client: Flag evaluation
146+
147+
Client->>HookB: after(hookContext: {data: {key: 'data for B'}, ... }, detail, hints)
148+
activate HookB
149+
150+
HookB-->>Client: (return)
151+
deactivate HookB
152+
153+
Client->>HookA: after(hookContext: {data: {'key': 'data for A'}, ... })
154+
activate HookA
155+
156+
HookA-->>Client: (return)
157+
deactivate HookA
158+
159+
Client-->>Application: true
160+
deactivate Client
161+
162+
```
163+
93164
#### Condition 4.3.2
94165

95166
> The implementation uses the dynamic-context paradigm.
@@ -230,3 +301,33 @@ see: [Flag evaluation options](./01-flag-evaluation.md#evaluation-options)
230301
#### Requirement 4.5.3
231302

232303
> The hook **MUST NOT** alter the `hook hints` structure.
304+
305+
### 4.6. Hook data
306+
307+
Hook data exists to allow hook stages to share data for a specific evaluation. For instance a span
308+
for OpenTelemetry could be created in a `before` stage and closed in an `after` stage.
309+
310+
Hook data is scoped to a specific hook instance. The different stages of a hook share the same data,
311+
but different hooks have different hook data instances.
312+
313+
```Java
314+
public Optional<EvaluationContext> before(HookContext context, HookHints hints) {
315+
SpanBuilder builder = tracer.spanBuilder('sample')
316+
.setParent(Context.current().with(Span.current()));
317+
Span span = builder.startSpan()
318+
context.hookData.set("span", span);
319+
}
320+
321+
public void after(HookContext context, FlagEvaluationDetails details, HookHints hints) {
322+
// Only accessible by this hook for this specific evaluation.
323+
Object value = context.hookData.get("span");
324+
if (value instanceof Span) {
325+
Span span = (Span) value;
326+
span.end();
327+
}
328+
}
329+
```
330+
331+
#### Requirement 4.6.1
332+
333+
> `hook data` **MUST** be a structure supporting the definition of arbitrary properties, with keys of type `string`, and values of type `boolean | string | number | datetime | structure`.

0 commit comments

Comments
 (0)