diff --git a/lib/flagsmith_client.ex b/lib/flagsmith_client.ex index de44558..e3d0514 100644 --- a/lib/flagsmith_client.ex +++ b/lib/flagsmith_client.ex @@ -1,4 +1,6 @@ defmodule Flagsmith.Client do + require Logger + alias Flagsmith.Schemas alias Flagsmith.Configuration @@ -74,18 +76,65 @@ defmodule Flagsmith.Client do @doc false def get_environment_request(%Configuration{} = config) do - case Tesla.get(http_client(config), @api_paths.environment) do - {:ok, %{status: status, body: body}} when status >= 200 and status < 300 -> - {:ok, - body - |> Schemas.Environment.from_response() - |> Schemas.Environment.add_client_config(config)} + started_at = System.monotonic_time(:millisecond) + + with {:ok, body} <- get_environment_document(http_client(config)) do + warn_if_slower_than_refresh_interval(started_at, config) + + {:ok, + body + |> Schemas.Environment.from_response() + |> Schemas.Environment.add_client_config(config)} + end + end + + defp get_environment_document(client, query \\ [], document \\ nil) do + case Tesla.get(client, @api_paths.environment, query: query) do + {:ok, %{status: status, body: body} = response} when status >= 200 and status < 300 -> + document = append_identity_overrides(document, body) + + case next_page_id(response) do + nil -> {:ok, document} + page_id -> get_environment_document(client, [page_id: page_id], document) + end error_resp -> return_error(error_resp) end end + defp append_identity_overrides(nil, page), do: page + + defp append_identity_overrides(document, page) do + Map.put( + document, + "identity_overrides", + (document["identity_overrides"] || []) ++ (page["identity_overrides"] || []) + ) + end + + defp next_page_id(response) do + with link when is_binary(link) <- Tesla.get_header(response, "link"), + [_, next_url] <- Regex.run(~r/<([^>]+)>;\s*rel="next"/, link), + %URI{query: query} when is_binary(query) <- URI.parse(next_url) do + query |> URI.decode_query() |> Map.get("page_id") + else + _ -> nil + end + end + + defp warn_if_slower_than_refresh_interval(started_at, %Configuration{ + environment_refresh_interval_milliseconds: interval + }) do + elapsed = System.monotonic_time(:millisecond) - started_at + + if elapsed > interval do + Logger.warning( + "Fetching the environment document took #{elapsed}ms, longer than the environment refresh interval of #{interval}ms; raise the refresh interval or reduce the environment size." + ) + end + end + @doc """ Returns an `:ok` tuple containing a list of `t:Flagsmith.Schemas.Flag.t/0` structs, either from the local evaluation or API depending on the configuration used, or an diff --git a/test/flagsmith_client_test.exs b/test/flagsmith_client_test.exs index 3003095..fd717bc 100644 --- a/test/flagsmith_client_test.exs +++ b/test/flagsmith_client_test.exs @@ -1,7 +1,7 @@ defmodule Flagsmith.Client.Test do use ExUnit.Case - import Mox, only: [verify_on_exit!: 1, expect: 3] + import Mox, only: [verify_on_exit!: 1, expect: 3, expect: 4] import Flagsmith.Test.Helpers, only: [assert_request: 2] alias Flagsmith.Engine.Test @@ -461,4 +461,73 @@ defmodule Flagsmith.Client.Test do Flagsmith.Client.get_environment(config) end end + + describe "paginated environment document" do + test "get_environment follows the next page links", %{config: config} do + first_page = Test.Generators.map_env() + [identity_override] = first_page["identity_overrides"] + + override_page = fn identifier -> + %{"identity_overrides" => [Map.put(identity_override, "identifier", identifier)]} + end + + next_link = fn page_id -> + [ + {"link", + "; rel=\"next\""} + ] + end + + expect(Tesla.Adapter.Mock, :call, 3, fn tesla_env, _options -> + case tesla_env.query do + [] -> + {:ok, + %Tesla.Env{ + status: 200, + body: first_page, + headers: next_link.("identity_override:1:page-2") + }} + + [page_id: "identity_override:1:page-2"] -> + {:ok, + %Tesla.Env{ + status: 200, + body: override_page.("page-2-id"), + headers: next_link.("identity_override:1:page-3") + }} + + [page_id: "identity_override:1:page-3"] -> + {:ok, %Tesla.Env{status: 200, body: override_page.("page-3-id")}} + end + end) + + assert {:ok, %Schemas.Environment{} = env} = Flagsmith.Client.get_environment(config) + + assert ["overridden-id", "page-2-id", "page-3-id"] = + Enum.map(env.identity_overrides, & &1.identifier) + + assert length(env.feature_states) == length(first_page["feature_states"]) + end + + test "get_environment warns when fetching is slower than the refresh interval" do + config = + Flagsmith.Client.new( + environment_key: "client_test_key", + environment_refresh_interval_milliseconds: 1 + ) + + expect(Tesla.Adapter.Mock, :call, fn _tesla_env, _options -> + Process.sleep(5) + {:ok, %Tesla.Env{status: 200, body: Test.Generators.map_env()}} + end) + + log = + ExUnit.CaptureLog.capture_log(fn -> + assert {:ok, %Schemas.Environment{}} = Flagsmith.Client.get_environment(config) + end) + + assert log =~ "Fetching the environment document took" + assert log =~ "longer than the environment refresh interval of 1ms" + end + end end