Skip to content

fix: chain beforeSend hooks and fall back on exception instead of dropping - #697

Draft
posthog[bot] wants to merge 1 commit into
mainfrom
posthog/fix-before-send-hook-chaining
Draft

fix: chain beforeSend hooks and fall back on exception instead of dropping#697
posthog[bot] wants to merge 1 commit into
mainfrom
posthog/fix-before-send-hook-chaining

Conversation

@posthog

@posthog posthog Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Brings the Before Send Hook contract closer to compliance with the before-send-hook spec tracked in compliance/posthog-android.md note n17 (and the identical posthog-java.md note n2, since both posthog-android and posthog-server share this code path via PostHogStateless).

The spec requires that when multiple beforeSend hooks are registered, each one receives the previous hook's output, and that a hook throwing an exception must not silently drop the event.

PostHogStateless.buildEvent() had two bugs:

  1. Chaining bug — the loop called beforeSend.run(postHogEvent) (the original, pre-chain event) on every iteration instead of beforeSend.run(eventChecked) (the running value). With 2+ hooks, hook Bugfix: fix screen event name back to screen and change key to screen… #2 never saw hook Change screen key to $screen_name to match ios #1's mutation.
  2. Exception handling — a throwing hook returned null from buildEvent(), dropping the event entirely, rather than falling back to the last good (pre-exception) event.

Why this is backward-compatible

  • Fix (1) only changes behavior for callers with 2+ registered hooks where an earlier hook mutates the event — a case that was silently broken before, so there's no previously-correct behavior to regress.
  • Fix (2) is strictly less destructive than today's behavior: an event that used to be dropped on a hook exception is now delivered instead. No signature changes on any public API.

Added as a patch changeset for both posthog and posthog-server (they share the fixed code).

💚 How did you test it?

  • Added two new tests to PostHogBeforeSendTest.kt: one asserting a second hook sees the first hook's mutation, one asserting the event is still delivered (with the last-good properties) when a hook throws.
  • Updated the existing PostHogStatelessTest.kt > beforeSend error handling does not crash test, which had asserted the old (buggy) drop-on-exception behavior — it now asserts the event is delivered.
  • Ran ./gradlew :posthog:test :posthog-server:test, spotlessCheck, and :posthog:apiCheck :posthog-server:apiCheck locally — all green, no public API surface changes.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

🤖 Agent context

Autonomy: Fully autonomous

This PR was opened by an unattended daily SDK-compliance-implementer loop (Claude Code) that reads the compliance matrices in PostHog/sdk-specs and implements the single best-scoped, backward-compatible gap each run. This contract/gap was selected over ~90 other open gaps across 12 SDKs for being backward-compatible, high-impact (core capture pipeline correctness), narrowly scoped (a two-bug fix confined to one function), and not already covered by an open PR. No human directed this specific change — please assign an owner on review.


Created with PostHog Desktop

…of dropping

`buildEvent()` in `PostHogStateless` called `beforeSend.run(postHogEvent)` on every loop iteration instead of `beforeSend.run(eventChecked)`, so with 2+ registered hooks each one transformed the *original* event rather than the previous hook's output — mutations from an earlier hook were invisible to later hooks. A throwing hook also dropped the event outright (`return null`) instead of falling back to the last good value.

Both bugs affect `posthog-android` and `posthog-server`, which share this code via `PostHogStateless`.

Generated-By: PostHog Desktop
Task-Id: ea027967-6134-4b68-bcce-ef6a81e507b0
@github-actions

Copy link
Copy Markdown
Contributor

posthog-android Compliance Report

Date: 2026-08-11 16:25:40 UTC
Duration: 118725ms

✅ All Tests Passed!

46/46 tests passed


Capture Tests

29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields 435ms
Format Validation.Event Has Uuid 36ms
Format Validation.Event Has Lib Properties 36ms
Format Validation.Distinct Id Is String 30ms
Format Validation.Token Is Present 36ms
Format Validation.Custom Properties Preserved 37ms
Format Validation.Event Has Timestamp 33ms
Retry Behavior.Retries On 503 7034ms
Retry Behavior.Does Not Retry On 400 4028ms
Retry Behavior.Does Not Retry On 401 4032ms
Retry Behavior.Respects Retry After Header 7032ms
Retry Behavior.Implements Backoff 17030ms
Retry Behavior.Retries On 500 7021ms
Retry Behavior.Retries On 502 7022ms
Retry Behavior.Retries On 504 7023ms
Retry Behavior.Max Retries Respected 17040ms
Deduplication.Generates Unique Uuids 48ms
Deduplication.Preserves Uuid On Retry 7019ms
Deduplication.Preserves Uuid And Timestamp On Retry 12034ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry 7016ms
Deduplication.No Duplicate Events In Batch 58ms
Deduplication.Different Events Have Different Uuids 33ms
Compression.Sends Gzip When Enabled 26ms
Batch Format.Uses Proper Batch Structure 23ms
Batch Format.Flush With No Events Sends Nothing 16ms
Batch Format.Multiple Events Batched Together 45ms
Error Handling.Does Not Retry On 403 4025ms
Error Handling.Does Not Retry On 413 4024ms
Error Handling.Retries On 408 5029ms

Feature_Flags Tests

17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id 39ms
Request Payload.Flags Request Uses V2 Query Param 32ms
Request Payload.Flags Request Hits Flags Path Not Decide 25ms
Request Payload.Flags Request Omits Authorization Header 30ms
Request Payload.Token In Flags Body Matches Init 27ms
Request Payload.Groups Round Trip 28ms
Request Payload.Groups Default To Empty Object 27ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False 28ms
Request Payload.Disable Geoip Omitted Defaults To False 25ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key 30ms
Request Lifecycle.No Flags Request On Init Alone 14ms
Request Lifecycle.No Flags Request On Normal Capture 28ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests 51ms
Request Lifecycle.Mock Response Value Is Returned To Caller 26ms
Retry Behavior.Retries Flags On 502 329ms
Retry Behavior.Retries Flags On 504 329ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event 28ms

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.

0 participants