Skip to content

feat: add HookDecorator so a hook wrapper forwards the stages it does not override - #387

Draft
abelonogov-ld wants to merge 1 commit into
mainfrom
andrey/hook-decorator
Draft

feat: add HookDecorator so a hook wrapper forwards the stages it does not override#387
abelonogov-ld wants to merge 1 commit into
mainfrom
andrey/hook-decorator

Conversation

@abelonogov-ld

Copy link
Copy Markdown
Contributor

Requirements

  • I have added test coverage for new or changed functionality
  • I have followed the repository's pull request submission guidelines
  • I have validated my changes against all supported platform versions

Related issues

Follow-up to #380 (per-hook evaluation exposure deduplication). HookDecorator was part of that work and was removed from it during review; this restores it without the getDelegate() accessor that the review objected to.

Describe the solution you've provided

Hook's stages each have a default implementation that does nothing, so a wrapper that omits a stage swallows it rather than passing it on. Wrapping a hook therefore meant reimplementing beforeIdentify, afterIdentify, and afterTrack as pure forwarding just to keep them working, and a wrapper that forgot one dropped it silently.

HookDecorator forwards every stage to the hook it wraps, so a subclass overrides only the stages it changes. DedupingHook extends it and is down to the three stages it actually changes, having shed getMetadata, afterIdentify, afterTrack, and its duplicated nameOf helper.

Two properties worth calling out:

  • The wrapped hook is private to the decorator, reachable only through super. There is no accessor, so nothing can unwrap a decorator and act on what it finds.
  • beforeIdentify, afterIdentify, and afterTrack are @CallSuper, so Android Lint reports (at severity Error) an override that stops forwarding one. Without it, a decorator wrapping a DedupingHook could keep it from being told to forget what it has reported, and it would go on suppressing evaluations across an identify. The evaluation stages are deliberately not annotated, because suppressing an evaluation series is what a decorator is for, and lint's check is satisfied by a conditional super call.

Also drops the remaining suggestions to subclass from the DedupingHook and EvaluationExposureDeduper docs, which the iOS SDK had already removed, and aligns their code samples.

Describe alternatives you've considered

  • Documenting the forwarding requirement instead of providing a base class. That is the status quo, and it fails silently: the compiler accepts a wrapper that omits a stage.
  • Keeping the protected getDelegate() accessor the earlier version had. It invites code to unwrap a decorator rather than treat it as the hook it stands in for, so forwarding now goes through super only.
  • Making the stages abstract, which would force a subclass to implement all six, i.e. the boilerplate this removes.

Additional context

Verified that the lint annotation works rather than assuming: a temporary decorator overriding afterIdentify and afterTrack without forwarding produced MissingSuperCall errors ("Overriding method should call super.afterIdentify"); with it removed, lintDebug reports none, and javap confirms the annotations are written into HookDecorator.class so a consumer's lint reads them from the AAR.

DedupingHookTest gains aDecoratorForwardsTheStagesItDoesNotOverride, and its CountingDecorator helper now overrides only the evaluation stages, the way a customer's wrapper would. Full module unit tests and javadoc pass.

The same change is going into the iOS SDK for parity; Swift has no @CallSuper equivalent, so there the failure mode is documented instead.

Made with Cursor

… not override

A hook's stages each default to doing nothing, so a wrapper that omits one
swallows it rather than passing it on: wrapping a hook meant reimplementing
beforeIdentify, afterIdentify, and afterTrack as pure forwarding just to keep
them working, and a wrapper that forgot dropped them silently. HookDecorator
forwards every stage, so a subclass overrides only the stages it changes.
DedupingHook extends it and is down to the three it changes.

The wrapped hook is private to the decorator, reachable only through super, so
nothing can unwrap a decorator to decide what to do with what it finds. The
identify and track stages are @callsuper, so lint reports an override that stops
forwarding one: a decorator wrapping a DedupingHook could otherwise keep it from
being told to forget what it has reported, and it would go on suppressing across
an identify. The evaluation stages are not, because suppressing an evaluation
series is what a decorator is for.

Also drops the remaining suggestions to subclass from the deduper docs, which the
iOS SDK had already removed.

Co-authored-by: Cursor <cursoragent@cursor.com>
@abelonogov-ld
abelonogov-ld requested a review from a team as a code owner August 13, 2026 20:31
@abelonogov-ld
abelonogov-ld marked this pull request as draft August 13, 2026 22:31
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.

1 participant