Skip to content

feat: add hooks support#240

Merged
keelerm84 merged 7 commits intomainfrom
mk/sdk-1255/hooks-support
Apr 22, 2026
Merged

feat: add hooks support#240
keelerm84 merged 7 commits intomainfrom
mk/sdk-1255/hooks-support

Conversation

@keelerm84
Copy link
Copy Markdown
Member

@keelerm84 keelerm84 commented Apr 21, 2026

Implements the Hooks specification (evaluation series + track series) to
give users and LaunchDarkly integration packages an extension point for
observing flag evaluations and track calls.

  • Public API under LaunchDarkly\Hooks: Hook (abstract base class),
    Metadata, EvaluationSeriesContext, TrackSeriesContext.
  • LDClient accepts a hooks option at construction and exposes
    addHook() for runtime registration.
  • beforeEvaluation / afterEvaluation fire around every variation,
    variationDetail, and migrationVariation call, with afterEvaluation
    running in reverse registration order per spec 1.3.4.
  • afterTrack fires after a custom event is enqueued. It does not fire
    for trackMigrationOperation (not a custom event) or for track calls
    rejected due to an invalid context.
  • Hook exceptions are caught and logged at the error level; when a stage
    errors, subsequent stages receive the previous successful stage's data
    (spec 1.3.7.1).
  • environmentId is intentionally omitted from the series contexts until
    the SDK can populate it reliably; the spec lists it as optional.
  • Identify series (1.4) and configuration handlers (1.5) are skipped
    because they are client-side only.
  • Contract test service declares evaluation-hooks and track-hooks
    capabilities and wires harness-provided hook configs through a new
    PostingHook implementation.

Note

Medium Risk
Adds synchronous hook execution around flag evaluation and custom event tracking, which can affect evaluation latency/ordering and introduces new error-isolation/logging paths in core client flows. Risk is mitigated by no-op defaults, hasHooks() fast-path, and tests covering ordering and exception handling.

Overview
Adds a new public hooks API (LaunchDarkly\Hooks\Hook, Metadata, EvaluationSeriesContext, TrackSeriesContext) and wires hook execution into LDClient.

LDClient now accepts a hooks constructor option and a runtime addHook() method; it runs beforeEvaluation/afterEvaluation around variation, variationDetail, and migrationVariation (with reverse order for afterEvaluation), and runs afterTrack after successful track() calls. Hook stage failures are caught/logged and isolated via the new internal Impl\Hooks\HookRunner, and evaluation error handling is widened to catch \Throwable.

Updates the contract test service to advertise evaluation-hooks/track-hooks and to instantiate harness-configured hooks via a new PostingHook, and adds unit tests for ordering, data propagation, and exception isolation. Also updates the Psalm baseline for the new code and version.

Reviewed by Cursor Bugbot for commit c53211b. Bugbot is set up for automated code reviews on this repo. Configure here.

Implements the Hooks specification (evaluation series + track series) to
give users and LaunchDarkly integration packages an extension point for
observing flag evaluations and track calls.

- Public API under `LaunchDarkly\Hooks`: `Hook` (abstract base class),
  `Metadata`, `EvaluationSeriesContext`, `TrackSeriesContext`.
- `LDClient` accepts a `hooks` option at construction and exposes
  `addHook()` for runtime registration.
- `beforeEvaluation` / `afterEvaluation` fire around every `variation`,
  `variationDetail`, and `migrationVariation` call, with `afterEvaluation`
  running in reverse registration order per spec 1.3.4.
- `afterTrack` fires after a custom event is enqueued. It does not fire
  for `trackMigrationOperation` (not a custom event) or for `track` calls
  rejected due to an invalid context.
- Hook exceptions are caught and logged at the error level; when a stage
  errors, subsequent stages receive the previous successful stage's data
  (spec 1.3.7.1).
- `environmentId` is intentionally omitted from the series contexts until
  the SDK can populate it reliably; the spec lists it as optional.
- Identify series (1.4) and configuration handlers (1.5) are skipped
  because they are client-side only.
- Contract test service declares `evaluation-hooks` and `track-hooks`
  capabilities and wires harness-provided hook configs through a new
  `PostingHook` implementation.
@keelerm84 keelerm84 requested a review from a team as a code owner April 21, 2026 19:55
@keelerm84 keelerm84 requested a review from kinyoklion April 21, 2026 19:55
CI flagged an UnusedBaselineEntry for NullEventProcessor's UnusedClass
error. The entry was added when I regenerated the baseline locally, but
CI's psalm doesn't produce that error — so the baseline entry is stale
from CI's perspective. Remove it to match what CI sees.

The root cause of the local/CI divergence appears to be environmental
(same psalm 6.16.1, --no-cache, same composer.lock) but isn't worth
chasing for this PR.
Comment thread test-service/PostingHook.php Outdated
Co-authored-by: Ryan Lamb <4955475+kinyoklion@users.noreply.github.com>
Comment thread tests/Hooks/HookRunnerTest.php Outdated
RecordingHook took its shared log as a by-reference constructor-promoted
parameter (`private ?array &$sharedLog`). PHP CS Fixer 3.80.0 — the
version CI ends up running under the prefer-lowest matrix entry — has a
bug in its visibility_required fixer that mutates this into invalid
syntax (`private ?array &public $sharedLog`), breaking `make lint`.

Swap the reference for a small CallLog value object that holds a
mutable calls array. Cross-hook ordering tests read `$shared->calls`
instead of relying on PHP array-by-reference semantics.
The previous assertion indexed `$a->calls[0]` (the beforeEvaluation
record), whose `data` field is always `[]` because HookRunner always
passes empty data into every hook's beforeEvaluation. That made the test
trivially pass regardless of whether the spec 1.3.7.1 fallback logic
actually worked.

Switch the assertion to `$a->calls[1]` (the afterEvaluation record),
where `data` is the value HookRunner chose to pass forward after
beforeEvaluation errored — which is what the test is meant to verify.
Also add explicit stage-name assertions so the intent is clear.
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 250a6a2. Configure here.

Comment thread src/LaunchDarkly/LDClient.php
variationDetailInternal previously called evaluateInternal unguarded,
so a \Throwable that is not a \Exception (e.g. a \TypeError from a
downstream component) would bypass evaluateInternal's catch(\Exception)
and skip afterEvaluation, violating spec 1.3.4.

Wrap the evaluation + afterEvaluation call in try/finally with a
fallback EvaluationDetail initialized to EXCEPTION_ERROR. On normal
completion, $detail is overwritten with the real detail before the
finally runs; on propagation, the finally sees the fallback and
afterEvaluation still fires before the Throwable continues up the stack.
PHP's \Error hierarchy (TypeError, ArgumentCountError, etc.) extends
\Throwable but not \Exception, so a downstream component raising a
TypeError during evaluation bypassed the safety net and propagated up —
also skipping afterEvaluation, violating spec 1.3.4.

Broadening the catch to \Throwable means the SDK always returns a valid
detail (with an EXCEPTION_ERROR reason when the underlying evaluation
fails unexpectedly), which keeps variation calls non-throwing and lets
variationDetailInternal keep its straight-line hook wiring.

Replaces the try/finally wrapping in variationDetailInternal from the
previous commit with the simpler root-cause fix.
@keelerm84 keelerm84 merged commit 77a644d into main Apr 22, 2026
18 checks passed
@keelerm84 keelerm84 deleted the mk/sdk-1255/hooks-support branch April 22, 2026 18:36
keelerm84 pushed a commit that referenced this pull request Apr 28, 2026
🤖 I have created a release *beep* *boop*
---


##
[6.8.0](6.7.0...6.8.0)
(2026-04-28)


### Features

* add hooks support
([#240](#240))
([77a644d](77a644d))
* expose environmentId on EvaluationSeriesContext
([#242](#242))
([5fcbce4](5fcbce4))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.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.

2 participants