Skip to content

Conversation

alexandraoberaigner
Copy link
Contributor

@alexandraoberaigner alexandraoberaigner commented Sep 19, 2025

This PR

this re-adds hook data support avoiding breaking changes in the binary (we introduced with v1.18.0)

Related Issues

#1472

###Notes

With v1.18.1 we reverted the hook data feature:

revert hook data to resolve bytecode incompatibility (#1613) (9845601)

Signed-off-by: Alexandra Oberaigner <alexandra.oberaigner@dynatrace.com>
@alexandraoberaigner alexandraoberaigner requested review from a team as code owners September 19, 2025 12:24
Copy link
Contributor

Summary of Changes

Hello @alexandraoberaigner, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request reintroduces and refines the support for HookData within the OpenFeature SDK, allowing individual hooks to maintain state across their lifecycle stages during a flag evaluation. This feature was previously removed in v1.18.0 and is now brought back without introducing breaking changes to the binary. The implementation ensures that each hook instance receives its own isolated data store, enabling more complex and stateful hook logic.

Highlights

  • Reintroduction of Hook Data Support: The pull request re-enables the ability for hooks to store and retrieve data across their various execution stages during a flag evaluation, a feature previously removed in v1.18.0.
  • New HookData Interface and Implementation: A new HookData interface is introduced, along with a DefaultHookData implementation, providing a mechanism for hooks to maintain state isolated to a single flag evaluation.
  • Enhanced HookContext: The HookContext class is updated to include a HookData field, allowing hooks to access their specific data store throughout the evaluation process.
  • Updated Hook Lifecycle Management: The HookSupport class is refactored to manage HookData instances, ensuring they are created per hook and passed correctly through the before, after, error, and finallyAfter stages.
  • Comprehensive Testing: New unit tests for HookData and HookContextWithData are added, and existing HookSupport tests are extended to validate the correct isolation and persistence of hook data across stages.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request re-introduces hook data support, which is a great addition for allowing hooks to maintain state across execution stages. The implementation is solid, with good test coverage for the new functionality. I've identified a few areas for improvement, mainly concerning code clarity, consistency, and a potential thread-safety issue in the HookData implementation. My detailed comments and suggestions are provided below.

Comment on lines +31 to +32
executeHooksUnchecked(
flagValueType, hookDataPairs, hookContext, (hook, ctx) -> hook.after(ctx, details, hints));
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The flagValueType parameter is passed to executeHooksUnchecked, but it's no longer used there because the hooks are pre-filtered in getHookDataPairs. You can remove this parameter from the call to simplify the code. This change should be made in conjunction with updating the executeHooksUnchecked method signature.

        executeHooksUnchecked(hookDataPairs, hookContext, (hook, ctx) -> hook.after(ctx, details, hints));

Comment on lines +76 to +77
Hook hook = hookDataPair.getLeft();
HookData hookData = hookDataPair.getRight();
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

To align with the suggested change in Pair.java for consistent naming, getLeft() and getRight() should be updated to getKey() and getValue().

                Hook hook = hookDataPair.getKey();
                HookData hookData = hookDataPair.getValue();

Comment on lines +103 to +107
private <T> void executeHooksUnchecked(
FlagValueType flagValueType,
List<Pair<Hook, HookData>> hookDataPairs,
HookContext hookContext,
BiConsumer<Hook<T>, HookContext> hookCode) {
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The flagValueType parameter is no longer used in this method, as the hookDataPairs list is already filtered. It can be removed to simplify the method signature.

    private <T> void executeHooksUnchecked(
            List<Pair<Hook, HookData>> hookDataPairs,
            HookContext hookContext,
            BiConsumer<Hook<T>, HookContext> hookCode) {

Comment on lines +110 to +111
Hook hook = hookDataPair.getLeft();
HookData hookData = hookDataPair.getRight();
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

To align with the suggested change in Pair.java for consistent naming, getLeft() and getRight() should be updated to getKey() and getValue().

                Hook hook = hookDataPair.getKey();
                HookData hookData = hookDataPair.getValue();

Comment on lines +129 to +130
Hook hook = hookDataPair.getLeft();
HookData hookData = hookDataPair.getRight();
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

To align with the suggested change in Pair.java for consistent naming, getLeft() and getRight() should be updated to getKey() and getValue().

            Hook hook = hookDataPair.getKey();
            HookData hookData = hookDataPair.getValue();

Comment on lines 176 to 177
hookContext =
HookContext.from(key, type, this.getMetadata(), provider.getMetadata(), new ImmutableContext(), defaultValue);
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The ImmutableContext class now provides a static EMPTY instance. It's better to use this constant instead of creating a new instance with new ImmutableContext(), especially in a performance-sensitive method like evaluateFlag. This improves consistency and avoids an unnecessary object allocation.

Suggested change
hookContext =
HookContext.from(key, type, this.getMetadata(), provider.getMetadata(), new ImmutableContext(), defaultValue);
hookContext =
HookContext.from(key, type, this.getMetadata(), provider.getMetadata(), ImmutableContext.EMPTY, defaultValue);

Comment on lines +12 to +18
public K getLeft() {
return key;
}

public V getRight() {
return value;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The getter methods getLeft() and getRight() are inconsistent with the field names (key, value), constructor parameters, and toString() implementation, which all use "key" and "value". For better consistency throughout this new class, consider renaming these getters to getKey() and getValue(). The usages in HookSupport.java will also need to be updated.

Suggested change
public K getLeft() {
return key;
}
public V getRight() {
return value;
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No strong opinion about it. @aepfli didn' we talk about this being good practice to use left/right?

@alexandraoberaigner alexandraoberaigner changed the title feat: add hook data support [DRAFT] feat: add hook data support Sep 19, 2025
@@ -0,0 +1,14 @@
package dev.openfeature.sdk;

public class HookContextWithData<T> extends HookContext<T> {
Copy link
Member

Choose a reason for hiding this comment

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

i am not sure we want to duplicate everything, i think the previous implementation with helper methods to access the object within is fine.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What do you mean with duplicate everything exactly? If I make HookContext a class instead of an interface I need to call its ctor and pass the data...or did I miss something here? or are you suggesting something else than inheritance?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thinking of using a decorator...do you think this is overkill?

Copy link
Member

Choose a reason for hiding this comment

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

i think a decorator pattern is a perfect fit for this. we can even leave the hookcontext as a @value class - and keep its immutability

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we cannot keep @value unfortunately since the decorator extends from HookContext...pls let me know if I missed something

Copy link
Member

Choose a reason for hiding this comment

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

i thought when you said decorator about something like

public class HookContextWithData<T> extends HookContext<T> {
    private final HookContext<T> hookContext;

    public HookContextWithData(HookContext hookcontext, HookData hookData) {
         this.hookdata = hookdata;
         this.hookContext = hookContext;
    }

    public getFlagKey() {
        return hookContext.getFlagKey()
    }

    // ... and for the others except hookdata

    public getHookData() {
          return hookdata;
    }
}

this way, we do not need to duplicate the other properties of HookContext, just the hookData is special to each of them.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I meant the decorator pattern 🙈

Copy link
Contributor Author

@alexandraoberaigner alexandraoberaigner Sep 22, 2025

Choose a reason for hiding this comment

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

With your example I'd need to add a parameterless ctor to HookContext - which is not feasable because of the @nonnull annotations on the members - as soon as we extend we need to call the super() ctor

Copy link
Member

Choose a reason for hiding this comment

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

but we could add a protected constructor to the hookContext, passing null for all the values, can't we? theoretically? (lombok makes everthing hard)

Copy link
Contributor Author

@alexandraoberaigner alexandraoberaigner Sep 22, 2025

Choose a reason for hiding this comment

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

Well, I guess we can ignore the hint... but feels wrong
image

return Optional.ofNullable(new ImmutableContext());
}
});
client.addHooks(new Hook<String>() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
client.addHooks(new Hook<String>() {
client.addHooks(new StringHook () {

I think we need to use the dedicated implementations for the filter for flag type to work

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As part of this PR or in general?

Signed-off-by: Alexandra Oberaigner <alexandra.oberaigner@dynatrace.com>
@alexandraoberaigner alexandraoberaigner changed the title [DRAFT] feat: add hook data support feat: add hook data support Sep 22, 2025
Copy link

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.

3 participants