Skip to content

feat: Add hook-data concept for hooks.#273

Merged
toddbaert merged 12 commits intomainfrom
rlamb/hook-data
Jan 15, 2025
Merged

feat: Add hook-data concept for hooks.#273
toddbaert merged 12 commits intomainfrom
rlamb/hook-data

Conversation

@kinyoklion
Copy link
Copy Markdown
Member

This PR

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

Related discussion in a PR comment.
open-feature/java-sdk#1049 (comment)

Comment thread specification/sections/04-hooks.md
Comment thread specification/sections/04-hooks.md Outdated
Comment thread specification/sections/04-hooks.md
Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
@kinyoklion kinyoklion marked this pull request as ready for review September 30, 2024 16:01
@kinyoklion kinyoklion requested a review from beeme1mr September 30, 2024 16:02
Copy link
Copy Markdown
Member

@beeme1mr beeme1mr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this proposal, but I would like to see how it could be implemented in an SDK. Properly scoping hook data may be complicated.

Comment thread specification/sections/04-hooks.md Outdated
Co-authored-by: Michael Beemer <beeme1mr@users.noreply.github.com>
Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Comment thread specification/sections/04-hooks.md Outdated
Comment thread specification/sections/04-hooks.md Outdated
Copy link
Copy Markdown
Member

@toddbaert toddbaert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely support this idea. I left a few comments. My biggest question is how we can define this so it's non-breaking for existing hooks.

Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
@kinyoklion
Copy link
Copy Markdown
Member Author

I like this proposal, but I would like to see how it could be implemented in an SDK. Properly scoping hook data may be complicated.

I should be able to find time to update a OF SDK to demonstrate, but it may take me a bit.

It will be a little different now that we added it to hook context, but you can see what we did in the LD SDKs as its own argument:
https://github.com/launchdarkly/js-core/blob/ae2b5bbed9ec32d9640f1c5a168badaad28174ec/packages/shared/sdk-server/src/hooks/HookRunner.ts#L109

In our case we create the data when we call the hook, and the return is the new data. Which means we can just map it to an array.

But, if that was not the case, as it will be in the OF example you instead do this.

  1. Create an array the same size as the number of hooks being invoked.
  2. As you iterate the hooks pass the data at the same index in the array as the hook is in its array.
  3. Maintain the array for the execution of that series of stages.

Some pseudocode:

const hookData = new Array(hooksForInvocation.length);
hookData.fill({})

...
hooksForInvocation.forEach((hook, index) => executeBeforeEvaluation(hookContext.withData(hookData[index])))

The hooks are stable for a specific invocation of stages, so you just need an equal sized list of hook data.

Copy link
Copy Markdown
Member

@lukas-reining lukas-reining left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the change and as far as I can see, adding the hook data does not break the old hooks when it is implemented.

Comment thread specification/sections/04-hooks.md Outdated
kinyoklion and others added 2 commits December 19, 2024 10:51
Co-authored-by: Lukas Reining <lukas.reining@codecentric.de>
Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
@kinyoklion kinyoklion requested a review from toddbaert December 19, 2024 18:53
Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
@kinyoklion
Copy link
Copy Markdown
Member Author

I like this proposal, but I would like to see how it could be implemented in an SDK. Properly scoping hook data may be complicated.

I think this was resolved via the pseudocode.

Copy link
Copy Markdown
Member

@guidobrei guidobrei left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that my approval changes anything, but this proposal looks really promising.

Copy link
Copy Markdown
Member

@toddbaert toddbaert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 This has been open for a while. @kinyoklion feel free to merge whenever, and we can do a release.

I'd like to propose additional hardening of the specification soon, and possibly moving this section out of experimental (probably after we've implemented what's outlined in this PR in a few SDKs). When this is merged I'll create a parent issue in the spec and sub issues in each SDK.

@toddbaert toddbaert merged commit c287b58 into main Jan 15, 2025
@toddbaert toddbaert deleted the rlamb/hook-data branch January 15, 2025 14:29
github-merge-queue bot pushed a commit to open-feature/dotnet-sdk that referenced this pull request Apr 18, 2025
<!-- Please use this template for your pull request. -->
<!-- Please use the sections that you need and delete other sections -->

## This PR

Adds support for hook data.
open-feature/spec#273

### Related Issues
<!-- add here the GitHub issue that this PR resolves if applicable -->

### Notes
<!-- any additional notes for this PR -->

I realized that the 4.6.1 section of the spec wasn't consistent with the
expected usage. Basically it over-specifies the typing of the hook data
matching that of the evaluation context. That is one possible approach,
it would just mean a bit more work on the part of the hook implementers.

In the earlier example in the spec I put a `Span` in the hook data:
```
  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();
    }
  }
```

This is only possible if the hook data allows specification of any
`object` instead of being limited to the immutable types of a context.
For hooks hook data this is safe because only the hook mutating the data
will have access to that data. Additionally the execution of the hook
will be in sequence with the evaluation (likely in a single thread).

The alternative would be to store data in the hook, and use the hook
data to know when to remove it.
Something like this:
```
  public Optional<EvaluationContext> before(HookContext context, HookHints hints) {
    SpanBuilder builder = tracer.spanBuilder('sample')
    .setParent(Context.current().with(Span.current()));
    Span span = builder.startSpan()

    String storageId = Uuid();
    this.tmpData.set(storageId, span);
    context.hookData.set("span", storageId);
  }
  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) {
      String id = value.AsString();
      Span span= this.tmpData.get(id);
      span.end();
    }
  }
```


### Follow-up Tasks
<!-- anything that is related to this PR but not done here should be
noted under this section -->
<!-- if there is a need for a new issue, please link it here -->

### How to test
<!-- if applicable, add testing instructions under this section -->

---------

Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Co-authored-by: chrfwow <christian.lutnik@dynatrace.com>
WeihanLi pushed a commit to WeihanLi/openfeature-dotnet-sdk that referenced this pull request May 14, 2025
<!-- Please use this template for your pull request. -->
<!-- Please use the sections that you need and delete other sections -->

## This PR

Adds support for hook data.
open-feature/spec#273

### Related Issues
<!-- add here the GitHub issue that this PR resolves if applicable -->

### Notes
<!-- any additional notes for this PR -->

I realized that the 4.6.1 section of the spec wasn't consistent with the
expected usage. Basically it over-specifies the typing of the hook data
matching that of the evaluation context. That is one possible approach,
it would just mean a bit more work on the part of the hook implementers.

In the earlier example in the spec I put a `Span` in the hook data:
```
  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();
    }
  }
```

This is only possible if the hook data allows specification of any
`object` instead of being limited to the immutable types of a context.
For hooks hook data this is safe because only the hook mutating the data
will have access to that data. Additionally the execution of the hook
will be in sequence with the evaluation (likely in a single thread).

The alternative would be to store data in the hook, and use the hook
data to know when to remove it.
Something like this:
```
  public Optional<EvaluationContext> before(HookContext context, HookHints hints) {
    SpanBuilder builder = tracer.spanBuilder('sample')
    .setParent(Context.current().with(Span.current()));
    Span span = builder.startSpan()

    String storageId = Uuid();
    this.tmpData.set(storageId, span);
    context.hookData.set("span", storageId);
  }
  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) {
      String id = value.AsString();
      Span span= this.tmpData.get(id);
      span.end();
    }
  }
```

### Follow-up Tasks
<!-- anything that is related to this PR but not done here should be
noted under this section -->
<!-- if there is a need for a new issue, please link it here -->

### How to test
<!-- if applicable, add testing instructions under this section -->

---------

Signed-off-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Co-authored-by: chrfwow <christian.lutnik@dynatrace.com>
Signed-off-by: Weihan Li <weihanli@outlook.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants