Skip to content
Open
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
61 changes: 55 additions & 6 deletions lib/flagsmith_client.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
defmodule Flagsmith.Client do
require Logger

alias Flagsmith.Schemas
alias Flagsmith.Configuration

Expand Down Expand Up @@ -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
Expand Down
71 changes: 70 additions & 1 deletion test/flagsmith_client_test.exs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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",
"</api/v1/environment-document/?page_id=#{URI.encode_www_form(page_id)}>; 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