From 056c509c29d59ef154ab67be6750565d5716ed99 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Sun, 5 Jul 2026 12:26:57 +0200 Subject: [PATCH 1/8] feat: add before send callback --- lib/posthog.ex | 33 ++++++++++++++++++++++++++++++++- lib/posthog/config.ex | 6 ++++++ lib/posthog/registry.ex | 1 + test/posthog_test.exs | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 1 deletion(-) diff --git a/lib/posthog.ex b/lib/posthog.ex index a6cc9c9..09c8dff 100644 --- a/lib/posthog.ex +++ b/lib/posthog.ex @@ -3,6 +3,8 @@ defmodule PostHog do Main API for working with PostHog """ + require Logger + @typedoc "Name under which an instance of PostHog supervision tree is registered." @type supervisor_name() :: atom() @@ -78,7 +80,36 @@ defmodule PostHog do properties: properties } - PostHog.Sender.send(event, name) + case run_before_send(Map.get(config, :before_send), event) do + nil -> :ok + event -> PostHog.Sender.send(event, name) + end + end + + defp run_before_send(nil, event), do: event + + defp run_before_send(before_send, event) when is_function(before_send, 1) do + case before_send.(event) do + nil -> + nil + + %{} = event -> + event + + other -> + Logger.error( + "PostHog before_send callback returned #{inspect(other)} instead of an event map or nil; dropping event" + ) + + nil + end + rescue + exception -> + Logger.error( + "PostHog before_send callback raised; dropping event: #{Exception.message(exception)}" + ) + + nil end @doc false diff --git a/lib/posthog/config.ex b/lib/posthog/config.ex index fb82a6d..828ea53 100644 --- a/lib/posthog/config.ex +++ b/lib/posthog/config.ex @@ -60,6 +60,12 @@ defmodule PostHog.Config do default: %{}, doc: "Map of properties that should be added to all events" ], + before_send: [ + type: {:or, [{:fun, 1}, nil]}, + default: nil, + doc: + "Callback invoked with the fully enriched event before it is queued. Return the event to send a modified version, or nil to drop it." + ], is_server: [ type: :boolean, default: true, diff --git a/lib/posthog/registry.ex b/lib/posthog/registry.ex index 0a787ba..90e22cd 100644 --- a/lib/posthog/registry.ex +++ b/lib/posthog/registry.ex @@ -21,6 +21,7 @@ defmodule PostHog.Registry do enabled: false, api_client: nil, global_properties: %{}, + before_send: nil, test_mode: false } end diff --git a/test/posthog_test.exs b/test/posthog_test.exs index 324d864..ce19d5f 100644 --- a/test/posthog_test.exs +++ b/test/posthog_test.exs @@ -86,6 +86,27 @@ defmodule PostHogTest do refute Map.has_key?(properties, :"$is_server") end + @tag config: [ + global_properties: %{source: "global"}, + before_send: &__MODULE__.modify_before_send/1, + supervisor_name: PostHog + ] + test "before_send can modify fully enriched events" do + PostHog.bare_capture("case tested", "distinct_id", %{secret: "remove"}) + + assert [%{properties: properties}] = all_captured() + assert properties[:before_send] == true + assert properties[:saw_fully_enriched_event] == true + refute Map.has_key?(properties, :secret) + end + + @tag config: [before_send: &__MODULE__.drop_before_send/1, supervisor_name: PostHog] + test "before_send can drop events" do + assert :ok = PostHog.bare_capture("case tested", "distinct_id") + + assert [] = all_captured() + end + @tag config: [supervisor_name: CustomPostHog] test "simple call for custom supervisor" do PostHog.bare_capture(CustomPostHog, "case tested", "distinct_id") @@ -260,6 +281,21 @@ defmodule PostHogTest do end end + def modify_before_send(event) do + saw_fully_enriched_event = + event.properties[:"$lib"] == "posthog-elixir" and + is_binary(event.properties[:"$lib_version"]) and + event.properties[:"$is_server"] == true and + event.properties[:source] == "global" + + event + |> put_in([:properties, :before_send], true) + |> put_in([:properties, :saw_fully_enriched_event], saw_fully_enriched_event) + |> update_in([:properties], &Map.delete(&1, :secret)) + end + + def drop_before_send(_event), do: nil + describe "set_event_context/2 + get_event_context/2" do test "default scope" do PostHog.set_event_context("$exception", %{foo: "bar"}) From 3c5f4be10b38875e34981ce6e42685a9a2596692 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Sun, 5 Jul 2026 13:38:57 +0200 Subject: [PATCH 2/8] test: cover before send edge cases --- lib/posthog.ex | 2 +- test/posthog_test.exs | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/posthog.ex b/lib/posthog.ex index 09c8dff..7b77221 100644 --- a/lib/posthog.ex +++ b/lib/posthog.ex @@ -80,7 +80,7 @@ defmodule PostHog do properties: properties } - case run_before_send(Map.get(config, :before_send), event) do + case run_before_send(config.before_send, event) do nil -> :ok event -> PostHog.Sender.send(event, name) end diff --git a/test/posthog_test.exs b/test/posthog_test.exs index ce19d5f..c3bd039 100644 --- a/test/posthog_test.exs +++ b/test/posthog_test.exs @@ -100,11 +100,17 @@ defmodule PostHogTest do refute Map.has_key?(properties, :secret) end - @tag config: [before_send: &__MODULE__.drop_before_send/1, supervisor_name: PostHog] - test "before_send can drop events" do - assert :ok = PostHog.bare_capture("case tested", "distinct_id") - - assert [] = all_captured() + for {name, callback} <- [ + {"returns nil", &__MODULE__.drop_before_send/1}, + {"returns invalid value", &__MODULE__.invalid_before_send/1}, + {"raises", &__MODULE__.raise_before_send/1} + ] do + @tag config: [before_send: callback, supervisor_name: PostHog] + test "before_send drops events when callback #{name}" do + assert :ok = PostHog.bare_capture("case tested", "distinct_id") + + assert [] = all_captured() + end end @tag config: [supervisor_name: CustomPostHog] @@ -296,6 +302,10 @@ defmodule PostHogTest do def drop_before_send(_event), do: nil + def invalid_before_send(_event), do: :invalid + + def raise_before_send(_event), do: raise("before_send failed") + describe "set_event_context/2 + get_event_context/2" do test "default scope" do PostHog.set_event_context("$exception", %{foo: "bar"}) From 41c48b3cf50a3d5f317ccfd04176ae72e26a5e36 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Sun, 5 Jul 2026 14:26:55 +0200 Subject: [PATCH 3/8] chore: add before send changeset --- .sampo/changesets/regal-knight-aurelien.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .sampo/changesets/regal-knight-aurelien.md diff --git a/.sampo/changesets/regal-knight-aurelien.md b/.sampo/changesets/regal-knight-aurelien.md new file mode 100644 index 0000000..85c544b --- /dev/null +++ b/.sampo/changesets/regal-knight-aurelien.md @@ -0,0 +1,5 @@ +--- +hex/posthog: minor +--- + +Add before_send callback support for filtering captured events From caa5cefac811d5ab7a27364a0641d634089b9f84 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 6 Jul 2026 08:30:02 +0200 Subject: [PATCH 4/8] fix: avoid logging before_send failures --- lib/posthog.ex | 18 ++++-------------- test/posthog_test.exs | 16 ++++++++++++---- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/lib/posthog.ex b/lib/posthog.ex index 7b77221..025d08a 100644 --- a/lib/posthog.ex +++ b/lib/posthog.ex @@ -3,8 +3,6 @@ defmodule PostHog do Main API for working with PostHog """ - require Logger - @typedoc "Name under which an instance of PostHog supervision tree is registered." @type supervisor_name() :: atom() @@ -96,20 +94,12 @@ defmodule PostHog do %{} = event -> event - other -> - Logger.error( - "PostHog before_send callback returned #{inspect(other)} instead of an event map or nil; dropping event" - ) - - nil + _other -> + event end rescue - exception -> - Logger.error( - "PostHog before_send callback raised; dropping event: #{Exception.message(exception)}" - ) - - nil + _exception -> + event end @doc false diff --git a/test/posthog_test.exs b/test/posthog_test.exs index c3bd039..55fd6c0 100644 --- a/test/posthog_test.exs +++ b/test/posthog_test.exs @@ -100,16 +100,24 @@ defmodule PostHogTest do refute Map.has_key?(properties, :secret) end + @tag config: [before_send: &__MODULE__.drop_before_send/1, supervisor_name: PostHog] + test "before_send drops events when callback returns nil" do + assert :ok = PostHog.bare_capture("case tested", "distinct_id") + + assert [] = all_captured() + end + for {name, callback} <- [ - {"returns nil", &__MODULE__.drop_before_send/1}, {"returns invalid value", &__MODULE__.invalid_before_send/1}, {"raises", &__MODULE__.raise_before_send/1} ] do @tag config: [before_send: callback, supervisor_name: PostHog] - test "before_send drops events when callback #{name}" do - assert :ok = PostHog.bare_capture("case tested", "distinct_id") + test "before_send sends the original event when callback #{name}" do + assert :ok = PostHog.bare_capture("case tested", "distinct_id", %{original: true}) - assert [] = all_captured() + assert [%{event: "case tested", properties: properties}] = all_captured() + assert properties[:original] == true + refute properties[:before_send] end end From fa874cf648928abe807b2ca9ada048398ebc70f2 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 6 Jul 2026 08:34:07 +0200 Subject: [PATCH 5/8] fix: catch before_send throws and exits --- lib/posthog.ex | 3 +++ test/posthog_test.exs | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/posthog.ex b/lib/posthog.ex index 025d08a..4e57847 100644 --- a/lib/posthog.ex +++ b/lib/posthog.ex @@ -100,6 +100,9 @@ defmodule PostHog do rescue _exception -> event + catch + _kind, _reason -> + event end @doc false diff --git a/test/posthog_test.exs b/test/posthog_test.exs index 55fd6c0..714e75c 100644 --- a/test/posthog_test.exs +++ b/test/posthog_test.exs @@ -109,7 +109,9 @@ defmodule PostHogTest do for {name, callback} <- [ {"returns invalid value", &__MODULE__.invalid_before_send/1}, - {"raises", &__MODULE__.raise_before_send/1} + {"raises", &__MODULE__.raise_before_send/1}, + {"throws", &__MODULE__.throw_before_send/1}, + {"exits", &__MODULE__.exit_before_send/1} ] do @tag config: [before_send: callback, supervisor_name: PostHog] test "before_send sends the original event when callback #{name}" do @@ -314,6 +316,10 @@ defmodule PostHogTest do def raise_before_send(_event), do: raise("before_send failed") + def throw_before_send(_event), do: throw(:before_send_failed) + + def exit_before_send(_event), do: exit(:before_send_failed) + describe "set_event_context/2 + get_event_context/2" do test "default scope" do PostHog.set_event_context("$exception", %{foo: "bar"}) From 4959496db287f7ddef5b6acd8577ab08887c82e7 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 6 Jul 2026 08:46:47 +0200 Subject: [PATCH 6/8] fix: log before_send failures without self-capture --- lib/posthog.ex | 31 +++++++++++++++++++++++++------ lib/posthog/handler.ex | 2 ++ test/posthog/handler_test.exs | 11 +++++++++++ test/posthog_test.exs | 6 ++---- 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/lib/posthog.ex b/lib/posthog.ex index 4e57847..43252c8 100644 --- a/lib/posthog.ex +++ b/lib/posthog.ex @@ -3,6 +3,10 @@ defmodule PostHog do Main API for working with PostHog """ + require Logger + + @before_send_log_metadata [posthog_skip_capture: true] + @typedoc "Name under which an instance of PostHog supervision tree is registered." @type supervisor_name() :: atom() @@ -94,15 +98,30 @@ defmodule PostHog do %{} = event -> event - _other -> - event + other -> + Logger.error( + "PostHog before_send callback returned #{inspect(other)} instead of an event map or nil; dropping event", + @before_send_log_metadata + ) + + nil end rescue - _exception -> - event + exception -> + Logger.error( + "PostHog before_send callback raised; dropping event: #{Exception.message(exception)}", + @before_send_log_metadata + ) + + nil catch - _kind, _reason -> - event + kind, reason -> + Logger.error( + "PostHog before_send callback #{kind}; dropping event: #{inspect(reason)}", + @before_send_log_metadata + ) + + nil end @doc false diff --git a/lib/posthog/handler.ex b/lib/posthog/handler.ex index 947afbc..eea6c42 100644 --- a/lib/posthog/handler.ex +++ b/lib/posthog/handler.ex @@ -12,6 +12,8 @@ defmodule PostHog.Handler do # credo:disable-for-next-line Credo.Check.Design.TagTODO # TODO: add @impl :logger_handler once we drop support for OTP < 27 @doc false + def log(%{meta: %{posthog_skip_capture: true}}, _config), do: :ok + def log(log_event, %{config: config}) do maybe_properties = cond do diff --git a/test/posthog/handler_test.exs b/test/posthog/handler_test.exs index 6d8d45d..a95ea94 100644 --- a/test/posthog/handler_test.exs +++ b/test/posthog/handler_test.exs @@ -119,6 +119,17 @@ defmodule PostHog.HandlerTest do assert [] = all_captured(supervisor_name) end + @tag config: [capture_level: :warning] + test "ignores logs marked to skip PostHog capture", %{ + handler_ref: ref, + config: %{supervisor_name: supervisor_name} + } do + Logger.error("PostHog SDK diagnostic", posthog_skip_capture: true) + LoggerHandlerKit.Assert.assert_logged(ref) + + assert [] = all_captured(supervisor_name) + end + @tag config: [capture_level: :warning] test "logs with crash reason always captured", %{ handler_ref: ref, diff --git a/test/posthog_test.exs b/test/posthog_test.exs index 714e75c..8f418f6 100644 --- a/test/posthog_test.exs +++ b/test/posthog_test.exs @@ -114,12 +114,10 @@ defmodule PostHogTest do {"exits", &__MODULE__.exit_before_send/1} ] do @tag config: [before_send: callback, supervisor_name: PostHog] - test "before_send sends the original event when callback #{name}" do + test "before_send drops events when callback #{name}" do assert :ok = PostHog.bare_capture("case tested", "distinct_id", %{original: true}) - assert [%{event: "case tested", properties: properties}] = all_captured() - assert properties[:original] == true - refute properties[:before_send] + assert [] = all_captured() end end From 898f552ab0c0490557bdd067233ea3bb1cc6e0c3 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 6 Jul 2026 09:01:09 +0200 Subject: [PATCH 7/8] fix: skip capture for config diagnostics --- lib/posthog/config.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/posthog/config.ex b/lib/posthog/config.ex index 828ea53..853ae01 100644 --- a/lib/posthog/config.ex +++ b/lib/posthog/config.ex @@ -303,7 +303,8 @@ defmodule PostHog.Config do defp log_blank_api_key(validated) do if blank_api_key?(validated) do Logger.warning( - "posthog api_key is empty after trimming whitespace; PostHog will start in disabled/no-op mode" + "posthog api_key is empty after trimming whitespace; PostHog will start in disabled/no-op mode", + posthog_skip_capture: true ) end end From f4c2fc8bec98a73ee8619f94888ab86be9d884eb Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 6 Jul 2026 09:08:27 +0200 Subject: [PATCH 8/8] fix: capture events with invalid before_send return --- lib/posthog.ex | 4 ++-- test/posthog_test.exs | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/posthog.ex b/lib/posthog.ex index 43252c8..d3c4c61 100644 --- a/lib/posthog.ex +++ b/lib/posthog.ex @@ -100,11 +100,11 @@ defmodule PostHog do other -> Logger.error( - "PostHog before_send callback returned #{inspect(other)} instead of an event map or nil; dropping event", + "PostHog before_send callback returned #{inspect(other)} instead of an event map or nil; sending original event", @before_send_log_metadata ) - nil + event end rescue exception -> diff --git a/test/posthog_test.exs b/test/posthog_test.exs index 8f418f6..ccfb8e5 100644 --- a/test/posthog_test.exs +++ b/test/posthog_test.exs @@ -107,8 +107,16 @@ defmodule PostHogTest do assert [] = all_captured() end + @tag config: [before_send: &__MODULE__.invalid_before_send/1, supervisor_name: PostHog] + test "before_send sends the original event when callback returns invalid value" do + assert :ok = PostHog.bare_capture("case tested", "distinct_id", %{original: true}) + + assert [%{event: "case tested", properties: properties}] = all_captured() + assert properties[:original] == true + refute properties[:before_send] + end + for {name, callback} <- [ - {"returns invalid value", &__MODULE__.invalid_before_send/1}, {"raises", &__MODULE__.raise_before_send/1}, {"throws", &__MODULE__.throw_before_send/1}, {"exits", &__MODULE__.exit_before_send/1}