Skip to content

feat: Implement BeforeSend hook for message modification and filtering.#154

Open
hyeomans wants to merge 2 commits intoPostHog:masterfrom
hyeomans:hyeomans-implement-before-send-go
Open

feat: Implement BeforeSend hook for message modification and filtering.#154
hyeomans wants to merge 2 commits intoPostHog:masterfrom
hyeomans:hyeomans-implement-before-send-go

Conversation

@hyeomans
Copy link

Implements a BeforeSend hook that allows users to modify or drop messages before they're sent to PostHog, matching the Ruby SDK's before_send functionality.

Motivation

Users need the ability to:

  • Scrub sensitive data from events before sending (e.g., PII, passwords, credit card numbers)
  • Add metadata to all events (e.g., environment, app version, custom properties)
  • Filter events based on conditions (e.g., drop debug events in production)
  • Implement sampling to reduce event volume
  • Transform data before it reaches PostHog

This is a common pattern across SDKs - the Ruby SDK already has this feature, and now Go does too.

Changes

Core Implementation (posthog.go)

  1. extractEventName() helper - Extracts descriptive event names for logging
  2. processBeforeSend() method - Core hook processing with:
    • Panic recovery (user hooks can't crash the SDK)
    • Nil return handling (drop messages)
    • Message validation after modification
    • Fallback to original message on errors
  3. Hook integration in Enqueue() - Called after type-specific enrichment but before APIfy(), so users work with strongly-typed Message objects

Documentation (config.go)

  • Comprehensive documentation with 3 practical examples:
    • Adding custom properties to all events
    • Filtering out debug events
    • Scrubbing sensitive data from multiple message types

Example Usage

Add metadata to all events

client, _ := posthog.NewWithConfig("api-key", posthog.Config{
    BeforeSend: func(msg posthog.Message) posthog.Message {
        if capture, ok := msg.(posthog.Capture); ok {
            if capture.Properties == nil {
                capture.Properties = posthog.NewProperties()
            }
            capture.Properties["environment"] = "production"
            capture.Properties["app_version"] = "1.2.3"
            return capture
        }
        return msg
    },
})

// Filter out debug events

client, _ := posthog.NewWithConfig("api-key", posthog.Config{
    BeforeSend: func(msg posthog.Message) posthog.Message {
        if capture, ok := msg.(posthog.Capture); ok {
            if capture.Event == "debug_event" {
                return nil  // Drop this event
            }
        }
        return msg
    },
})

// Scrub sensitive data

client, _ := posthog.NewWithConfig("api-key", posthog.Config{
    BeforeSend: func(msg posthog.Message) posthog.Message {
        switch m := msg.(type) {
        case posthog.Capture:
            if m.Properties != nil {
                delete(m.Properties, "password")
                delete(m.Properties, "credit_card")
            }
            return m
        case posthog.Identify:
            if m.Properties != nil {
                delete(m.Properties, "ssn")
            }
            return m
        default:
            return msg
        }
    },
})

Error Handling

The implementation is resilient to buggy user hooks:

Scenario Behavior
Hook panics Panic caught, error logged, original message sent
Hook returns nil Warning logged, message dropped
Hook returns invalid message Error logged, original message sent
Hook not configured No overhead, message passes through unchanged

Design Decisions

Hook Placement

After enrichment, before APIfy() - This ensures:

  • User sees fully enriched messages (timestamp, type, properties merged)
  • User works with strongly-typed objects (Capture, Identify, etc.) not raw JSON
  • Modifications are preserved during serialization

Ruby SDK Compatibility

Ruby SDK Go SDK
before_send.call(action) c.BeforeSend(msg)
Returns nil to drop Returns nil to drop
rescue StandardError defer recover() panic handling
Logs warning on nil Warnf("message dropped...")
Logs error on exception Errorf("panic in BeforeSend...")
Uses original on error Returns original message

Breaking Changes

  • None

Checklist

  • Implementation complete
  • Comprehensive tests added
  • All tests pass
  • No regressions in existing tests
  • Documentation added with examples
  • Backward compatible (no breaking changes)
  • Ruby SDK behavior matched

Related Issues

Closes #135

Implements a `BeforeSend` hook that allows users to modify or drop messages before they're sent to PostHog, matching the Ruby SDK's `before_send` functionality.
@hyeomans
Copy link
Author

Hey PostHog team! 👋

I noticed issue #135 about implementing a BeforeSend hook (similar to the Ruby SDK) and thought I'd take a stab at it.

What I've implemented:

  • Full BeforeSend hook functionality with panic recovery and validation
  • Comprehensive test cases (all passing)
  • Ruby SDK behavior parity
  • Detailed documentation with practical examples
  • No breaking changes, backward compatible

The implementation allows users to modify/filter events, scrub sensitive data, add metadata, and implement sampling - all before events are sent to PostHog.

Happy to make any changes or add more tests based on your feedback!

Full transparency: I recently applied for a Product Engineer position and have a call with your recruiter this Thursday. I wanted to contribute something meaningful to show my interest in PostHog and demonstrate that I can ship quality work. Regardless of the outcome, I hope this is useful for the community! 🚀

Looking forward to your feedback!

@rafaeelaudibert
Copy link
Member

Hey @hyeomans! Thanks for this! Mind fixing the merge conflicts before we can take a look at this?

// }
// },
// })
BeforeSend func(Message) Message
Copy link

@ioannisj ioannisj Jan 21, 2026

Choose a reason for hiding this comment

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

Was working recently on flutter implementation of this and one of the feedback I got is that other sdks support a single function and or an array of before functions so that they can be chained. Should we be consistent here as well? cc @marandaneto

Copy link
Author

Choose a reason for hiding this comment

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

If I may suggest: a BeforeSendHooks []BeforeSendFunc function could be added in a follow-up PR.

That would let consumers register either a single BeforeSend hook or multiple hooks that run in order.

We could also support defining both (BeforeSend + BeforeSendHooks) and execute them sequentially inside processBeforeSend (dropping the message if any hook returns nil, and preserving the existing panic/validation handling).

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