Skip to content

feat!: implement HookData - #140

Open
jbovet wants to merge 1 commit into
open-feature:mainfrom
jbovet:feat/hook-data
Open

feat!: implement HookData #140
jbovet wants to merge 1 commit into
open-feature:mainfrom
jbovet:feat/hook-data

Conversation

@jbovet

@jbovet jbovet commented Aug 4, 2026

Copy link
Copy Markdown

This PR

  • Implements Hook Data spec 4.6: a per-hook, per-evaluation key-value store on HookContext (context.data) that lets a hook carry state across its own stages. e.g. start a timer or open a telemetry span in before and finish it in after/finally
  • Adds HookData with set / get::<T> / take::<T> / with_mut::<T,_>
  • Each hook instance gets its own HookData, shared across that hook's beforeafter/errorfinally stages and isolated from every other hook
  • Adds a runnable example and a README "Hook data" subsection

Related Issues

Closes #102

Notes

  • HookContext keeps PartialEq/Debug via manual impls that exclude the type-erased data field, so no trait impls are lost

Follow-up Tasks

  • None

How to test

cargo test --features test-util
cargo run --example hook_data

Also verified: cargo clippy -- -D warnings and cargo fmt --check pass.

@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 923a9328-90c2-43d2-8874-2eddbbdeb43d

📥 Commits

Reviewing files that changed from the base of the PR and between 604862c and 7b3121e.

📒 Files selected for processing (3)
  • README.md
  • src/hooks/data.rs
  • src/hooks/mod.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The SDK adds HookData for typed, thread-safe state sharing within one hook evaluation. HookContext exposes this data, and the client preserves isolated data across before, after, error, and finally stages. Documentation and a timing example demonstrate usage.

Changes

Hook data support

Layer / File(s) Summary
HookData store and operations
src/hooks/data.rs
Adds typed storage, mutation, ownership transfer, collection operations, mutex-poison recovery, and comprehensive tests.
HookContext data integration
src/hooks/mod.rs
Exports HookData, adds it to HookContext, updates context behavior, and tests persistence and isolation.
Hook lifecycle data propagation
src/api/client.rs, examples/hook_data.rs, README.md
Creates per-hook data, passes it through all lifecycle stages, and documents a runnable timing-hook example.

Estimated code review effort: 4 (Complex) | ~45 minutes

Merge Risk: ⚪ Minimal · up to 7b312

The PR adds per-hook state sharing across hook stages with documented examples and no actionable merge-blocking risk remains.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 52 functions across 2 files. (1 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: implementing HookData. It is concise and related to the changeset.
Description check ✅ Passed The description accurately covers HookData, lifecycle sharing, isolation, compatibility, documentation, examples, testing, and issue #102.
Linked Issues check ✅ Passed The implementation satisfies issue #102 by adding typed, mutable, thread-safe HookData; integrating it into HookContext; preserving data across hook stages; isolating data per hook and evaluation; and…
Out of Scope Changes check ✅ Passed The changes are within scope for issue #102. The README updates, runnable example, HookData implementation, hook lifecycle integration, and related tests all support the stated objective.
Full details: Linked Issues check

Explanation

The implementation satisfies issue #102 by adding typed, mutable, thread-safe HookData; integrating it into HookContext; preserving data across hook stages; isolating data per hook and evaluation; and adding tests, documentation, and an example.

Full details: Docstring Coverage

Explanation

Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 52 functions across 2 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/api/client.rs (1)

332-349: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider renaming the closures to avoid shadowing the runner methods.

before_hooks and after_hooks are also method names on Client (lines 403 and 434). Line 352 reads self.before_hooks(before_hooks(), ...), where the two identifiers mean different things. The code compiles because method-call syntax and local bindings resolve separately. Names such as hooks_in_before_order and hooks_in_after_order remove the ambiguity.

♻️ Proposed rename
-        // INFO: (hook, its data) in before order: API(global), Client, Invocation, Provider
-        let before_hooks = || ordered_hooks.iter().copied().zip(hook_data.iter());
-
-        // INFO: Hooks called after the resolution are in reverse order
-        // Provider, Invocation, Client, API(global)
-        let after_hooks = || before_hooks().rev();
+        // INFO: (hook, its data) in before order: API(global), Client, Invocation, Provider
+        let hooks_in_before_order = || ordered_hooks.iter().copied().zip(hook_data.iter());
+
+        // INFO: Hooks called after the resolution are in reverse order
+        // Provider, Invocation, Client, API(global)
+        let hooks_in_after_order = || hooks_in_before_order().rev();

Update the four call sites at lines 352, 361, 364, 380, 384, 392, and 397 accordingly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/client.rs` around lines 332 - 349, Rename the local closures
before_hooks and after_hooks to unambiguous names such as hooks_in_before_order
and hooks_in_after_order, then update every closure invocation in the
surrounding hook evaluation flow while leaving the Client::before_hooks and
Client::after_hooks method calls unchanged.
README.md (1)

244-247: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

State that hook data is recreated for each evaluation.

The prose describes isolation between hooks. It does not state isolation between evaluations. The client creates a new HookData per hook per evaluation, and src/hooks/mod.rs verifies this in hook_data_not_shared_between_evaluations. Readers who register one hook instance on a client need this fact.

📝 Proposed wording
 A hook can carry state across its own stages within a single evaluation via `context.data`.
 Each hook instance gets its own `HookData`, shared across that hook's `before`, `after`, `error`, and `finally`
 stages and isolated from every other hook. This is the recommended place to stash state such as a start
 timestamp or a telemetry span opened in `before` and consumed in `after`/`finally`.
+A fresh `HookData` is created for every evaluation, so a hook instance reused across evaluations never
+observes state from a previous evaluation.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 244 - 247, Update the hook state documentation around
context.data to explicitly state that each hook instance receives a newly
created HookData for every evaluation, while retaining the existing isolation
across hook instances and sharing across that evaluation’s stages.
src/hooks/data.rs (1)

140-292: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a test for the poison-recovery path.

The tests do not exercise the PoisonError::into_inner branch in lock. A panic inside a with_mut closure poisons the mutex. A test proves the store stays usable afterwards.

🧪 Proposed test
#[test]
fn store_remains_usable_after_panic_in_with_mut() {
    let data = HookData::new();
    data.set("key", 1_i64);

    let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
        data.with_mut::<i64, _>("key", |_| panic!("boom"));
    }));
    assert!(result.is_err());

    // The mutex is poisoned, but `lock` recovers the guard.
    assert_eq!(data.get::<i64>("key"), Some(1));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/hooks/data.rs` around lines 140 - 292, Add a test in the `tests` module
covering mutex poison recovery: invoke `with_mut` inside
`std::panic::catch_unwind` with an `AssertUnwindSafe` closure that panics,
assert the panic is captured, then verify `get` still returns the stored value
through the recovered lock.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/hooks/mod.rs`:
- Around line 1191-1226: Update the isolation test around IsolationHook to track
each hook’s after invocation count, using the same guard pattern as
hook_data_not_shared_between_evaluations. Assert that both hooks ran before
checking violations.is_empty(), so the test cannot pass when hooks are never
invoked.
- Around line 100-112: Document the public HookContext struct-literal breaking
change near its definition, explicitly instructing downstream users to add the
new data field and initialize it with HookData::default().

---

Nitpick comments:
In `@README.md`:
- Around line 244-247: Update the hook state documentation around context.data
to explicitly state that each hook instance receives a newly created HookData
for every evaluation, while retaining the existing isolation across hook
instances and sharing across that evaluation’s stages.

In `@src/api/client.rs`:
- Around line 332-349: Rename the local closures before_hooks and after_hooks to
unambiguous names such as hooks_in_before_order and hooks_in_after_order, then
update every closure invocation in the surrounding hook evaluation flow while
leaving the Client::before_hooks and Client::after_hooks method calls unchanged.

In `@src/hooks/data.rs`:
- Around line 140-292: Add a test in the `tests` module covering mutex poison
recovery: invoke `with_mut` inside `std::panic::catch_unwind` with an
`AssertUnwindSafe` closure that panics, assert the panic is captured, then
verify `get` still returns the stored value through the recovered lock.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 99a03dba-52d0-45a3-883b-93fb2f9de8c9

📥 Commits

Reviewing files that changed from the base of the PR and between abe65b8 and 604862c.

📒 Files selected for processing (5)
  • README.md
  • examples/hook_data.rs
  • src/api/client.rs
  • src/hooks/data.rs
  • src/hooks/mod.rs

Comment thread src/hooks/mod.rs
Comment thread src/hooks/mod.rs
@jbovet jbovet changed the title feat: implement HookData feat!: implement HookData Aug 4, 2026
Adds a per-hook, per-evaluation HookData store on HookContext, shared
across that hook's before/after/error/finally stages and isolated from
other hooks. The Hook trait signatures are unchanged, so existing hooks
are unaffected.

Includes README updates and panic-recovery test coverage.

BREAKING CHANGE: `HookContext` gains a public `data` field. Code that
constructs `HookContext` directly must add `data: HookData::default()`
(HookContext does not implement Default, and HookData is not exposed as
a public constant).

Signed-off-by: Jose Bovet Derpich <jose.bovet@gmail.com>
@jbovet

jbovet commented Aug 27, 2026

Copy link
Copy Markdown
Author

Hi team, I've pushed updates addressing review feedback @beeme1mr @gruebel could you review or tag the right person?

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.

Add Hook Data Support

1 participant