Repository: honeybadger-io/honeybadger-elixir
Pull request #602 (2025-04-02 16:30:04)

Add `event_filter/1`
Gives users the ability to modify or remove events for _all_ `Honeybadger.event` calls. I also implemented `Honeybadger.EventFilter.Mixin` for easier implementation.


Patch:
diff --git a/README.md b/README.md
index 67426bf..2b06a79 100644
--- a/README.md
+++ b/README.md
@@ -228,31 +228,41 @@ config :honeybadger, insights_config: %{
 
 ### Event Filtering
 
-You can filter out or customize automatic events sent to Honeybadger Insights
-by implementing the `Honeybadger.EventFilter` behaviour:
-
-```elixir
-defmodule MyApp.EventFilter do
-  @behaviour Honeybadger.EventFilter
-
-  @default_filter &Honeybadger.EventFilter.Default.filter/3
-
-  @impl Honeybadger.EventFilter
-  # Pattern match on the event type name
-  def filter_telemetry_event(data, raw, [:phoenix, :live_view, :update, :stop] = event) do
-    if data.duration < 100 do
-      data
-      |> Map.put(:slow, true)
-      # You might want to run the event through the default filter to remove
-      # any sensitive data
-      |> @default_filter.(raw, event)
-    else
-      # Ignore events that are fast
-      nil
+You can filter out or customize events sent to Honeybadger Insights by
+using the `Honeybadger.EventFilter.Mixin` module. You can customize both
+the event built from telemetry data (`filter_telemetry_event/3`) and the event
+right before it is sent to Honeybadger (`filter_event/1`):
+
+```elixir
+defmodule MyApp.MyFilter do
+  use Honeybadger.EventFilter.Mixin
+
+  # Drop analytics events by returning nil
+  def filter_event(%{event_type: "analytics"} = _event), do: nil
+
+  # Anonymize user data in login events
+  def filter_event(%{event_type: "login"} = event) do
+    event
+    |> update_in([:data, :user_email], fn _ -> "[REDACTED]" end)
+    |> put_in([:metadata, :filtered], true)
+  end
+
+  # For telemetry events, you can customize while still applying default filtering
+  def filter_telemetry_event(data, raw, event) do
+    # First apply default filtering
+    filtered_data = apply_default_telemetry_filtering(data)
+
+    # Then apply custom logic
+    case event do
+      [:auth, :login, :start] ->
+        Map.put(filtered_data, :security_filtered, true)
+      _ ->
+        filtered_data
     end
   end
 
-  def filter(data, raw, event), do: @default_filter.(data, raw, event)
+  # Keep all other events as they are
+  def filter_event(event), do: event
 end
 ```
 
diff --git a/lib/honeybadger.ex b/lib/honeybadger.ex
index c444705..7b51c9c 100644
--- a/lib/honeybadger.ex
+++ b/lib/honeybadger.ex
@@ -361,13 +361,16 @@ defmodule Honeybadger do
 
   @spec event(map()) :: :ok
   def event(event_data) do
-    ts = DateTime.utc_now(:millisecond) |> DateTime.to_iso8601()
+    event_data
+    |> Map.put_new(:ts, DateTime.utc_now(:millisecond) |> DateTime.to_iso8601())
+    |> maybe_add_request_id()
+    |> event_filter()
+    |> send_event()
+  end
 
-    data =
-      event_data
-      |> Map.put_new(:ts, ts)
-      |> maybe_add_request_id()
+  defp send_event(nil), do: :ok
 
+  defp send_event(data) do
     if get_env(:events_worker_enabled) do
       EventsWorker.push(data)
     else
@@ -375,6 +378,14 @@ defmodule Honeybadger do
     end
   end
 
+  defp event_filter(map) do
+    if get_env(:event_filter) do
+      get_env(:event_filter).filter_event(map)
+    else
+      map
+    end
+  end
+
   @doc """
   Stores a breadcrumb item.
 
diff --git a/lib/honeybadger/event_filter.ex b/lib/honeybadger/event_filter.ex
index d1e6198..481151b 100644
--- a/lib/honeybadger/event_filter.ex
+++ b/lib/honeybadger/event_filter.ex
@@ -25,4 +25,18 @@ defmodule Honeybadger.EventFilter do
   """
   @callback filter_telemetry_event(data :: map(), raw_event :: map(), event :: [atom(), ...]) ::
               map() | nil
+
+  @doc """
+  Filters all Insights events.
+
+  ## Parameters
+
+    * `event` - The current event map
+
+  ## Returns
+
+    The filtered metadata map that will be sent to Honeybadger or `nil` to skip
+    the event.
+  """
+  @callback filter_event(event :: map()) :: map() | nil
 end
diff --git a/lib/honeybadger/event_filter/default.ex b/lib/honeybadger/event_filter/default.ex
index 285520c..42a063f 100644
--- a/lib/honeybadger/event_filter/default.ex
+++ b/lib/honeybadger/event_filter/default.ex
@@ -1,22 +1,3 @@
 defmodule Honeybadger.EventFilter.Default do
-  @behaviour Honeybadger.EventFilter
-
-  alias Honeybadger.Utils
-
-  def filter_telemetry_event(data, _raw, _event) do
-    data
-    |> disable(:filter_disable_url, :url)
-    |> disable(:filter_disable_session, :session)
-    |> disable(:filter_disable_assigns, :assigns)
-    |> disable(:filter_disable_params, :params)
-    |> Utils.sanitize(remove_filtered: true)
-  end
-
-  defp disable(meta, config_key, map_key) do
-    if Honeybadger.get_env(config_key) do
-      Map.drop(meta, [map_key])
-    else
-      meta
-    end
-  end
+  use Honeybadger.EventFilter.Mixin
 end
diff --git a/lib/honeybadger/event_filter/mixin.ex b/lib/honeybadger/event_filter/mixin.ex
new file mode 100644
index 0000000..20a2c83
--- /dev/null
+++ b/lib/honeybadger/event_filter/mixin.ex
@@ -0,0 +1,80 @@
+defmodule Honeybadger.EventFilter.Mixin do
+  @moduledoc """
+  Provides a mixin for implementing the Honeybadger.EventFilter behaviour.
+
+  If you need to implement custom filtering for events, you can define your own filter module
+  and register it in the config. For example, to filter based on event_type:
+
+      defmodule MyApp.MyFilter do
+        use Honeybadger.EventFilter.Mixin
+
+        # Drop analytics events by returning nil
+        def filter_event(%{event_type: "analytics"} = _event), do: nil
+
+        # Anonymize user data in login events
+        def filter_event(%{event_type: "login"} = event) do
+          event
+          |> update_in([:data, :user_email], fn _ -> "[REDACTED]" end)
+          |> put_in([:metadata, :filtered], true)
+        end
+
+        # For telemetry events, you can customize while still applying default filtering
+        def filter_telemetry_event(data, raw, event) do
+          # First apply default filtering
+          filtered_data = apply_default_telemetry_filtering(data)
+
+          # Then apply custom logic
+          case event do
+            [:auth, :login, :start] ->
+              Map.put(filtered_data, :security_filtered, true)
+            _ ->
+              filtered_data
+          end
+        end
+
+        # Keep all other events as they are
+        def filter_event(event), do: event
+      end
+
+  And set the configuration to:
+
+      config :honeybadger,
+        event_filter: MyApp.MyFilter
+
+  Return `nil` from `filter_event/1` to prevent the event from being processed.
+  If you override `filter_telemetry_event/3`, you can still apply the default
+  filtering by calling `apply_default_telemetry_filtering/1`.
+  """
+
+  defmacro __using__(_) do
+    quote location: :keep do
+      @behaviour Honeybadger.EventFilter
+
+      def filter_event(event), do: event
+
+      def filter_telemetry_event(data, _raw, _event) do
+        apply_default_telemetry_filtering(data)
+      end
+
+      # Default filtering implementation as a public function
+      def apply_default_telemetry_filtering(data) do
+        data
+        |> disable(:filter_disable_url, :url)
+        |> disable(:filter_disable_session, :session)
+        |> disable(:filter_disable_assigns, :assigns)
+        |> disable(:filter_disable_params, :params)
+        |> Honeybadger.Utils.sanitize(remove_filtered: true)
+      end
+
+      defp disable(meta, config_key, map_key) do
+        if Honeybadger.get_env(config_key) do
+          Map.drop(meta, [map_key])
+        else
+          meta
+        end
+      end
+
+      defoverridable Honeybadger.EventFilter
+    end
+  end
+end


Test patch:
diff --git a/test/honeybadger/insights/base_test.exs b/test/honeybadger/insights/base_test.exs
index 01e7206..44e77de 100644
--- a/test/honeybadger/insights/base_test.exs
+++ b/test/honeybadger/insights/base_test.exs
@@ -2,6 +2,8 @@ defmodule Honeybadger.Insights.BaseTest do
   use Honeybadger.Case, async: false
 
   defmodule TestEventFilter do
+    use Honeybadger.EventFilter.Mixin
+
     def filter_telemetry_event(data, _raw, _name) do
       Map.put(data, :was_filtered, true)
     end
diff --git a/test/honeybadger_test.exs b/test/honeybadger_test.exs
index 3173440..69dd947 100644
--- a/test/honeybadger_test.exs
+++ b/test/honeybadger_test.exs
@@ -448,6 +448,36 @@ defmodule HoneybadgerTest do
       # Verify timestamp format matches DateTime.to_string() format
       assert {:ok, _, _} = DateTime.from_iso8601(ts)
     end
+
+    defmodule TestEventFilter do
+      use Honeybadger.EventFilter.Mixin
+
+      def filter_event(%{event_type: "test_event"} = event) do
+        Map.put(event, :filtered, true)
+      end
+
+      def filter_event(%{event_type: "remove_me"}), do: nil
+      def filter_event(event), do: event
+      def filter_telemetry_event(a, _b, _c), do: a
+    end
+
+    test "can be modified by filter" do
+      with_config([event_filter: TestEventFilter], fn ->
+        Honeybadger.event("test_event", %{key: "value"})
+
+        assert_receive {:api_request, [data]}
+        assert data["event_type"] == "test_event"
+        assert data["filtered"] == true
+      end)
+    end
+
+    test "can be removed by filter" do
+      with_config([event_filter: TestEventFilter], fn ->
+        Honeybadger.event("remove_me", %{key: "value"})
+
+        refute_receive {:api_request, _}
+      end)
+    end
   end
 
   describe "Honeybadger.event/1" do