Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .sampo/changesets/regal-knight-aurelien.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
hex/posthog: minor
---

Add before_send callback support for filtering captured events
45 changes: 44 additions & 1 deletion lib/posthog.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -78,7 +82,46 @@ defmodule PostHog do
properties: properties
}

PostHog.Sender.send(event, name)
case run_before_send(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; sending original event",
@before_send_log_metadata
)

event
end
rescue
Comment thread
marandaneto marked this conversation as resolved.
exception ->
Logger.error(
"PostHog before_send callback raised; dropping event: #{Exception.message(exception)}",
@before_send_log_metadata
)

nil
catch
kind, reason ->
Logger.error(
"PostHog before_send callback #{kind}; dropping event: #{inspect(reason)}",
@before_send_log_metadata
)

nil
end

@doc false
Expand Down
9 changes: 8 additions & 1 deletion lib/posthog/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -297,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
Expand Down
2 changes: 2 additions & 0 deletions lib/posthog/handler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/posthog/registry.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ defmodule PostHog.Registry do
enabled: false,
api_client: nil,
global_properties: %{},
before_send: nil,
test_mode: false
}
end
Expand Down
11 changes: 11 additions & 0 deletions test/posthog/handler_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
66 changes: 66 additions & 0 deletions test/posthog_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,49 @@ 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 drops events when callback returns nil" do
assert :ok = PostHog.bare_capture("case tested", "distinct_id")

assert [] = all_captured()
end
Comment thread
marandaneto marked this conversation as resolved.

@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} <- [
{"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 drops events when callback #{name}" do
assert :ok = PostHog.bare_capture("case tested", "distinct_id", %{original: true})

assert [] = all_captured()
end
end

@tag config: [supervisor_name: CustomPostHog]
test "simple call for custom supervisor" do
PostHog.bare_capture(CustomPostHog, "case tested", "distinct_id")
Expand Down Expand Up @@ -260,6 +303,29 @@ 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

def invalid_before_send(_event), do: :invalid

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"})
Expand Down
Loading