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
24 changes: 16 additions & 8 deletions test/integration_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ defmodule PostHog.IntegrationTest do
end
end

:logger.add_handler(:posthog, PostHog.Handler, %{config: config})
:ok = :logger.add_handler(:posthog, PostHog.Handler, %{config: config})
on_exit(fn -> :logger.remove_handler(:posthog) end)

%{wait_fun: wait}
end
Expand Down Expand Up @@ -98,26 +99,33 @@ defmodule PostHog.IntegrationTest do
end

test "supervisor report", %{wait_fun: wait} do
Application.stop(:logger)
Application.put_env(:logger, :handle_sasl_reports, true)
Application.put_env(:logger, :level, :info)
Application.start(:logger)
original =
Map.new([:handle_sasl_reports, :level], &{&1, Application.fetch_env(:logger, &1)})

on_exit(fn ->
Application.stop(:logger)
Application.put_env(:logger, :handle_sasl_reports, false)
Application.delete_env(:logger, :level)

Enum.each(original, fn
{key, {:ok, value}} -> Application.put_env(:logger, key, value)
{key, :error} -> Application.delete_env(:logger, key)
end)

Application.start(:logger)
end)

Application.stop(:logger)
Application.put_env(:logger, :handle_sasl_reports, true)
Application.put_env(:logger, :level, :info)
Application.start(:logger)

LoggerHandlerKit.Act.supervisor_progress_report(:failed_to_start_child)
wait.()
end
end

describe "event capture" do
test "captures event", %{test: test, wait_fun: wait} do
PostHog.capture("case tested", test, %{number: 1})
assert :ok = PostHog.capture("case tested", %{distinct_id: test, number: 1})
wait.()
end
end
Expand Down
69 changes: 69 additions & 0 deletions test/mix/tasks/posthog_package_source_code_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
defmodule Mix.Tasks.Posthog.PackageSourceCodeTest do
use ExUnit.Case, async: false

alias Mix.Tasks.Posthog.PackageSourceCode
alias PostHog.ErrorTracking.Sources

@moduletag :tmp_dir

setup %{tmp_dir: dir} do
on_exit(fn -> File.rm_rf!(dir) end)
keys = [:root_source_code_paths, :source_code_path_pattern, :source_code_exclude_patterns]
original = Map.new(keys, &{&1, Application.fetch_env(:posthog, &1)})
Enum.each(keys, &Application.delete_env(:posthog, &1))

on_exit(fn ->
Enum.each(original, fn
{key, {:ok, value}} -> Application.put_env(:posthog, key, value)
{key, :error} -> Application.delete_env(:posthog, key)
end)
end)
end

test "CLI roots override config and package multiple roots with default exclusions", %{
tmp_dir: dir
} do
first = Path.join(dir, "first")
second = Path.join(dir, "second")
output = Path.join([dir, "output", "source.map"])
File.mkdir_p!(Path.join(first, "lib"))
File.mkdir_p!(second)
File.write!(Path.join(first, "lib/one.ex"), "first line\nsecond line\n")
File.write!(Path.join(second, "two.ex"), "other root\n")
File.write!(Path.join(second, "ignored.exs"), "not production source")

for excluded <- ["_build", "priv", "test"] do
File.mkdir_p!(Path.join(first, excluded))
File.write!(Path.join([first, excluded, "ignored.ex"]), "excluded source")
end

Application.put_env(:posthog, :root_source_code_paths, [Path.join(dir, "unused")])

ExUnit.CaptureIO.capture_io(fn ->
PackageSourceCode.run(["--root-path", first, "--root-path", second, "--output", output])
end)

assert {:ok,
%{
"lib/one.ex" => %{1 => "first line", 2 => "second line"},
"two.ex" => %{1 => "other root"}
}} == Sources.decode_source_map(File.read!(output))
end

test "uses configured roots, pattern and exclusions when no CLI root is supplied", %{
tmp_dir: dir
} do
File.write!(Path.join(dir, "keep.exs"), "included\n")
File.write!(Path.join(dir, "skip.exs"), "excluded\n")
File.write!(Path.join(dir, "other.ex"), "wrong extension\n")
Application.put_env(:posthog, :root_source_code_paths, [dir])
Application.put_env(:posthog, :source_code_path_pattern, "*.exs")
Application.put_env(:posthog, :source_code_exclude_patterns, [~r/^skip/])
output = Path.join(dir, "source.map")

ExUnit.CaptureIO.capture_io(fn -> PackageSourceCode.run(["-o", output]) end)

assert {:ok, %{"keep.exs" => %{1 => "included"}}} ==
Sources.decode_source_map(File.read!(output))
end
end
66 changes: 66 additions & 0 deletions test/mix/tasks/posthog_public_api_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
defmodule Mix.Tasks.Posthog.PublicApiTest do
use ExUnit.Case, async: false

alias Mix.Tasks.Posthog.PublicApi

@moduletag :tmp_dir

setup %{tmp_dir: dir} do
on_exit(fn -> File.rm_rf!(dir) end)
end

test "committed snapshot matches the public API" do
assert ExUnit.CaptureIO.capture_io(fn -> PublicApi.run(["--check"]) end) =~
"Public API snapshot is up to date"
end

test "update creates parent directories and writes the committed contract", %{tmp_dir: dir} do
path = Path.join([dir, "nested", "api.snapshot"])

ExUnit.CaptureIO.capture_io(fn -> PublicApi.run(["--update", "--snapshot", path]) end)

assert File.read!(path) == File.read!("public_api.snapshot")

assert ExUnit.CaptureIO.capture_io(fn -> PublicApi.run(["--snapshot", path]) end) =~
"Public API snapshot is up to date"
end

test "check rejects a stale snapshot without overwriting it", %{tmp_dir: dir} do
path = Path.join(dir, "api.snapshot")
File.write!(path, "obsolete API\n")

error =
assert_raise Mix.Error, fn -> PublicApi.run(["--check", "--snapshot", path]) end

assert error.message =~ "Public API snapshot is out of date"
assert error.message =~ "-obsolete API"
assert error.message =~ "+PostHog"
assert File.read!(path) == "obsolete API\n"
end

test "check reports a missing snapshot without creating it", %{tmp_dir: dir} do
path = Path.join(dir, "missing.snapshot")

assert_raise Mix.Error, ~r/Public API snapshot does not exist/, fn ->
PublicApi.run(["--snapshot", path])
end

refute File.exists?(path)
end

test "check reports unreadable snapshot paths", %{tmp_dir: dir} do
assert_raise Mix.Error, ~r/Could not read .*: :eisdir/, fn ->
PublicApi.run(["--snapshot", dir])
end
end

test "check and update cannot be combined", %{tmp_dir: dir} do
path = Path.join(dir, "api.snapshot")

assert_raise Mix.Error, "Pass only one of --check or --update", fn ->
PublicApi.run(["--check", "--update", "--snapshot", path])
end

refute File.exists?(path)
end
end
16 changes: 13 additions & 3 deletions test/posthog/api/client_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ defmodule PostHog.API.ClientTest do
test "client/2 sets the posthog-elixir User-Agent" do
%Client{client: req} = Client.client("phc_test", "https://us.i.posthog.com")

assert req.headers["user-agent"] == [Client.user_agent()]
version = Application.spec(:posthog, :vsn) |> to_string()
expected = "posthog-elixir/#{version}"
assert Client.user_agent() == expected
assert req.headers["user-agent"] == [expected]
end

for {case_name, response_or_exception, expected} <- [
Expand Down Expand Up @@ -45,15 +48,15 @@ defmodule PostHog.API.ClientTest do
send(
parent,
{:request, Req.Request.fetch_option(req, :compress_body),
Req.Request.get_header(req, "content-encoding")}
Req.Request.get_header(req, "content-encoding"), req.body}
)

{req, Req.Response.new(status: 200, body: %{})}
end
)

assert {:ok, %{status: 200}} = Client.request(req, :post, "/", [])
assert_received {:request, {:ok, false}, []}
assert_received {:request, {:ok, false}, [], [123_456]}
end

test "request retries keep the compressed body reusable" do
Expand All @@ -78,6 +81,13 @@ defmodule PostHog.API.ClientTest do
assert_received {:request, ["gzip"], first_body}
assert_received {:request, ["gzip"], second_body}
assert_received {:request, ["gzip"], third_body}

assert first_body |> :zlib.gunzip() |> Jason.decode!() == %{
"event" => "test",
"api_key" => nil
}

assert Agent.get(calls, & &1) == 3
assert :zlib.gunzip(first_body) == :zlib.gunzip(second_body)
assert :zlib.gunzip(second_body) == :zlib.gunzip(third_body)
end
Expand Down
8 changes: 4 additions & 4 deletions test/posthog/context_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ defmodule PostHog.ContextTest do
Context.set(:all, :all, %{hello: "world"})
Logger.metadata(foo: "baz")

assert %{foo: "bar", hello: "world"} = Context.get(PostHog, "$exception")
assert %{foo: "baz", hello: "world"} = Context.get(MyPostHog, "$exception")
assert %{hello: "world"} = Context.get(MyPostHog, "$exception_list")
assert %{hello: "world"} = Context.get(FooBar, "$exception")
assert Context.get(PostHog, "$exception") == %{foo: "bar", hello: "world"}
assert Context.get(MyPostHog, "$exception") == %{foo: "baz", hello: "world"}
assert Context.get(MyPostHog, "$exception_list") == %{hello: "world"}
assert Context.get(FooBar, "$exception") == %{hello: "world"}
end

test "in case of overlapping keys prefer more specific scope" do
Expand Down
51 changes: 29 additions & 22 deletions test/posthog/error_tracking/sources_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -181,28 +181,35 @@ defmodule PostHog.ErrorTracking.SourcesTest do
assert source_lines[line_number] == source_line
end

test "excludes directories matching the default patterns" do
# Default patterns use ~r"/dir/" — match mid-path occurrences.
# Explicitly verify the custom-pattern path works correctly.
result =
Sources.load_files(
root_source_code_paths: [File.cwd!()],
source_code_exclude_patterns: [~r"/posthog/", ~r"/mix/"]
)

refute Enum.any?(result, fn {path, _} ->
String.contains?(path, "/posthog/") or String.contains?(path, "/mix/")
end)
end

test "respects custom exclude patterns" do
result =
Sources.load_files(
root_source_code_paths: [File.cwd!()],
source_code_exclude_patterns: [~r"/posthog/"]
)

refute Enum.any?(result, fn {path, _} -> String.contains?(path, "/posthog/") end)
@tag :tmp_dir
test "excludes directories matching the default patterns", %{tmp_dir: root} do
on_exit(fn -> File.rm_rf!(root) end)

for path <- ["lib/keep.ex", "_build/drop.ex", "priv/drop.ex", "test/drop.ex"] do
absolute = Path.join(root, path)
File.mkdir_p!(Path.dirname(absolute))
File.write!(absolute, "sentinel\n")
end

assert %{"lib/keep.ex" => %{1 => "sentinel"}} ==
Sources.load_files(root_source_code_paths: [root])
end

@tag :tmp_dir
test "respects custom exclude patterns", %{tmp_dir: root} do
on_exit(fn -> File.rm_rf!(root) end)

for path <- ["lib/keep.ex", "lib/posthog/drop.ex"] do
absolute = Path.join(root, path)
File.mkdir_p!(Path.dirname(absolute))
File.write!(absolute, "sentinel\n")
end

assert %{"lib/keep.ex" => %{1 => "sentinel"}} ==
Sources.load_files(
root_source_code_paths: [root],
source_code_exclude_patterns: [~r"/posthog/"]
)
end
end
end
30 changes: 23 additions & 7 deletions test/posthog/feature_flags/definition_loader_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -260,19 +260,26 @@ defmodule PostHog.FeatureFlags.DefinitionLoaderTest do

for status <- statuses do
name = Module.concat(__MODULE__, "Status#{status}")
start_supervised!({PostHog.Supervisor, config(name, feature_flags_poll_interval_ms: 20)})

start_supervised!(
{PostHog.Supervisor, config(name, feature_flags_poll_interval_ms: 60_000)}
)

assert DefinitionLoader.ready?(name)
capture_log(fn -> DefinitionLoader.refresh(name) end)
assert_receive {:status_request, ^status}

if status == 429 do
assert DefinitionLoader.ready?(name)
refute_receive {:status_request, 429}, 100
loader = GenServer.whereis(PostHog.Registry.via(name, DefinitionLoader))
state = :sys.get_state(loader)
assert state.quota_backoff_ms == 120_000
assert Process.read_timer(state.timer_ref) > 60_000
else
refute DefinitionLoader.ready?(name)
end

stop_supervised(name)
assert :ok = stop_supervised(name)
end
end

Expand Down Expand Up @@ -340,12 +347,12 @@ defmodule PostHog.FeatureFlags.DefinitionLoaderTest do
assert Agent.get(activity, & &1.max) == 1
end

test "blocked definition requests are canceled before bounded shutdown and provider cleanup" do
test "timed-out definition requests are killed before bounded shutdown and provider cleanup" do
owner = self()
stub_with(PostHog.API.Mock, PostHog.API.Stub)

expect(PostHog.API.Mock, :request, fn :stub_client, :get, "/flags/definitions", _opts ->
send(owner, :definition_request_started)
send(owner, {:definition_request_started, self()})
receive do: (:never -> :ok)
end)

Expand All @@ -357,7 +364,16 @@ defmodule PostHog.FeatureFlags.DefinitionLoaderTest do
)

start_supervised!({PostHog.Supervisor, cfg})
assert_receive :definition_request_started
assert_receive {:definition_request_started, request_worker}
request_monitor = Process.monitor(request_worker)
assert_receive {:DOWN, ^request_monitor, :process, ^request_worker, _reason}
refute Process.alive?(request_worker)

loader = GenServer.whereis(PostHog.Registry.via(__MODULE__.Blocked, DefinitionLoader))
assert :sys.get_state(loader).initial_load_complete?
refute DefinitionLoader.ready?(__MODULE__.Blocked)
refute_received :provider_shutdown

started = System.monotonic_time(:millisecond)
assert :ok = stop_supervised(__MODULE__.Blocked)
assert System.monotonic_time(:millisecond) - started < 500
Expand Down Expand Up @@ -449,7 +465,7 @@ defmodule PostHog.FeatureFlags.DefinitionLoaderTest do
send(loader, {:boundary_result, make_ref(), self(), {:ok, :late}})
send(loader, {:DOWN, make_ref(), :process, self(), :normal})
send(loader, {:unexpected, :message})
Process.sleep(10)
assert :sys.get_state(loader).definition_generation == state.definition_generation
assert Process.alive?(loader)
assert DefinitionLoader.ready?(__MODULE__.LateMessages)
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ defmodule PostHog.FeatureFlags.FlagDefinitionCacheProviderTest do

owner = self()

stub(PostHog.API.Mock, :request, fn _client, _method, _path, _opts ->
send(owner, :unexpected_api_fetch)
{:error, :unexpected_api_fetch}
end)

{:ok, provider} =
Agent.start_link(fn ->
%{owner: owner, decision: false, read: envelope("cached"), store: :ok, shutdown: :ok}
Expand All @@ -196,6 +201,7 @@ defmodule PostHog.FeatureFlags.FlagDefinitionCacheProviderTest do
assert definitions.minimal_flag_called_events
assert :ok = stop_supervised(__MODULE__.Cached)
assert_receive :shutdown
refute_receive :unexpected_api_fetch
end

test "positive decision fetches, publishes, and stores the complete envelope" do
Expand Down
Loading
Loading