Skip to content

Commit

Permalink
feat: Add hook-data concept for hooks. (#273)
Browse files Browse the repository at this point in the history
<!-- 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]>
  • Loading branch information
4 people authored Jan 15, 2025
1 parent d261f68 commit c287b58
Show file tree
Hide file tree
Showing 2 changed files with 126 additions and 4 deletions.
23 changes: 22 additions & 1 deletion specification.json
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,7 @@
{
"id": "Requirement 4.1.1",
"machine_id": "requirement_4_1_1",
"content": "Hook context MUST provide: the `flag key`, `flag value type`, `evaluation context`, and the `default value`.",
"content": "Hook context MUST provide: the `flag key`, `flag value type`, `evaluation context`, `default value`, and `hook data`.",
"RFC 2119 keyword": "MUST",
"children": []
},
Expand Down Expand Up @@ -718,6 +718,13 @@
}
]
},
{
"id": "Requirement 4.1.5",
"machine_id": "requirement_4_1_5",
"content": "The `hook data` MUST be mutable.",
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Requirement 4.2.1",
"machine_id": "requirement_4_2_1",
Expand Down Expand Up @@ -761,6 +768,13 @@
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Requirement 4.3.2",
"machine_id": "requirement_4_3_2",
"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.",
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Condition 4.3.2",
"machine_id": "condition_4_3_2",
Expand Down Expand Up @@ -911,6 +925,13 @@
"RFC 2119 keyword": "MUST NOT",
"children": []
},
{
"id": "Requirement 4.6.1",
"machine_id": "requirement_4_6_1",
"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`.",
"RFC 2119 keyword": "MUST",
"children": []
},
{
"id": "Requirement 5.1.1",
"machine_id": "requirement_5_1_1",
Expand Down
107 changes: 104 additions & 3 deletions specification/sections/04-hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,20 @@ Hooks can be configured to run globally (impacting all flag evaluations), per cl
### Definitions

**Hook**: Application author/integrator-supplied logic that is called by the OpenFeature framework at a specific stage.
**Stage**: An explicit portion of the flag evaluation lifecycle. e.g. `before` being "before" the [resolution](../glossary.md#resolving-flag-values) is run.

**Stage**: An explicit portion of the flag evaluation lifecycle. e.g. `before` being "before the [resolution](../glossary.md#resolving-flag-values) is run.

**Invocation**: A single call to evaluate a flag. `client.getBooleanValue(..)` is an invocation.

**API**: The global API singleton.

### 4.1. Hook context

Hook context exists to provide hooks with information about the invocation.
Hook context exists to provide hooks with information about the invocation and propagate data between hook stages.

#### Requirement 4.1.1

> Hook context **MUST** provide: the `flag key`, `flag value type`, `evaluation context`, and the `default value`.
> Hook context **MUST** provide: the `flag key`, `flag value type`, `evaluation context`, `default value`, and `hook data`.
#### Requirement 4.1.2

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

> The evaluation context **MUST** be mutable only within the `before` hook.
#### Requirement 4.1.5

> The `hook data` **MUST** be mutable.
Either the `hook data` reference itself must be mutable, or it must allow mutation of its contents.

Mutable reference:
```
hookContext.hookData = {'my-key': 'my-value'}
```

Mutable content:
```
hookContext.hookData.set('my-key', 'my-value')
```

### 4.2. Hook Hints

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

> Hooks **MUST** specify at least one stage.
#### Requirement 4.3.2

> `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.
Example showing data between `before` and `after` stage for two different hooks.
```mermaid
sequenceDiagram
actor Application
participant Client
participant HookA
participant HookB
Application->>Client: getBooleanValue('my-bool', myContext, false)
activate Client
Client-->>Client: create hook data for HookA
Client->>HookA: before(hookContext: {data: {}, ... })
activate HookA
HookA-->>HookA: hookContext.hookData.set('key',' data for A')
HookA-->>Client: (return)
deactivate HookA
Client-->>Client: create hook data for HookB
Client->>HookB: before(hookContext: {data: {}, ... }, hints)
activate HookB
HookB-->>HookB: hookContext.hookData.set('key', 'data for B')
deactivate HookB
Client-->>Client: Flag evaluation
Client->>HookB: after(hookContext: {data: {key: 'data for B'}, ... }, detail, hints)
activate HookB
HookB-->>Client: (return)
deactivate HookB
Client->>HookA: after(hookContext: {data: {'key': 'data for A'}, ... })
activate HookA
HookA-->>Client: (return)
deactivate HookA
Client-->>Application: true
deactivate Client
```

#### Condition 4.3.2

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

> The hook **MUST NOT** alter the `hook hints` structure.

### 4.6. Hook data

Hook data exists to allow hook stages to share data for a specific evaluation. For instance a span
for OpenTelemetry could be created in a `before` stage and closed in an `after` stage.

Hook data is scoped to a specific hook instance. The different stages of a hook share the same data,
but different hooks have different hook data instances.

```Java
public Optional<EvaluationContext> before(HookContext context, HookHints hints) {
SpanBuilder builder = tracer.spanBuilder('sample')
.setParent(Context.current().with(Span.current()));
Span span = builder.startSpan()
context.hookData.set("span", span);
}
public void after(HookContext context, FlagEvaluationDetails details, HookHints hints) {
// Only accessible by this hook for this specific evaluation.
Object value = context.hookData.get("span");
if (value instanceof Span) {
Span span = (Span) value;
span.end();
}
}
```

#### Requirement 4.6.1

> `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 comments on commit c287b58

Please sign in to comment.