diff --git a/lib/posthog/config.ex b/lib/posthog/config.ex index af62033..b2a2034 100644 --- a/lib/posthog/config.ex +++ b/lib/posthog/config.ex @@ -18,9 +18,10 @@ defmodule PostHog.Config do ], api_key: [ type: :string, - required: true, + default: "", doc: """ Your PostHog Project API key. Find it in your project's settings under the Project ID section. + If omitted or empty after trimming whitespace, PostHog starts in disabled/no-op mode. """ ], api_client_module: [ @@ -184,15 +185,24 @@ defmodule PostHog.Config do with {:ok, validated} <- NimbleOptions.validate(normalized_options, @compiled_configuration_schema) do + api_key_blank? = blank_api_key?(validated) log_blank_api_key(validated) config = Map.new(validated) - client = config.api_client_module.client(config.api_key, config.api_host) + + client = + if api_key_blank? do + nil + else + config.api_client_module.client(config.api_key, config.api_host) + end + global_properties = Map.merge(config.global_properties, @system_global_properties) final_config = config |> Map.put(:api_client, client) + |> Map.put(:enabled, not api_key_blank?) |> Map.put( :in_app_modules, config.in_app_otp_apps |> Enum.flat_map(&Application.spec(&1, :modules)) |> MapSet.new() @@ -219,11 +229,21 @@ defmodule PostHog.Config do normalized_options end end) + |> then(fn normalized_options -> + if disabled_options?(normalized_options) and + not Keyword.has_key?(normalized_options, :api_host) do + Keyword.put(normalized_options, :api_host, @default_api_host) + else + normalized_options + end + end) end defp normalize_api_key(api_key) when is_binary(api_key), do: String.trim(api_key) defp normalize_api_key(api_key), do: api_key + defp disabled_options?(options), do: Keyword.get(options, :api_key, "") == "" + defp normalize_api_host(api_host) when is_binary(api_host) do api_host |> String.trim() @@ -235,10 +255,12 @@ defmodule PostHog.Config do defp normalize_api_host(api_host), do: api_host + defp blank_api_key?(validated), do: validated[:api_key] == "" + defp log_blank_api_key(validated) do - if validated[:api_key] == "" do + if blank_api_key?(validated) do Logger.error( - "posthog api_key is empty after trimming whitespace; check your project API key" + "posthog api_key is empty after trimming whitespace; PostHog will start in disabled/no-op mode" ) end end diff --git a/lib/posthog/sender.ex b/lib/posthog/sender.ex index b7b93c7..5d17aac 100644 --- a/lib/posthog/sender.ex +++ b/lib/posthog/sender.ex @@ -30,15 +30,28 @@ defmodule PostHog.Sender do supervisor_name |> PostHog.Registry.config() |> case do + %{enabled: false} -> + :ok + %{test_mode: true} -> PostHog.Test.remember_event(supervisor_name, event) _ -> - senders = - supervisor_name - |> PostHog.Registry.registry_name() - |> Registry.select([{{{__MODULE__, :_}, :"$1", :"$2"}, [], [{{:"$2", :"$1"}}]}]) + send_to_sender(event, supervisor_name) + end + end + + defp send_to_sender(event, supervisor_name) do + senders = + supervisor_name + |> PostHog.Registry.registry_name() + |> Registry.select([{{{__MODULE__, :_}, :"$1", :"$2"}, [], [{{:"$2", :"$1"}}]}]) + + case senders do + [] -> + :ok + senders -> # Pick the first available sender, otherwise random busy one. senders |> Keyword.get_lazy(:available, fn -> diff --git a/lib/posthog/supervisor.ex b/lib/posthog/supervisor.ex index 91da77a..28ed1e5 100644 --- a/lib/posthog/supervisor.ex +++ b/lib/posthog/supervisor.ex @@ -37,6 +37,8 @@ defmodule PostHog.Supervisor do Supervisor.init(children, strategy: :one_for_one) end + defp sources(%{enabled: false}), do: [] + defp sources(config) do if config.enable_source_code_context do opts = [ @@ -53,6 +55,8 @@ defmodule PostHog.Supervisor do end end + defp senders(%{enabled: false}), do: [] + defp senders(config) do pool_size = Map.get(config, :sender_pool_size, max(System.schedulers_online(), 2)) diff --git a/test/posthog/application_config_test.exs b/test/posthog/application_config_test.exs new file mode 100644 index 0000000..9db9533 --- /dev/null +++ b/test/posthog/application_config_test.exs @@ -0,0 +1,41 @@ +defmodule PostHog.ApplicationConfigTest do + use ExUnit.Case, async: false + + import ExUnit.CaptureLog + import Mox + + setup :verify_on_exit! + + setup do + previous_env = Application.get_all_env(:posthog) + + on_exit(fn -> + :posthog + |> Application.get_all_env() + |> Keyword.keys() + |> Enum.each(&Application.delete_env(:posthog, &1)) + + Enum.each(previous_env, fn {key, value} -> + Application.put_env(:posthog, key, value) + end) + end) + end + + test "read! does not raise when enabled and api_key is missing" do + Application.put_env(:posthog, :enable, true) + Application.put_env(:posthog, :api_client_module, PostHog.API.Mock) + Application.delete_env(:posthog, :api_key) + Application.delete_env(:posthog, :api_host) + + log = + capture_log(fn -> + assert {%{enable: true}, %{api_key: "", enabled: false, api_client: nil} = config} = + PostHog.Config.read!() + + assert config.api_host == "https://us.i.posthog.com" + end) + + assert log =~ + "posthog api_key is empty after trimming whitespace; PostHog will start in disabled/no-op mode" + end +end diff --git a/test/posthog/config_test.exs b/test/posthog/config_test.exs index d09ef88..db3138b 100644 --- a/test/posthog/config_test.exs +++ b/test/posthog/config_test.exs @@ -43,14 +43,42 @@ defmodule PostHog.ConfigTest do assert config.api_host == "https://us.i.posthog.com" end - test "validate logs when api_key is blank after trimming whitespace" do - expect(PostHog.API.Mock, :client, fn api_key, api_host -> - assert api_key == "" - assert api_host == "https://us.i.posthog.com" + test "validate disables PostHog when api_key is missing" do + log = + capture_log(fn -> + assert {:ok, config} = + PostHog.Config.validate(api_client_module: PostHog.API.Mock) - %PostHog.API.Client{client: :stub_client, module: PostHog.API.Mock} - end) + assert config.api_key == "" + assert config.api_host == "https://us.i.posthog.com" + assert config.enabled == false + assert config.api_client == nil + end) + + assert log =~ + "posthog api_key is empty after trimming whitespace; PostHog will start in disabled/no-op mode" + end + + test "validate disables PostHog when api_key is empty" do + log = + capture_log(fn -> + assert {:ok, config} = + PostHog.Config.validate( + api_key: "", + api_host: "https://us.i.posthog.com", + api_client_module: PostHog.API.Mock + ) + + assert config.api_key == "" + assert config.enabled == false + assert config.api_client == nil + end) + + assert log =~ + "posthog api_key is empty after trimming whitespace; PostHog will start in disabled/no-op mode" + end + test "validate disables PostHog when api_key is blank after trimming whitespace" do log = capture_log(fn -> assert {:ok, config} = @@ -61,8 +89,11 @@ defmodule PostHog.ConfigTest do ) assert config.api_key == "" + assert config.enabled == false + assert config.api_client == nil end) - assert log =~ "posthog api_key is empty after trimming whitespace; check your project API key" + assert log =~ + "posthog api_key is empty after trimming whitespace; PostHog will start in disabled/no-op mode" end end diff --git a/test/posthog/supervisor_test.exs b/test/posthog/supervisor_test.exs new file mode 100644 index 0000000..d38dbb0 --- /dev/null +++ b/test/posthog/supervisor_test.exs @@ -0,0 +1,27 @@ +defmodule PostHog.SupervisorTest do + use ExUnit.Case, async: true + + import ExUnit.CaptureLog + + @supervisor_name __MODULE__ + + test "disabled config starts without senders and captures as no-op" do + {config, _log} = + with_log(fn -> + PostHog.Config.validate!( + api_key: " \n\t ", + api_host: "https://us.i.posthog.com", + supervisor_name: @supervisor_name + ) + end) + + start_link_supervised!({PostHog.Supervisor, config}) + + assert [] = + @supervisor_name + |> PostHog.Registry.registry_name() + |> Registry.select([{{{PostHog.Sender, :_}, :"$1", :"$2"}, [], [:"$1"]}]) + + assert :ok = PostHog.bare_capture(@supervisor_name, "disabled capture", "distinct_id", %{}) + end +end