From ab98ddb005836c167d6f9764547cbff3476da3e0 Mon Sep 17 00:00:00 2001 From: martosaur Date: Fri, 8 May 2026 21:27:11 -0700 Subject: [PATCH 1/2] Make sure Sender will not attempt sending empty event batches --- lib/posthog/sender.ex | 4 +++- test/posthog/sender_test.exs | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/lib/posthog/sender.ex b/lib/posthog/sender.ex index b7b93c7..69d2bc1 100644 --- a/lib/posthog/sender.ex +++ b/lib/posthog/sender.ex @@ -88,10 +88,12 @@ defmodule PostHog.Sender do end @impl GenServer - def handle_info(:batch_time_reached, state) do + def handle_info(:batch_time_reached, %{num_events: n} = state) when n > 0 do {:noreply, state, {:continue, :send_batch}} end + def handle_info(:batch_time_reached, state), do: {:noreply, state} + @impl GenServer def handle_continue(:send_batch, state) do # Before we initiate an HTTP request that might block the process diff --git a/test/posthog/sender_test.exs b/test/posthog/sender_test.exs index ae47faf..7668fd8 100644 --- a/test/posthog/sender_test.exs +++ b/test/posthog/sender_test.exs @@ -207,5 +207,49 @@ defmodule PostHog.SenderTest do assert :ok = GenServer.stop(pid) end + + test "does not send empty batch", %{api_client: api_client, registry: registry} do + test_pid = self() + + pid = + start_link_supervised!( + {Sender, + supervisor_name: @supervisor_name, + index: 1, + api_client: api_client, + max_batch_time_ms: 60_000, + max_batch_events: 2} + ) + + expect(API.Mock, :request, fn _client, method, url, opts -> + assert method == :post + assert url == "/batch" + + assert opts[:json] == %{ + batch: ["bar", "foo"] + } + + send(test_pid, :ready) + + receive do + :go -> :ok + end + + send(test_pid, :done) + end) + + Sender.send("foo", @supervisor_name) + Sender.send("bar", @supervisor_name) + + assert_receive :ready + [{^pid, :busy}] = Registry.lookup(registry, {PostHog.Sender, 1}) + send(pid, :go) + assert_receive :done + :sys.get_status(pid) + [{^pid, :available}] = Registry.lookup(registry, {PostHog.Sender, 1}) + + send(pid, :batch_time_reached) + refute_receive :ready + end end end From b4e32905fefecb390901318fcd0f67e5340e29c6 Mon Sep 17 00:00:00 2001 From: martosaur Date: Fri, 8 May 2026 21:33:48 -0700 Subject: [PATCH 2/2] Cancel current timer whenever batch is sent based on max_batch_size --- lib/posthog/sender.ex | 9 ++++++--- test/posthog/sender_test.exs | 8 ++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/posthog/sender.ex b/lib/posthog/sender.ex index 69d2bc1..db1bb26 100644 --- a/lib/posthog/sender.ex +++ b/lib/posthog/sender.ex @@ -8,6 +8,7 @@ defmodule PostHog.Sender do :api_client, :max_batch_time_ms, :max_batch_events, + :timer_ref, events: [], num_events: 0 ] @@ -74,13 +75,15 @@ defmodule PostHog.Sender do def handle_cast({:event, event}, state) do case state do %{num_events: n, events: events} when n + 1 >= state.max_batch_events -> + if state.timer_ref, do: Process.cancel_timer(state.timer_ref, async: true, info: false) + {:noreply, %{state | events: [event | events], num_events: n + 1}, {:continue, :send_batch}} %{num_events: 0, events: events} -> - Process.send_after(self(), :batch_time_reached, state.max_batch_time_ms) + ref = Process.send_after(self(), :batch_time_reached, state.max_batch_time_ms) - {:noreply, %{state | events: [event | events], num_events: 1}} + {:noreply, %{state | events: [event | events], num_events: 1, timer_ref: ref}} %{num_events: n, events: events} -> {:noreply, %{state | events: [event | events], num_events: n + 1}} @@ -103,7 +106,7 @@ defmodule PostHog.Sender do Registry.update_value(state.registry, registry_key(state.index), fn _ -> :busy end) PostHog.API.batch(state.api_client, state.events) Registry.update_value(state.registry, registry_key(state.index), fn _ -> :available end) - {:noreply, %{state | events: [], num_events: 0}} + {:noreply, %{state | events: [], num_events: 0, timer_ref: nil}} end @impl GenServer diff --git a/test/posthog/sender_test.exs b/test/posthog/sender_test.exs index 7668fd8..d9e5d45 100644 --- a/test/posthog/sender_test.exs +++ b/test/posthog/sender_test.exs @@ -97,7 +97,7 @@ defmodule PostHog.SenderTest do assert %{events: ["my_event"]} = :sys.get_state(pid) end - test "immediately sends after reaching max_batch_events", %{ + test "immediately sends after reaching max_batch_events and cancels timer", %{ api_client: api_client, registry: registry } do @@ -129,6 +129,10 @@ defmodule PostHog.SenderTest do end) Sender.send("foo", @supervisor_name) + + %{timer_ref: ref} = :sys.get_state(pid) + assert is_reference(ref) + Sender.send("bar", @supervisor_name) assert_receive :ready @@ -136,7 +140,7 @@ defmodule PostHog.SenderTest do [{^pid, :busy}] = Registry.lookup(registry, {PostHog.Sender, 1}) send(pid, :go) - assert %{events: []} = :sys.get_state(pid) + assert %{events: [], timer_ref: nil} = :sys.get_state(pid) [{^pid, :available}] = Registry.lookup(registry, {PostHog.Sender, 1}) end